UNPKG

@homebridge-plugins/homebridge-august

Version:

The August plugin allows you to access your August & Yale device(s) from HomeKit.

372 lines 20.2 kB
/* Copyright(C) 2021-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. * * lock.ts: homebridge-august. */ import August from 'august-yale'; import { interval, Subject } from 'rxjs'; import { debounceTime, skipWhile, tap } from 'rxjs/operators'; import { deviceBase } from './device.js'; /** * Platform Accessory * An instance of this class is created for each accessory your platform registers * Each accessory may expose multiple services of different service types. */ export class LockMechanism extends deviceBase { platform; // Service LockMechanism; Battery; ContactSensor; // Lock Mechanism lockEvent; lockStatus; lockDetails; // Lock Updates lockUpdateInProgress; doLockUpdate; constructor(platform, accessory, device) { super(platform, accessory, device); this.platform = platform; // this is subject we use to track when we need to POST changes to the August API this.doLockUpdate = new Subject(); this.lockUpdateInProgress = false; // Initialize Lock Mechanism Service if (device.lock?.hide_lock) { if (this.LockMechanism?.Service) { this.debugLog('Removing Lock Mechanism Service'); this.LockMechanism.Service = accessory.getService(this.hap.Service.LockMechanism); accessory.removeService(this.LockMechanism.Service); accessory.context.LockMechanism = {}; } } else { accessory.context.LockMechanism = accessory.context.LockMechanism ?? {}; this.LockMechanism = { Name: accessory.displayName, Service: accessory.getService(this.hap.Service.LockMechanism) ?? accessory.addService(this.hap.Service.LockMechanism), LockTargetState: accessory.context.LockTargetState ?? this.hap.Characteristic.LockTargetState.SECURED, LockCurrentState: accessory.context.LockCurrentState ?? this.hap.Characteristic.LockCurrentState.SECURED, }; accessory.context.LockMechanism = this.LockMechanism; // Initialize Lock Mechanism Characteristics this.LockMechanism.Service .setCharacteristic(this.hap.Characteristic.Name, this.LockMechanism.Name) .getCharacteristic(this.hap.Characteristic.LockTargetState) .onGet(() => { return this.LockMechanism.LockTargetState; }) .onSet(this.setLockTargetState.bind(this)); } // Initialize Contact Sensor Service if (device.lock?.hide_contactsensor) { if (this.ContactSensor?.Service) { this.debugLog('Removing Conact Sensor Service'); this.ContactSensor.Service = accessory.getService(this.hap.Service.ContactSensor); accessory.removeService(this.ContactSensor.Service); accessory.context.ContactSensor = {}; } } else { accessory.context.ContactSensor = accessory.context.ContactSensor ?? {}; this.ContactSensor = { Name: `${accessory.displayName} Contact Sensor`, Service: accessory.getService(this.hap.Service.ContactSensor) ?? accessory.addService(this.hap.Service.ContactSensor), ContactSensorState: accessory.context.ContactSensorState ?? this.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED, }; accessory.context.ContactSensor = this.ContactSensor; // Initialize Conact Sensor Characteristics this.ContactSensor.Service .setCharacteristic(this.hap.Characteristic.Name, this.ContactSensor.Name) .getCharacteristic(this.hap.Characteristic.ContactSensorState) .onGet(() => { return this.ContactSensor.ContactSensorState; }); } // Initialize Battery Service accessory.context.Battery = accessory.context.Battery ?? {}; this.Battery = { Name: `${accessory.displayName} Battery`, Service: accessory.getService(this.hap.Service.Battery) ?? accessory.addService(this.hap.Service.Battery), BatteryLevel: accessory.context.BatteryLevel ?? 100, StatusLowBattery: accessory.context.StatusLowBattery ?? this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL, ChargingState: accessory.context.ChargingState ?? this.hap.Characteristic.ChargingState.NOT_CHARGING, }; accessory.context.Battery = this.Battery; // Initialize Battery Characteristics this.Battery.Service .setCharacteristic(this.hap.Characteristic.Name, this.Battery.Name) .setCharacteristic(this.hap.Characteristic.ChargingState, this.hap.Characteristic.ChargingState.NOT_CHARGEABLE) .getCharacteristic(this.hap.Characteristic.BatteryLevel) .onGet(() => { return this.Battery.BatteryLevel; }); this.Battery.Service .getCharacteristic(this.hap.Characteristic.StatusLowBattery) .onGet(() => { return this.Battery.StatusLowBattery; }); // Initial Device Refresh this.refreshStatus(); // Subscribe to august changes this.subscribeAugust(); // Start an update interval interval(this.deviceRefreshRate * 1000) .pipe(skipWhile(() => this.lockUpdateInProgress)) .subscribe(async () => { await this.refreshStatus(); }); // Watch for Lock change events // We put in a debounce of 100ms so we don't make duplicate calls if (!device.lock?.hide_lock) { this.doLockUpdate .pipe(tap(() => { this.lockUpdateInProgress = true; }), debounceTime(this.devicePushRate * 1000)) .subscribe(async () => { try { await this.pushChanges(); } catch (e) { await this.statusCode('pushChanges', e); await this.errorLog(`doLockUpdate pushChanges: ${e.message ?? e}`); } this.lockUpdateInProgress = false; }); } } /** * Parse the device status from the August api */ async parseStatus() { await this.debugLog('parseStatus'); const retryCount = 1; if (this.lockStatus) { if (this.lockStatus.state) { // Lock Mechanism this.platform.augustConfig.addSimpleProps(this.lockStatus); if (this.LockMechanism && (this.lockStatus.state.unlocking || this.lockStatus.state.locking)) { await this.warnLog(`LockCurrentState: ${this.LockMechanism.LockCurrentState}, locking/unlocking parseStatus` + ` lockEvent: ${JSON.stringify(this.lockEvent)}`); } if (!this.device.lock?.hide_lock && this.LockMechanism?.Service && (this.lockEvent.state.locked !== this.lockEvent.state.unlocked)) { this.LockMechanism.LockCurrentState = this.lockStatus.state.locked ? this.hap.Characteristic.LockCurrentState.SECURED : this.lockStatus.state.unlocked ? this.hap.Characteristic.LockCurrentState.UNSECURED : retryCount > 1 ? this.hap.Characteristic.LockCurrentState.JAMMED : this.hap.Characteristic.LockCurrentState.UNKNOWN; this.LockMechanism.LockTargetState = this.LockMechanism.LockCurrentState; if (this.LockMechanism.LockCurrentState === this.hap.Characteristic.LockCurrentState.UNKNOWN) { await this.warnLog(`LockCurrentState: ${this.LockMechanism.LockCurrentState}, (UNKNOWN) parseStatus` + ` lockEvent: ${JSON.stringify(this.lockEvent)}`); } await this.debugLog(`LockCurrentState: ${this.LockMechanism.LockCurrentState}`); await this.debugLog(`LockTargetState: ${this.LockMechanism.LockTargetState}`); } // Contact Sensor if (!this.device.lock?.hide_contactsensor && this.ContactSensor?.Service) { // ContactSensorState this.ContactSensor.ContactSensorState = this.lockStatus.state.open ? this.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : this.lockStatus.state.closed ? this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED : this.lockStatus.doorState.includes('open') ? this.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : this.lockStatus.doorState.includes('closed') ? this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED : this.ContactSensor.ContactSensorState; await this.debugLog(`ContactSensorState: ${this.ContactSensor.ContactSensorState}`); } } else { await this.debugWarnLog(`lockStatus state: ${JSON.stringify(this.lockStatus)}`); } } if (this.lockDetails) { // BatteryLevel this.Battery.BatteryLevel = Math.min(Math.max(Number((this.lockDetails.battery * 100).toFixed()), 0), 100); await this.debugLog(`BatteryLevel: ${this.Battery.BatteryLevel}`); // StatusLowBattery this.Battery.StatusLowBattery = this.Battery.BatteryLevel < 15 ? this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; await this.debugLog(`StatusLowBattery: ${this.Battery.StatusLowBattery}`); // Firmware Version if (this.accessory.context.currentFirmwareVersion !== this.lockDetails.currentFirmwareVersion) { await this.warnLog(`Firmware Version changed to Current Firmware Version: ${this.lockDetails.currentFirmwareVersion}`); this.accessory .getService(this.hap.Service.AccessoryInformation) .setCharacteristic(this.hap.Characteristic.HardwareRevision, this.lockDetails.currentFirmwareVersion) .setCharacteristic(this.hap.Characteristic.FirmwareRevision, this.lockDetails.currentFirmwareVersion) .getCharacteristic(this.hap.Characteristic.FirmwareRevision) .updateValue(this.lockDetails.currentFirmwareVersion); this.accessory.context.currentFirmwareVersion = this.lockDetails.currentFirmwareVersion; } } } /** * Parse the device status from the August api */ async parseEventStatus() { await this.debugLog('parseEventStatus'); const retryCount = 1; if (this.lockEvent) { if (this.lockEvent.state) { this.debugLog(`lockEvent: ${JSON.stringify(this.lockEvent)}`); // Lock Mechanism this.platform.augustConfig.addSimpleProps(this.lockEvent); if (this.LockMechanism && (this.lockEvent.state.unlocking || this.lockEvent.state.locking)) { await this.debugLog(`is ${this.lockEvent.state.unlocking ? 'Unlocking' : this.lockEvent.state.locking ? 'Locking' : ''}, parseEventStatus` + ` lockEventState: ${JSON.stringify(this.lockEvent.state)}`); return; } if (!this.device.lock?.hide_lock && this.LockMechanism?.Service && (this.lockEvent.state.locked !== this.lockEvent.state.unlocked)) { this.LockMechanism.LockCurrentState = this.lockEvent.state.locked ? this.hap.Characteristic.LockCurrentState.SECURED : this.lockEvent.state.unlocked ? this.hap.Characteristic.LockCurrentState.UNSECURED : retryCount > 1 ? this.hap.Characteristic.LockCurrentState.JAMMED : this.hap.Characteristic.LockCurrentState.UNKNOWN; this.LockMechanism.LockTargetState = this.LockMechanism.LockCurrentState; if (this.LockMechanism.LockCurrentState === this.hap.Characteristic.LockCurrentState.UNKNOWN) { await this.warnLog(`LockCurrentState: ${this.LockMechanism.LockCurrentState}, (UNKNOWN) parseEventStatus` + ` lockEvent: ${JSON.stringify(this.lockEvent)}`); } await this.debugLog(`LockCurrentState: ${this.LockMechanism.LockCurrentState}`); await this.debugLog(`LockTargetState: ${this.LockMechanism.LockTargetState}`); } // Contact Sensor if (!this.device.lock?.hide_contactsensor && this.ContactSensor?.Service) { // ContactSensorState this.ContactSensor.ContactSensorState = this.lockEvent.state.open ? this.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : this.lockEvent.state.closed ? this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED : this.lockEvent.doorState === 'open' ? this.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : this.lockEvent.doorState === 'closed' ? this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED : this.ContactSensor.ContactSensorState; await this.debugLog(`ContactSensorState: ${this.ContactSensor.ContactSensorState}`); } } else { await this.debugWarnLog(`lovckEvent state: ${JSON.stringify(this.lockStatus)}`); } } } /** * Asks the August Home API for the latest device information */ async refreshStatus() { if (this.deviceRefreshRate !== 0) { try { // Update Lock Details if (this.platform.augustConfig && this.platform.augustConfig.details) { const lockDetails = await this.platform.augustConfig.details(this.device.lockId); await this.debugSuccessLog(`(refreshStatus) lockDetails: ${JSON.stringify(lockDetails)}`); // Update HomeKit this.lockDetails = lockDetails; this.lockStatus = lockDetails.LockStatus; await this.parseStatus(); await this.updateHomeKitCharacteristics(); } else { await this.errorLog('(refreshStatus) lockDetails: No details'); } } catch (e) { await this.statusCode('(refreshStatus) lockDetails', e); await this.errorLog(`(refreshStatus) lockDetails: ${e.message ?? e}`); } } else { await this.debugLog(`(refreshStatus) deviceRefreshRate: ${this.deviceRefreshRate}`); } } /** * Pushes the requested changes to the August API */ async pushChanges() { try { // await this.platform.augustCredentials(); if (this.LockMechanism) { if (this.LockMechanism.LockTargetState !== this.LockMechanism.LockCurrentState) { if (this.LockMechanism.LockTargetState === this.hap.Characteristic.LockTargetState.UNSECURED) { await this.platform.augustConfig.unlock(this.device.lockId); } else { await this.platform.augustConfig.lock(this.device.lockId); } await this.successLog(`Sending request to August API: ${this.LockMechanism.LockTargetState === 1 ? 'Locked' : 'Unlocked'}`); } else { await this.debugLog(`No changes, LockTargetState: ${this.LockMechanism.LockTargetState},` + ` LockCurrentState: ${this.LockMechanism.LockCurrentState}`); this.LockMechanism.LockTargetState = this.LockMechanism.LockCurrentState === this.hap.Characteristic.LockCurrentState.SECURED ? this.hap.Characteristic.LockTargetState.SECURED : this.hap.Characteristic.LockTargetState.UNSECURED; } await this.updateHomeKitCharacteristics(); } else { await this.errorLog(`lockTargetState: ${JSON.stringify(this.LockMechanism)}`); } } catch (e) { await this.statusCode('pushChanges', e); await this.debugLog(`pushChanges: ${e.message ?? e}`); } } /** * Updates the status for each of the HomeKit Characteristics */ async updateHomeKitCharacteristics() { await this.debugLog('updateHomeKitCharacteristics'); // Lock Mechanism if (!this.device.lock?.hide_lock && this.LockMechanism?.Service) { // LockTargetState await this.updateCharacteristic(this.LockMechanism.Service, 'LockMechanism', this.hap.Characteristic.LockTargetState, this.LockMechanism.LockTargetState, 'LockTargetState'); // LockCurrentState await this.updateCharacteristic(this.LockMechanism.Service, 'LockMechanism', this.hap.Characteristic.LockCurrentState, this.LockMechanism.LockCurrentState, 'LockCurrentState', 1, 'Locked', 'Unlocked'); } // Battery // BatteryLevel await this.updateCharacteristic(this.Battery.Service, 'Battery', this.hap.Characteristic.BatteryLevel, this.Battery.BatteryLevel, 'BatteryLevel'); // StatusLowBattery await this.updateCharacteristic(this.Battery.Service, 'Battery', this.hap.Characteristic.StatusLowBattery, this.Battery.StatusLowBattery, 'StatusLowBattery'); // Contact Sensor if (!this.device.lock?.hide_contactsensor && this.ContactSensor?.Service) { // ContactSensorState await this.updateCharacteristic(this.ContactSensor.Service, 'ContactSensor', this.hap.Characteristic.ContactSensorState, this.ContactSensor.ContactSensorState, 'ContactSensorState', 1, 'Opened', 'Closed'); } } async setLockTargetState(value) { if (this.LockMechanism) { if (this.LockMechanism.LockTargetState !== this.LockMechanism.LockCurrentState) { await this.debugLog(`Set LockTargetState: ${value}`); } else { await this.debugLog(`No changes, LockTargetState: ${this.LockMechanism.LockTargetState},` + ` LockCurrentState: ${this.LockMechanism.LockCurrentState}`); } this.accessory.context.LockTargetState = this.LockMechanism.LockTargetState = value; this.doLockUpdate.next(); } } async subscribeAugust() { await this.debugLog('subscribeAugust'); await this.platform.augustCredentials(); if (this.config.credentials) { await August.subscribe(this.config.credentials, this.device.lockId, async (AugustEvent, timestamp) => { await this.debugLog(`AugustEvent: ${JSON.stringify(AugustEvent)}, ${JSON.stringify(timestamp)}`); // Update HomeKit this.lockEvent = AugustEvent; await this.parseEventStatus(); await this.updateHomeKitCharacteristics(); }); } else { await this.errorLog('subscribeAugust: No credentials'); } } } //# sourceMappingURL=lock.js.map