Skip to content

Commit

Permalink
Fixed unknown action detector
Browse files Browse the repository at this point in the history
### FIXED
- Plugin sent the context instead of the panel/sensor actions.
  • Loading branch information
mrjackyliang committed Dec 31, 2023
1 parent 06a8afa commit cebc3f3
Show file tree
Hide file tree
Showing 5 changed files with 244 additions and 112 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "homebridge-adt-pulse",
"displayName": "Homebridge ADT Pulse",
"version": "3.0.0-beta.13",
"version": "3.0.0-beta.14",
"description": "Homebridge security system platform for ADT Pulse",
"main": "./build/src/index.js",
"exports": "./build/src/index.js",
Expand Down
49 changes: 38 additions & 11 deletions src/lib/accessory.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import chalk from 'chalk';

import { detectedNewDeviceContext } from '@/lib/detect.js';
import { detectedUnknownPanelAction, detectedUnknownSensorAction } from '@/lib/detect.js';
import { condensedSensorTypeItems } from '@/lib/items.js';
import { condensePanelStates, generateHash } from '@/lib/utility.js';
import type {
Expand All @@ -23,8 +23,11 @@ import type {
ADTPulseAccessoryGetSensorStatusReturns,
ADTPulseAccessoryInstance,
ADTPulseAccessoryLog,
ADTPulseAccessoryNewInformationDispatcherContext,
ADTPulseAccessoryNewInformationDispatcherData,
ADTPulseAccessoryNewInformationDispatcherPanel,
ADTPulseAccessoryNewInformationDispatcherReturns,
ADTPulseAccessoryNewInformationDispatcherSensor,
ADTPulseAccessoryNewInformationDispatcherType,
ADTPulseAccessoryReportedHashes,
ADTPulseAccessoryServices,
ADTPulseAccessorySetPanelStatusArm,
Expand Down Expand Up @@ -302,11 +305,11 @@ export class ADTPulseAccessory {
zone,
} = context;

const sensor = this.#state.data.sensorsStatus.find((sensorStatus) => zone !== null && sensorStatus.name === name && sensorStatus.zone === zone);
const matchedSensorStatus = this.#state.data.sensorsStatus.find((sensorStatus) => zone !== null && sensorStatus.name === name && sensorStatus.zone === zone);

// If sensor is not found or sensor type is not supported.
if (
sensor === undefined
matchedSensorStatus === undefined
|| type === 'gateway'
|| type === 'panel'
|| !condensedSensorTypeItems.includes(type)
Expand All @@ -316,7 +319,7 @@ export class ADTPulseAccessory {
throw new this.#api.hap.HapStatusError(this.#api.hap.HAPStatus.RESOURCE_DOES_NOT_EXIST);
}

const { statuses } = sensor;
const { statuses } = matchedSensorStatus;

// If sensor is currently "Offline" or "Unknown".
if (statuses.includes('Offline') || statuses.includes('Unknown')) {
Expand Down Expand Up @@ -387,7 +390,7 @@ export class ADTPulseAccessory {

this.#log.warn(`Attempted to get sensor status on ${chalk.underline(name)} (id: ${id}, uuid: ${uuid}) accessory but actions have not been implemented yet.`);

await this.newInformationDispatcher(context);
await this.newInformationDispatcher(type, matchedSensorStatus);

throw new this.#api.hap.HapStatusError(this.#api.hap.HAPStatus.INVALID_VALUE_IN_REQUEST);
}
Expand Down Expand Up @@ -460,7 +463,7 @@ export class ADTPulseAccessory {

this.#log.warn(`Attempted to get panel status on ${chalk.underline(name)} (id: ${id}, uuid: ${uuid}) accessory but actions have not been implemented yet.`);

await this.newInformationDispatcher(context);
await this.newInformationDispatcher(type, this.#state.data.panelStatus);

throw new this.#api.hap.HapStatusError(this.#api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
}
Expand Down Expand Up @@ -552,20 +555,44 @@ export class ADTPulseAccessory {
/**
* ADT Pulse Accessory - New information dispatcher.
*
* @param {ADTPulseAccessoryNewInformationDispatcherContext} context - Context.
* @param {ADTPulseAccessoryNewInformationDispatcherType} type - Type.
* @param {ADTPulseAccessoryNewInformationDispatcherData} data - Data.
*
* @private
*
* @returns {ADTPulseAccessoryNewInformationDispatcherReturns}
*
* @since 1.0.0
*/
private async newInformationDispatcher(context: ADTPulseAccessoryNewInformationDispatcherContext): ADTPulseAccessoryNewInformationDispatcherReturns {
const dataHash = generateHash(JSON.stringify(context));
private async newInformationDispatcher(type: ADTPulseAccessoryNewInformationDispatcherType, data: ADTPulseAccessoryNewInformationDispatcherData): ADTPulseAccessoryNewInformationDispatcherReturns {
const dataHash = generateHash(`${type}: ${JSON.stringify(data)}`);

// If the detector has not reported this event before.
if (this.#reportedHashes.find((reportedHash) => dataHash === reportedHash) === undefined) {
const detectedNew = await detectedNewDeviceContext(context, this.#log, this.#debugMode);
let detectedNew = false;

// Determine what information needs to be checked.
switch (type) {
case 'panel':
detectedNew = await detectedUnknownPanelAction(data as ADTPulseAccessoryNewInformationDispatcherPanel, this.#log, this.#debugMode);
break;
case 'co':
case 'doorWindow':
case 'fire':
case 'flood':
case 'glass':
case 'keypad':
case 'motion':
case 'panic':
case 'shock':
case 'supervisory':
case 'temperature':
case 'unknown':
detectedNew = await detectedUnknownSensorAction(data as ADTPulseAccessoryNewInformationDispatcherSensor, this.#log, this.#debugMode);
break;
default:
break;
}

// Save this hash so the detector does not detect the same thing multiple times.
if (detectedNew) {
Expand Down
255 changes: 170 additions & 85 deletions src/lib/detect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,6 @@ import {
stackTracer,
} from '@/lib/utility.js';
import type {
DetectedNewDeviceContextContext,
DetectedNewDeviceContextDebugMode,
DetectedNewDeviceContextLogger,
DetectedNewDeviceContextReturns,
DetectedNewDoSubmitHandlersDebugMode,
DetectedNewDoSubmitHandlersHandlers,
DetectedNewDoSubmitHandlersLogger,
Expand Down Expand Up @@ -63,89 +59,16 @@ import type {
DetectedNewSensorsStatusLogger,
DetectedNewSensorsStatusReturns,
DetectedNewSensorsStatusSensors,
DetectedUnknownPanelActionDebugMode,
DetectedUnknownPanelActionLogger,
DetectedUnknownPanelActionReturns,
DetectedUnknownPanelActionStatus,
DetectedUnknownSensorActionDebugMode,
DetectedUnknownSensorActionLogger,
DetectedUnknownSensorActionReturns,
DetectedUnknownSensorActionStatus,
} from '@/types/index.d.ts';

/**
* Detected new device context.
*
* @param {DetectedNewDeviceContextContext} context - Context.
* @param {DetectedNewDeviceContextLogger} logger - Logger.
* @param {DetectedNewDeviceContextDebugMode} debugMode - Debug mode.
*
* @returns {DetectedNewDeviceContextReturns}
*
* @since 1.0.0
*/
export async function detectedNewDeviceContext(context: DetectedNewDeviceContextContext, logger: DetectedNewDeviceContextLogger, debugMode: DetectedNewDeviceContextDebugMode): DetectedNewDeviceContextReturns {
const cleanedData = removePersonalIdentifiableInformation(context);

// If outdated, it means plugin may already have support.
try {
const outdated = await isPluginOutdated();

if (outdated) {
if (logger !== null) {
logger.warn('Plugin has detected new device context. You are running an older plugin version, please update soon.');
}

// This is intentionally duplicated if using Homebridge debug mode.
if (debugMode) {
debugLog(logger, 'detect.ts / detectedNewDeviceContext()', 'warn', 'Plugin has detected new device context. You are running an older plugin version, please update soon');
}

// Do not send analytics for users running outdated plugin versions.
return false;
}
} catch (error) {
if (debugMode === true) {
debugLog(logger, 'detect.ts / detectedNewDeviceContext()', 'error', 'Failed to check if plugin is outdated');
stackTracer('serialize-error', serializeError(error));
}

// Try to check if plugin is outdated later on.
return false;
}

if (logger !== null) {
logger.warn('Plugin has detected new device context. Notifying plugin author about this discovery ...');
}

// This is intentionally duplicated if using Homebridge debug mode.
if (debugMode) {
debugLog(logger, 'detect.ts / detectedNewDeviceContext()', 'warn', 'Plugin has detected new device context. Notifying plugin author about this discovery');
}

// Show content being sent to author.
stackTracer('detect-content', cleanedData);

try {
await axios.post(
'https://fs65kt4c5xf8.ntfy.mrjackyliang.com',
[
'Please update the plugin as soon as possible.',
JSON.stringify(cleanedData, null, 2),
].join('\n\n'),
{
family: 4,
headers: {
'User-Agent': 'homebridge-adt-pulse',
'X-Title': 'Detected new device context',
},
},
);

return true;
} catch (error) {
if (debugMode === true) {
debugLog(logger, 'detect.ts / detectedNewDeviceContext()', 'error', 'Failed to notify plugin author about the new device context');
stackTracer('serialize-error', serializeError(error));
}

// Try to send information to author later.
return false;
}
}

/**
* Detected new do submit handlers.
*
Expand Down Expand Up @@ -877,3 +800,165 @@ export async function detectedNewSensorsStatus(sensors: DetectedNewSensorsStatus

return false;
}

/**
* Detected unknown panel action.
*
* @param {DetectedUnknownPanelActionStatus} status - Status.
* @param {DetectedUnknownPanelActionLogger} logger - Logger.
* @param {DetectedUnknownPanelActionDebugMode} debugMode - Debug mode.
*
* @returns {DetectedUnknownPanelActionReturns}
*
* @since 1.0.0
*/
export async function detectedUnknownPanelAction(status: DetectedUnknownPanelActionStatus, logger: DetectedUnknownPanelActionLogger, debugMode: DetectedUnknownPanelActionDebugMode): DetectedUnknownPanelActionReturns {
const cleanedData = removePersonalIdentifiableInformation(status);

// If outdated, it means plugin may already have support.
try {
const outdated = await isPluginOutdated();

if (outdated) {
if (logger !== null) {
logger.warn('Plugin has detected unknown panel action. You are running an older plugin version, please update soon.');
}

// This is intentionally duplicated if using Homebridge debug mode.
if (debugMode) {
debugLog(logger, 'detect.ts / detectedUnknownPanelAction()', 'warn', 'Plugin has detected unknown panel action. You are running an older plugin version, please update soon');
}

// Do not send analytics for users running outdated plugin versions.
return false;
}
} catch (error) {
if (debugMode === true) {
debugLog(logger, 'detect.ts / detectedUnknownPanelAction()', 'error', 'Failed to check if plugin is outdated');
stackTracer('serialize-error', serializeError(error));
}

// Try to check if plugin is outdated later on.
return false;
}

if (logger !== null) {
logger.warn('Plugin has detected unknown panel action. Notifying plugin author about this discovery ...');
}

// This is intentionally duplicated if using Homebridge debug mode.
if (debugMode) {
debugLog(logger, 'detect.ts / detectedUnknownPanelAction()', 'warn', 'Plugin has detected unknown panel action. Notifying plugin author about this discovery');
}

// Show content being sent to author.
stackTracer('detect-content', cleanedData);

try {
await axios.post(
'https://fs65kt4c5xf8.ntfy.mrjackyliang.com',
[
'Please update the plugin as soon as possible.',
JSON.stringify(cleanedData, null, 2),
].join('\n\n'),
{
family: 4,
headers: {
'User-Agent': 'homebridge-adt-pulse',
'X-Title': 'Detected unknown panel action',
},
},
);

return true;
} catch (error) {
if (debugMode === true) {
debugLog(logger, 'detect.ts / detectedUnknownPanelAction()', 'error', 'Failed to notify plugin author about the unknown panel action');
stackTracer('serialize-error', serializeError(error));
}

// Try to send information to author later.
return false;
}
}

/**
* Detected unknown sensor action.
*
* @param {DetectedUnknownSensorActionStatus} status - Status.
* @param {DetectedUnknownSensorActionLogger} logger - Logger.
* @param {DetectedUnknownSensorActionDebugMode} debugMode - Debug mode.
*
* @returns {DetectedUnknownSensorActionReturns}
*
* @since 1.0.0
*/
export async function detectedUnknownSensorAction(status: DetectedUnknownSensorActionStatus, logger: DetectedUnknownSensorActionLogger, debugMode: DetectedUnknownSensorActionDebugMode): DetectedUnknownSensorActionReturns {
const cleanedData = removePersonalIdentifiableInformation(status);

// If outdated, it means plugin may already have support.
try {
const outdated = await isPluginOutdated();

if (outdated) {
if (logger !== null) {
logger.warn('Plugin has detected unknown sensor action. You are running an older plugin version, please update soon.');
}

// This is intentionally duplicated if using Homebridge debug mode.
if (debugMode) {
debugLog(logger, 'detect.ts / detectedUnknownSensorAction()', 'warn', 'Plugin has detected unknown sensor action. You are running an older plugin version, please update soon');
}

// Do not send analytics for users running outdated plugin versions.
return false;
}
} catch (error) {
if (debugMode === true) {
debugLog(logger, 'detect.ts / detectedUnknownSensorAction()', 'error', 'Failed to check if plugin is outdated');
stackTracer('serialize-error', serializeError(error));
}

// Try to check if plugin is outdated later on.
return false;
}

if (logger !== null) {
logger.warn('Plugin has detected unknown sensor action. Notifying plugin author about this discovery ...');
}

// This is intentionally duplicated if using Homebridge debug mode.
if (debugMode) {
debugLog(logger, 'detect.ts / detectedUnknownSensorAction()', 'warn', 'Plugin has detected unknown sensor action. Notifying plugin author about this discovery');
}

// Show content being sent to author.
stackTracer('detect-content', cleanedData);

try {
await axios.post(
'https://fs65kt4c5xf8.ntfy.mrjackyliang.com',
[
'Please update the plugin as soon as possible.',
JSON.stringify(cleanedData, null, 2),
].join('\n\n'),
{
family: 4,
headers: {
'User-Agent': 'homebridge-adt-pulse',
'X-Title': 'Detected unknown sensor action',
},
},
);

return true;
} catch (error) {
if (debugMode === true) {
debugLog(logger, 'detect.ts / detectedUnknownSensorAction()', 'error', 'Failed to notify plugin author about the unknown sensor action');
stackTracer('serialize-error', serializeError(error));
}

// Try to send information to author later.
return false;
}
}
Loading

0 comments on commit cebc3f3

Please sign in to comment.