diff --git a/plejd/ble.js b/plejd/ble.js index 9e386f5..edfc739 100644 --- a/plejd/ble.js +++ b/plejd/ble.js @@ -3,7 +3,6 @@ const crypto = require('crypto'); const xor = require('buffer-xor'); const _ = require('lodash'); const EventEmitter = require('events'); -const sleep = require('sleep'); let debug = 'console'; @@ -47,8 +46,11 @@ class PlejdService extends EventEmitter { this.devices = {}; // Keeps track of the currently connected device this.device = null; + this.deviceAddress = null; this.deviceIdx = 0; + this.writeQueue = []; + // Holds a reference to all characteristics this.characteristicState = STATE_UNINITIALIZED; this.characteristics = { @@ -62,6 +64,27 @@ class PlejdService extends EventEmitter { this.wireEvents(); } + turnOn(id, brightness) { + logger('turning on ' + id + ' at brightness ' + brightness); + + var payload; + if (!brightness) { + payload = Buffer.from((id).toString(16).padStart(2, '0') + '0110009701', 'hex'); + } else { + brightness = brightness << 8 | brightness; + payload = Buffer.from((id).toString(16).padStart(2, '0') + '0110009801' + (brightness).toString(16).padStart(4, '0'), 'hex'); + } + + this.write(payload); + } + + turnOff(id) { + logger('turning off ' + id); + + var payload = Buffer.from((id).toString(16).padStart(2, '0') + '0110009700', 'hex'); + this.write(payload); + } + scan() { logger('scan()'); @@ -72,7 +95,7 @@ class PlejdService extends EventEmitter { this.state = STATE_SCANNING; noble.startScanning([PLEJD_SERVICE]); - + setTimeout(() => { noble.stopScanning(); this.state = STATE_IDLE; @@ -112,6 +135,14 @@ class PlejdService extends EventEmitter { return; } + this.deviceAddress = this._reverseBuffer( + Buffer.from( + String(this.device.address) + .replace(/\-/g, '') + .replace(/\:/g, ''), 'hex' + ) + ); + logger('connecting to ' + this.device.id + ' with addr ' + this.device.address + ' and rssi ' + this.device.rssi); setTimeout(() => { if (self.state !== STATE_CONNECTED && self.state !== STATE_AUTHENTICATED) { @@ -141,7 +172,12 @@ class PlejdService extends EventEmitter { return; } + clearInterval(this.pingRef); + + this.unsubscribeCharacteristics(); this.device.disconnect(); + + this.state = STATE_DISCONNECTED; } authenticate() { @@ -179,6 +215,22 @@ class PlejdService extends EventEmitter { }); } + write(data) { + if (this.state !== STATE_AUTHENTICATED) { + logger('error: not connected.'); + this.writeQueue.push(data); + return false; + } + + const encryptedData = this._encryptDecrypt(this.cryptoKey, this.deviceAddress, data); + this.characteristics.data.write(encryptedData, false); + + let writeData; + while ((writeData = this.writeQueue.shift()) !== undefined) { + this.characteristics.data.write(this._encryptDecrypt(this.cryptoKey, this.deviceAddress, writeData), false); + } + } + onAuthenticated() { // Start ping logger('onAuthenticated()'); @@ -194,6 +246,9 @@ class PlejdService extends EventEmitter { logger('ping'); this.ping(); } + else if (this.state === STATE_DISCONNECTED) { + console.log('warning: device disconnected, stop ping.'); + } else { console.log('error: ping failed, not connected.'); } @@ -210,7 +265,9 @@ class PlejdService extends EventEmitter { logger('stopping ping and reconnecting.'); clearInterval(this.pingRef); + this.unsubscribeCharacteristics(); this.state = STATE_DISCONNECTED; + this.connect(this.device.id); } @@ -322,6 +379,10 @@ class PlejdService extends EventEmitter { && self.characteristics.ping) { self.characteristicState = STATE_INITIALIZED; + + // subscribe to notifications + this.subscribeCharacteristics(); + self.emit('deviceCharacteristicsComplete', self.device); } }); @@ -367,6 +428,28 @@ class PlejdService extends EventEmitter { } } + onLastDataUpdated(data, isNotification) { + const decoded = this._encryptDecrypt(this.cryptoKey, this.deviceAddress, data); + + let state = 0; + let dim = 0; + let device = parseInt(decoded[0], 10); + + if (decoded.toString('hex', 3, 5) === '00c8' || decoded.toString('hex', 3, 5) === '0098') { + state = parseInt(decoded.toString('hex', 5, 6), 10); + dim = parseInt(decoded.toString('hex', 6, 8), 16) >> 8; + + logger('d: ' + device + ' got state+dim update: ' + state + ' - ' + dim); + this.emit('dimChanged', device, state, dim); + } + else if (decoded.toString('hex', 3, 5) === '0097') { + state = parseInt(decoded.toString('hex', 5, 6), 10); + + logger('d: ' + device + ' got state update: ' + state); + this.emit('stateChanged', device, state); + } + } + wireEvents() { logger('wireEvents()'); const self = this; @@ -383,6 +466,27 @@ class PlejdService extends EventEmitter { this.on('pingSuccess', this.onPingSuccess.bind(self)); } + subscribeCharacteristics() { + if (this.characteristics.lastData) { + this.characteristics.lastData.subscribe((err) => { + if (err) { + console.log('error: could not subscribe to event.'); + } + }); + this.characteristics.lastData.on('data', this.onLastDataUpdated.bind(this)); + } + } + + unsubscribeCharacteristics() { + if (this.characteristics.lastData) { + this.characteristics.lastData.unsubscribe((err) => { + if (err) { + console.log('error: could not unsubscribe from event.'); + } + }); + } + } + _createChallengeResponse(key, challenge) { const intermediate = crypto.createHash('sha256').update(xor(key, challenge)).digest(); const part1 = intermediate.subarray(0, 16); diff --git a/plejd/package-lock.json b/plejd/package-lock.json index 7c8ec8a..d1315fa 100644 --- a/plejd/package-lock.json +++ b/plejd/package-lock.json @@ -24,6 +24,11 @@ "node-addon-api": "^1.1.0" } }, + "@types/zen-observable": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.0.tgz", + "integrity": "sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg==" + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -134,6 +139,11 @@ "readable-stream": "> 1.0.0 < 3.0.0" } }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + }, "chownr": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.3.tgz", @@ -311,6 +321,11 @@ "ext": "^1.1.2" } }, + "esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==" + }, "event-emitter": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", @@ -550,6 +565,14 @@ "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", "integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=" }, + "is-observable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz", + "integrity": "sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA==", + "requires": { + "symbol-observable": "^1.1.0" + } + }, "is-relative": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", @@ -818,6 +841,11 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, + "observable-fns": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/observable-fns/-/observable-fns-0.4.0.tgz", + "integrity": "sha512-2BFtEqza7sjLpgImAmagHK97mBwh3+bkwAZS/qF/4n2S8RzKsbdsdOczRBh+Piz7QgQZRAjTzI5vtxtOUgU+cQ==" + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1090,6 +1118,11 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + }, "tar": { "version": "4.4.13", "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", @@ -1142,6 +1175,20 @@ } } }, + "threads": { + "version": "1.0.0-beta.9", + "resolved": "https://registry.npmjs.org/threads/-/threads-1.0.0-beta.9.tgz", + "integrity": "sha512-ZjpQvqA78p+y4jtlhnQsKc8V9AwUvrWwOhy9FkFKWO24JHKte3oWllmjvUw896YqrZymsJvqJwlbUHV1CpVtKw==", + "requires": { + "@types/zen-observable": "^0.8.0", + "callsites": "^3.1.0", + "debug": "^4.1.1", + "is-observable": "^1.1.0", + "observable-fns": "^0.4.0", + "tiny-worker": ">= 2", + "zen-observable": "^0.8.14" + } + }, "through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -1160,6 +1207,14 @@ "xtend": "~4.0.0" } }, + "tiny-worker": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tiny-worker/-/tiny-worker-2.3.0.tgz", + "integrity": "sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g==", + "requires": { + "esm": "^3.2.25" + } + }, "to-absolute-glob": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", @@ -1282,6 +1337,11 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "zen-observable": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", + "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==" } } } diff --git a/plejd/package.json b/plejd/package.json index fbf3ece..09b1f77 100644 --- a/plejd/package.json +++ b/plejd/package.json @@ -10,4 +10,4 @@ "mqtt": "^3.0.0", "sleep": "^6.1.0" } -} +} \ No newline at end of file