homebridge-adt-pulse
Version:
Homebridge security system platform for ADT Pulse
554 lines • 28.6 kB
JavaScript
import chalk from 'chalk';
import { itemCondensedSensorTypes } from './items.js';
import { condensePanelStates, convertPanelCharacteristicValue, isPanelAlarmActive, stackTracer, } from './utility.js';
export class ADTPulseAccessory {
#accessory;
#activity;
#api;
#characteristic;
#config;
#instance;
#log;
#services;
#state;
constructor(accessory, state, config, instance, service, characteristic, api, log) {
this.#accessory = accessory;
this.#activity = {
isBusy: false,
setCurrentValue: null,
setTargetValue: null,
setValue: null,
};
this.#api = api;
this.#characteristic = characteristic;
this.#config = config;
this.#instance = instance;
this.#log = log;
this.#services = {};
this.#state = state;
const { context } = this.#accessory;
const { firmware, hardware, manufacturer, model, name, serial, software, type, } = context;
this.#services.Information = this.#accessory.getService(service.AccessoryInformation) ?? this.#accessory.addService(service.AccessoryInformation);
this.#services.Information
.setCharacteristic(this.#characteristic.Identify, false)
.setCharacteristic(this.#characteristic.Manufacturer, manufacturer ?? 'N/A')
.setCharacteristic(this.#characteristic.Model, model ?? 'N/A')
.setCharacteristic(this.#characteristic.Name, name)
.setCharacteristic(this.#characteristic.SerialNumber, serial ?? 'N/A')
.setCharacteristic(this.#characteristic.FirmwareRevision, firmware ?? 'N/A')
.setCharacteristic(this.#characteristic.HardwareRevision, hardware ?? 'N/A')
.setCharacteristic(this.#characteristic.SoftwareRevision, software ?? 'N/A');
switch (type) {
case 'gateway':
break;
case 'panel':
this.#services.Primary = this.#accessory.getService(service.SecuritySystem) ?? this.#accessory.addService(service.SecuritySystem);
break;
case 'panelSwitch':
this.#services.Primary = this.#accessory.getService(service.Switch) ?? this.#accessory.addService(service.Switch);
break;
default:
break;
}
switch (type) {
case 'co':
this.#services.Primary = this.#accessory.getService(service.CarbonMonoxideSensor) ?? this.#accessory.addService(service.CarbonMonoxideSensor);
break;
case 'doorWindow':
this.#services.Primary = this.#accessory.getService(service.ContactSensor) ?? this.#accessory.addService(service.ContactSensor);
break;
case 'fire':
this.#services.Primary = this.#accessory.getService(service.SmokeSensor) ?? this.#accessory.addService(service.SmokeSensor);
break;
case 'flood':
this.#services.Primary = this.#accessory.getService(service.LeakSensor) ?? this.#accessory.addService(service.LeakSensor);
break;
case 'glass':
this.#services.Primary = this.#accessory.getService(service.OccupancySensor) ?? this.#accessory.addService(service.OccupancySensor);
break;
case 'heat':
this.#services.Primary = this.#accessory.getService(service.OccupancySensor) ?? this.#accessory.addService(service.OccupancySensor);
break;
case 'motion':
this.#services.Primary = this.#accessory.getService(service.MotionSensor) ?? this.#accessory.addService(service.MotionSensor);
break;
case 'shock':
this.#services.Primary = this.#accessory.getService(service.OccupancySensor) ?? this.#accessory.addService(service.OccupancySensor);
break;
case 'temperature':
this.#services.Primary = this.#accessory.getService(service.TemperatureSensor) ?? this.#accessory.addService(service.TemperatureSensor);
break;
default:
break;
}
}
updater() {
const { context } = this.#accessory;
const { id, name, type, uuid, } = context;
if (this.#services.Primary === undefined) {
if (type !== 'gateway') {
this.#log.error(`Failed to update ${chalk.underline(name)} (id: ${id}, uuid: ${uuid}) accessory because the primary service does not exist ...`);
}
return;
}
switch (type) {
case 'gateway':
break;
case 'panel':
this.#services.Primary.getCharacteristic(this.#characteristic.SecuritySystemCurrentState)
.updateValue(this.getPanelStatus('current'));
this.#services.Primary.getCharacteristic(this.#characteristic.SecuritySystemTargetState)
.updateValue(this.getPanelStatus('target'));
this.#services.Primary.getCharacteristic(this.#characteristic.SecuritySystemTargetState)
.onSet(async (value) => this.setPanelStatus(value));
break;
case 'panelSwitch':
this.#services.Primary.getCharacteristic(this.#characteristic.On)
.updateValue(this.getPanelSwitchStatus());
this.#services.Primary.getCharacteristic(this.#characteristic.On)
.onSet(async (value) => this.setPanelSwitchStatus(value));
break;
default:
break;
}
switch (type) {
case 'gateway':
break;
case 'panel':
this.#services.Primary.getCharacteristic(this.#characteristic.SecuritySystemAlarmType)
.updateValue(this.getPanelStatus('alarmType'));
this.#services.Primary.getCharacteristic(this.#characteristic.StatusFault)
.updateValue(this.getPanelStatus('fault'));
this.#services.Primary.getCharacteristic(this.#characteristic.StatusTampered)
.updateValue(this.getPanelStatus('tamper'));
break;
case 'panelSwitch':
break;
default:
break;
}
switch (type) {
case 'co':
this.#services.Primary.getCharacteristic(this.#characteristic.CarbonMonoxideDetected)
.updateValue(this.getSensorStatus('status'));
break;
case 'doorWindow':
this.#services.Primary.getCharacteristic(this.#characteristic.ContactSensorState)
.updateValue(this.getSensorStatus('status'));
break;
case 'fire':
this.#services.Primary.getCharacteristic(this.#characteristic.SmokeDetected)
.updateValue(this.getSensorStatus('status'));
break;
case 'flood':
this.#services.Primary.getCharacteristic(this.#characteristic.LeakDetected)
.updateValue(this.getSensorStatus('status'));
break;
case 'glass':
this.#services.Primary.getCharacteristic(this.#characteristic.OccupancyDetected)
.updateValue(this.getSensorStatus('status'));
break;
case 'heat':
this.#services.Primary.getCharacteristic(this.#characteristic.OccupancyDetected)
.updateValue(this.getSensorStatus('status'));
break;
case 'motion':
this.#services.Primary.getCharacteristic(this.#characteristic.MotionDetected)
.updateValue(this.getSensorStatus('status'));
break;
case 'shock':
this.#services.Primary.getCharacteristic(this.#characteristic.OccupancyDetected)
.updateValue(this.getSensorStatus('status'));
break;
case 'temperature':
this.#services.Primary.getCharacteristic(this.#characteristic.CurrentTemperature)
.updateValue(this.getSensorStatus('status'));
break;
default:
break;
}
switch (type) {
case 'co':
case 'doorWindow':
case 'fire':
case 'flood':
case 'glass':
case 'heat':
case 'motion':
case 'shock':
case 'temperature':
this.#services.Primary.getCharacteristic(this.#characteristic.StatusActive)
.updateValue(this.getSensorStatus('active'));
this.#services.Primary.getCharacteristic(this.#characteristic.StatusFault)
.updateValue(this.getSensorStatus('fault'));
this.#services.Primary.getCharacteristic(this.#characteristic.StatusLowBattery)
.updateValue(this.getSensorStatus('lowBattery'));
this.#services.Primary.getCharacteristic(this.#characteristic.StatusTampered)
.updateValue(this.getSensorStatus('tamper'));
break;
default:
break;
}
}
getSensorStatus(mode) {
const { context } = this.#accessory;
const { id, name, originalName, type, uuid, zone, } = context;
const matchedSensorStatus = this.#state.data.sensorsStatus.find((sensorStatus) => originalName === sensorStatus.name && zone !== null && sensorStatus.zone === zone);
let hapStatus;
if (matchedSensorStatus === undefined
|| type === 'gateway'
|| type === 'panel'
|| type === 'panelSwitch'
|| !itemCondensedSensorTypes.includes(type)) {
hapStatus = new this.#api.hap.HapStatusError(-70409);
this.#log.error(`Attempted to get sensor status on ${chalk.underline(name)} (id: ${id}, uuid: ${uuid}) accessory but sensor is not found or sensor type is not supported.`);
return hapStatus;
}
const { icon, statuses } = matchedSensorStatus;
if (mode === 'active') {
return (!statuses.includes('Offline')
&& !statuses.includes('Unknown')
&& icon !== 'devStatOffline'
&& icon !== 'devStatUnknown');
}
if (mode === 'fault') {
if (statuses.includes('ALARM')
|| statuses.includes('Bypassed')
|| statuses.includes('Trouble')
|| icon === 'devStatAlarm') {
return this.#characteristic.StatusFault.GENERAL_FAULT;
}
return this.#characteristic.StatusFault.NO_FAULT;
}
if (mode === 'lowBattery') {
if (statuses.includes('Low Battery')
|| icon === 'devStatLowBatt') {
return this.#characteristic.StatusLowBattery.BATTERY_LEVEL_LOW;
}
return this.#characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL;
}
if (mode === 'tamper') {
if (statuses.includes('Tampered')
|| icon === 'devStatTamper') {
return this.#characteristic.StatusTampered.TAMPERED;
}
return this.#characteristic.StatusTampered.NOT_TAMPERED;
}
switch (type) {
case 'co':
if (statuses.includes('ALARM') || statuses.includes('Tripped')) {
return this.#characteristic.CarbonMonoxideDetected.CO_LEVELS_ABNORMAL;
}
if (statuses.includes('Okay')) {
return this.#characteristic.CarbonMonoxideDetected.CO_LEVELS_NORMAL;
}
break;
case 'doorWindow':
if (statuses.includes('ALARM') || statuses.includes('Open')) {
return this.#characteristic.ContactSensorState.CONTACT_NOT_DETECTED;
}
if (statuses.includes('Closed')) {
return this.#characteristic.ContactSensorState.CONTACT_DETECTED;
}
break;
case 'fire':
if (statuses.includes('ALARM') || statuses.includes('Tripped')) {
return this.#characteristic.SmokeDetected.SMOKE_DETECTED;
}
if (statuses.includes('Okay')) {
return this.#characteristic.SmokeDetected.SMOKE_NOT_DETECTED;
}
break;
case 'flood':
if (statuses.includes('ALARM') || statuses.includes('Tripped')) {
return this.#characteristic.LeakDetected.LEAK_DETECTED;
}
if (statuses.includes('Okay')) {
return this.#characteristic.LeakDetected.LEAK_NOT_DETECTED;
}
break;
case 'glass':
if (statuses.includes('ALARM') || statuses.includes('Tripped')) {
return this.#characteristic.OccupancyDetected.OCCUPANCY_DETECTED;
}
if (statuses.includes('Okay')) {
return this.#characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED;
}
break;
case 'heat':
if (statuses.includes('ALARM') || statuses.includes('Tripped')) {
return this.#characteristic.OccupancyDetected.OCCUPANCY_DETECTED;
}
if (statuses.includes('Okay')) {
return this.#characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED;
}
break;
case 'motion':
if (statuses.includes('ALARM') || statuses.includes('Motion')) {
return true;
}
if (statuses.includes('No Motion') || statuses.includes('Okay')) {
return false;
}
break;
case 'shock':
if (statuses.includes('ALARM') || statuses.includes('Tripped')) {
return this.#characteristic.OccupancyDetected.OCCUPANCY_DETECTED;
}
if (statuses.includes('Okay')) {
return this.#characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED;
}
break;
case 'temperature':
if (statuses.includes('ALARM') || statuses.includes('Tripped')) {
return 100;
}
if (statuses.includes('Okay')) {
return 0;
}
break;
default:
break;
}
if (statuses.includes('Offline') || statuses.includes('Unknown')) {
hapStatus = new this.#api.hap.HapStatusError(-70412);
this.#log.warn(`Attempted to get sensor status on ${chalk.underline(name)} (id: ${id}, uuid: ${uuid}) accessory but sensor is currently "Offline" or "Unknown".`);
return hapStatus;
}
hapStatus = new this.#api.hap.HapStatusError(-70410);
this.#log.warn(`Attempted to get sensor status on ${chalk.underline(name)} (id: ${id}, uuid: ${uuid}) accessory but actions have not been implemented yet.`);
return hapStatus;
}
getPanelStatus(mode) {
const { context } = this.#accessory;
const { id, name, type, uuid, } = context;
let hapStatus;
if (type !== 'panel') {
hapStatus = new this.#api.hap.HapStatusError(-70410);
this.#log.error(`Attempted to get panel status on ${chalk.underline(name)} (id: ${id}, uuid: ${uuid}) accessory but device is not a security panel.`);
return hapStatus;
}
if (this.#state.data.panelStatus === null
|| this.#state.data.panelStatus.panelStates.length === 0
|| this.#state.data.panelStatus.panelStatuses.length === 0) {
hapStatus = new this.#api.hap.HapStatusError(-70403);
this.#log.debug(`Attempted to get panel status on ${chalk.underline(name)} (id: ${id}, uuid: ${uuid}) accessory but panel status has not been retrieved yet.`);
return hapStatus;
}
const { panelStates, panelStatuses } = this.#state.data.panelStatus;
if (mode === 'alarmType') {
if (isPanelAlarmActive(panelStatuses, this.#state.data.orbSecurityButtons, this.#config?.options.includes('ignoreSensorProblemStatus') ?? false)) {
return 1;
}
return 0;
}
if (mode === 'fault') {
if (!panelStatuses.includes('All Quiet')) {
return this.#characteristic.StatusFault.GENERAL_FAULT;
}
return this.#characteristic.StatusFault.NO_FAULT;
}
if (mode === 'tamper') {
if (panelStatuses.includes('Sensor Problem') || panelStatuses.includes('Sensor Problems')) {
return this.#characteristic.StatusTampered.TAMPERED;
}
return this.#characteristic.StatusTampered.NOT_TAMPERED;
}
switch (true) {
case mode === 'current' && this.#activity.isBusy && this.#activity.setCurrentValue !== null:
return this.#activity.setCurrentValue;
case mode === 'current' && isPanelAlarmActive(panelStatuses, this.#state.data.orbSecurityButtons, this.#config?.options.includes('ignoreSensorProblemStatus') ?? false):
return this.#characteristic.SecuritySystemCurrentState.ALARM_TRIGGERED;
case mode === 'current' && panelStates.includes('Armed Stay'):
return this.#characteristic.SecuritySystemCurrentState.STAY_ARM;
case mode === 'current' && panelStates.includes('Armed Away'):
return this.#characteristic.SecuritySystemCurrentState.AWAY_ARM;
case mode === 'current' && panelStates.includes('Armed Night'):
return this.#characteristic.SecuritySystemCurrentState.NIGHT_ARM;
case mode === 'current' && panelStates.includes('Disarmed'):
return this.#characteristic.SecuritySystemCurrentState.DISARMED;
default:
break;
}
switch (true) {
case mode === 'target' && this.#activity.isBusy && this.#activity.setTargetValue !== null:
return this.#activity.setTargetValue;
case mode === 'target' && panelStates.includes('Armed Stay'):
return this.#characteristic.SecuritySystemTargetState.STAY_ARM;
case mode === 'target' && panelStates.includes('Armed Away'):
return this.#characteristic.SecuritySystemTargetState.AWAY_ARM;
case mode === 'target' && panelStates.includes('Armed Night'):
return this.#characteristic.SecuritySystemTargetState.NIGHT_ARM;
case mode === 'target' && panelStates.includes('Disarmed'):
return this.#characteristic.SecuritySystemTargetState.DISARM;
default:
break;
}
if (panelStates.includes('Status Unavailable')) {
hapStatus = new this.#api.hap.HapStatusError(-70403);
this.#log.warn(`Attempted to get panel status on ${chalk.underline(name)} (id: ${id}, uuid: ${uuid}) accessory but panel state is "Status Unavailable".`);
return hapStatus;
}
hapStatus = new this.#api.hap.HapStatusError(-70402);
this.#log.warn(`Attempted to get panel status on ${chalk.underline(name)} (id: ${id}, uuid: ${uuid}) accessory but actions have not been implemented yet.`);
return hapStatus;
}
getPanelSwitchStatus() {
const { context } = this.#accessory;
const { id, name, uuid } = context;
let hapStatus;
if (this.#state.data.panelStatus === null || this.#state.data.panelStatus.panelStatuses.length === 0) {
hapStatus = new this.#api.hap.HapStatusError(-70403);
this.#log.debug(`Attempted to get panel switch status on ${chalk.underline(name)} (id: ${id}, uuid: ${uuid}) accessory but panel switch status has not been retrieved yet.`);
return hapStatus;
}
const { panelStatuses } = this.#state.data.panelStatus;
if (this.#activity.isBusy && this.#activity.setValue !== null) {
return this.#activity.setValue;
}
return isPanelAlarmActive(panelStatuses, this.#state.data.orbSecurityButtons, this.#config?.options.includes('ignoreSensorProblemStatus') ?? false);
}
async setPanelStatus(arm) {
const { context } = this.#accessory;
const { id, name, type, uuid, } = context;
let hapStatus;
let result = {
success: false,
};
let unknownArmValue = false;
if (type !== 'panel') {
hapStatus = new this.#api.hap.HapStatusError(-70410);
this.#log.error(`Attempted to set panel status on ${chalk.underline(name)} (id: ${id}, uuid: ${uuid}) accessory but device is not a security panel.`);
throw hapStatus;
}
if (this.#state.data.panelStatus === null || this.#state.data.panelStatus.panelStates.length === 0) {
hapStatus = new this.#api.hap.HapStatusError(-70403);
this.#log.warn(`Attempted to set panel status on ${chalk.underline(name)} (id: ${id}, uuid: ${uuid}) accessory but panel status has not been retrieved yet.`);
throw hapStatus;
}
const { panelStates } = this.#state.data.panelStatus;
const condensedPanelStates = condensePanelStates(this.#characteristic, panelStates);
const isAlarmActive = isPanelAlarmActive(this.#state.data.panelStatus.panelStatuses, this.#state.data.orbSecurityButtons, this.#config?.options.includes('ignoreSensorProblemStatus') ?? false);
if (condensedPanelStates === undefined) {
hapStatus = new this.#api.hap.HapStatusError(-70403);
this.#log.warn(`Attempted to set panel status on ${chalk.underline(name)} (id: ${id}, uuid: ${uuid}) accessory but panel status cannot be found or most likely "Status Unavailable".`);
throw hapStatus;
}
if (!this.#activity.isBusy
&& condensedPanelStates.characteristicValue.target !== arm) {
const setCurrentValue = convertPanelCharacteristicValue('target-to-current', this.#characteristic, arm);
if (setCurrentValue === undefined) {
hapStatus = new this.#api.hap.HapStatusError(-70410);
this.#log.error(`Attempted to set panel status on ${chalk.underline(name)} (id: ${id}, uuid: ${uuid}) accessory but current characteristic value does not exist.`);
throw hapStatus;
}
this.#activity = {
isBusy: true,
setCurrentValue,
setTargetValue: arm,
setValue: null,
};
switch (arm) {
case this.#characteristic.SecuritySystemTargetState.STAY_ARM:
result = await this.#instance.setPanelStatus(condensedPanelStates.armValue, 'stay', isAlarmActive);
break;
case this.#characteristic.SecuritySystemTargetState.AWAY_ARM:
result = await this.#instance.setPanelStatus(condensedPanelStates.armValue, 'away', isAlarmActive);
break;
case this.#characteristic.SecuritySystemTargetState.NIGHT_ARM:
result = await this.#instance.setPanelStatus(condensedPanelStates.armValue, 'night', isAlarmActive);
break;
case this.#characteristic.SecuritySystemTargetState.DISARM:
result = await this.#instance.setPanelStatus(condensedPanelStates.armValue, 'off', isAlarmActive);
break;
default:
unknownArmValue = true;
break;
}
this.#activity = {
isBusy: false,
setCurrentValue: null,
setTargetValue: null,
setValue: null,
};
if (unknownArmValue) {
hapStatus = new this.#api.hap.HapStatusError(-70410);
this.#log.error(`Attempted to set panel status on ${chalk.underline(name)} (id: ${id}, uuid: ${uuid}) accessory but request has unknown arm value.`);
throw hapStatus;
}
if (!result.success) {
hapStatus = new this.#api.hap.HapStatusError(-70408);
this.#log.error(`Attempted to set panel status on ${chalk.underline(name)} (id: ${id}, uuid: ${uuid}) accessory but request was not successful.`);
stackTracer('api-response', result);
throw hapStatus;
}
}
}
async setPanelSwitchStatus(on) {
const { context } = this.#accessory;
const { id, name, type, uuid, } = context;
let hapStatus;
let result = {
success: false,
};
let unknownArmValue = false;
if (type !== 'panelSwitch') {
hapStatus = new this.#api.hap.HapStatusError(-70410);
this.#log.error(`Attempted to set panel switch status on ${chalk.underline(name)} (id: ${id}, uuid: ${uuid}) accessory but device is not a panel switch.`);
throw hapStatus;
}
if (this.#state.data.panelStatus === null || this.#state.data.panelStatus.panelStates.length === 0) {
hapStatus = new this.#api.hap.HapStatusError(-70403);
this.#log.warn(`Attempted to set panel switch status on ${chalk.underline(name)} (id: ${id}, uuid: ${uuid}) accessory but panel status has not been retrieved yet.`);
throw hapStatus;
}
if (on === true) {
hapStatus = new this.#api.hap.HapStatusError(0);
this.#log.error(`Attempted to set panel switch status on ${chalk.underline(name)} (id: ${id}, uuid: ${uuid}) accessory but switch is only used for turning off ringing alarm and clearing alarm.`);
throw hapStatus;
}
const { panelStates } = this.#state.data.panelStatus;
const condensedPanelStates = condensePanelStates(this.#characteristic, panelStates);
const isAlarmActive = isPanelAlarmActive(this.#state.data.panelStatus.panelStatuses, this.#state.data.orbSecurityButtons, this.#config?.options.includes('ignoreSensorProblemStatus') ?? false);
if (condensedPanelStates === undefined) {
hapStatus = new this.#api.hap.HapStatusError(-70403);
this.#log.warn(`Attempted to set panel switch status on ${chalk.underline(name)} (id: ${id}, uuid: ${uuid}) accessory but panel status cannot be found or most likely "Status Unavailable".`);
throw hapStatus;
}
if (!this.#activity.isBusy
&& isAlarmActive) {
this.#activity = {
isBusy: true,
setCurrentValue: null,
setTargetValue: null,
setValue: false,
};
switch (on) {
case false:
result = await this.#instance.setPanelStatus(condensedPanelStates.armValue, 'off', isAlarmActive);
break;
default:
unknownArmValue = true;
break;
}
this.#activity = {
isBusy: false,
setCurrentValue: null,
setTargetValue: null,
setValue: null,
};
if (unknownArmValue) {
hapStatus = new this.#api.hap.HapStatusError(-70410);
this.#log.error(`Attempted to set panel switch status on ${chalk.underline(name)} (id: ${id}, uuid: ${uuid}) accessory but request has unknown arm value.`);
throw hapStatus;
}
if (!result.success) {
hapStatus = new this.#api.hap.HapStatusError(-70408);
this.#log.error(`Attempted to set panel switch status on ${chalk.underline(name)} (id: ${id}, uuid: ${uuid}) accessory but request was not successful.`);
stackTracer('api-response', result);
throw hapStatus;
}
}
}
}
//# sourceMappingURL=accessory.js.map