homebridge-virtual-accessories
Version:
Virtual HomeKit accessories for Homebridge.
282 lines • 13.8 kB
JavaScript
/* 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