UNPKG

homebridge-adt-pulse

Version:
554 lines 28.6 kB
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