├── .eslintrc ├── .gitignore ├── src ├── logger │ └── message │ │ ├── snapshot.js │ │ └── event.js ├── state │ ├── is-triggered-reducer.js │ ├── persist.js │ ├── init.js │ └── merge-persisted-state.js ├── api │ └── index.js └── index.js ├── package.json ├── example.config.json └── README.md /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | yarn.lock -------------------------------------------------------------------------------- /src/logger/message/snapshot.js: -------------------------------------------------------------------------------- 1 | module.exports = 2 | (snapshot, extra) => ( 3 | { 4 | type: 'snapshot', 5 | snapshot, 6 | ...extra, 7 | } 8 | ); 9 | -------------------------------------------------------------------------------- /src/logger/message/event.js: -------------------------------------------------------------------------------- 1 | module.exports = 2 | (zone, trigger, value, extra) => ( 3 | { 4 | type: 'event', 5 | zone, 6 | trigger, 7 | value, 8 | ...extra, 9 | } 10 | ); 11 | -------------------------------------------------------------------------------- /src/state/is-triggered-reducer.js: -------------------------------------------------------------------------------- 1 | const { forEach } = require('lodash'); 2 | 3 | module.exports = (elements) => { 4 | let isTriggered = 0; 5 | 6 | forEach(elements, (el) => { 7 | if (el.triggered) { 8 | isTriggered = 1; 9 | } 10 | }); 11 | 12 | return isTriggered; 13 | }; 14 | -------------------------------------------------------------------------------- /src/state/persist.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const getPersistedState = (path) => { 4 | const fileContent = fs.readFileSync(path); 5 | return JSON.parse(fileContent); 6 | }; 7 | 8 | const persistState = (path, state) => { 9 | fs.writeFileSync( 10 | path, 11 | JSON.stringify(state), 12 | ); 13 | }; 14 | 15 | module.exports = { 16 | getPersistedState, 17 | persistState, 18 | }; 19 | -------------------------------------------------------------------------------- /src/state/init.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | const { keyBy } = require('lodash'); 3 | 4 | const decorate = (array, uuidGenerator, prefix = '') => 5 | array.map((el) => { 6 | el.id = `${prefix}${uuidGenerator(el.name)}`; 7 | el.triggered = 0; 8 | return el; 9 | }); 10 | 11 | const idAsKey = array => 12 | keyBy(array, el => el.id); 13 | 14 | module.exports = (zones, uuidGenerator) => { 15 | const zonesWithId = decorate(zones, uuidGenerator); 16 | 17 | return idAsKey(zonesWithId.map((zone) => { 18 | zone.triggers = idAsKey(decorate(zone.triggers, uuidGenerator, `${zone.id}_`)); 19 | return zone; 20 | })); 21 | }; 22 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bodyParser = require('body-parser'); 3 | const debug = require('debug')('homebridge-automation-presence/api'); 4 | 5 | module.exports = (host, port, hooks) => { 6 | const app = express(); 7 | 8 | app.disable('x-powered-by'); 9 | app.use(bodyParser.json()); 10 | app.use(bodyParser.urlencoded({ 11 | extended: true, 12 | })); 13 | 14 | app.get('/state', (req, res) => { 15 | debug('GET /state'); 16 | const state = hooks.getState(); 17 | res.json({ success: true, state }); 18 | }); 19 | 20 | app.post('/state', (req, res) => { 21 | const { zoneId, triggerId, triggered } = req.body; 22 | debug(`POST /state -- ${JSON.stringify({ zoneId, triggerId, triggered })}`); 23 | 24 | hooks.setState(zoneId, triggerId, triggered); 25 | res.json({ success: true }); 26 | }); 27 | 28 | app.listen(port, host, () => debug(`API running on ${host}:${port}`)); 29 | }; 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homebridge-automation-presence", 3 | "version": "0.3.0", 4 | "description": "Manage presence in your home, based on data collected from your sensors", 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-presence.git" 15 | }, 16 | "bugs": { 17 | "url": "http://github.com/paolotremadio/homebridge-automation-presence/issues" 18 | }, 19 | "main": "src/index.js", 20 | "engines": { 21 | "node": ">=8.0.0", 22 | "homebridge": ">=0.4.0" 23 | }, 24 | "dependencies": { 25 | "body-parser": "^1.19.0", 26 | "debug": "^4.1.1", 27 | "express": "^4.17.1", 28 | "fakegato-history": "^0.5.2", 29 | "homeautomation-winston-logger": "^0.0.2", 30 | "lodash": "^4.17.10", 31 | "moment": "^2.24.0" 32 | }, 33 | "devDependencies": { 34 | "eslint": "^4.16.0", 35 | "eslint-config-airbnb-base": "^12.1.0", 36 | "eslint-plugin-import": "^2.8.0" 37 | }, 38 | "scripts": { 39 | "lint": "./node_modules/eslint/bin/eslint.js . --ext .js" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/state/merge-persisted-state.js: -------------------------------------------------------------------------------- 1 | const { cloneDeep, forOwn } = require('lodash'); 2 | 3 | const isTriggeredReducer = require('./is-triggered-reducer'); 4 | 5 | module.exports = (persistedState, newState) => { 6 | const state = cloneDeep(newState); 7 | 8 | forOwn(state, (zoneDetails, zoneId) => { 9 | // If the zone was persisted before 10 | if (persistedState[zoneId] && persistedState[zoneId].triggers) { 11 | forOwn(zoneDetails.triggers, (triggerDetails, triggerId) => { 12 | // If the trigger was persisted before 13 | if (persistedState[zoneId].triggers[triggerId]) { 14 | state[zoneId].triggers[triggerId].resetAt = 15 | persistedState[zoneId].triggers[triggerId].resetAt; 16 | 17 | state[zoneId].triggers[triggerId].triggered = 18 | persistedState[zoneId].triggers[triggerId].triggered; 19 | 20 | state[zoneId].triggers[triggerId].lastUpdate = 21 | persistedState[zoneId].triggers[triggerId].lastUpdate; 22 | } 23 | }); 24 | } 25 | 26 | state[zoneId].triggered = isTriggeredReducer(state[zoneId].triggers); 27 | state[zoneId].lastUpdate = persistedState[zoneId] ? persistedState[zoneId].lastUpdate : null; 28 | }); 29 | 30 | return state; 31 | }; 32 | -------------------------------------------------------------------------------- /example.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "zones": [ 3 | { 4 | "name": "Living Room", 5 | "triggers": [ 6 | { 7 | "name": "Couch vibration 1" 8 | }, 9 | { 10 | "name": "Couch vibration 2" 11 | }, 12 | { 13 | "name": "Bay window vibration 1" 14 | }, 15 | { 16 | "name": "Motion sensor 1" 17 | }, 18 | { 19 | "name": "Motion sensor 2" 20 | }, 21 | { 22 | "name": "Bay window switch 1" 23 | }, 24 | { 25 | "name": "Kitchen switch 1" 26 | }, 27 | { 28 | "name": "Living room dimmer 1" 29 | }, 30 | { 31 | "name": "Audio streaming" 32 | }, 33 | { 34 | "name": "TV playing" 35 | } 36 | ] 37 | }, 38 | { 39 | "name": "Bedroom", 40 | "triggers": [ 41 | { 42 | "name": "Motion sensor 1" 43 | }, 44 | { 45 | "name": "Motion sensor 2" 46 | }, 47 | { 48 | "name": "Bed vibration 1" 49 | }, 50 | { 51 | "name": "Bed vibration 2" 52 | }, 53 | { 54 | "name": "Bedroom dimmer 1" 55 | }, 56 | { 57 | "name": "Audio streaming" 58 | } 59 | ] 60 | }, 61 | { 62 | "name": "Bathroom", 63 | "triggers": [ 64 | { 65 | "name": "Motion sensor 1" 66 | }, 67 | { 68 | "name": "Audio streaming" 69 | } 70 | ] 71 | }, 72 | { 73 | "name": "Living Room Couch", 74 | "triggers": [ 75 | { 76 | "name": "Couch vibration 1" 77 | }, 78 | { 79 | "name": "Couch vibration 2" 80 | } 81 | ] 82 | }, 83 | { 84 | "name": "Bedroom Bed", 85 | "triggers": [ 86 | { 87 | "name": "Bed vibration 1" 88 | }, 89 | { 90 | "name": "Bed vibration 2" 91 | } 92 | ] 93 | }, 94 | { 95 | "name": "People - Cleaner", 96 | "triggers": [ 97 | { 98 | "name": "Key's tag" 99 | } 100 | ] 101 | }, 102 | { 103 | "name": "People - Guest 1", 104 | "enabled": 0, 105 | "triggers": [ 106 | { 107 | "name": "Key's tag" 108 | } 109 | ] 110 | }, 111 | { 112 | "name": "People - Guest 2", 113 | "enabled": 0, 114 | "triggers": [ 115 | { 116 | "name": "Key's tag" 117 | } 118 | ] 119 | } 120 | ] 121 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Automation - Presence 3 | 4 | ## Why this plugin 5 | 6 | HomeKit does a brilliant job detecting when someone is home but it only works with iOS devices. 7 | If you have a guest, you must invite him/her to the Home app (assuming he/she has an iPhone). 8 | HomeKit can tell you if someone is home but can't tell you which room is occupied. 9 | 10 | This plugin: 11 | - Support for people presence using devices that are not iPhones. 12 | - Support for "Zones" so you can determine the presence in areas of your house. 13 | - Easier automation by track presence by zone without having to check the value of multiple sensors. 14 | - Support for a "Master presence" sensor to know if there's presence if any of your zones. The true "someone is home". 15 | - State is persisted on disk, so it will survive reboot/restart of homebridge. 16 | - Every change in state is logged on file for future analysis. 17 | 18 | ## What is a zone? 19 | 20 | This plugin lets you create one or more "zones". A zone can be a room (e.g. `Living Room`), a group of rooms (e.g. `Upstairs`), 21 | a group of devices (e.g. `Guests`) or a group of sensors (e.g. `Someone is in bed`). 22 | 23 | You give a "zone" the semantic you like. 24 | 25 | Some example of zones: 26 | - **By room**: Living Room, Bedroom, Bathroom 27 | - **By area**: Upstairs, Downstairs, Outside 28 | - **By collection of sensors**: Bedroom bed, Couch (to link multiple sensors together and stop lights to turn on if someone is in bed, for example) 29 | - **By personal devices**, people presence: myself, partner, kid, cleaner, guests 30 | 31 | ## What are triggers? 32 | For every zone you can define a list of triggers. When one or more triggers are On, the zone is On. 33 | 34 | For example, let's assume you have a zone Living room. 35 | 36 | Here's the triggers you could have to detect presence: 37 | - Motion sensors 38 | - Audio streaming (turn On the switch when you stream audio and turn the switch Off when you stop streaming; works well with [homebridge-automation-chromecast](https://github.com/paolotremadio/homebridge-automation-chromecast)) 39 | - Lights are on (unless they are triggered by motion sensors) 40 | - Media player is playing (e.g. Plex media player, see [homebridge-plex](https://github.com/mpbzh/homebridge-plex)) 41 | - Vibration sensors for your furniture (e.g. the Xiaomi/Aqara Vibration sensor, see [homebridge-hue with deconz](https://github.com/dresden-elektronik/deconz-rest-plugin/issues/748)) 42 | 43 | 44 | ## About using non-iPhones to track people presence 45 | You can use Bluetooth to track who's home. 46 | 47 | For example, you could track personal devices like phones, smart watches, headphones / earphones or fitness trackers. 48 | 49 | You could also buy some cheap ["iTag" devices](https://www.gearbest.com/itag-_gear/) or [Tile trackers](https://www.thetileapp.com/) to attach to the dog, your keys, the keys you give to your guests, etc. 50 | 51 | You can detect if a Bluetooth device is at home by using my [homebridge-automation-bluetooth-presence](https://github.com/paolotremadio/homebridge-automation-bluetooth-presence) plugin. 52 | 53 | 54 | 55 | ## Config 56 | 57 | Example config.json: 58 | 59 | ```json 60 | { 61 | "accessory": "AutomationPresence", 62 | "name": "Home Presence", 63 | "masterPresenceOffDelay": 600, 64 | "zones": [ 65 | { 66 | "name": "Zone 1 name", 67 | "triggers": [ 68 | { 69 | "name": "Trigger 1 name" 70 | }, 71 | { 72 | "name": "Trigger 2 name" 73 | }, 74 | { 75 | "name": "Trigger 3 name" 76 | }, 77 | { 78 | "name": "..." 79 | } 80 | ] 81 | }, 82 | { 83 | "name": "Zone 2 name", 84 | "triggers": [ 85 | { 86 | "name": "Trigger 1 name" 87 | }, 88 | { 89 | "name": "..." 90 | } 91 | ] 92 | }, 93 | { 94 | "name": "...", 95 | "triggers": [ 96 | { 97 | "name": "..." 98 | } 99 | ] 100 | } 101 | ] 102 | } 103 | ``` 104 | 105 | This accessory will create a switch for every Trigger and a motion sensor for every Zone, including a `Master` motion sensor. 106 | 107 | Turning On one or more switches will turn on the Zone they belong to. If one of more Zone is on, the Master switch is On. 108 | 109 | Turning on all the switches will turn off the master zone after the `masterPresenceOffDelay` (in seconds). 110 | 111 | ## Configuration options 112 | 113 | | Attribute | Required | Usage | Example | 114 | |-----------|----------|-------|---------| 115 | | name | Yes | A unique name for the accessory. It will be used as the accessory name in HomeKit. | `Home Presence` | 116 | | masterPresenceOffDelay | No | Number of seconds before turning Off the `Master` sensor, after no presence is detected | `600` (600 seconds, 10 minutes) | 117 | | zones | Yes | A list of one or more Zones and their Triggers | n/a | 118 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const { forEach, values, cloneDeep } = require('lodash'); 2 | const fakegatoHistory = require('fakegato-history'); 3 | const logger = require('homeautomation-winston-logger'); 4 | const moment = require('moment'); 5 | const debug = require('debug')('homebridge-automation-presence'); 6 | 7 | const logEvent = require('./logger/message/event'); 8 | const logSnapshot = require('./logger/message/snapshot'); 9 | const initState = require('./state/init'); 10 | const statePersist = require('./state/persist'); 11 | const mergePersisted = require('./state/merge-persisted-state'); 12 | const isTriggeredReducer = require('./state/is-triggered-reducer'); 13 | const Api = require('./api'); 14 | 15 | const pkginfo = require('../package'); 16 | 17 | let Characteristic; 18 | let Service; 19 | let UUIDGen; 20 | let FakeGatoHistoryService; 21 | let storagePath; 22 | 23 | class AutomationPresence { 24 | constructor(log, config) { 25 | this.homebridgeLog = log; 26 | this.name = config.name; 27 | 28 | this.accessoryUUID = UUIDGen.generate(this.name); 29 | this.stateStorageFile = `${storagePath}/accessories/presence_state_${this.accessoryUUID}.json`; 30 | 31 | const persistedState = this.getPersistedState(); 32 | const initialState = initState(config.zones, UUIDGen.generate); 33 | this.zones = mergePersisted(persistedState, initialState); 34 | this.persistState(); 35 | 36 | 37 | const isMasterTriggered = isTriggeredReducer(this.zones); 38 | const masterResetAfter = { 39 | seconds: config.masterPresenceOffDelay || 5, 40 | }; 41 | 42 | this.masterZone = { 43 | id: 'master', 44 | name: 'Master', 45 | triggered: isMasterTriggered, 46 | resetAfter: masterResetAfter, 47 | lastUpdate: moment().format(), 48 | }; 49 | 50 | 51 | this.logger = logger(`${storagePath}/presence.log`, config.debug); 52 | this.logger.debug('Service started'); 53 | 54 | if (config.api) { 55 | Api( 56 | config.api.host, 57 | config.api.port, 58 | { 59 | getState: () => ({ master: this.masterZone, zones: this.zones }), 60 | setState: (zoneId, triggerId, triggered) => { 61 | debug(`API setState() - Zone ID: ${zoneId} - Trigger ID: ${triggerId} - Triggered: ${triggered}`); 62 | return this.handleTriggerEvent(zoneId, triggerId, triggered ? 1 : 0, true); 63 | }, 64 | }, 65 | ); 66 | } 67 | 68 | this.services = this.createServices(); 69 | this.startStateSnapshot(); 70 | this.resetExpiredTriggers(); 71 | } 72 | 73 | getPersistedState() { 74 | try { 75 | return statePersist.getPersistedState(this.stateStorageFile); 76 | } catch (e) { 77 | this.homebridgeLog('No previous state persisted on file'); 78 | return {}; 79 | } 80 | } 81 | 82 | persistState() { 83 | try { 84 | statePersist.persistState(this.stateStorageFile, this.zones); 85 | } catch (e) { 86 | this.homebridgeLog(`Cannot persist state: ${e.message}`); 87 | } 88 | } 89 | 90 | startStateSnapshot() { 91 | const generateSnapshot = () => { 92 | this.logger.info(logSnapshot({ 93 | ...cloneDeep(this.zones), 94 | master: cloneDeep(this.masterZone), 95 | })); 96 | }; 97 | generateSnapshot(); 98 | setInterval(generateSnapshot, 10 * 60 * 1000); 99 | } 100 | 101 | getAccessoryInformationService() { 102 | this.logger.debug({ appVersion: pkginfo.version }); 103 | 104 | const accessoryInformationService = new Service.AccessoryInformation(); 105 | 106 | accessoryInformationService 107 | .setCharacteristic(Characteristic.Name, this.name) 108 | .setCharacteristic(Characteristic.Manufacturer, pkginfo.author.name || pkginfo.author) 109 | .setCharacteristic(Characteristic.Model, pkginfo.name) 110 | .setCharacteristic(Characteristic.SerialNumber, 'n/a') 111 | .setCharacteristic(Characteristic.FirmwareRevision, pkginfo.version) 112 | .setCharacteristic(Characteristic.HardwareRevision, pkginfo.version); 113 | 114 | return accessoryInformationService; 115 | } 116 | 117 | getZoneServices() { 118 | this.zoneServices = {}; 119 | this.zoneTriggers = {}; 120 | 121 | forEach(this.zones, (zone) => { 122 | const { id: zoneId, name: zoneName } = zone; 123 | 124 | // Main sensor 125 | const sensor = new Service.MotionSensor( 126 | zoneName, 127 | zoneId, 128 | ); 129 | 130 | sensor 131 | .getCharacteristic(Characteristic.MotionDetected) 132 | .on('get', callback => callback(null, zone.triggered)); 133 | 134 | sensor 135 | .getCharacteristic(Characteristic.StatusActive) 136 | .on('get', callback => callback(null, true)); 137 | 138 | // Add triggers 139 | forEach(zone.triggers, (trigger) => { 140 | const { id: triggerId, name: triggerName } = trigger; 141 | 142 | const triggerSwitch = new Service.Switch(`${zoneName} - ${triggerName}`, triggerId); 143 | 144 | triggerSwitch 145 | .getCharacteristic(Characteristic.On) 146 | .on('get', callback => callback(null, trigger.triggered)) 147 | .on('set', (on, callback) => { 148 | debug(`Homekit switch set - Zone ID: ${zoneId} - Trigger ID: ${triggerId} - Triggered: ${on}`); 149 | this.handleTriggerEvent(zoneId, triggerId, on ? 1 : 0); 150 | callback(); 151 | }); 152 | 153 | this.zoneTriggers[triggerId] = triggerSwitch; 154 | }); 155 | 156 | // Add to the list 157 | this.zoneServices[zoneId] = sensor; 158 | }); 159 | 160 | return [ 161 | ...values(this.zoneServices), 162 | ...values(this.zoneTriggers), 163 | ]; 164 | } 165 | 166 | getMasterPresenceSensor() { 167 | this.masterPresenceSensor = new Service.MotionSensor( 168 | `${this.name} (master)`, 169 | 'master', 170 | ); 171 | 172 | const masterTriggered = this.masterZone.triggered; 173 | 174 | this.masterPresenceSensor 175 | .on('get', callback => callback(null, masterTriggered)); 176 | 177 | this.masterPresenceSensor 178 | .getCharacteristic(Characteristic.MotionDetected) 179 | .updateValue(masterTriggered); 180 | 181 | // Add history 182 | this.masterPresenceSensor.log = this.homebridgeLog; 183 | 184 | this.masterPresenceSensorHistory = new FakeGatoHistoryService( 185 | 'motion', 186 | this.masterPresenceSensor, 187 | { 188 | storage: 'fs', 189 | path: `${storagePath}/accessories`, 190 | filename: `history_presence_master_${this.accessoryUUID}.json`, 191 | }, 192 | ); 193 | 194 | return [this.masterPresenceSensor, this.masterPresenceSensorHistory]; 195 | } 196 | 197 | createServices() { 198 | return [ 199 | this.getAccessoryInformationService(), 200 | ...this.getZoneServices(), 201 | ...this.getMasterPresenceSensor(), 202 | ]; 203 | } 204 | 205 | updateTrigger(zoneId, triggerId, value, notifyHomekit) { 206 | const zone = zoneId && this.zones[zoneId]; 207 | const trigger = zone && triggerId && this.zones[zoneId].triggers[triggerId]; 208 | 209 | debug(`updateTrigger() - Zone ID: ${zoneId} - Trigger ID: ${triggerId} - Value: ${value}`); 210 | 211 | trigger.triggered = value; 212 | trigger.lastUpdate = moment().format(); 213 | 214 | if (value && trigger.resetAfter) { 215 | trigger.resetAt = moment().add(trigger.resetAfter).format(); 216 | debug(`updateTrigger() - Zone ID: ${zoneId} - Trigger ID: ${triggerId} - Reset after: ${JSON.stringify(trigger.resetAfter)} - Will reset at: ${trigger.resetAt}`); 217 | } else { 218 | trigger.resetAt = undefined; 219 | } 220 | 221 | const eventExtras = { 222 | zoneName: zone.name, 223 | triggerName: trigger.name, 224 | }; 225 | 226 | this.logger.info(logEvent(zoneId, triggerId, value, eventExtras)); 227 | this.persistState(); 228 | 229 | if (notifyHomekit) { 230 | this.zoneTriggers[triggerId] 231 | .getCharacteristic(Characteristic.On) 232 | .updateValue(value); 233 | } 234 | } 235 | 236 | updateZone(zoneId) { 237 | const value = isTriggeredReducer(this.zones[zoneId].triggers); 238 | const zone = zoneId && this.zones[zoneId]; 239 | 240 | debug(`updateZone() - Zone ID: ${zoneId} - Reduced value: ${value}`); 241 | 242 | zone.triggered = value; 243 | zone.lastUpdate = moment().format(); 244 | 245 | this.zoneServices[zoneId] 246 | .getCharacteristic(Characteristic.MotionDetected) 247 | .updateValue(value); 248 | 249 | this.logger.info(logEvent(zoneId, null, value, { zoneName: zone.name })); 250 | this.persistState(); 251 | } 252 | 253 | updateMasterSensor(status, fromTimer) { 254 | if (fromTimer) { 255 | debug(`updateMasterSensor() - Timer ran out - Old status: ${this.masterZone.triggered} - New status: ${status}`); 256 | } else { 257 | debug(`updateMasterSensor() - Instant update - Old status: ${this.masterZone.triggered} - New status: ${status}`); 258 | } 259 | 260 | this.masterZone.triggered = status; 261 | 262 | this.masterPresenceSensor 263 | .getCharacteristic(Characteristic.MotionDetected) 264 | .updateValue(status); 265 | 266 | this.masterPresenceSensorHistory 267 | .addEntry({ time: new Date().getTime(), status }); 268 | 269 | this.logger.info(logEvent(null, null, status, { master: true })); 270 | } 271 | 272 | updateMaster() { 273 | const isMasterTriggered = isTriggeredReducer(this.zones); 274 | debug(`updateMaster() - Old status: ${this.masterZone.triggered} - New status: ${isMasterTriggered}`); 275 | 276 | if (isMasterTriggered) { 277 | debug('updateMaster() - Is Triggered - Unset resetAt'); 278 | this.masterZone.resetAt = undefined; 279 | } 280 | 281 | this.masterZone.lastUpdate = moment().format(); 282 | 283 | // Update only if something as change (to avoid polluting the logs) 284 | if (isMasterTriggered !== this.masterZone.triggered) { 285 | // Evaluate what to do 286 | if (isMasterTriggered) { 287 | // Update immediately 288 | debug('updateMaster() - Status differ - Update immediately'); 289 | this.updateMasterSensor(isMasterTriggered, false); 290 | } else { 291 | // Schedule to reset 292 | debug('updateMaster() - Status differ - Schedule for reset'); 293 | this.masterZone.resetAt = moment().add(this.masterZone.resetAfter).format(); 294 | } 295 | } 296 | 297 | this.persistState(); 298 | } 299 | 300 | handleTriggerEvent(zoneId, triggerId, value, notifyHomekit = false) { 301 | // Update trigger 302 | this.updateTrigger(zoneId, triggerId, value, notifyHomekit); 303 | 304 | // Update zone 305 | this.updateZone(zoneId); 306 | 307 | // Update master service 308 | this.updateMaster(); 309 | } 310 | 311 | resetExpiredTriggers() { 312 | forEach(this.zones, ({ id: zoneId, name: zoneName, triggers }) => { 313 | forEach(triggers, ({ id: triggerId, name: triggerName, resetAt }) => { 314 | if (resetAt && moment().isAfter(resetAt)) { 315 | this.homebridgeLog(`Zone "${zoneName}" - Trigger "${triggerName}" - Expired. Resetting...`); 316 | debug(`resetExpiredTriggers() - Zone ID: ${zoneId} - Trigger ID: ${triggerId} - Expired; resetting...`); 317 | this.handleTriggerEvent(zoneId, triggerId, 0, true); 318 | } 319 | }); 320 | }); 321 | 322 | if (this.masterZone.resetAt && moment().isAfter(this.masterZone.resetAt)) { 323 | debug('resetExpiredTriggers() - Master - Expired; resetting...'); 324 | this.masterZone.resetAt = undefined; 325 | this.updateMasterSensor(0, true); 326 | } 327 | 328 | setTimeout(() => this.resetExpiredTriggers(), 1000); 329 | } 330 | 331 | getServices() { 332 | return this.services; 333 | } 334 | } 335 | 336 | module.exports = (homebridge) => { 337 | Service = homebridge.hap.Service; // eslint-disable-line 338 | Characteristic = homebridge.hap.Characteristic; // eslint-disable-line 339 | UUIDGen = homebridge.hap.uuid; // eslint-disable-line 340 | storagePath = homebridge.user.storagePath(); // eslint-disable-line 341 | 342 | FakeGatoHistoryService = fakegatoHistory(homebridge); 343 | homebridge.registerAccessory('homebridge-automation-presence', 'AutomationPresence', AutomationPresence); 344 | }; 345 | --------------------------------------------------------------------------------