├── config-sample.json ├── package.json ├── LICENSE ├── controller.js ├── README.md └── index.js /config-sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "bridge": { 3 | "name": "Homebridge", 4 | "username": "CC:22:3D:E3:CE:30", 5 | "port": 51826, 6 | "pin": "031-45-154" 7 | }, 8 | 9 | "platforms": [ 10 | { 11 | "platform" : "websocket", 12 | "name" : "websocket", 13 | "port": 4050 14 | }, 15 | 16 | { 17 | "platform" : "Neeo", 18 | "neeoBrain": "neeo-hostname", 19 | "scenes": [ 20 | { 21 | "name": "evening", 22 | "label": "Evening" 23 | }, 24 | { 25 | "name": "dinner-time", 26 | "label": "Dinner Time" 27 | }, 28 | { 29 | "name": "watch-tv", 30 | "label": "Watch TV" 31 | }, 32 | { 33 | "name": "movie-time", 34 | "label": "Movie Time" 35 | }, 36 | { 37 | "name": "bedtime", 38 | "label": "Bedtime" 39 | } 40 | ] 41 | }] 42 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homebridge-neeo", 3 | "version": "0.0.1", 4 | "description": "NEEO plugin for homebridge: https://github.com/nfarina/homebridge", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node index.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/humayunh/homebridge-neeo.git" 12 | }, 13 | "keywords": [ 14 | "neeo", 15 | "homekit", 16 | "homebridge", 17 | "homebridge-plugin" 18 | ], 19 | "author": "Humayun Hasan", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/humayunh/homebridge-neeo/issues" 23 | }, 24 | "homepage": "https://github.com/humayunh/homebridge-neeo#readme", 25 | "engines": { 26 | "node": ">=0.12.0", 27 | "homebridge": ">=0.2.0" 28 | }, 29 | "dependencies": { 30 | "request": "^2.83.0", 31 | "url": "^0.11.0", 32 | "ws": "^3.2.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 humayunh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /controller.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | const WebSocket = require('ws'); 5 | 6 | /* 7 | * Device Controller 8 | * This controller forwards NEEO remote button presses to homebridge 9 | * which triggers the respective scene in HomeKit 10 | */ 11 | module.exports.onButtonPressed = function onButtonPressed(name) { 12 | const ws = new WebSocket('ws://localhost:4050/'); 13 | console.log(`[NEEO CONTROLLER] ${name} button pressed`); 14 | ws.on('open', function open() { 15 | 16 | // Update this list with scene names, which Programmable Switch was programmed for that scene, 17 | // and which Programmable Switch Event was assigned to trigger the scene in the Home app 18 | // Single Press = 0 19 | // Double Press = 1 20 | // Long Press = 2 21 | if (name == "evening") 22 | var SocketMsg = "{\"topic\": \"set\", \"payload\": {\"name\": \"NEEO Buttons 1\", \"characteristic\": \"ProgrammableSwitchEvent\", \"value\": \"0\"}}"; 23 | else if (name == "dinner-time") 24 | var SocketMsg = "{\"topic\": \"set\", \"payload\": {\"name\": \"NEEO Buttons 1\", \"characteristic\": \"ProgrammableSwitchEvent\", \"value\": \"1\"}}"; 25 | else if (name == "watch-tv") 26 | var SocketMsg = "{\"topic\": \"set\", \"payload\": {\"name\": \"NEEO Buttons 1\", \"characteristic\": \"ProgrammableSwitchEvent\", \"value\": \"2\"}}"; 27 | else if (name == "movie-time") 28 | var SocketMsg = "{\"topic\": \"set\", \"payload\": {\"name\": \"NEEO Buttons 2\", \"characteristic\": \"ProgrammableSwitchEvent\", \"value\": \"0\"}}"; 29 | else if (name == "bedtime") 30 | var SocketMsg = "{\"topic\": \"set\", \"payload\": {\"name\": \"NEEO Buttons 2\", \"characteristic\": \"ProgrammableSwitchEvent\", \"value\": \"1\"}}"; 31 | else if (name == "great-room-lights") 32 | var SocketMsg = "{\"topic\": \"set\", \"payload\": {\"name\": \"NEEO Buttons 2\", \"characteristic\": \"ProgrammableSwitchEvent\", \"value\": \"2\"}}"; 33 | 34 | ws.send(SocketMsg); 35 | // console.log('JSON message sent'); 36 | 37 | ws.on('message', function incoming(data) { 38 | console.log('Looking for return message'); 39 | console.log(data); 40 | }); 41 | 42 | ws.close(); 43 | 44 | ws.on('close', function close() { 45 | // console.log('disconnected'); 46 | }); 47 | }); 48 | }; 49 | 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # homebridge-neeo 2 | Homebridge plugin for the NEEO brain and remote 3 | 4 | This Homebridge plugin allows triggering HomeKit scenes from the NEEO remote and triggering NEEO recipes from the iOS Home app as well as Siri commands. 5 | 6 | Before installing, you will need to install the homebridge package. Please read through the Homebridge readme. There are instructions available there for running homebridge on a Raspberry Pi at startup. Once you have homebridge installed and running, install the NEEO plugin by typing "npm install -g homebridge-neeo". The "-g" flag installs the plugin globally which is usually needed when running Homebridge as a startup process. 7 | 8 | To configure the homebridge-neeo plugin for your environment, edit the included config-sample.json file and modify the following items: 9 | 1. neeoBrain - update this value with your NEEO Brain's hostname. You can get this from the NEEO remote's settings screen. 10 | 2. Edit the scenes list to include your HomeKit scenes that you would like to be able to trigger from your NEEO remote. 11 | 3. Modify the bridge section of the config file if you modified any of the Homebridge default settings when you installed Homebridge 12 | 4. Save the config-sample.json as config.json file to Homebridge's home directory. This location depends on whether you are running Homebridge as a local process or a startup process. 13 | 5. Start Homebridge 14 | 15 | This plugin is a combination of homebridge plugin and neeo driver. It provides two capabilities. 16 | 17 | The first is the ability to trigger HomeKit scenes from the NEEO remote or the mobile app. This is achieved by adding a custom accessory to NEEO. You can add this accessory by searching for homekit or scene when adding a new device to NEEO. This accessory will show up as Homebridge HomeKit Buttons and provides shortcuts for the scenes that you defined in the config.json file and added as buttons in the index.js file. To complete the configuration, open the iOS Home app and look for a new button names 'NEEO Buttons 1'. Each button can trigger upto three NEEO recipes so the plugin will automatically create additional buttons if you have configured more than three HomeKit scenes in the plugin. Long press on the 'NEEO Buttons 1' button and open the Details page. Scroll to the bottom of the Details page where you will find configurations for Single Press (0), Double Press (1) and Long Press (2). Assign a scene to each of these making sure to match the configuration in your index.js. Once configured, you should be able to add these NEEO shortcuts to a recipe and activate HomeKit scenes by pressing the NEEO shortcut button. 18 | 19 | The second ability allows triggering NEEO recipes from the iOS Home app or using Siri commands like "Hey Siri, turn on the TV switch". The homebridge-neeo plugin will automatically retieve all recipes from NEEO and create switches for each recipe in HomeKit. When you add the Homebridge accessory to the Home app for the first time, these switches will be added as well. You should now be able to activate NEEO recipes by pressing the switch button in the Home app or instructing Siri to turn it on. 20 | 21 | The plugin has room for improvement (use dynamic homebridge platform, build NEEO shortcuts and associated HomeKit buttons using the homebridge API rather than the websocket method). My coding is very rusty so it will be slow going. I'm open to suggestions or collaborators. 22 | 23 | Sources for inspiration came from homebridge-savant and homebridge-harmony projects and also from the various NEEO drivers already on github. 24 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | //'use strict'; 2 | var Service, Characteristic; 3 | const request = require('request'); 4 | const url = require('url'); 5 | const WebSocket = require('ws'); 6 | // The homebridge-websocket plugin is required and must be installed separately 7 | 8 | console.log('--------------HomeBridge adapter-------------'); 9 | 10 | /* 11 | * NEEO API Initialization 12 | */ 13 | 14 | const neeoapi = require('neeo-sdk'); 15 | const controller = require('./controller'); 16 | 17 | // Build HomeKitSceneDevice for NEEO Brain 18 | // Add or change the button names and labels below to macth your HomeKit scenes 19 | // This device can be searched for in the NEEO app when adding new device. You can add the buttons defined below as shortcuts in your NEEO recipes. 20 | // To-do: add buttons dynamically based on list in config.json 21 | const homekitSceneDevice = neeoapi.buildDevice('HomeKit Buttons') 22 | .setManufacturer('HomeBridge') 23 | .addAdditionalSearchToken('scene') 24 | .setType('ACCESSOIRE') 25 | 26 | // Then we add the capabilities of the device 27 | .addButton({ name: 'evening', label: 'Evening' }) 28 | .addButton({ name: 'dinner-time', label: 'Dinner Time' }) 29 | .addButton({ name: 'watch-tv', label: 'Watch TV' }) 30 | .addButton({ name: 'movie-time', label: 'Movie Time' }) 31 | .addButton({ name: 'bedtime', label: 'Bedtime' }) 32 | .addButton({ name: 'great-room-lights', label: 'Great Room Lights' }) 33 | .addButtonHander(controller.onButtonPressed); 34 | 35 | 36 | function startHomebridgeDriver(brain) { 37 | console.log('NEEO - Start HomeBridge Driver'); 38 | neeoapi.startServer({ 39 | brain, 40 | port: 6336, 41 | name: 'homebridge-driver', 42 | devices: [homekitSceneDevice] 43 | }) 44 | .then(() => { 45 | console.log('NEEO READY! use the NEEO app to search for "HomeKit Buttons".'); 46 | }) 47 | .catch((error) => { 48 | //if there was any error, print message out to console 49 | console.error('NEEO ERROR!', error.message); 50 | process.exit(1); 51 | }); 52 | } 53 | 54 | /* 55 | * NEEO API Initialization Complete 56 | */ 57 | 58 | // Function to parse URLs and send to NEEO Brain for device or recipe control 59 | // neeoURL is expected in format http://:3000/v1/projects/home/ 60 | // To-do: Needs to be updated to support PUT 61 | function callNeeo(neeoUrl, callback){ 62 | request({ 63 | url: neeoUrl, 64 | method: 'GET', 65 | }, 66 | function (error, response, body) { 67 | if (error) { 68 | console.log('ERROR: ' + error.message); 69 | console.log('STATUS: ' + response); 70 | return next(error); 71 | } 72 | let json = JSON.parse(body); 73 | callback(json); 74 | }); 75 | } 76 | 77 | // Initialize the homebridge-neeo plugin for Homebridge 78 | module.exports = function(homebridge) { 79 | Service = homebridge.hap.Service; 80 | Characteristic = homebridge.hap.Characteristic; 81 | homebridge.registerPlatform('homebridge-neeo', 'Neeo', NeeoPlatform); 82 | } 83 | 84 | // Define the Neeo Platform for Homebridge and start the NEEO API server 85 | // NEEO Brain IP or hostname (preferred) can be defined in config.json 86 | // Had some issues when using NEEO Brain auto-discovery. Providing NEEO Brain's hostname seemed more stable 87 | function NeeoPlatform(log, config) { 88 | this.log = log; 89 | this.config = config; 90 | 91 | const brainIp = this.config["neeoBrain"] + '.local'; 92 | if (brainIp) { 93 | console.log('NEEO - use NEEO Brain IP from env variable', brainIp); 94 | startHomebridgeDriver(brainIp); 95 | } 96 | else { 97 | console.log('NEEO - discover one NEEO Brain...'); 98 | neeoapi.discoverOneBrain() 99 | .then((brain) => { 100 | console.log('NEEO - Brain discovered:', brain.name); 101 | startHomebridgeDriver(brain); 102 | }); 103 | } 104 | } 105 | 106 | NeeoPlatform.prototype = { 107 | accessories: function(callback) { 108 | 109 | // Need to update HomeKit button creation to use Homebridge API rather than websocket calls 110 | const ws = new WebSocket('ws://localhost:4050/'); 111 | var myAccessories = []; 112 | var that = this; 113 | 114 | // Retrieve names and labels of scenes from config.json 115 | var neeoButtons = this.config.scenes; 116 | // Calculate how many Stateless Programmable Switches (buttons) are needed. The button has three states 117 | // each of which can be assigned to trigger a different scene in the iOS Home app. 118 | var neeoButtonsCount = Math.ceil(neeoButtons.length/3); 119 | this.log('Create ' + neeoButtonsCount + ' NEEO buttons in HomeKit for ' + neeoButtons.length + ' scenes'); 120 | 121 | // The below method is used to create buttons using the Homebridge API 122 | // This can replace the websocket method further below once issue with triggering these buttons can be resolved 123 | /*for (i = 1; i <= neeoButtonsCount; i++) { 124 | var accessory = new NeeoAccessory(this.log, 'NEEO Buttons ' + i, 'button'); 125 | myAccessories.push(accessory); 126 | that.log('Created ' + accessory.name + ' Accessory'); 127 | */ 128 | 129 | // Uses the homebridge-websocket plugin to create new HomeKit buttons 130 | // These buttons will show up in the iOS Home as NEEO Buttons X 131 | ws.on('open', function open() { 132 | //console.log('Socket Opened'); 133 | for (i = 1; i <= neeoButtonsCount; i++) { 134 | var SocketMsg = "{\"topic\": \"add\", \"payload\": {\"name\": \"NEEO Buttons " + i + "\", \"service\": \"StatelessProgrammableSwitch\"}}"; 135 | ws.send(SocketMsg); 136 | 137 | ws.on('message', function incoming(data) { 138 | console.log('Looking for return message'); 139 | console.log(data); 140 | }); 141 | }; 142 | ws.close(); 143 | ws.on('close', function close() { 144 | //console.log('disconnected'); 145 | }); 146 | }); 147 | 148 | // Retrieve list of all recipes from NEEO Brain and add them as switches to HomeKit 149 | this.neeoBrain = this.config["neeoBrain"]; 150 | this.getRecipeUrl = url.parse('http://' + this.neeoBrain + '.local:3000/v1/api/recipes'); 151 | 152 | this.log('Fetching Neeo Recipes...'); 153 | callNeeo(this.getRecipeUrl, function(foundAccessories) { 154 | that.log('Retrieved ' + foundAccessories.length + ' recipes from NEEO Brain'); 155 | for (i in foundAccessories) { 156 | recipeDetail = JSON.parse(JSON.stringify(foundAccessories[i].detail)); 157 | recipeName = recipeDetail.devicename.replace("%20", " "); 158 | recipeUrl = JSON.parse(JSON.stringify(foundAccessories[i].url)); 159 | 160 | that.log('Adding Accessory: ' + recipeName); 161 | var accessory = new NeeoAccessory(this.log, foundAccessories[i], 'switch'); 162 | that.log('Created ' + accessory.name + ' Accessory'); 163 | 164 | myAccessories.push(accessory); 165 | }; 166 | that.log('Returning ' + myAccessories.length + ' accessories'); 167 | callback(myAccessories); 168 | }); 169 | } 170 | } 171 | 172 | function NeeoAccessory(log, recipe, deviceType) { 173 | this.log = log; 174 | this.recipe = recipe; 175 | this.deviceType = deviceType; 176 | 177 | // If creating a switch accessory, then define switch name and NEEO recipe power control URLs 178 | if (deviceType == 'switch') { 179 | var detail = JSON.parse(JSON.stringify(this.recipe.detail)); 180 | this.name = detail.devicename.replace("%20", " "); 181 | var powerUrl = JSON.parse(JSON.stringify(this.recipe.url)); 182 | 183 | this.onCommand = powerUrl.setPowerOn; 184 | this.offCommand = powerUrl.setPowerOff; 185 | this.queryCommand = powerUrl.getPowerState; 186 | } 187 | else if (deviceType == 'button') { 188 | this.log('Creating button ' + recipe); 189 | this.name = recipe; 190 | } 191 | } 192 | 193 | NeeoAccessory.prototype = { 194 | getServices: function() { 195 | var informationService = new Service.AccessoryInformation(); 196 | informationService 197 | .setCharacteristic(Characteristic.Manufacturer, 'NEEO') 198 | .setCharacteristic(Characteristic.Model, 'Brain') 199 | //.setCharacteristic(Characteristic.SerialNumber, 'Neeo Serial Number'); 200 | 201 | if (this.deviceType == 'switch') { 202 | var switchService = new Service.Switch(this.name); 203 | switchService 204 | .getCharacteristic(Characteristic.On) 205 | .on('get', this.getPowerState.bind(this)) 206 | .on('set', this.setState.bind(this)); 207 | console.log('Adding switch services'); 208 | this.informationService = informationService; 209 | this.switchService = switchService; 210 | return [informationService, switchService]; 211 | } 212 | else if (this.deviceType == 'button') { 213 | var buttonService = new Service.StatelessProgrammableSwitch(this.name); 214 | buttonService 215 | .getCharacteristic(Characteristic.ProgrammableSwitchEvent) 216 | //.on('get', this.getPowerState.bind(this)) 217 | .on('set', this.setEvent.bind(this)); 218 | informationService.setCharacteristic(Characteristic.Name, this.name); 219 | console.log('Adding button services'); 220 | this.informationService = informationService; 221 | this.buttonService = buttonService; 222 | return [informationService, buttonService]; 223 | } 224 | }, 225 | 226 | // For future use 227 | setEvent: function(callback) { 228 | console.log('Button pressed'); 229 | callback(); 230 | }, 231 | 232 | // This function is called when user toggles a switch in the iOS Home app 233 | // The NEEO API server is called with the requested recipe power state URL 234 | setState: function(powerOn, callback) { 235 | var accessory = this; 236 | 237 | if (powerOn == 0) neeoCommand = url.parse(accessory.offCommand) 238 | else neeoCommand = url.parse(accessory.onCommand); 239 | 240 | callNeeo(neeoCommand, function(response) { 241 | console.log(accessory.name + ' power set to ' + powerOn); 242 | callback(); 243 | }); 244 | }, 245 | 246 | // This function updates the current state of all NEEO recipe switches in HomeKit 247 | // Currently, state is updated only when the Home app is opened or when user switches from one room to another 248 | // Needs work so that all NEEO recipe switch states are updated when any switch state changes 249 | // E.g. If Watch TV recipe is currently active and user then presses the Watch Apple TV switch, 250 | // both switches will show On in the Home app, however NEEO remote will only show Watch Apple TV as active 251 | getPowerState: function(callback) { 252 | //console.log("Calling the function to get current state..."); 253 | var accessory = this; 254 | var getneeo = url.parse(accessory.queryCommand); 255 | 256 | callNeeo(getneeo, function(switchState) { 257 | //console.log(accessory.name + ' power state: ' + switchState.active); 258 | callback(null, switchState.active); 259 | }); 260 | } 261 | } --------------------------------------------------------------------------------