├── README.md ├── index.js └── package.json /README.md: -------------------------------------------------------------------------------- 1 | # homebridge-flic 2 | 3 | [![npm package](https://nodei.co/npm/homebridge-flic.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/homebridge-flic/) 4 | 5 | [![donate](https://img.shields.io/badge/%24-Buy%20me%20a%20coffee-ff69b4.svg)](https://www.buymeacoffee.com/devbobo) 6 | [![Slack Channel](https://img.shields.io/badge/slack-homebridge--flic-e01563.svg)](https://homebridgeteam.slack.com/messages/C560YBZ8E/) 7 | 8 | [Flic](https://flic.io) plugin for [Homebridge](https://github.com/nfarina/homebridge). 9 | 10 | # Requirements 11 | 12 | This plugin requires the Flic Daemon to be installed on a machine to run. 13 | 14 | There are platform specific versions to choose from... 15 | - [fliclib-linux-hci](https://github.com/50ButtonsEach/fliclib-linux-hci) 16 | - [flic-service-osx](https://github.com/50ButtonsEach/flic-service-osx) 17 | - [fliclib-windows](https://github.com/50ButtonsEach/fliclib-windows) 18 | 19 | # Installation 20 | 21 | 1. Install homebridge using: npm install -g homebridge 22 | 2. Install this plugin using: npm install -g homebridge-flic 23 | 3. Update your configuration file. See the sample below. 24 | 25 | # Updating 26 | 27 | - npm update -g homebridge-flic 28 | 29 | # Configuration 30 | 31 | Configuration sample: 32 | 33 | ```javascript 34 | "platforms": [ 35 | { 36 | "platform": "Flic", 37 | "name": "Flic", 38 | "controllers": [ 39 | {"host": "localhost", "port": 5551} 40 | ] 41 | } 42 | ] 43 | 44 | ``` 45 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // ** Sample config.json ** 4 | // 5 | //"platforms": [ 6 | // { 7 | // "platform": "Flic", 8 | // "name": "Flic", 9 | // "controllers": [ 10 | // {"host": "localhost", "port": 5551} 11 | // ] 12 | // 13 | // ** optional parameters ** 14 | // "autoDisconnectTime": 511, // optional 15 | // "latencyMode": "NormalLatency" // optional: latencyMode "NormalLatency", "LowLatency" or "HighLatency" 16 | // } 17 | //] 18 | 19 | var fliclib = require('fliclib-daemon-client'); 20 | var FlicClient = fliclib.FlicClient; 21 | var FlicConnectionChannel = fliclib.FlicConnectionChannel; 22 | var FlicScanner = fliclib.FlicScanner; 23 | 24 | var Accessory, Characteristic, Constants, Service, UUIDGen; 25 | 26 | module.exports = function (homebridge) { 27 | Accessory = homebridge.platformAccessory; 28 | Characteristic = homebridge.hap.Characteristic; 29 | Service = homebridge.hap.Service; 30 | UUIDGen = homebridge.hap.uuid; 31 | 32 | Constants = { 33 | DEFAULT_HOST: 'localhost', 34 | DEFAULT_PORT: 5551, 35 | CLICK_TYPE: { 36 | 'ButtonSingleClick': Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS, 37 | 'ButtonDoubleClick': Characteristic.ProgrammableSwitchEvent.DOUBLE_PRESS, 38 | 'ButtonHold': Characteristic.ProgrammableSwitchEvent.LONG_PRESS 39 | } 40 | } 41 | 42 | homebridge.registerPlatform('homebridge-flic', 'Flic', FlicPlatform, true); 43 | }; 44 | 45 | function FlicPlatform(log, config, api) { 46 | if (!config) { 47 | log.warn("Ignoring Flic Platform setup because it is not configured"); 48 | this.disabled = true; 49 | return; 50 | } 51 | 52 | var self = this; 53 | 54 | this.config = config; 55 | this.api = api; 56 | this.accessories = {}; 57 | this.controllers = this.config.controllers || [{host: Constants.DEFAULT_HOST, port: Constants.DEFAULT_PORT}]; 58 | this.log = log; 59 | 60 | this.options = {} 61 | 62 | if (this.config.autoDisconnectTime !== undefined && parseInt(this.config.autoDisconnectTime) !== undefined) { 63 | this.options.autoDisconnectTime = parseInt(this.config.autoDisconnectTime); 64 | } 65 | 66 | if (this.config.latencyMode !== undefined && (["NormalLatency", "LowLatency", "HighLatency"].indexOf(this.config.latencyMode) !== -1)) { 67 | this.options.latencyMode = this.config.latencyMode; 68 | } 69 | 70 | this.api.on('didFinishLaunching', function() { 71 | self.controllers.forEach( 72 | function(controller) { 73 | self.connectController(controller); 74 | } 75 | ); 76 | }); 77 | } 78 | 79 | FlicPlatform.prototype.addAccessory = function(bdAddr) { 80 | var serial = bdAddr.replace(/:/g, ''); 81 | var name = 'Flic ' + serial.replace(/80e4da/, ''); 82 | 83 | this.log("Found: %s (%s)", name, serial); 84 | 85 | var accessory = new Accessory(name, UUIDGen.generate(bdAddr)); 86 | 87 | accessory.addService(Service.StatelessProgrammableSwitch, name); 88 | 89 | this.accessories[accessory.UUID] = accessory; 90 | this.api.registerPlatformAccessories("homebridge-flic", "Flic", [accessory]); 91 | 92 | return accessory; 93 | } 94 | 95 | FlicPlatform.prototype.configureAccessory = function(accessory) { 96 | this.accessories[accessory.UUID] = accessory; 97 | } 98 | 99 | FlicPlatform.prototype.configurationRequestHandler = function(context, request, callback) { 100 | var self = this; 101 | var respDict = {}; 102 | 103 | if (request && request.type === "Terminate") { 104 | context.onScreen = null; 105 | } 106 | 107 | var sortAccessories = function() { 108 | context.sortedAccessories = Object.keys(self.accessories).map( 109 | function(k){return this[k]}, 110 | self.accessories 111 | ).sort(function(a,b) {if (a.displayName < b.displayName) return -1; if (a.displayName > b.displayName) return 1; return 0}); 112 | 113 | return Object.keys(context.sortedAccessories).map(function(k) {return this[k].displayName}, context.sortedAccessories); 114 | } 115 | 116 | switch(context.onScreen) { 117 | case "DoRemove": 118 | if (request.response.selections) { 119 | for (var i in request.response.selections.sort()) { 120 | this.removeAccessory(context.sortedAccessories[request.response.selections[i]]); 121 | } 122 | 123 | respDict = { 124 | "type": "Interface", 125 | "interface": "instruction", 126 | "title": "Finished", 127 | "detail": "Accessory removal was successful." 128 | } 129 | 130 | context.onScreen = null; 131 | callback(respDict); 132 | } 133 | else { 134 | context.onScreen = null; 135 | callback(respDict, "platform", true, this.config); 136 | } 137 | break; 138 | case "Menu": 139 | context.onScreen = request && request.response && request.response.selections[0] == 1 ? "Remove" : "Add"; 140 | 141 | switch(context.onScreen) { 142 | case "Add": 143 | self.controllers.forEach( 144 | function(controller) { 145 | if (controller.client === undefined) { 146 | return; 147 | } 148 | 149 | self.log("Controller [%s:%s] - Scanner added", controller.host, controller.port); 150 | controller.scanner = new FlicScanner(); 151 | 152 | controller.scanner.on("advertisementPacket", function(bdAddr, name, rssi, isPrivate, alreadyVerified) { 153 | clearTimeout(context.scanTimeout); 154 | 155 | if (alreadyVerified) { 156 | return; 157 | } 158 | else if (isPrivate && context.onScreen != "isPrivate") { 159 | context.onScreen = "isPrivate"; 160 | callback({ 161 | "type": "Interface", 162 | "interface": "instruction", 163 | "title": "Private button found", 164 | "detail": "Hold down for 7 seconds to make it public.", 165 | "showActivityIndicator": true 166 | }); 167 | return; 168 | } 169 | else if (isPrivate) { 170 | return; 171 | } 172 | 173 | self.controllers.forEach( 174 | function(controller) { 175 | if (controller.scanner === undefined) { 176 | return; 177 | } 178 | 179 | try { 180 | self.log("Controller [%s:%s] - Scanner removed", controller.host, controller.port); 181 | controller.client.removeScanner(controller.scanner); 182 | } 183 | catch(e) { 184 | 185 | } 186 | 187 | delete controller.scanner; 188 | } 189 | ); 190 | 191 | var cc = new FlicConnectionChannel(bdAddr, self.options); 192 | 193 | cc.on("createResponse", function(error, connectionStatus) { 194 | if (connectionStatus == "Ready") { 195 | // Got verified by someone else between scan result and this event 196 | controller.client.removeConnectionChannel(cc); 197 | self.connectButton(controller.client, bdAddr, name); 198 | 199 | callback({ 200 | "type": "Interface", 201 | "interface": "instruction", 202 | "title": "Sweet", 203 | "detail": "Done." 204 | }); 205 | } else if (error != "NoError") { 206 | self.log("Controller [%s:%s] - Scanner failed: Too many pending connections", controller.host, controller.port); 207 | 208 | callback({ 209 | "type": "Interface", 210 | "interface": "instruction", 211 | "title": "Scan failed", 212 | "detail": "Too many pending connections" 213 | }); 214 | } else { 215 | self.log("Found a public button. Now connecting..."); 216 | context.buttonTimeout = setTimeout(function() { 217 | controller.client.removeConnectionChannel(cc); 218 | }, 45 * 1000); 219 | callback({ 220 | "type": "Interface", 221 | "interface": "instruction", 222 | "title": "Public button found.", 223 | "detail": "Connecting...", 224 | "showActivityIndicator": true 225 | }); 226 | } 227 | }); 228 | cc.on("connectionStatusChanged", function(connectionStatus, disconnectReason) { 229 | if (connectionStatus == "Ready") { 230 | clearTimeout(context.buttonTimeout); 231 | controller.client.removeConnectionChannel(cc); 232 | self.connectButton(controller.client, bdAddr, name); 233 | 234 | callback({ 235 | "type": "Interface", 236 | "interface": "instruction", 237 | "title": "Sweet", 238 | "detail": "Done." 239 | }); 240 | } 241 | }); 242 | cc.on("removed", function(removedReason) { 243 | if (removedReason == "RemovedByThisClient") { 244 | removedReason = "Timed out"; 245 | } 246 | 247 | self.log("Controller [%s:%s] - Scanner failed: %s", controller.host, controller.port, removedReason); 248 | 249 | callback({ 250 | "type": "Interface", 251 | "interface": "instruction", 252 | "title": "Scan failed", 253 | "detail": removedReason 254 | }); 255 | }); 256 | 257 | controller.client.addConnectionChannel(cc); 258 | }); 259 | 260 | controller.client.addScanner(controller.scanner); 261 | } 262 | ); 263 | 264 | respDict = { 265 | "type": "Interface", 266 | "interface": "instruction", 267 | "title": "Scanning...", 268 | "detail": "Press your Flic button to add it.", 269 | "showActivityIndicator": true 270 | } 271 | 272 | context.scanTimeout = setTimeout(function () { 273 | self.controllers.forEach( 274 | function(controller) { 275 | if (controller.scanner === undefined) { 276 | return; 277 | } 278 | 279 | try { 280 | self.log("Controller [%s:%s] - Scanner removed", controller.host, controller.port); 281 | controller.client.removeScanner(controller.scanner); 282 | } 283 | catch(e) { 284 | 285 | } 286 | 287 | delete controller.scanner; 288 | } 289 | ); 290 | 291 | callback({ 292 | "type": "Interface", 293 | "interface": "instruction", 294 | "title": "Finished", 295 | "detail": "Scanning timeout" 296 | }); 297 | }, 60000); 298 | 299 | context.onScreen = null; 300 | break; 301 | case "Modify": 302 | case "Remove": 303 | respDict = { 304 | "type": "Interface", 305 | "interface": "list", 306 | "title": "Select accessory to " + context.onScreen.toLowerCase(), 307 | "allowMultipleSelection": context.onScreen == "Remove", 308 | "items": sortAccessories() 309 | } 310 | 311 | context.onScreen = "Do" + context.onScreen; 312 | break; 313 | } 314 | 315 | callback(respDict); 316 | break; 317 | default: 318 | if (request && (request.response || request.type === "Terminate")) { 319 | context.onScreen = null; 320 | callback(respDict, "platform", true, this.config); 321 | } 322 | else { 323 | respDict = { 324 | "type": "Interface", 325 | "interface": "list", 326 | "title": "Select option", 327 | "allowMultipleSelection": false, 328 | "items": ["Add Accessory", "Remove Accessory"] 329 | } 330 | 331 | context.onScreen = "Menu"; 332 | callback(respDict); 333 | } 334 | } 335 | } 336 | 337 | FlicPlatform.prototype.connectButton = function(client, bdAddr) { 338 | var self = this; 339 | var uuid = UUIDGen.generate(bdAddr); 340 | var serial = bdAddr.replace(/:/g, ''); 341 | var accessory = this.accessories[uuid]; 342 | var timeout; 343 | 344 | if (accessory === undefined) { 345 | accessory = this.addAccessory(bdAddr); 346 | } 347 | 348 | accessory.getService(Service.AccessoryInformation) 349 | .setCharacteristic(Characteristic.Manufacturer, "Shortcut Labs") 350 | .setCharacteristic(Characteristic.Model, "Flic") 351 | .setCharacteristic(Characteristic.SerialNumber, serial); 352 | 353 | accessory 354 | .getService(Service.StatelessProgrammableSwitch) 355 | .getCharacteristic(Characteristic.ProgrammableSwitchEvent) 356 | .setProps({maxValue: Characteristic.ProgrammableSwitchEvent.LONG_PRESS}); 357 | 358 | var cc = new FlicConnectionChannel(bdAddr); 359 | 360 | client.addConnectionChannel(cc); 361 | 362 | cc.on("buttonSingleOrDoubleClickOrHold", function(clickType, wasQueued, timeDiff) { 363 | if (wasQueued == true && timeDiff > 5) { 364 | return; 365 | } 366 | 367 | self.log("%s - %s", serial, clickType); 368 | accessory 369 | .getService(Service.StatelessProgrammableSwitch) 370 | .getCharacteristic(Characteristic.ProgrammableSwitchEvent) 371 | .setValue(Constants.CLICK_TYPE[clickType] || Constants.CLICK_TYPE['ButtonSingleClick']); 372 | }); 373 | 374 | cc.on("connectionStatusChanged", function(connectionStatus, disconnectReason) { 375 | self.log("%s - %s%s", serial, connectionStatus, (connectionStatus == "Disconnected" ? " " + disconnectReason : "")); 376 | }); 377 | 378 | cc.on("removed", function(reason) { 379 | self.log("%s - Connection Removed (%s)", serial, reason); 380 | }); 381 | } 382 | 383 | FlicPlatform.prototype.connectController = function(controller) { 384 | var self = this; 385 | 386 | if (typeof controller !== 'object') { 387 | controller = {host: Constants.DEFAULT_HOST, port: Constants.DEFAULT_PORT}; 388 | } 389 | 390 | if (controller.host === undefined) { 391 | controller.host = Constants.DEFAULT_HOST; 392 | } 393 | 394 | if (controller.port === undefined) { 395 | controller.port = Constants.DEFAULT_PORT; 396 | } 397 | 398 | controller.buttons = []; 399 | controller.client = new FlicClient(controller.host, controller.port); 400 | 401 | controller.client.once('ready', function() { 402 | self.log("Controller [%s:%s] - Connected", controller.host, controller.port); 403 | 404 | controller.client.getInfo(function(info) { 405 | info.bdAddrOfVerifiedButtons.forEach(function(bdAddr) { 406 | controller.buttons.push(bdAddr); 407 | self.connectButton(controller.client, bdAddr); 408 | }); 409 | }); 410 | }); 411 | 412 | controller.client.on("bluetoothControllerStateChange", function(state) { 413 | self.log("Controller [%s:%s] - %s", controller.host, controller.port, state); 414 | }); 415 | 416 | controller.client.on("newVerifiedButton", function(bdAddr) { 417 | controller.buttons.push(bdAddr); 418 | self.connectButton(controller.client, bdAddr); 419 | }); 420 | 421 | controller.client.on("error", function(error) { 422 | self.log("Controller [%s:%s] - Error: %s", controller.host, controller.port, error); 423 | }); 424 | 425 | controller.client.on("close", function(hadError) { 426 | self.log("Controller [%s:%s] - Disconnected", controller.host, controller.port); 427 | }); 428 | } 429 | 430 | FlicPlatform.prototype.removeAccessory = function(accessory) { 431 | this.log("Remove: %s", accessory.displayName); 432 | 433 | if (this.accessories[accessory.UUID]) { 434 | delete this.accessories[accessory.UUID]; 435 | } 436 | 437 | this.api.unregisterPlatformAccessories("homebridge-flic", "Flic", [accessory]); 438 | } 439 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homebridge-flic", 3 | "version": "0.0.6", 4 | "description": "Flic plugin for homebridge", 5 | "repository": { 6 | "type": "git", 7 | "url": "git://github.com/devbobo/homebridge-flic.git" 8 | }, 9 | "licenses" : [ 10 | { 11 | "type" : "MIT", 12 | "url" : "https://github.com/devbobo/homebridge-flic/raw/master/LICENSE" 13 | } 14 | ], 15 | "bugs": { 16 | "url": "http://github.com/devbobo/homebridge-flic/issues" 17 | }, 18 | "engines": { 19 | "node": ">=0.12.0", 20 | "homebridge": ">=0.4.22" 21 | }, 22 | "keywords": [ 23 | "homebridge-plugin", 24 | "flic", 25 | "smart button", 26 | "wireless button" 27 | ], 28 | "dependencies": { 29 | "fliclib-daemon-client": ">=0.2.0" 30 | } 31 | } 32 | --------------------------------------------------------------------------------