homebridge-virtual-accessories
Version:
Virtual HomeKit accessories for Homebridge.
409 lines • 23.9 kB
JavaScript
/* 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