homebridge-rachio-irrigation
Version:
Rachio Irrigation System platform plugin for [Homebridge](https://github.com/nfarina/homebridge).
262 lines • 17.1 kB
JavaScript
/* eslint-disable @typescript-eslint/no-explicit-any */
import http from 'http';
import https from 'https';
import { readFileSync } from 'fs';
import { createHmac } from 'crypto';
import RachioUpdate from './rachioupdate.js';
export default class listen {
platform;
log;
rachio;
Service;
Characteristic;
eventMsg;
constructor(platform, log = platform.log, rachio = new RachioUpdate(platform)) {
this.platform = platform;
this.log = log;
this.rachio = rachio;
this.Service = platform.Service;
this.Characteristic = platform.Characteristic;
}
configureListener() {
this.webMessage(this.rachio.updateService.bind(this));
//this.localMessage(this.rachio.updateService.bind(this));
const secure = this.platform.useHttps ? https : http;
let options = {};
if (secure == https) {
options = {
key: readFileSync(this.platform.key),
cert: readFileSync(this.platform.cert),
};
}
if ((this.platform.external_IP_address && this.platform.external_webhook_address && this.platform.internal_webhook_port) || (this.platform.relay_address && this.platform.internal_IP_address && this.platform.internal_webhook_port)) {
this.log.debug(`Will listen for Webhooks matching Webhook ID ${this.platform.webhook_key}`);
const server = http.createServer(options, (request, response) => {
let webhookAuthentication;
if (request.method === 'GET' && request.url === '/test') {
const jsonBody = request.body;
//check v1 authentication
if (request.headers.authorization) {
webhookAuthentication = this.checkAuth(request);
this.log.debug(`Webhook v1 Authentication ${webhookAuthentication ? 'passed' : 'failed'}`);
if (!webhookAuthentication) {
this.log.warn('Webhook v1 Authentication failed unknown sender');
}
}
//check v2 authentication SHT
if (request.headers['x-signature']) {
webhookAuthentication = this.checkKey(this.platform.token, request.headers['x-signature'], JSON.stringify(jsonBody));
this.log.debug(`Webhook v2 Authentication ${webhookAuthentication ? 'passed' : 'failed'}`);
if (!webhookAuthentication) {
this.log.warn('Webhook v2 Authentication failed unknown sender');
}
}
this.log.info(`Test received on Rachio listener. Webhooks are configured correctly! Authorization ${webhookAuthentication ? 'passed' : 'failed'}`);
response.writeHead(200);
const x = `${new Date().toTimeString()} \nWebhooks are configured correctly! \nAuthorization ${webhookAuthentication ? 'passed' : 'failed'}`;
response.write(x);
response.write('\nFailing authorization is expected with this test.');
return response.end();
}
else if (request.method === 'POST' && request.url === '/') {
let body = [];
request.on('data', (chunk) => {
body.push(chunk);
});
request.on('end', () => {
try {
body = Buffer.concat(body).toString().trim();
const jsonBody = JSON.parse(body);
//check v1 authentication
if (request.headers.authorization) {
webhookAuthentication = this.checkAuth(request);
this.log.debug(`Webhook v1 Authentication ${webhookAuthentication ? 'passed' : 'failed'}`);
if (!webhookAuthentication) {
this.log.warn('Webhook v1 Authentication failed unknown sender');
response.writeHead(403);
return response.end();
}
}
//check v2 authentication SHT
if (request.headers['x-signature']) {
webhookAuthentication = this.checkKey(this.platform.token, request.headers['x-signature'], JSON.stringify(jsonBody));
this.log.debug(`Webhook v2 Authentication ${webhookAuthentication ? 'passed' : 'failed'}`);
if (!webhookAuthentication) {
this.log.warn('Webhook v2 Authentication failed unknown sender');
response.writeHead(403);
return response.end();
}
}
//check for basic auth enabled
if (this.platform.useBasicAuth && !request.headers.authorization && !request.headers['x-signature']) {
webhookAuthentication = false;
this.log.debug(`Webhook Authentication ${webhookAuthentication ? 'passed' : 'failed'}`);
if (!webhookAuthentication) {
this.log.warn('Webhook Authentication failed');
response.writeHead(403);
return response.end();
}
}
if (this.platform.showWebhookMessages) {
this.log.debug(`webhook request received from <${jsonBody.externalId}> ${JSON.stringify(jsonBody, null, 2)}`);
}
switch (jsonBody.resourceType) {
case 'IRRIGATION_CONTROLLER':
if (jsonBody.externalId === this.platform.webhook_key) {
const index = this.platform.accessories.findIndex(accessory => accessory.UUID === jsonBody.resourceId);
const irrigationAccessory = this.platform.accessories[index];
const irrigationSystemService = irrigationAccessory.getService(this.Service.IrrigationSystem);
if (jsonBody.payload.zoneNumber) {
const valveIndex = this.platform.zoneList.filter((check) => {
return check.deviceId == jsonBody.resourceId;
}).findIndex((zone) => zone.zone == jsonBody.payload.zoneNumber);
const zoneId = this.platform.zoneList[valveIndex].zoneId;
const zone = irrigationAccessory.getServiceById(this.Service.Valve, zoneId);
this.log.debug(`Webhook match found for ${zone.getCharacteristic(this.Characteristic.Name).value} will update zone service`);
this.eventMsg(irrigationSystemService, zone, jsonBody);
}
else if (jsonBody.payload.scheduleId) {
const schedule = irrigationAccessory.getServiceById(this.Service.Switch, jsonBody.payload.scheduleId);
if (this.platform.showSchedules) {
this.log.debug(`Webhook match found for ${schedule.getCharacteristic(this.Characteristic.Name).value} will update schedule service`);
this.eventMsg(irrigationSystemService, schedule, jsonBody);
}
else {
this.log.debug(`Skipping Webhook for ${jsonBody.scheduleName} service, optional schedule switch is not configured`);
}
}
response.writeHead(204);
return response.end();
}
else {
this.log.warn(`Webhook received from an unknown external id ${jsonBody.externalId}`);
response.writeHead(404);
return response.end();
}
case 'VALVE':
if (jsonBody.externalId === this.platform.webhook_key) {
const index = this.platform.accessories.findIndex(accessory => accessory.UUID === jsonBody.resourceId);
const valveAccessory = this.platform.accessories[index];
const valveService = valveAccessory.getService(this.Service.Valve);
this.log.debug(`Webhook match found for ${valveService.getCharacteristic(this.Characteristic.Name).value} will update valve service`);
this.eventMsg(null, valveService, jsonBody);
response.writeHead(204);
return response.end();
}
else {
this.log.warn(`Webhook received from an unknown external id ${jsonBody.externalId}`);
response.writeHead(404);
return response.end();
}
case 'PROGRAM':
if (jsonBody.externalId === this.platform.webhook_key) {
const index = this.platform.accessories.findIndex(accessory => accessory.UUID === jsonBody.resourceId);
const valveAccessory = this.platform.accessories[index];
const program = valveAccessory.getService(this.Service.Switch);
this.log.debug(`Webhook match found for ${program.getCharacteristic(this.Characteristic.Name).value} will update valve service`);
this.eventMsg(null, program, jsonBody);
response.writeHead(204);
return response.end();
}
else {
this.log.warn(`Webhook received from an unknown external id ${jsonBody.externalId}`);
response.writeHead(404);
return response.end();
}
default: //v1 webhooks
if (jsonBody.externalId === this.platform.webhook_key) {
const index = this.platform.accessories.findIndex(accessory => accessory.UUID === jsonBody.resourceId);
const irrigationAccessory = this.platform.accessories[index];
const irrigationSystemService = irrigationAccessory.getService(this.Service.IrrigationSystem);
if (jsonBody.zoneId) {
const zone = irrigationAccessory.getServiceById(this.Service.Valve, jsonBody.zoneId);
this.log.debug(`Webhook match found for ${zone.getCharacteristic(this.Characteristic.Name).value} will update zone service`);
this.eventMsg(irrigationSystemService, zone, jsonBody);
}
else if (jsonBody.deviceId && jsonBody.subType.includes('SLEEP')) {
if (this.platform.showStandby) {
const switchService = irrigationAccessory.getServiceById(this.Service.Switch, this.platform.genUUID(jsonBody.deviceName + ' Standby'));
this.log.debug(`Webhook match found for ${irrigationSystemService.getCharacteristic(this.Characteristic.Name).value} will update irrigation service`);
this.eventMsg(irrigationSystemService, switchService, jsonBody);
}
else {
this.log.debug(`Skipping Webhook for ${jsonBody.deviceName} service, optional standby switch is not configured`);
}
}
response.writeHead(204);
return response.end();
}
else {
this.log.warn(`Webhook received from an unknown external id ${jsonBody.externalId}`);
response.writeHead(404);
return response.end();
}
}
}
catch (err) {
this.log.error(`Error parsing webhook request ${err}`);
response.writeHead(404);
return response.end();
}
});
}
});
server.listen(this.platform.internal_webhook_port, () => {
this.log.success(`This server is listening on port ${this.platform.internal_webhook_port}`);
if (this.platform.useBasicAuth) {
this.log.info('Using HTTP basic authentication for Webhooks');
}
this.log.info(`Make sure your router has port fowarding turned on for port ${this.platform.external_webhook_port} to this server's IP address and this port ${this.platform.internal_webhook_port}, unless you are using a relay service.`);
});
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
this.log.error(`Port ${this.platform.internal_webhook_port} is already in use, will try to resolve`);
setTimeout(() => {
this.log.warn('Attempting to resolve port in use');
this.configureListener();
}, 5000);
//setTimeout(() => process.exit(143), 30000);
}
;
});
}
else {
this.log.warn('Webhook support is disabled. This plugin will not sync Homekit to realtime events from other sources without Webhooks support.');
}
return;
}
checkKey(secret, signature, message) {
let pass = false;
const hash = createHmac('sha256', secret)
.update(message)
.digest('hex');
if (signature == hash) {
pass = true;
}
return pass;
}
checkAuth(message) {
let pass = false;
const b64encoded = Buffer.from(this.platform.user + ':' + this.platform.password, 'utf8').toString('base64');
if (message.headers.authorization == 'Basic ' + b64encoded) {
pass = true;
}
else {
this.log.debug(`webhook v1 request authorization header = ${message.headers.authorization}`);
this.log.debug(`webhook v1 expected authorization header = ${'Basic ' + b64encoded}`);
}
return pass;
}
webMessage(listener) {
this.eventMsg = (systemService, service, myJson) => {
listener(systemService, service, myJson);
};
}
localMessage(systemService, service, myJson) {
if (this.platform.showWebhookMessages) {
this.log.debug(`webhook recieved from <${this.platform.webhook_key_local}> ${JSON.stringify(myJson, null, 2)}`);
}
this.rachio.updateService(systemService, service, myJson);
}
}
//# sourceMappingURL=listener.js.map