UNPKG

homebridge-virtual-accessories

Version:
409 lines 23.9 kB
/* eslint-disable brace-style */ import { Accessory } from './accessory.js'; import { Utils } from '../utils/utils.js'; import { TLVDeviceCredentialResponse, TLVReaderKeyResponse, TLVRequest, TLVUtils } from '../utils/tlv.js'; /** * Lock - Accessory implementation */ export class Lock extends Accessory { static ACCESSORY_TYPE_NAME = 'Lock'; static UNSECURED = 0; // Characteristic.LockCurrentState.UNSECURED static SECURED = 1; // Characteristic.LockCurrentState.SECURED static JAMMED = 2; // Characteristic.LockCurrentState.JAMMED static UNKNOWN = 3; // Characteristic.LockCurrentState.UNKNOWN static SECURED_REMOTELY = 6; // Characteristic.LockLastKnownAction.SECURED_REMOTELY static UNSECURED_REMOTELY = 7; // Characteristic.LockLastKnownAction.UNSECURED_REMOTELY static SECURED_BY_AUTO_SECURE_TIMEOUT = 8; // Characteristic.LockLastKnownAction.SECURED_BY_AUTO_SECURE_TIMEOUT stateStorageKey = 'LockState'; securityTimeoutStorageKey = 'LockAutoSecurityTimeout'; lastKnownActionStorageKey = 'LockLastKnownAction'; deviceCredentialPublicKeysStorageKey = 'DeviceCredentialPublicKeys'; readerPrivateKeysStorageKey = 'readerPrivateKeys'; // base64 encoded hex "010110020110"; 16 keys each deviceCredentialPublicKeysCount = 16; readerPrivateKeysCount = 16; nfcAccessSupportedConfiguration = 'AQEQAgEQ'; deviceCredentialPublicKeys = new Map(); // Issuer Key Identifier - Device Credential Public Key readerPrivateKeys = new Map(); // Key Identifier - Reader Private Key setupHomeKey; // base64 encoded hex lockHardwareFinish = { 'default': 'AQT///8A', // 0104FFFFFF00 'tan': 'AQTO1doA', // 0104CED5DA00 'gold': 'AQSq1uwA', // 0104AAD6EC00 'silver': 'AQTj4+MA', // 0104E3E3E300 'black': 'AQQAAAAA', // 010400000000 }; securityTimerId; states = { LockCurrentState: Lock.SECURED, LockTargetState: Lock.SECURED, LockManagementAutoSecurityTimeout: 0, LockLastKnownAction: Lock.UNSECURED_REMOTELY, }; constructor(platform, accessory, accessoryConfiguration) { super(platform, accessory, accessoryConfiguration); // First configure the device based on the accessory details this.defaultState = this.accessoryConfiguration.lock.defaultState === 'unlocked' ? Lock.UNSECURED : Lock.SECURED; const autoSecurityTimeout = this.accessoryConfiguration.lock.autoSecurityTimeout; // const walletKeyColor = this.accessoryConfiguration.lock.walletKeyColor || 'default'; // HomeKey appears to be broken right now, so temporarily leaving NFC out if no HomeKey card color is selected const walletKeyColor = (this.accessoryConfiguration.lock.walletKeyColor !== undefined) ? this.accessoryConfiguration.lock.walletKeyColor : undefined; this.setupHomeKey = (walletKeyColor === undefined) ? false : true; this.states.LockCurrentState = this.defaultState; this.states.LockManagementAutoSecurityTimeout = autoSecurityTimeout; this.states.LockLastKnownAction = Lock.UNSECURED_REMOTELY; // There is no "unknown" value // If the accessory is stateful retrieve stored state if (this.accessoryConfiguration.accessoryIsStateful) { const accessoryState = this.loadAccessoryState(this.storagePath); const cachedState = accessoryState[this.stateStorageKey]; const cachedSecurityTimeout = accessoryState[this.securityTimeoutStorageKey]; const cachedLastKnownAction = accessoryState[this.lastKnownActionStorageKey]; const jsonDeviceCredentialPublicKeys = accessoryState[this.deviceCredentialPublicKeysStorageKey]; const cachedDeviceCredentialPublicKeys = (jsonDeviceCredentialPublicKeys !== undefined) ? Utils.jsonToMap(jsonDeviceCredentialPublicKeys) : undefined; const jsonReaderPrivateKeys = accessoryState[this.readerPrivateKeysStorageKey]; const cachedReaderPrivateKeys = (jsonReaderPrivateKeys !== undefined) ? Utils.jsonToMap(jsonReaderPrivateKeys) : undefined; if (cachedState !== undefined) { this.states.LockCurrentState = cachedState; } if (cachedSecurityTimeout !== undefined) { this.states.LockManagementAutoSecurityTimeout = cachedSecurityTimeout; } if (cachedLastKnownAction !== undefined) { this.states.LockLastKnownAction = cachedLastKnownAction; } if (cachedDeviceCredentialPublicKeys !== undefined) { this.deviceCredentialPublicKeys = cachedDeviceCredentialPublicKeys; } if (cachedReaderPrivateKeys !== undefined) { this.readerPrivateKeys = cachedReaderPrivateKeys; } } this.states.LockTargetState = this.states.LockCurrentState; if (this.setupHomeKey) { this.accessoryInformationService.setCharacteristic(this.platform.Characteristic.HardwareFinish, this.lockHardwareFinish[walletKeyColor]); } this.service = this.accessory.getService(this.platform.Service.LockMechanism) || this.accessory.addService(this.platform.Service.LockMechanism); this.service.setCharacteristic(this.platform.Characteristic.Name, this.accessoryConfiguration.accessoryName); // Update the initial state of the accessory this.log.debug(`[${this.accessoryConfiguration.accessoryName}] Setting Lock Current State: ${Lock.getStateName(this.states.LockCurrentState)}`); this.service.updateCharacteristic(this.platform.Characteristic.LockCurrentState, (this.states.LockCurrentState)); this.service.updateCharacteristic(this.platform.Characteristic.LockTargetState, (this.states.LockTargetState)); // register handlers this.service.getCharacteristic(this.platform.Characteristic.LockCurrentState) .onGet(this.getLockCurrentState.bind(this)); // GET - bind to the 'handleLockCurrentStateGet` method below this.service.getCharacteristic(this.platform.Characteristic.LockTargetState) .onSet(this.setLockTargetState.bind(this)) // SET - bind to the `handleLockTargetStateSet` method below .onGet(this.getLockTargetState.bind(this)); // GET - bind to the `handleLockTargetStateGet` method below /** * Creating multiple services of the same type. * * To avoid "Cannot add a Service with the same UUID another Service without also defining a unique 'subtype' property." error, * when creating multiple services of the same type, you need to use the following syntax to specify a name and subtype id: * this.accessory.getService('NAME') || this.accessory.addService(this.platform.Service.Lightbulb, 'NAME', 'USER_DEFINED_SUBTYPE_ID'); * * The USER_DEFINED_SUBTYPE must be unique to the platform accessory (if you platform exposes multiple accessories, each accessory * can use the same subtype id.) */ // Creating Lock Management service const lockManagementServiceName = `${this.accessoryConfiguration.accessoryName} Management`; const lockManagementService = this.accessory.getService(lockManagementServiceName) || this.accessory.addService(this.platform.Service.LockManagement, lockManagementServiceName, this.accessory.UUID + '-LMS'); lockManagementService.getCharacteristic(this.platform.Characteristic.LockControlPoint) .onSet(this.setLockControlPoint.bind(this)); lockManagementService.getCharacteristic(this.platform.Characteristic.Version) .onGet(this.getVersion.bind(this)); lockManagementService.getCharacteristic(this.platform.Characteristic.LockManagementAutoSecurityTimeout) .onSet(this.setLockManagementAutoSecurityTimeout.bind(this)) .onGet(this.getLockManagementAutoSecurityTimeout.bind(this)) .setProps({ minValue: 0, maxValue: 3600, minStep: 1, unit: "seconds" /* Units.SECONDS */, }); lockManagementService.getCharacteristic(this.platform.Characteristic.LockLastKnownAction) .onGet(this.getLockLastKnownAction.bind(this)); if (this.setupHomeKey) { // Creating Nfc Access service const nfcAccessServiceName = `${this.accessoryConfiguration.accessoryName} Nfc Access`; const nfcAccessService = this.accessory.getService(nfcAccessServiceName) || this.accessory.addService(this.platform.Service.NFCAccess, nfcAccessServiceName, this.accessory.UUID + '-NFC'); nfcAccessService.getCharacteristic(this.platform.Characteristic.ConfigurationState) .onGet(this.getConfigurationState.bind(this)); nfcAccessService.getCharacteristic(this.platform.Characteristic.NFCAccessControlPoint) .onSet(this.setNFCAccessControlPoint.bind(this)) .onGet(this.getNFCAccessControlPoint.bind(this)); nfcAccessService.getCharacteristic(this.platform.Characteristic.NFCAccessSupportedConfiguration) .onGet(this.getNFCAccessSupportedConfiguration.bind(this)); } } // Handlers async getLockCurrentState() { const lockState = this.states.LockCurrentState; this.log.debug(`[${this.accessoryConfiguration.accessoryName}] Getting Current State: ${Lock.getStateName(lockState)}`); return lockState; } async setLockTargetState(value) { this.states.LockTargetState = value; this.log.info(`[${this.accessoryConfiguration.accessoryName}] Setting Target State: ${Lock.getStateName(this.states.LockTargetState)}`); this.states.LockCurrentState = this.states.LockTargetState; this.service.setCharacteristic(this.platform.Characteristic.LockCurrentState, (this.states.LockCurrentState)); this.states.LockLastKnownAction = (this.states.LockCurrentState === Lock.SECURED) ? Lock.SECURED_REMOTELY : Lock.UNSECURED_REMOTELY; this.storeState(); this.log.info(`[${this.accessoryConfiguration.accessoryName}] Setting Current State: ${Lock.getStateName(this.states.LockCurrentState)}`); // Run auto lock timeout this.startAutoSecurityTimeout(); } async getLockTargetState() { const lockState = this.states.LockTargetState; this.log.debug(`[${this.accessoryConfiguration.accessoryName}] Getting Target State: ${Lock.getStateName(lockState)}`); return lockState; } // Lock Management Service handlers async setLockControlPoint(value) { const lockControlPoint = value; this.log.info(`[${this.accessoryConfiguration.accessoryName}] Setting Lock Control Point: ${lockControlPoint}`); } async getVersion() { const version = '1.0.0'; this.log.debug(`[${this.accessoryConfiguration.accessoryName}] Getting Lock Management Version: ${version}`); return version; } async setLockManagementAutoSecurityTimeout(value) { this.states.LockManagementAutoSecurityTimeout = value; // eslint-disable-next-line max-len this.log.info(`[${this.accessoryConfiguration.accessoryName}] Setting Lock Management Auto Security Timeout: ${this.states.LockManagementAutoSecurityTimeout}`); } async getLockManagementAutoSecurityTimeout() { const lockManagementAutoSecurityTimeout = this.states.LockManagementAutoSecurityTimeout; this.log.debug(`[${this.accessoryConfiguration.accessoryName}] Getting Lock Management Auto Security Timeout: ${lockManagementAutoSecurityTimeout}`); return lockManagementAutoSecurityTimeout; } async getLockLastKnownAction() { const lockLastKnownAction = this.states.LockLastKnownAction; this.log.debug(`[${this.accessoryConfiguration.accessoryName}] Getting Lock Last Known Action: ${lockLastKnownAction}`); return lockLastKnownAction; } // NFC Service handlers async getConfigurationState() { const configurationState = 0; // Successful this.log.debug(`[${this.accessoryConfiguration.accessoryName}] Getting NFC Access Configuration State: ${configurationState}`); return configurationState; } async setNFCAccessControlPoint(value) { const nfcAccessControlPoint = value; try { const response = this.processAccessControlPointRequest(nfcAccessControlPoint); this.log.debug(`[${this.accessoryConfiguration.accessoryName}] Setting NFC Access Control Point: ${nfcAccessControlPoint}`); this.log.debug(`[${this.accessoryConfiguration.accessoryName}] NFC Access Control Point Response: "${response}"`); return response; } catch (error) { this.log.error(`Caught error ${error}`); if (error instanceof Error) { this.log.error(`Error message: ${error.message}`); this.log.error(`Error stack: ${error.stack}`); } } return ''; } async getNFCAccessControlPoint() { const nfcAccessControlPoint = ''; this.log.debug(`[${this.accessoryConfiguration.accessoryName}] Getting NFC Access Control Point: ${nfcAccessControlPoint}`); return nfcAccessControlPoint; } async getNFCAccessSupportedConfiguration() { const nfcAccessSupportedConfiguration = this.nfcAccessSupportedConfiguration; this.log.debug(`[${this.accessoryConfiguration.accessoryName}] Getting NFC Access Supported Configuration: ${nfcAccessSupportedConfiguration}`); return nfcAccessSupportedConfiguration; } getJsonState() { const jsonState = { [this.stateStorageKey]: this.states.LockCurrentState, [this.securityTimeoutStorageKey]: this.states.LockManagementAutoSecurityTimeout, [this.lastKnownActionStorageKey]: this.states.LockLastKnownAction, }; if (this.setupHomeKey) { Object.assign(jsonState, { [this.deviceCredentialPublicKeysStorageKey]: Utils.mapToJson(this.deviceCredentialPublicKeys) }); Object.assign(jsonState, { [this.readerPrivateKeysStorageKey]: Utils.mapToJson(this.readerPrivateKeys) }); } const json = JSON.stringify(jsonState); return json; } getAccessoryTypeName() { return Lock.ACCESSORY_TYPE_NAME; } static getStateName(state) { let stateName; switch (state) { case undefined: { stateName = 'undefined'; break; } case Lock.UNSECURED: { stateName = 'UNSECURED'; break; } case Lock.SECURED: { stateName = 'SECURED'; break; } case Lock.JAMMED: { stateName = 'JAMMED'; break; } case Lock.UNKNOWN: { stateName = 'UNKNOWN'; break; } default: { stateName = state.toString(); } } return stateName; } startAutoSecurityTimeout() { if (this.states.LockTargetState !== this.defaultState && this.states.LockManagementAutoSecurityTimeout > 0) { const securityTimeoutMillis = this.states.LockManagementAutoSecurityTimeout * 1000; this.securityTimerId = setTimeout(() => { // Reset timer clearTimeout(this.securityTimerId); this.service.setCharacteristic(this.platform.Characteristic.LockTargetState, (this.defaultState)); this.states.LockLastKnownAction = Lock.SECURED_BY_AUTO_SECURE_TIMEOUT; }, securityTimeoutMillis) .unref(); const timeout = Utils.secondsToHHmmss(this.states.LockManagementAutoSecurityTimeout); this.log.info(`[${this.accessoryConfiguration.accessoryName}] Security Timeout in ${timeout}`); } else { this.log.info(`[${this.accessoryConfiguration.accessoryName}] No Security Timeout defined`); } } GET_DEVICE_CREDENTIAL_REQUEST = Utils.concatenate(TLVUtils.OPERATION_GET, TLVUtils.DEVICE_CREDENTIAL_REQUEST); GET_READER_KEY_REQUEST = Utils.concatenate(TLVUtils.OPERATION_GET, TLVUtils.READER_KEY_REQUEST); ADD_DEVICE_CREDENTIAL_REQUEST = Utils.concatenate(TLVUtils.OPERATION_ADD, TLVUtils.DEVICE_CREDENTIAL_REQUEST); ADD_GET_READER_KEY_REQUEST = Utils.concatenate(TLVUtils.OPERATION_ADD, TLVUtils.READER_KEY_REQUEST); RFEMOVE_DEVICE_CREDENTIAL_REQUEST = Utils.concatenate(TLVUtils.OPERATION_REMOVE, TLVUtils.DEVICE_CREDENTIAL_REQUEST); REMOVE_GET_READER_KEY_REQUEST = Utils.concatenate(TLVUtils.OPERATION_REMOVE, TLVUtils.READER_KEY_REQUEST); processAccessControlPointRequest(base64TlvRequest) { const hexTlvRequest = Utils.base64DecodeToHexString(base64TlvRequest); const tlvRequest = new TLVRequest(hexTlvRequest, this.log); this.log.debug(`[${this.accessoryConfiguration.accessoryName}] hexTlvRequest: "${hexTlvRequest}"`); let hexTlvResponse = ''; const controlPointRequest = Utils.concatenate(tlvRequest.operation.value, tlvRequest.request.type); switch (controlPointRequest) { // Not called case this.GET_DEVICE_CREDENTIAL_REQUEST: { this.log.info(`[${this.accessoryConfiguration.accessoryName}] Access Control Point: GET Device Credential`); if (this.deviceCredentialPublicKeys.size > 0) { const issuerKeyIdentifier = this.deviceCredentialPublicKeys.keys().next().value; if (issuerKeyIdentifier !== undefined) { const response = TLVDeviceCredentialResponse.getResponseForGetOperation(issuerKeyIdentifier); hexTlvResponse = response.toHexString(); } } break; } case this.GET_READER_KEY_REQUEST: { this.log.info(`[${this.accessoryConfiguration.accessoryName}] Access Control Point: GET Reader Key`); if (this.readerPrivateKeys.size > 0) { const readerKeyIdentifier = this.readerPrivateKeys.keys().next().value; if (readerKeyIdentifier !== undefined) { const response = TLVReaderKeyResponse.getResponseForGetOperation(readerKeyIdentifier); hexTlvResponse = response.toHexString(); } } break; } case this.ADD_DEVICE_CREDENTIAL_REQUEST: { this.log.info(`[${this.accessoryConfiguration.accessoryName}] Access Control Point: ADD Device Credential`); const request = tlvRequest.requestPayload; const issuerKeyIdentifier = request.issuerKeyIdentifier.value; const deviceCredentialPublicKey = request.deviceCredentialPublicKey.value; // const keyState: number = request.keyState!.value as number; // const keyType: number = request.keyType!.value as number; let status = TLVUtils.STATUS_SUCCESS; if (this.deviceCredentialPublicKeys.size >= this.deviceCredentialPublicKeysCount) { status = TLVUtils.STATUS_OUT_OF_RESOURCES; } else if (this.deviceCredentialPublicKeys.get(issuerKeyIdentifier) !== undefined) { status = TLVUtils.STATUS_DUPLICATE; } else { this.deviceCredentialPublicKeys.set(issuerKeyIdentifier, deviceCredentialPublicKey); } const response = TLVDeviceCredentialResponse.getResponseForAddOperation(issuerKeyIdentifier, status); hexTlvResponse = response.toHexString(); break; } case this.ADD_GET_READER_KEY_REQUEST: { this.log.info(`[${this.accessoryConfiguration.accessoryName}] Access Control Point: ADD Reader Key`); const request = tlvRequest.requestPayload; const readerPrivateKey = request.readerPrivateKey.value; // const keyType: number = request.keyType!.value as number; // const unknown: string = request.unknown!.value as string; const readerKeyIdentifier = TLVUtils.getReaderIdentifier(readerPrivateKey); let status = TLVUtils.STATUS_SUCCESS; if (this.readerPrivateKeys.size >= this.readerPrivateKeysCount) { status = TLVUtils.STATUS_OUT_OF_RESOURCES; } else if (this.readerPrivateKeys.get(readerKeyIdentifier) !== undefined) { status = TLVUtils.STATUS_DUPLICATE; } else { this.readerPrivateKeys.set(readerKeyIdentifier, readerPrivateKey); } const response = TLVReaderKeyResponse.getResponseForAddOperation(status); hexTlvResponse = response.toHexString(); break; } // Not called case this.RFEMOVE_DEVICE_CREDENTIAL_REQUEST: { this.log.info(`[${this.accessoryConfiguration.accessoryName}] Access Control Point: REMOVE Device Credential`); const request = tlvRequest.requestPayload; const issuerKeyIdentifier = request.issuerKeyIdentifier.value; //const keyIdentifier: number = request.keyIdentifier!.value as number; let status = TLVUtils.STATUS_SUCCESS; if (this.deviceCredentialPublicKeys.get(issuerKeyIdentifier) === undefined) { status = TLVUtils.STATUS_DOES_NOT_EXIST; } else { this.deviceCredentialPublicKeys.delete(issuerKeyIdentifier); } const response = TLVDeviceCredentialResponse.getResponseForRemoveOperation(status); hexTlvResponse = response.toHexString(); break; } case this.REMOVE_GET_READER_KEY_REQUEST: { this.log.info(`[${this.accessoryConfiguration.accessoryName}] Access Control Point: REMOVE Reader Key`); const request = tlvRequest.requestPayload; const keyIdentifier = request.keyIdentifier.value; let status = TLVUtils.STATUS_SUCCESS; if (this.readerPrivateKeys.get(keyIdentifier) === undefined) { status = TLVUtils.STATUS_DOES_NOT_EXIST; } else { this.readerPrivateKeys.delete(keyIdentifier); } const response = TLVReaderKeyResponse.getResponseForRemoveOperation(status); hexTlvResponse = response.toHexString(); break; } default: { if (!TLVUtils.OPERATIONS.includes(tlvRequest.operation.type)) { this.log.error(`[${this.accessoryConfiguration.accessoryName}] Invalid operation: "${tlvRequest.operation.value}"`); } if (!TLVUtils.REQUESTS.includes(tlvRequest.request.type)) { this.log.error(`[${this.accessoryConfiguration.accessoryName}] Invalid request: "${tlvRequest.request.type}"`); } } } this.log.debug(`[${this.accessoryConfiguration.accessoryName}] hexTlvResponse: "${hexTlvResponse}"`); const base64TlvResponse = Utils.hexStringEncodeToBase64(hexTlvResponse); return base64TlvResponse; } } //# sourceMappingURL=virtualAccessoryLock.js.map