UNPKG

@ronniepettersson/homebridge-dummy

Version:

Create Homebridge accessories to help with automation and control — scheduling, delays, sensors, commands, webhooks, and more

173 lines 7.31 kB
import escape from 'escape-html'; import express from 'express'; import { isValidTemperatureUnits, printableValues, TemperatureUnits, WebhookCommand } from './enums.js'; import { assert } from '../tools/validation.js'; import { strings } from '../i18n/i18n.js'; import { fromCelsius, toCelsius } from '../tools/temperature.js'; const DEFAULT_PORT = 63743; const MINIMUM_TEMPERATURE = 10; const MAXIMUM_TEMPERATURE = 38; export class Webhook { id; command; callback; constructor(id, command, callback) { this.id = id; this.command = command; this.callback = callback; } } export class WebhookManager { Characteristic; log; port; server = undefined; webhooks = new Map(); constructor(Characteristic, log, port) { this.Characteristic = Characteristic; this.log = log; this.port = port; this.port = port ?? DEFAULT_PORT; if (typeof this.port !== 'number') { log.error(strings.webhook.badPort, DEFAULT_PORT); this.port = DEFAULT_PORT; } } registerAccessory(accessory) { for (const webhook of accessory.webhooks()) { let callbacks = this.webhooks.get(webhook.command); if (callbacks === undefined) { callbacks = new Map(); this.webhooks.set(webhook.command, callbacks); } callbacks.set(webhook.id, webhook.callback); this.log.ifVerbose(strings.webhook.register, `'${webhook.id}'`, `'${webhook.command}'`); } } startServer() { if (this.server !== undefined) { throw new Error('Trying to start webhook server when it is already running'); } if (this.webhooks.size === 0) { return; } const exp = express(); exp.use(express.urlencoded({ extended: true })); exp.use(express.json()); exp.post('/', (request, response) => { this.requestHandler(request, response); }); this.server = exp.listen(this.port, () => { this.log.ifVerbose(strings.webhook.started, this.port); }); } teardown() { this.log.ifVerbose(strings.webhook.stopping); this.server?.close(() => { this.log.ifVerbose(strings.webhook.stopped); }); } ; requestHandler(request, response) { const body = request.body; this.log.ifVerbose(`${strings.webhook.received}\n${JSON.stringify(body)}`); if (!assert(this.log, 'Webhook', body, 'id', 'command', 'value')) { const missingValues = ['id', 'command', 'value'].filter((key) => body[key] === undefined); this.onBadRequest(response, `${strings.webhook.missing} ${missingValues.join(', ')}`, false); return; } const id = body.id; const command = body.command; let value = body.value; let validRequest; let requirements; switch (command) { case WebhookCommand.Brightness: { validRequest = this.isValueWithinRange(value, 0, 100); requirements = strings.webhook.validRange.replace('%s', `'${command}'`).replace('%s', '0').replace('%s', '100'); break; } case WebhookCommand.LockTargetState: { validRequest = this.isValidValue(value, [this.Characteristic.LockTargetState.UNSECURED, this.Characteristic.LockTargetState.SECURED]); const validValues = '0 (UNSECURED), 1 (SECURED)'; requirements = `${strings.webhook.validValues.replace('%s', `'${command}'`)} ${validValues}`; break; } case WebhookCommand.On: { validRequest = this.isValidValue(value, [true, false]); requirements = `${strings.webhook.validValues.replace('%s', `'${command}'`)} true, false`; break; } case WebhookCommand.TargetHeatingCoolingState: { validRequest = this.isValidValue(value, [ this.Characteristic.TargetHeatingCoolingState.OFF, this.Characteristic.TargetHeatingCoolingState.HEAT, this.Characteristic.TargetHeatingCoolingState.COOL, this.Characteristic.TargetHeatingCoolingState.AUTO, ]); const validValues = '0 (OFF), 1 (HEAT), 2 (COOL), 3 (AUTO)'; requirements = `${strings.webhook.validValues.replace('%s', `'${command}'`)} ${validValues}`; break; } case WebhookCommand.TargetPosition: { validRequest = this.isValueWithinRange(value, 0, 100); requirements = strings.webhook.validRange.replace('%s', `'${command}'`).replace('%s', '0').replace('%s', '100'); break; } case WebhookCommand.TargetTemperature: { if (!isValidTemperatureUnits(body.units)) { validRequest = false; requirements = `${strings.webhook.badUnits.replace('%s', `'${command}'`).replace('%s', `'${body.units}'`)} ${printableValues(TemperatureUnits)}`; break; } const units = body.units ?? TemperatureUnits.CELSIUS; const minTemp = fromCelsius(MINIMUM_TEMPERATURE, units); const maxTemp = fromCelsius(MAXIMUM_TEMPERATURE, units); validRequest = this.isValueWithinRange(value, minTemp, maxTemp); requirements = strings.webhook.validRange.replace('%s', `'${command}'`).replace('%s', `${minTemp}`).replace('%s', `${maxTemp}`); if (validRequest) { value = toCelsius(value, units); } break; } default: this.onBadRequest(response, strings.webhook.unsupportedCommand.replace('%s', `'${command}'`)); return; } if (!validRequest) { this.onBadRequest(response, requirements); return; } const callbacks = this.webhooks.get(command); if (callbacks === undefined) { this.onBadRequest(response, strings.webhook.unregisteredCommand.replace('%s', `'${command}'`)); return; } const callback = callbacks.get(id); if (callback === undefined) { this.onBadRequest(response, strings.webhook.unregisteredId.replace('%s', `'${id}'`)); return; } const message = callback(value); response.status(200).send(`{ "success": "${escape(message)}" }\n`); } isValidValue(value, validValues) { if (typeof value === 'boolean' || typeof value === 'number') { return validValues.includes(value); } return false; } isValueWithinRange(value, min, max) { if (typeof value !== 'number') { return false; } return value >= min && value <= max; } onBadRequest(response, errorMessage, alsoLog = true) { response.status(400).send(`{ "error": "${escape(errorMessage)}" }\n`); if (alsoLog) { this.log.error(errorMessage); } } } //# sourceMappingURL=webhook.js.map