UNPKG

homebridge-virtual-accessories

Version:
330 lines 16.1 kB
/* eslint-disable brace-style */ import { SecurityServiceTriggerType } from './accessories/virtualAccessorySecuritySystem.js'; import { BinarySensor } from './sensors/binarySensor.js'; import { SensorValueUpdateNotAllowed } from './errors.js'; import express from 'express'; function ToBoolean(value) { switch (value) { case 'true': return true; case 'false': return false; default: throw new Error('Invalid boolean string'); } } /** * WebhookServer */ export class WebhookServer { static accessoryIdPattern = '^[A-Za-z0-9\\-]{5,}$'; accessories = new Map(); log; serverName = 'Sensor Server'; server = express(); httpServer; port; UseQueryParamsHeader = 'Use-Query-Params'; constructor(log, port, accessories) { this.log = log; this.port = port; // parse application/x-www-form-urlencoded this.server.use(express.urlencoded({ extended: true })); // parse application/json this.server.use(express.json()); if (accessories) { this.addAccessories(accessories); } // Routes // id // value: number const routeHumidity = '/humidity'; this.log.info(`[${this.serverName}] Setting up route: ${routeHumidity}`); this.server.post(routeHumidity, (request, response) => { const useQueryParams = this.usingQueryParams(request); const accessoryId = (useQueryParams) ? request.query.id : request.body.id; const humidity = (useQueryParams) ? request.query.value : request.body.value; if (this.parametersArePresent(request, response) && this.accessoryIdIsValid(accessoryId, response) && this.percentageIsValid(humidity, response)) { this.processRequest(routeHumidity, accessoryId, ['humidifierdehumidifier', 'measurement'], Number(humidity), response); } }); // id // value: number const routeTemperature = '/temperature'; this.log.info(`[${this.serverName}] Setting up route: ${routeTemperature}`); this.server.post(routeTemperature, (request, response) => { const useQueryParams = this.usingQueryParams(request); const accessoryId = (useQueryParams) ? request.query.id : request.body.id; const temperature = (useQueryParams) ? request.query.value : request.body.value; if (this.parametersArePresent(request, response) && this.accessoryIdIsValid(accessoryId, response) && this.numberIsValid(temperature, response)) { this.processRequest(routeTemperature, accessoryId, ['heatercooler', 'measurement'], Number(temperature), response); } }); // id // value: boolean const routeObstruction = '/obstruction'; this.log.info(`[${this.serverName}] Setting up route: ${routeObstruction}`); this.server.post(routeObstruction, (request, response) => { const useQueryParams = this.usingQueryParams(request); const accessoryId = (useQueryParams) ? request.query.id : request.body.id; const obstruction = ((useQueryParams) ? request.query.value : request.body.value).toString(); if (this.parametersArePresent(request, response) && this.accessoryIdIsValid(accessoryId, response) && this.booleanIsValid(obstruction, response)) { this.processRequest(routeObstruction, accessoryId, ['garagedoor'], ToBoolean(obstruction), response); } }); // id // value: boolean const routeTriggerAlarm = '/triggeralarm'; this.log.info(`[${this.serverName}] Setting up route: ${routeTriggerAlarm}`); this.server.post(routeTriggerAlarm, (request, response) => { const useQueryParams = this.usingQueryParams(request); const accessoryId = (useQueryParams) ? request.query.id : request.body.id; const trigger = ((useQueryParams) ? request.query.value : request.body.value).toString(); if (this.parametersArePresent(request, response) && this.accessoryIdIsValid(accessoryId, response) && this.booleanIsValid(trigger, response)) { // eslint-disable-next-line max-len this.processRequest(routeTriggerAlarm, accessoryId, ['securitysystem'], (ToBoolean(trigger) ? SecurityServiceTriggerType.TriggerAlarm : SecurityServiceTriggerType.None), response); } }); // id // value: boolean const routeTriggerPanic = '/triggerpanic'; this.log.info(`[${this.serverName}] Setting up route: ${routeTriggerPanic}`); this.server.post(routeTriggerPanic, (request, response) => { const useQueryParams = this.usingQueryParams(request); const accessoryId = (useQueryParams) ? request.query.id : request.body.id; const trigger = ((useQueryParams) ? request.query.value : request.body.value).toString(); if (this.parametersArePresent(request, response) && this.accessoryIdIsValid(accessoryId, response) && this.booleanIsValid(trigger, response)) { // eslint-disable-next-line max-len this.processRequest(routeTriggerPanic, accessoryId, ['securitysystem'], (ToBoolean(trigger) ? SecurityServiceTriggerType.TriggerPanic : SecurityServiceTriggerType.None), response); } }); // id // value: boolean const routeTriggerSensor = '/triggersensor'; this.log.info(`[${this.serverName}] Setting up route: ${routeTriggerSensor}`); this.server.post(routeTriggerSensor, (request, response) => { const useQueryParams = this.usingQueryParams(request); const accessoryId = (useQueryParams) ? request.query.id : request.body.id; const trigger = ((useQueryParams) ? request.query.value : request.body.value).toString(); if (this.parametersArePresent(request, response) && this.accessoryIdIsValid(accessoryId, response) && this.booleanIsValid(trigger, response)) { this.processRequest(routeTriggerSensor, accessoryId, ['sensor'], ToBoolean(trigger), response); } }); // id // charging: boolean // charge: number const routeChargingState = '/chargingstate'; this.log.info(`[${this.serverName}] Setting up route: ${routeChargingState}`); this.server.post(routeChargingState, (request, response) => { const useQueryParams = this.usingQueryParams(request); const accessoryId = (useQueryParams) ? request.query.id : request.body.id; const charging = ((useQueryParams) ? request.query.charging : request.body.charging).toString(); const charge = (useQueryParams) ? Number(request.query.charge) : request.body.charge; if (this.parametersArePresent(request, response) && this.accessoryIdIsValid(accessoryId, response) && this.booleanIsValid(charging, response) && this.numberIsValid(charge, response)) { const chargingState = new ChargingState(ToBoolean(charging), Number(charge)); this.processRequest(routeChargingState, accessoryId, ['battery'], chargingState, response); } }); } start() { this.log.info(`[${this.serverName}] Starting Sensor Server`); this.httpServer = this.server.listen(this.port, () => { this.log.info(`[${this.serverName}] Sensor Server running on port ${this.port}`); }); } stop() { this.log.info(`[${this.serverName}] Stopping Sensor Server`); this.httpServer?.close(() => { this.log.info(`[${this.serverName}] Sensor Server terminated`); }); } addAccessories(accessories) { accessories.forEach((accessory) => { this.addAccessory(accessory); }); } addAccessory(accessory) { let addedAccessory = false; if (accessory.triggerAlarm !== undefined || accessory.triggerSensor !== undefined || accessory.updateChargingState !== undefined || accessory.updateObstruction !== undefined || accessory.updateMeasurementSensor !== undefined) { this.accessories.set(accessory.accessoryConfiguration.accessoryID, accessory); addedAccessory = true; } else if (accessory instanceof BinarySensor) { const trigger = accessory.getTrigger(); if (trigger.triggerSensor !== undefined) { this.accessories.set(accessory.accessoryConfiguration.accessoryID, accessory); addedAccessory = true; } } if (addedAccessory === true) { this.log.info(`[${this.serverName}] Added accessory ${accessory.accessoryConfiguration.accessoryName} (${accessory.accessoryConfiguration.accessoryID})`); } else { // eslint-disable-next-line max-len this.log.debug(`[${this.serverName}] Skipping accessory ${accessory.accessoryConfiguration.accessoryName} (${accessory.accessoryConfiguration.accessoryID})`); } } removeAccessory(accessory) { const found = this.accessories.delete(accessory.accessoryConfiguration.accessoryID); this.log.info(`[${this.serverName}] Removed accessory ${accessory.accessoryConfiguration.accessoryName} (${accessory.accessoryConfiguration.accessoryID})`); return found; } getAccessories() { const accessories = [...this.accessories.values()]; return accessories; } accessoryIdIsValid(accessoryId, response) { const patternRegex = new RegExp(WebhookServer.accessoryIdPattern); const isValidAccessoryId = ((accessoryId !== undefined) && patternRegex.test(accessoryId)); if (!isValidAccessoryId) { const errorMsg = `Invalid accessory id: ${accessoryId}`; this.log.error(`[${this.serverName}] ${errorMsg}`); response.status(HttpResponse.BadRequest).send(`${errorMsg}`); return false; } return true; } percentageIsValid(value, response) { const valuePercent = Number(value); if (isNaN(valuePercent) || valuePercent < 0 || valuePercent > 100) { const errorMsg = `Invalid value: ${value}. Value must be a percentage`; this.log.error(`[${this.serverName}] ${errorMsg}`); response.status(HttpResponse.BadRequest).send(`${errorMsg}`); return false; } return true; } numberIsValid(value, response) { const valueNumber = Number(value); if (isNaN(valueNumber)) { const errorMsg = `Invalid value: ${value}. Value must be a number`; this.log.error(`[${this.serverName}] ${errorMsg}`); response.status(HttpResponse.BadRequest).send(`${errorMsg}`); return false; } return true; } booleanIsValid(value, response) { const valueBoolean = ['true', 'false'].includes(value.toLowerCase()); if (!valueBoolean) { const errorMsg = `Invalid value: ${value}. Value must be a boolean`; this.log.error(`[${this.serverName}] ${errorMsg}`); response.status(HttpResponse.BadRequest).send(`${errorMsg}`); return false; } return true; } processRequest(route, accessoryId, accessoryTypes, value, response) { const accessory = this.accessories.get(accessoryId); if (accessory !== undefined && accessoryTypes.includes(accessory.accessoryConfiguration.accessoryType)) { try { if (accessory.triggerAlarm !== undefined) { accessory.triggerAlarm(value, accessoryId); } else if (accessory.triggerSensor !== undefined) { accessory.triggerSensor(value, accessoryId); } else if (accessory.updateChargingState !== undefined) { const chargingState = value; accessory.updateChargingState(chargingState.charging, chargingState.charge, accessoryId); } else if (accessory.updateObstruction !== undefined) { accessory.updateObstruction(value, accessoryId); } // Heater/Cooler, Humidifier/Dehumidifier, Temperature Sensor, Humidity Sensor else if (accessory.updateMeasurementSensor !== undefined) { accessory.updateMeasurementSensor(value, accessoryId); } else if (accessory instanceof BinarySensor) { const trigger = accessory.getTrigger(); if (trigger.triggerSensor !== undefined) { trigger.triggerSensor(value, accessoryId); } } const msgValue = (typeof value === 'number' || typeof value === 'boolean') ? value.toString() : JSON.stringify(value); const message = `Set accessory with id: ${accessoryId} to value: ${msgValue}`; this.log.info(`[${this.serverName}] ${message}`); response.status(HttpResponse.Ok).send(`${message}`); } catch (error) { let errorMsg = error; if (error instanceof SensorValueUpdateNotAllowed) { errorMsg = error.message; } this.log.error(`[${this.serverName}] ${errorMsg}`); response.status(HttpResponse.BadRequest).send(`${errorMsg}`); } } else { const errorMsg = `No accessory found with id '${accessoryId}' and able to respond to request '${route}'`; this.log.error(`[${this.serverName}] ${errorMsg}`); response.status(HttpResponse.NotFound).send(`${errorMsg}`); } } parametersArePresent(request, response) { const useQueryParams = this.usingQueryParams(request); this.log.debug(`[${this.serverName}] ${this.UseQueryParamsHeader}: ${useQueryParams}`); this.log.debug(`[${this.serverName}] Request: ${request.method} ${request.path}`); this.log.debug(`[${this.serverName}] POST body: ${JSON.stringify(request.body)}`); this.log.debug(`[${this.serverName}] POST query: ${JSON.stringify(request.query)}`); // POST body if (!useQueryParams && JSON.stringify(request.body) === '{}') { const errorMsg = 'No parameters found in POST body'; this.log.error(`[${this.serverName}] ${errorMsg}`); response.status(HttpResponse.BadRequest).send(`${errorMsg}`); return false; } // POST query if (useQueryParams && JSON.stringify(request.query) === '{}') { const errorMsg = 'No parameters found in POST query. The webhook server is using query parameters'; this.log.error(`[${this.serverName}] ${errorMsg}`); response.status(HttpResponse.BadRequest).send(`${errorMsg}`); return false; } return true; } usingQueryParams(request) { const useQueryParamsHeader = request.header(this.UseQueryParamsHeader); return (useQueryParamsHeader === undefined) ? false : ToBoolean(useQueryParamsHeader); } } class HttpResponse { static Ok = 200; static BadRequest = 400; static NotFound = 404; } class ChargingState { charging; charge; constructor(charging, charge) { this.charging = charging; this.charge = charge; } isEmpty() { return (this.charging === undefined && this.charge === undefined); } } //# sourceMappingURL=webhookServer.js.map