├── .gitignore ├── .npmignore ├── src ├── interfaces │ ├── Config.ts │ ├── API.ts │ └── HAP.ts ├── Logger.ts ├── util │ └── Mutex.ts ├── HAP.ts ├── Server.ts ├── LockPlatform.ts ├── Client.ts └── LockAccessory.ts ├── Makefile ├── index.ts ├── tsconfig.json ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .homebridge 3 | node_modules 4 | dist -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | index.ts 2 | src/**/*.ts 3 | tsconfig.json 4 | .homebridge 5 | .vscode -------------------------------------------------------------------------------- /src/interfaces/Config.ts: -------------------------------------------------------------------------------- 1 | export interface Config { 2 | token: string; 3 | debug: boolean; 4 | port: number; 5 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | HOMEBRIDGE=/usr/local/bin/homebridge 2 | PWD=$(shell pwd) 3 | 4 | default: build run 5 | 6 | debug: build inspect 7 | 8 | build: 9 | tsc 10 | 11 | run: 12 | $(HOMEBRIDGE) -D -U $(PWD)/.homebridge -P $(PWD) 13 | 14 | inspect: 15 | node --inspect-brk $(HOMEBRIDGE) -D -U $(PWD)/.homebridge 16 | 17 | clean: 18 | rm -r $(PWD)/.homebridge/accessories 19 | rm -r $(PWD)/.homebridge/persist -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import { LockPlatform } from './src/LockPlatform'; 2 | import { HAP } from './src/HAP'; 3 | 4 | export = (homebridge) => { 5 | HAP.Accessory = homebridge.platformAccessory; 6 | HAP.Service = homebridge.hap.Service; 7 | HAP.Characteristic = homebridge.hap.Characteristic; 8 | HAP.UUID = homebridge.hap.uuid; 9 | 10 | homebridge.registerPlatform('homebridge-sesame', 'Sesame', LockPlatform, true); 11 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "target": "es6", 5 | "module": "commonjs", 6 | "outDir": "dist", 7 | "sourceMap": true, 8 | "noUnusedLocals": true, 9 | "noUnusedParameters": true 10 | }, 11 | "files": [ 12 | "./index.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.ts" 16 | ], 17 | "exclude": [ 18 | "node_modules" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/interfaces/API.ts: -------------------------------------------------------------------------------- 1 | export interface Lock { 2 | device_id: string; 3 | serial: string; 4 | nickname: string; 5 | } 6 | 7 | export interface LockStatus { 8 | locked: boolean; 9 | responsive: boolean; 10 | battery: number; 11 | } 12 | 13 | export interface Control { 14 | task_id: string; 15 | } 16 | 17 | export interface TaskResult { 18 | task_id: string; 19 | status: string; 20 | successful: boolean; 21 | error: string; 22 | } -------------------------------------------------------------------------------- /src/Logger.ts: -------------------------------------------------------------------------------- 1 | import { Log } from './interfaces/HAP'; 2 | 3 | class SesameLogger { 4 | public log: Log; 5 | private debugMode: boolean; 6 | 7 | setLogger(log: Log, debugMode: boolean): void { 8 | this.log = log; 9 | this.debugMode = debugMode; 10 | } 11 | 12 | debug(message: string, data?: any): void { 13 | if (!this.debugMode) return; 14 | 15 | let result = message; 16 | if (data) { 17 | result += `: ${JSON.stringify(data)}` 18 | } 19 | 20 | this.log(result); 21 | } 22 | 23 | error(message: string, error?: Error) { 24 | let result = message; 25 | 26 | if (error) { 27 | result += `. Error: ${error.message}`; 28 | } 29 | 30 | this.log.error(result); 31 | } 32 | } 33 | 34 | export const Logger = new SesameLogger(); -------------------------------------------------------------------------------- /src/util/Mutex.ts: -------------------------------------------------------------------------------- 1 | import { Deferred } from 'ts-deferred'; 2 | 3 | export class Mutex { 4 | private deferred: Deferred; 5 | private locked: boolean; 6 | 7 | constructor() { 8 | this.deferred = new Deferred(); 9 | this.locked = false; 10 | } 11 | 12 | async wait(task: () => Promise): Promise { 13 | if (this.locked) { 14 | let result = await this.deferred; 15 | return result.promise; 16 | } 17 | 18 | this.locked = true; 19 | 20 | try { 21 | let result = await task(); 22 | this.deferred.resolve(result); 23 | 24 | return result; 25 | } catch(e) { 26 | this.deferred.reject(e); 27 | throw new Error(e); 28 | } finally { 29 | this.releaseLock(); 30 | } 31 | } 32 | 33 | private releaseLock(): void { 34 | setTimeout(() => { 35 | this.deferred = new Deferred(); 36 | this.locked = false; 37 | }, 1000); 38 | } 39 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homebridge-sesame", 3 | "version": "2.1.0", 4 | "description": "Homebridge plugin for the Sesame smart lock", 5 | "main": "./dist/index.js", 6 | "author": "Andrew Schaper", 7 | "license": "MIT", 8 | "dependencies": { 9 | "express": "^4.17.1", 10 | "request": "^2.88.0", 11 | "request-promise": "^4.2.4", 12 | "store": "^2.0.12", 13 | "ts-deferred": "^1.0.4" 14 | }, 15 | "devDependencies": { 16 | "@types/express": "^4.17.0", 17 | "@types/request": "^2.48.2", 18 | "@types/request-promise": "^4.1.44", 19 | "@types/storejs": "^2.0.3" 20 | }, 21 | "engines": { 22 | "node": ">=0.12.0", 23 | "homebridge": ">=0.2.0" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/aschaper/homebridge-sesame" 28 | }, 29 | "scripts": { 30 | "prepublishOnly": "tsc -p . --outDir dist" 31 | }, 32 | "keywords": [ 33 | "sesame", 34 | "homebridge-sesame", 35 | "homebridge-plugin" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /src/HAP.ts: -------------------------------------------------------------------------------- 1 | import { Accessory, Characteristic, Service, UUID } from './interfaces/HAP'; 2 | 3 | export class HAP { 4 | private static _accessory: Accessory; 5 | private static _service: Service; 6 | private static _characteristic: Characteristic; 7 | private static _uuid: UUID; 8 | 9 | public static get Accessory() { 10 | return this._accessory; 11 | } 12 | 13 | public static set Accessory(accessory) { 14 | this._accessory = accessory; 15 | } 16 | 17 | public static get Service() { 18 | return this._service; 19 | } 20 | 21 | public static set Service(hap) { 22 | this._service = hap; 23 | } 24 | 25 | public static get Characteristic() { 26 | return this._characteristic; 27 | } 28 | 29 | public static set Characteristic(characteristic) { 30 | this._characteristic = characteristic; 31 | } 32 | 33 | public static get UUID() { 34 | return this._uuid; 35 | } 36 | 37 | public static set UUID(uuid) { 38 | this._uuid = uuid; 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /src/Server.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { LockAccessory } from './LockAccessory'; 3 | import { Logger } from './Logger'; 4 | 5 | export class Server { 6 | locks: Map; 7 | port: number; 8 | api: express.Express; 9 | 10 | DEFAULT_PORT = 33892; 11 | 12 | constructor(port: number) { 13 | this.port = port || this.DEFAULT_PORT; 14 | 15 | this.locks = new Map(); 16 | 17 | this.api = express(); 18 | this.api.use(express.json()); 19 | 20 | this.api.post('/', (req, res) => { 21 | try { 22 | this.handleRequest(req); 23 | } catch(e) { 24 | Logger.error(e); 25 | } 26 | 27 | res.end(); 28 | }); 29 | } 30 | 31 | listen(): void { 32 | this.api.listen(this.port, () => Logger.log(`Listening for webhooks on port ${this.port}`)); 33 | } 34 | 35 | handleRequest(request: express.Request): void { 36 | let id = request.body.device_id; 37 | let locked = request.body.locked; 38 | 39 | if (id == null || locked == null) { 40 | Logger.log(`Unexpected webhook request body: ${JSON.stringify(request.body)}`); 41 | return; 42 | } 43 | 44 | let lockAccessory = this.locks.get(id); 45 | if (!lockAccessory) { 46 | Logger.log(`No lock accessory found from webhook request. Device ID: ${id}`); 47 | return; 48 | } 49 | 50 | lockAccessory.updateCurrentLockState(locked); 51 | 52 | Logger.log(`Set ${lockAccessory.lock.nickname} to ${locked ? 'locked' : 'unlocked'} from webhook`); 53 | } 54 | } -------------------------------------------------------------------------------- /src/interfaces/HAP.ts: -------------------------------------------------------------------------------- 1 | export interface Accessory { 2 | new (name: string, uuid: string): Accessory; 3 | 4 | UUID: string; 5 | reachability: boolean; 6 | services: Service[]; 7 | context: any; 8 | 9 | on(...args: any[]): void; 10 | getService(...args: any[]): Service; 11 | addService(...args: any[]): Service; 12 | removeService(...args: any[]): void; 13 | getServiceByUUIDAndSubType(...args: any[]): Service; 14 | updateReachability(reachable: boolean): void; 15 | } 16 | 17 | export interface Service { 18 | new (displayName: string): Service; 19 | 20 | UUID: string; 21 | AccessoryInformation: Service; 22 | 23 | LockMechanism: Service; 24 | BatteryService: Service; 25 | 26 | addCharacteristic(characteristic: Characteristic): Characteristic; 27 | setCharacteristic(...args: any[]): Service; 28 | getCharacteristic(...args: any[]): Characteristic; 29 | } 30 | 31 | export interface Characteristic { 32 | Manufacturer: Characteristic; 33 | Model: Characteristic; 34 | SerialNumber: Characteristic; 35 | 36 | LockCurrentState: LockCurrentState; 37 | LockTargetState: Characteristic; 38 | BatteryLevel: Characteristic; 39 | ChargingState: ChargingState; 40 | StatusLowBattery: StatusLowBattery; 41 | 42 | on(...args: any[]): Characteristic; 43 | updateValue(...args: any[]): Characteristic; 44 | } 45 | 46 | export interface LockCurrentState { 47 | new(): Characteristic; 48 | SECURED: number; 49 | UNSECURED: number; 50 | } 51 | 52 | export interface ChargingState { 53 | NOT_CHARGING: number; 54 | } 55 | 56 | export interface StatusLowBattery { 57 | BATTERY_LEVEL_LOW: number; 58 | BATTERY_LEVEL_NORMAL: number; 59 | } 60 | 61 | export interface Log { 62 | (...args: any[]): void; 63 | error(...args: any[]): void; 64 | } 65 | 66 | export interface Platform { 67 | on(...args: any[]): void 68 | registerPlatformAccessories(pluginName: string, platformname: string, accessories: Array): void; 69 | unregisterPlatformAccessories(pluginName: string, platformname: string, accessories: Array): void; 70 | } 71 | 72 | export interface UUID { 73 | generate(string): string; 74 | } -------------------------------------------------------------------------------- /src/LockPlatform.ts: -------------------------------------------------------------------------------- 1 | import * as store from 'store'; 2 | import { Client } from './Client'; 3 | import { HAP } from './HAP'; 4 | import { Lock } from './interfaces/API'; 5 | import { Accessory, Log, Platform } from './interfaces/HAP'; 6 | import { Config } from './interfaces/Config'; 7 | import { LockAccessory } from "./LockAccessory"; 8 | import { Logger } from './Logger'; 9 | import { Server } from './Server'; 10 | 11 | export class LockPlatform { 12 | log: Log; 13 | platform: Platform; 14 | accessories: Map; 15 | server: Server; 16 | 17 | constructor(log: Log, config: Config, platform: Platform) { 18 | Logger.setLogger(log, config.debug); 19 | 20 | this.platform = platform; 21 | this.accessories = new Map(); 22 | this.server = new Server(config.port); 23 | 24 | this.platform.on('didFinishLaunching', () => { 25 | if (!config.token) { 26 | throw new Error('A token was not found in the homebridge config. For more information, see: https://github.com/aschzero/homebridge-sesame#configuration'); 27 | } 28 | 29 | store.set('token', config.token); 30 | 31 | this.retrieveLocks(); 32 | 33 | this.server.listen(); 34 | }); 35 | } 36 | 37 | async retrieveLocks(): Promise { 38 | let client = new Client(); 39 | let locks: Lock[]; 40 | 41 | try { 42 | locks = await client.listLocks(); 43 | } catch(e) { 44 | Logger.error('Unable to retrieve locks', e); 45 | } 46 | 47 | locks.forEach(lock => this.addAccessory(lock)); 48 | } 49 | 50 | configureAccessory(accessory: Accessory): void { 51 | this.accessories.set(accessory.UUID, accessory); 52 | } 53 | 54 | addAccessory(lock: Lock): Accessory { 55 | let uuid = HAP.UUID.generate(lock.device_id); 56 | let accessory = this.accessories.get(uuid); 57 | 58 | if (!accessory) { 59 | accessory = new HAP.Accessory(lock.nickname, uuid); 60 | this.platform.registerPlatformAccessories('homebridge-sesame', 'Sesame', [accessory]); 61 | } 62 | 63 | let lockAccessory = new LockAccessory(lock, accessory); 64 | this.server.locks.set(lock.device_id, lockAccessory); 65 | 66 | Logger.log(`Found ${lock.nickname}`); 67 | 68 | return accessory; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # homebridge-sesame 2 | 3 | [![npm version](http://img.shields.io/npm/v/homebridge-sesame.svg)](https://npmjs.org/package/homebridge-sesame) 4 | 5 | Control and monitor your Sesame smart lock with HomeKit integration. 6 | 7 | ## Prerequisites 8 | 9 | * Installation of [Homebridge](https://github.com/nfarina/homebridge) 10 | * iOS 11 or later 11 | * Sesame lock with enabled cloud integration 12 | * Sesame Wifi Access Point 13 | 14 | ## Functionality 15 | 16 | * Automatic discovery of all connected locks 17 | * Lock control 18 | * WebHook support 19 | * Reports battery percentage levels 20 | * Alerting when battery runs low 21 | 22 | ## Installation 23 | 24 | ``` 25 | npm install -g homebridge-sesame 26 | ``` 27 | 28 | ### Obtain an API Token 29 | 30 | This plugin now uses v3 of the Sesame API which requires an existing API token. 31 | 32 | To create an API token, log into the [Sesame Dashboard](https://my.candyhouse.co/) and click on "API Settings" in the sidebar. The token you create will be used in your homebridge config file. 33 | 34 | ### Using WebHooks 35 | 36 | This plugin can respond to WebHooks sent from the Sesame API which allows locks to update in the background when a lock or unlock action occurs outside of the Home app. Please note this requires the IP address of your homebridge host to be exposed to the outside world via port forwarding, etc. 37 | 38 | #### Setting up the WebHook 39 | 40 | * Log into the [Sesame Dashboard](https://my.candyhouse.co/) and click on "API Settings" in the sidebar. 41 | * Under "Services", click the edit button next to "Webhook" and select "POST" from the dropdown. 42 | * Enter the publicly-accessible IP address of your homebridge server and the port used by this plugin (default: `33892`). For example: `http://:33892`. 43 | 44 | When the plugin receives a WebHook request, the corresponding lock in HomeKit will update its locked state accordingly. 45 | 46 | ## Configuration 47 | 48 | ### Example config 49 | 50 | At a minimum, you just need to provide your API token in the `token` field: 51 | 52 | ``` 53 | { 54 | "platforms": [ 55 | { 56 | "platform": "Sesame", 57 | "token": "YOUR_API_TOKEN" 58 | } 59 | ] 60 | } 61 | ``` 62 | 63 | The plugin will discover your connected locks when homebridge is restarted. 64 | 65 | ### Fields 66 | 67 | Variable | Description 68 | -------- | ----------- 69 | `token` | Your Sesame API token (required) 70 | `port` | The port of the WebHook listener (default: `33892`) 71 | `debug` | Set to `true` to enable additional debug logging (default: `false`) 72 | 73 | ### Example config changing default values 74 | 75 | ``` 76 | { 77 | "platforms": [ 78 | { 79 | "platform": "Sesame", 80 | "token": "YOUR_API_TOKEN", 81 | "port": 55901, 82 | "debug": true 83 | } 84 | ] 85 | } 86 | ``` -------------------------------------------------------------------------------- /src/Client.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'request-promise'; 2 | import * as store from 'store'; 3 | import { Lock, LockStatus, TaskResult } from './interfaces/API'; 4 | import { Logger } from './Logger'; 5 | 6 | export class Client { 7 | token: string; 8 | 9 | API_URI = 'https://api.candyhouse.co/public' 10 | MAX_RETRIES = 20; 11 | POLL_DELAY = 1000; 12 | 13 | constructor() { 14 | this.token = store.get('token'); 15 | } 16 | 17 | async listLocks(): Promise { 18 | let options = this.buildRequestOptions('sesames'); 19 | let response = await request.get(options); 20 | 21 | Logger.debug('Got listLocks response', response); 22 | 23 | return response; 24 | } 25 | 26 | async getStatus(id: string): Promise { 27 | let options = this.buildRequestOptions(`sesame/${id}`); 28 | 29 | let status = await request.get(options); 30 | Logger.debug('Got status response', status); 31 | 32 | return status; 33 | } 34 | 35 | async control(id: string, secure: boolean): Promise { 36 | let options = this.buildRequestOptions(`sesame/${id}`); 37 | options.body = {command: (secure ? 'lock' : 'unlock')} 38 | 39 | let response = await request.post(options); 40 | let result = await this.waitForTask(response.task_id); 41 | 42 | return result; 43 | } 44 | 45 | async sync(id: string): Promise { 46 | let options = this.buildRequestOptions(`sesame/${id}`); 47 | options.body = {command: 'sync'} 48 | 49 | let response = await request.post(options); 50 | let result = await this.waitForTask(response.task_id); 51 | 52 | return result; 53 | } 54 | 55 | private async getTaskStatus(task_id: string): Promise { 56 | let options = this.buildRequestOptions('action-result'); 57 | options.qs = {task_id: task_id}; 58 | 59 | let status = await request.get(options); 60 | 61 | return status; 62 | } 63 | 64 | private async waitForTask(task_id: string): Promise { 65 | let retries = this.MAX_RETRIES; 66 | let result: TaskResult; 67 | 68 | while (retries-- > 0) { 69 | if (retries == 0) { 70 | throw new Error('Control task took too long to complete.'); 71 | } 72 | 73 | Logger.debug(`Waiting for task to complete. Attempts remaining: ${retries}/${this.MAX_RETRIES}`); 74 | 75 | await this.delay(this.POLL_DELAY); 76 | 77 | result = await this.getTaskStatus(task_id); 78 | Logger.debug('Task response', result); 79 | 80 | if (result.status == 'processing') { 81 | continue; 82 | } 83 | 84 | return result; 85 | } 86 | } 87 | 88 | private buildRequestOptions(path: string): request.Options { 89 | let options: request.Options = { 90 | uri: `${this.API_URI}/${path}`, 91 | json: true, 92 | headers: { 93 | 'Content-Type': 'application/json', 94 | 'Authorization': this.token 95 | } 96 | } 97 | 98 | return options; 99 | } 100 | 101 | private async delay(ms: number) { 102 | return new Promise(resolve => setTimeout(resolve, ms)); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/LockAccessory.ts: -------------------------------------------------------------------------------- 1 | import { HAP } from "./HAP"; 2 | import { LockStatus, Lock } from "./interfaces/API"; 3 | import { Accessory, Service } from "./interfaces/HAP"; 4 | import { Logger } from "./Logger"; 5 | import { Client } from "./Client"; 6 | import { Mutex } from "./util/Mutex"; 7 | 8 | export class LockAccessory { 9 | lock: Lock; 10 | accessory: Accessory; 11 | 12 | client: Client; 13 | mutex: Mutex; 14 | 15 | constructor(lock: Lock, accessory: Accessory) { 16 | this.lock = lock; 17 | this.accessory = accessory; 18 | 19 | this.client = new Client(); 20 | this.mutex = new Mutex(); 21 | 22 | this.accessory.getService(HAP.Service.AccessoryInformation) 23 | .setCharacteristic(HAP.Characteristic.Manufacturer, 'CANDY HOUSE') 24 | .setCharacteristic(HAP.Characteristic.Model, 'Sesame') 25 | .setCharacteristic(HAP.Characteristic.SerialNumber, this.lock.serial); 26 | 27 | this.setupLockServiceCharacteristics(); 28 | this.setupBatteryServiceCharacteristics(); 29 | } 30 | 31 | getOrCreateHAPService(service: Service): Service { 32 | let hapService = this.accessory.getService(service); 33 | 34 | if (!hapService) { 35 | hapService = this.accessory.addService(service, this.lock.nickname); 36 | } 37 | 38 | return hapService; 39 | } 40 | 41 | setupLockServiceCharacteristics(): void { 42 | let lockService = this.getOrCreateHAPService(HAP.Service.LockMechanism); 43 | 44 | lockService.getCharacteristic(HAP.Characteristic.LockCurrentState) 45 | .on('get', this.getCurrentLockState.bind(this)); 46 | 47 | lockService.getCharacteristic(HAP.Characteristic.LockTargetState) 48 | .on('get', this.getTargetLockState.bind(this)) 49 | .on('set', this.setLockState.bind(this)); 50 | } 51 | 52 | setupBatteryServiceCharacteristics(): void { 53 | let batteryService = this.getOrCreateHAPService(HAP.Service.BatteryService); 54 | 55 | batteryService.getCharacteristic(HAP.Characteristic.BatteryLevel) 56 | .on('get', this.getBatteryLevel.bind(this)); 57 | 58 | batteryService.getCharacteristic(HAP.Characteristic.ChargingState) 59 | .on('get', this.getBatteryChargingState.bind(this)); 60 | 61 | batteryService.getCharacteristic(HAP.Characteristic.StatusLowBattery) 62 | .on('get', this.getLowBatteryStatus.bind(this)); 63 | } 64 | 65 | async getCurrentLockState(callback: Function): Promise { 66 | let status: LockStatus; 67 | 68 | try { 69 | status = await this.mutex.wait(() => this.client.getStatus(this.lock.device_id)); 70 | } catch(e) { 71 | Logger.error('Unable to get lock state', e); 72 | callback(e); 73 | } 74 | 75 | if (!status.responsive) { 76 | Logger.log(`${this.lock.nickname} is unresponsive, forcing a status sync...`); 77 | 78 | try { 79 | let result = await this.client.sync(this.lock.device_id); 80 | 81 | if (result.successful) { 82 | Logger.log(`${this.lock.nickname} sync successful.`) 83 | } else { 84 | Logger.error(`${this.lock.nickname} failed to sync, please check WiFi connectivity. API responded with: ${result.error}`); 85 | } 86 | } catch(e) { 87 | Logger.error(`Unable to sync`, e); 88 | } 89 | } 90 | 91 | if (status.locked) { 92 | callback(null, HAP.Characteristic.LockCurrentState.SECURED); 93 | } else { 94 | callback(null, HAP.Characteristic.LockCurrentState.UNSECURED); 95 | } 96 | } 97 | 98 | async getTargetLockState(callback: Function): Promise { 99 | try { 100 | let status = await this.mutex.wait(() => this.client.getStatus(this.lock.device_id)); 101 | 102 | if (status.locked) { 103 | callback(null, HAP.Characteristic.LockCurrentState.SECURED); 104 | } else { 105 | callback(null, HAP.Characteristic.LockCurrentState.UNSECURED); 106 | } 107 | } catch(e) { 108 | Logger.error('Unable to get lock state', e); 109 | callback(e); 110 | } 111 | } 112 | 113 | async setLockState(targetState: boolean, callback: Function): Promise { 114 | Logger.log(`${this.lock.nickname} is ${targetState ? 'locking' : 'unlocking'}...`); 115 | 116 | try { 117 | await this.client.control(this.lock.device_id, targetState); 118 | } catch(e) { 119 | Logger.error('Unable to set lock state', e); 120 | callback(e); 121 | } 122 | 123 | Logger.log(`${this.lock.nickname} is ${targetState ? 'locked' : 'unlocked'}`); 124 | 125 | this.updateCurrentLockState(targetState); 126 | 127 | callback(); 128 | } 129 | 130 | updateCurrentLockState(locked: boolean) { 131 | let lockService = this.getOrCreateHAPService(HAP.Service.LockMechanism); 132 | 133 | lockService.getCharacteristic(HAP.Characteristic.LockCurrentState) 134 | .updateValue(locked); 135 | } 136 | 137 | async getBatteryLevel(callback: Function): Promise { 138 | try { 139 | let status = await this.mutex.wait(() => this.client.getStatus(this.lock.device_id)); 140 | 141 | callback(null, status.battery); 142 | } catch(e) { 143 | callback(e); 144 | } 145 | } 146 | 147 | async getLowBatteryStatus(callback: Function): Promise { 148 | try { 149 | let status = await this.mutex.wait(() => this.client.getStatus(this.lock.device_id)); 150 | 151 | if (status.battery <= 20) { 152 | callback(null, HAP.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW); 153 | } else { 154 | callback(null, HAP.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL); 155 | } 156 | } catch(e) { 157 | callback(e); 158 | } 159 | } 160 | 161 | getBatteryChargingState(callback: Function): void { 162 | callback(null, HAP.Characteristic.ChargingState.NOT_CHARGING); 163 | } 164 | } --------------------------------------------------------------------------------