├── .eslintrc ├── .gitignore ├── README.md ├── bin └── automation-bt-detect-devices ├── detect.js ├── identify.js ├── index.js └── package.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | yarn.lock -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Automation - Bluetooth presence 3 | 4 | Example config.json: 5 | 6 | ```json 7 | { 8 | "accessories": [ 9 | { 10 | "accessory": "AutomationBluetoothPresence", 11 | "name": "Dad's iTag", 12 | "deviceId": "65504c9b6e5441f8927bbd768e455d4f", 13 | "gracePeriod": 600 14 | } 15 | ] 16 | } 17 | ``` 18 | 19 | This accessory will create a motion sensor linked with a Bluetooth device. 20 | 21 | When the device is found, the motion sensor is triggered. When the device is not seen for longer than `gracePeriod`, the motion sensor will stop detecting movement. 22 | 23 | The plugin will show the history history in the Elgato's [Eve](https://www.elgato.com/en/eve/eve-app) app. 24 | 25 | ## Installation 26 | Before installing this library, make sure you have met all the system dependencies. See the [Noble documentation](https://github.com/noble/noble#prerequisites). This plugin won't work unless all the system dependencies have been met. 27 | 28 | ## Configuration options 29 | 30 | | Attribute | Required | Usage | Example | 31 | |-----------|----------|-------|---------| 32 | | name | Yes | A unique name for the accessory. It will be used as the accessory name in HomeKit. | `Dad's iTag` | 33 | | deviceId | Yes | The device ID. | `65504c9b6e5441f8927bbd768e455d4f` | 34 | | gracePeriod | No (default: `600`) | The number of seconds to wait for announcements before considering the device gone. 10 minutes (600 seconds) is recommended. | `600` (seconds, equal to 10 mintues) | 35 | 36 | ## Find the device ID 37 | ### Method 1 - Clone the repo 38 | 1. Clone this repo 39 | 2. Run `npm install` or `yarn install` in the folder you've cloned the repo 40 | 3. Run `npm run detect-devices`. A list of devices will appear on screen. Grab the device ID from the list and add it to the config. 41 | 42 | ### Method 2 - Install the plugin globally 43 | 1. Run `npm install homebridge-automation-bluetooth-presence -g` 44 | 2. Run `automation-bt-detect-devices`. A list of devices will appear on screen. Grab the device ID from the list and add it to the config. 45 | 46 | ## Devices that can be monitored 47 | You can track: 48 | - Phones 49 | - Tables 50 | - (some) Computers 51 | - Smart watches 52 | - Headphones / Earphones 53 | - Fitness trackers 54 | - Cheap key fobs like the ["iTag" devices](https://www.gearbest.com/itag-_gear/) or [Tile trackers](https://www.thetileapp.com/) 55 | -------------------------------------------------------------------------------- /bin/automation-bt-detect-devices: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | process.title = 'automation-bluetooth-presence-detect-devices'; 6 | 7 | var path = require('path'); 8 | var fs = require('fs'); 9 | var lib = path.join(path.dirname(fs.realpathSync(__filename)), '..'); 10 | 11 | require(lib + '/detect'); 12 | -------------------------------------------------------------------------------- /detect.js: -------------------------------------------------------------------------------- 1 | const noble = require('noble'); 2 | 3 | const formatRSSI = (rssi) => { 4 | const signal = 2 * (rssi + 100); 5 | if (signal > 100) { 6 | return 100; 7 | } 8 | return signal; 9 | }; 10 | 11 | noble.on('stateChange', (state) => { 12 | if (state === 'poweredOn') { 13 | console.log('Scanning for devices... [Hit Ctrl+C to stop]'); 14 | noble.startScanning([], false); 15 | } else { 16 | noble.stopScanning(); 17 | } 18 | }); 19 | 20 | noble.on('discover', (peripheral) => { 21 | const id = process.platform === 'darwin' ? peripheral.id : peripheral.address; 22 | console.log(`[${new Date()}] ID: ${id}\tSignal: ${formatRSSI(peripheral.rssi)}%\tName: ${peripheral.advertisement.localName}`); 23 | }); 24 | -------------------------------------------------------------------------------- /identify.js: -------------------------------------------------------------------------------- 1 | const iTagIdentify = peripheral => 2 | (error, services, characteristics) => { 3 | if (!characteristics) { 4 | return; 5 | } 6 | 7 | const characteristic = characteristics[0]; 8 | characteristic.write(Buffer.from([0x02]), true, (er1) => { 9 | if (er1) { 10 | return; 11 | } 12 | 13 | setTimeout(() => { 14 | characteristic.write(Buffer.from([0x00]), true, (er2) => { 15 | if (er2) { 16 | return; 17 | } 18 | 19 | peripheral.disconnect(); 20 | }); 21 | }, 2000); 22 | }); 23 | }; 24 | 25 | 26 | module.exports = (device) => { 27 | const { peripheral } = device; 28 | 29 | peripheral.connect((error) => { 30 | if (!error) { 31 | // Support for iTag devices 32 | peripheral.discoverSomeServicesAndCharacteristics(['1802'], ['2a06'], iTagIdentify(peripheral)); 33 | } 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const noble = require('noble'); 2 | const debug = require('debug'); 3 | const fakegatoHistory = require('fakegato-history'); 4 | 5 | const identify = require('./identify'); 6 | const pkginfo = require('./package'); 7 | 8 | let Characteristic; 9 | let Service; 10 | let storagePath; 11 | 12 | let FakeGatoHistoryService; 13 | 14 | class AutomationBluetoothPresence { 15 | constructor(log, config) { 16 | this.log = log; 17 | this.name = config.name; 18 | this.debug = debug(`homebridge-automation-bluetooth-presence-${this.name}`); 19 | 20 | this.deviceId = config.deviceId; 21 | const configGracePeriod = config.gracePeriod || 10 * 60; // 10 minutes; 22 | this.gracePeriod = configGracePeriod * 1000; 23 | 24 | this.device = false; 25 | this.deviceFound = false; 26 | this.rescanTimeout = null; 27 | 28 | this.services = this.createServices(); 29 | 30 | this.discoverDevices(); 31 | } 32 | 33 | discoverDevices() { 34 | noble.on('stateChange', (state) => { 35 | if (state === 'poweredOn') { 36 | this.log('Looking for Bluetooth devices'); 37 | noble.startScanning([], false); 38 | } else { 39 | noble.stopScanning(); 40 | } 41 | }); 42 | 43 | noble.on('discover', this.deviceDiscovered.bind(this)); 44 | setInterval(this.deviceTimer.bind(this), 1000); 45 | } 46 | 47 | deviceDiscovered(peripheral) { 48 | const { id, address } = peripheral; 49 | if (id !== this.deviceId && address !== this.deviceId) { 50 | // ignore, not the right device 51 | this.debug(`Device ignored, wrong ID (looking for ${this.deviceId}) - ID: ${id} - Address: ${address} - Name: ${peripheral.advertisement.localName}`); 52 | return; 53 | } 54 | 55 | const entered = !this.device; 56 | if (entered) { 57 | this.device = { 58 | peripheral, 59 | }; 60 | 61 | this.log(`Device "${peripheral.advertisement.localName}" entered`); 62 | } 63 | 64 | this.setPresence(true); 65 | this.device.lastSeen = Date.now(); 66 | this.debug(`Device seen - Entered: ${entered} - Last seen: ${new Date()}`); 67 | 68 | this.startRescanTimer(); 69 | } 70 | 71 | startRescanTimer() { 72 | if (this.rescanTimeout) { 73 | clearTimeout(this.rescanTimeout); 74 | } 75 | 76 | this.rescanTimeout = setTimeout(() => { 77 | this.debug('Restart scanning'); 78 | noble.stopScanning(); 79 | noble.startScanning([], false); 80 | }, 60 * 1000); 81 | } 82 | 83 | deviceTimer() { 84 | if (!this.device) { 85 | return; 86 | } 87 | 88 | if (this.device.lastSeen < (Date.now() - this.gracePeriod)) { 89 | this.log('Device exited'); 90 | this.device = false; 91 | this.setPresence(false); 92 | } 93 | } 94 | 95 | setPresence(present) { 96 | if (this.deviceFound !== present) { 97 | this.loggingService.addEntry({ time: new Date().getTime(), status: present ? 1 : 0 }); 98 | } 99 | 100 | this.deviceFound = present; 101 | 102 | this.motionSensor 103 | .getCharacteristic(Characteristic.MotionDetected) 104 | .updateValue(present); 105 | } 106 | 107 | createServices() { 108 | this.motionSensor = new Service.MotionSensor(this.name); 109 | this.motionSensor 110 | .getCharacteristic(Characteristic.MotionDetected) 111 | .on('get', callback => callback(null, this.deviceFound)); 112 | this.motionSensor.log = this.log; 113 | 114 | const accessoryInformationService = new Service.AccessoryInformation(); 115 | accessoryInformationService 116 | .setCharacteristic(Characteristic.Name, this.name) 117 | .setCharacteristic(Characteristic.Manufacturer, pkginfo.author.name || pkginfo.author) 118 | .setCharacteristic(Characteristic.Model, pkginfo.name) 119 | .setCharacteristic(Characteristic.SerialNumber, this.deviceId) 120 | .setCharacteristic(Characteristic.FirmwareRevision, pkginfo.version) 121 | .setCharacteristic(Characteristic.HardwareRevision, pkginfo.version); 122 | 123 | this.loggingService = new FakeGatoHistoryService( 124 | 'motion', 125 | this.motionSensor, 126 | { 127 | storage: 'fs', 128 | path: `${storagePath}/accessories`, 129 | filename: `history_bp_${this.deviceId}.json`, 130 | }, 131 | ); 132 | 133 | return [ 134 | this.motionSensor, 135 | accessoryInformationService, 136 | this.loggingService, 137 | ]; 138 | } 139 | 140 | getServices() { 141 | return this.services; 142 | } 143 | 144 | identify(callback) { 145 | if (this.device) { 146 | this.log('Issuing identify request'); 147 | identify(this.device); 148 | } else { 149 | this.log('Device not found'); 150 | } 151 | 152 | callback(); 153 | } 154 | } 155 | 156 | module.exports = (homebridge) => { 157 | Service = homebridge.hap.Service; // eslint-disable-line 158 | Characteristic = homebridge.hap.Characteristic; // eslint-disable-line 159 | storagePath = homebridge.user.storagePath(); // eslint-disable-line 160 | 161 | FakeGatoHistoryService = fakegatoHistory(homebridge); 162 | homebridge.registerAccessory('homebridge-automation-bluetooth-presence', 'AutomationBluetoothPresence', AutomationBluetoothPresence); 163 | }; 164 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homebridge-automation-bluetooth-presence", 3 | "version": "0.2.1", 4 | "description": "Manage people presence based on Bluetooth devices (phones, watches, tables, bluetooth keychain tags)", 5 | "license": "ISC", 6 | "keywords": [ 7 | "homebridge-plugin" 8 | ], 9 | "author": { 10 | "name": "Paolo Tremadio" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/paolotremadio/homebridge-automation-bluetooth-presence.git" 15 | }, 16 | "bugs": { 17 | "url": "http://github.com/paolotremadio/homebridge-automation-bluetooth-presence/issues" 18 | }, 19 | "engines": { 20 | "node": ">=8.0.0", 21 | "homebridge": ">=0.4.0" 22 | }, 23 | "dependencies": { 24 | "debug": "^4.0.1", 25 | "fakegato-history": "^0.5.2", 26 | "hap-nodejs": "^0.4.28", 27 | "noble": "^1.9.1" 28 | }, 29 | "devDependencies": { 30 | "eslint": "^4.16.0", 31 | "eslint-config-airbnb-base": "^12.1.0", 32 | "eslint-plugin-import": "^2.8.0" 33 | }, 34 | "bin": { 35 | "automation-bt-detect-devices": "bin/automation-bt-detect-devices" 36 | }, 37 | "scripts": { 38 | "lint": "./node_modules/eslint/bin/eslint.js . --ext .js", 39 | "detect-devices": "node detect.js" 40 | } 41 | } 42 | --------------------------------------------------------------------------------