UNPKG

homebridge-virtual-accessories

Version:
282 lines 13.8 kB
/* eslint-disable brace-style */ import { SecurityServiceTriggerType } from './accessories/virtualAccessorySecuritySystem.js'; import { Sensor } from './sensors/sensor.js'; import { SensorValueUpdateNotAllowed } from './errors.js'; import express from 'express'; /** * WebhookServer */ export class WebhookServer { static accessoryIdPattern = '^[A-Za-z0-9\\-]{5,}$'; accessories = new Map(); log; serverName = 'Sensor Server'; server = express(); httpServer; port; 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 const routeHumidity = '/humidity'; this.log.info(`[${this.serverName}] Setting up route: ${routeHumidity}`); this.server.post(routeHumidity, (request, response) => { const accessoryId = request.body.id; const humidity = request.body.value; this.log.debug(`[${this.serverName}] Request: ${request.method} ${request.path}, ${JSON.stringify(request.body)}`); if (this.accessoryIdIsValid(accessoryId, response) && this.percentageIsValid(humidity, response)) { this.processRequest(accessoryId, 'humidifierdehumidifier', Number(humidity), response); } }); const routeTemperature = '/temperature'; this.log.info(`[${this.serverName}] Setting up route: ${routeTemperature}`); this.server.post(routeTemperature, (request, response) => { const accessoryId = request.body.id; const temperature = request.body.value; this.log.debug(`[${this.serverName}] Request: ${request.method} ${request.path}, ${JSON.stringify(request.body)}`); if (this.accessoryIdIsValid(accessoryId, response) && this.numberIsValid(temperature, response)) { this.processRequest(accessoryId, 'heatercooler', Number(temperature), response); } }); const routeObstruction = '/obstruction'; this.log.info(`[${this.serverName}] Setting up route: ${routeObstruction}`); this.server.post(routeObstruction, (request, response) => { const accessoryId = request.body.id; const obstruction = request.body.value; this.log.debug(`[${this.serverName}] Request: ${request.method} ${request.path}, ${JSON.stringify(request.body)}`); if (this.accessoryIdIsValid(accessoryId, response) && this.booleanIsValid(obstruction, response)) { this.processRequest(accessoryId, 'garagedoor', obstruction, response); } }); const routeTriggerAlarm = '/triggeralarm'; this.log.info(`[${this.serverName}] Setting up route: ${routeTriggerAlarm}`); this.server.post(routeTriggerAlarm, (request, response) => { const accessoryId = request.body.id; const trigger = request.body.value; this.log.debug(`[${this.serverName}] Request: ${request.method} ${request.path}, ${JSON.stringify(request.body)}`); if (this.accessoryIdIsValid(accessoryId, response) && this.booleanIsValid(trigger, response)) { this.processRequest(accessoryId, 'securitysystem', trigger ? SecurityServiceTriggerType.TriggerAlarm : SecurityServiceTriggerType.None, response); } }); const routeTriggerPanic = '/triggerpanic'; this.log.info(`[${this.serverName}] Setting up route: ${routeTriggerPanic}`); this.server.post(routeTriggerPanic, (request, response) => { const accessoryId = request.body.id; const trigger = request.body.value; this.log.debug(`[${this.serverName}] Request: ${request.method} ${request.path}, ${JSON.stringify(request.body)}`); if (this.accessoryIdIsValid(accessoryId, response) && this.booleanIsValid(trigger, response)) { this.processRequest(accessoryId, 'securitysystem', trigger ? SecurityServiceTriggerType.TriggerPanic : SecurityServiceTriggerType.None, response); } }); const routeTriggerSensor = '/triggersensor'; this.log.info(`[${this.serverName}] Setting up route: ${routeTriggerSensor}`); this.server.post(routeTriggerSensor, (request, response) => { const accessoryId = request.body.id; const trigger = request.body.value; this.log.debug(`[${this.serverName}] Request: ${request.method} ${request.path}, ${JSON.stringify(request.body)}`); if (this.accessoryIdIsValid(accessoryId, response) && this.booleanIsValid(trigger, response)) { this.processRequest(accessoryId, 'sensor', trigger, response); } }); const routeChargingState = '/chargingstate'; this.log.info(`[${this.serverName}] Setting up route: ${routeChargingState}`); this.server.post(routeChargingState, (request, response) => { const accessoryId = request.body.id; const charging = request.body.charging; const charge = request.body.charge; this.log.debug(`[${this.serverName}] Request: ${request.method} ${request.path}, ${JSON.stringify(request.body)}`); const chargingState = new ChargingState(charging, charge); if (this.accessoryIdIsValid(accessoryId, response) && this.chargingStateIsValid(chargingState, response)) { this.processRequest(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.updateSensor !== undefined) { this.accessories.set(accessory.accessoryConfiguration.accessoryID, accessory); addedAccessory = true; } else if (accessory instanceof Sensor) { 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) { if (typeof value !== 'boolean') { 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; } chargingStateIsValid(chargingState, response) { if (chargingState.isEmpty()) { const errorMsg = 'No values provided for chargeable, charging, or charge'; this.log.error(`[${this.serverName}] ${errorMsg}`); response.status(HttpResponse.BadRequest).send(`${errorMsg}`); return false; } const charging = chargingState.charging; const charge = chargingState.charge; if ((charging !== undefined) && typeof charging !== 'boolean') { const errorMsg = `Invalid charging value: ${charging}. Value must be a boolean`; this.log.error(`[${this.serverName}] ${errorMsg}`); response.status(HttpResponse.BadRequest).send(`${errorMsg}`); return false; } if ((charge !== undefined) && (isNaN(charge) || charge < 0 || charge > 100)) { const errorMsg = `Invalid charge value: ${charge}. Value must be a percentage`; this.log.error(`[${this.serverName}] ${errorMsg}`); response.status(HttpResponse.BadRequest).send(`${errorMsg}`); return false; } return true; } processRequest(accessoryId, accessoryType, value, response) { const accessory = this.accessories.get(accessoryId); if (accessory !== undefined && accessory.accessoryConfiguration.accessoryType === 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); } else if (accessory.updateSensor !== undefined) { accessory.updateSensor(value, accessoryId); } else if (accessory instanceof Sensor) { 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 type '${accessoryType}' and id '${accessoryId}'`; this.log.error(`[${this.serverName}] ${errorMsg}`); response.status(HttpResponse.NotFound).send(`${errorMsg}`); } } } 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