UNPKG

homebridge-gsh

Version:
393 lines 18.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Hap = void 0; const hap_client_1 = require("@homebridge/hap-client"); const fs = __importStar(require("fs")); const rxjs_1 = require("rxjs"); const operators_1 = require("rxjs/operators"); const hap_types_1 = require("./hap-types"); const door_1 = require("./types/door"); const node_crypto_1 = require("node:crypto"); const fan_1 = require("./types/fan"); const fan_v2_1 = require("./types/fan-v2"); const garage_door_opener_1 = require("./types/garage-door-opener"); const heater_cooler_1 = require("./types/heater-cooler"); const humidity_sensor_1 = require("./types/humidity-sensor"); const lightbulb_1 = require("./types/lightbulb"); const lock_mechanism_1 = require("./types/lock-mechanism"); const security_system_1 = require("./types/security-system"); const switch_1 = require("./types/switch"); const television_1 = require("./types/television"); const temperature_sensor_1 = require("./types/temperature-sensor"); const thermostat_1 = require("./types/thermostat"); const window_1 = require("./types/window"); const window_covering_1 = require("./types/window-covering"); class Hap { constructor(socket, log, pin, config, api) { this.services = []; this.types = { Door: new door_1.Door(), Fan: new fan_1.Fan(), Fanv2: new fan_v2_1.Fanv2(), GarageDoorOpener: new garage_door_opener_1.GarageDoorOpener(), HeaterCooler: new heater_cooler_1.HeaterCooler(this), HumiditySensor: new humidity_sensor_1.HumiditySensor(), Lightbulb: new lightbulb_1.Lightbulb(), LockMechanism: new lock_mechanism_1.LockMechanism(), Outlet: new switch_1.Switch('action.devices.types.OUTLET'), SecuritySystem: new security_system_1.SecuritySystem(), Switch: new switch_1.Switch('action.devices.types.SWITCH'), Television: new television_1.Television(), TemperatureSensor: new temperature_sensor_1.TemperatureSensor(this), Thermostat: new thermostat_1.Thermostat(this), Window: new window_1.Window(), WindowCovering: new window_covering_1.WindowCovering(), }; this.reportStateSubject = new rxjs_1.Subject(); this.pendingStateReport = []; this.evTypes = [ hap_types_1.Characteristic.Active, hap_types_1.Characteristic.On, hap_types_1.Characteristic.CurrentPosition, hap_types_1.Characteristic.TargetPosition, hap_types_1.Characteristic.CurrentDoorState, hap_types_1.Characteristic.TargetDoorState, hap_types_1.Characteristic.Brightness, hap_types_1.Characteristic.HeatingThresholdTemperature, hap_types_1.Characteristic.Hue, hap_types_1.Characteristic.Saturation, hap_types_1.Characteristic.LockCurrentState, hap_types_1.Characteristic.LockTargetState, hap_types_1.Characteristic.TargetHeatingCoolingState, hap_types_1.Characteristic.TargetTemperature, hap_types_1.Characteristic.CoolingThresholdTemperature, hap_types_1.Characteristic.CurrentTemperature, hap_types_1.Characteristic.CurrentRelativeHumidity, hap_types_1.Characteristic.SecuritySystemTargetState, hap_types_1.Characteristic.SecuritySystemCurrentState, ]; this.instanceBlacklist = []; this.accessoryFilter = []; this.accessorySerialFilter = []; this.waitForNoMoreDiscoveries = () => { if (this.discoveryTimeout) { clearTimeout(this.discoveryTimeout); } this.discoveryTimeout = setTimeout(() => { this.log.debug('No more instances discovered, publishing services'); this.hapClient.removeListener('instance-discovered', this.waitForNoMoreDiscoveries); this.start(); this.requestSync(); this.hapClient.on('instance-discovered', this.requestSync.bind(this)); }, this.configDiscoveryTimeout * 1000); }; this.config = config; this.socket = socket; this.log = log; this.pin = pin; this.api = api; this.configDiscoveryTimeout = (config.discoveryTimeout ? config.discoveryTimeout : 5); this.configDiscoveryWait = (config.discoveryWait ? config.discoveryWait : 15); this.accessoryFilter = config.accessoryFilter || []; this.accessoryFilterInverse = config.accessoryFilterInverse || false; this.accessorySerialFilter = config.accessorySerialFilter || []; this.instanceBlacklist = config.instanceDenylist || []; this.log.debug(`Waiting ${this.configDiscoveryWait} seconds before starting instance discovery, and ${this.configDiscoveryTimeout} seconds after last device is discovered to publish to Google.`); this.startTimeout = setTimeout(() => { this.discover(); }, this.configDiscoveryWait * 1000); this.reportStateSubject .pipe((0, operators_1.map)((i) => { if (!this.pendingStateReport.includes(i)) { this.pendingStateReport.push(i); } }), (0, operators_1.debounceTime)(1000)) .subscribe((data) => { const pendingStateReport = this.pendingStateReport; this.pendingStateReport = []; this.processPendingStateReports(pendingStateReport); }); } discover() { return __awaiter(this, void 0, void 0, function* () { this.hapClient = new hap_client_1.HapClient({ config: this.config, pin: this.pin, logger: this.log, }); this.waitForNoMoreDiscoveries(); this.hapClient.on('instance-discovered', this.waitForNoMoreDiscoveries); this.hapClient.on('hapEvent', (event) => { this.handleHapEvent(event); }); }); } start() { return __awaiter(this, void 0, void 0, function* () { this.services = yield this.loadAccessories(); this.log.info(`Discovered ${this.services.length} accessories`); this.ready = true; yield this.buildSyncResponse(); const evServices = this.services.filter(x => this.evTypes.some(uuid => x.serviceCharacteristics.find(c => c.uuid === uuid))); this.log.debug(`Monitoring ${evServices.length} services for changes`); const monitor = yield this.hapClient.monitorCharacteristics(evServices); monitor.on('service-update', (services) => { services.map((service) => { this.reportStateSubject.next(service.uniqueId); }); }); }); } buildSyncResponse() { return __awaiter(this, void 0, void 0, function* () { const devices = this.services.map((service) => { if (!this.types[service.type]) { return; } return this.types[service.type].sync(service); }); return devices; }); } requestSync() { return __awaiter(this, void 0, void 0, function* () { if (this.syncTimeout) { clearTimeout(this.syncTimeout); } this.syncTimeout = setTimeout(() => { this.log.info('Sending Sync Request'); this.socket.sendJson({ type: 'request-sync', }); }, 15000); }); } query(devices) { return __awaiter(this, void 0, void 0, function* () { const response = {}; for (const device of devices) { const service = this.services.find(x => x.uniqueId === device.id); if (service) { yield this.getStatus(service); response[device.id] = this.types[service.type].query(service); } else { response[device.id] = {}; } } return response; }); } execute(commands) { return __awaiter(this, void 0, void 0, function* () { const response = []; for (const command of commands) { for (const device of command.devices) { const service = this.services.find(x => x.uniqueId === device.id); this.log.debug(`Processing command ${command.execution[0].command} for ${device.id} and ${service}`); if (service) { if (this.config.twoFactorAuthPin && this.types[service.type].twoFactorRequired && this.types[service.type].is2faRequired(command) && !(command.execution.length && command.execution[0].challenge && command.execution[0].challenge.pin === this.config.twoFactorAuthPin.toString())) { this.log.info('Requesting Two Factor Authentication Pin'); response.push({ ids: [device.id], status: 'ERROR', errorCode: 'challengeNeeded', challengeNeeded: { type: 'pinNeeded', }, }); } else { try { response.push(yield this.types[service.type].execute(service, command)); } catch (error) { if (this.config.debug) { this.log.debug(`Error executing service: ${JSON.stringify(service)}`); this.log.debug(`Error executing command: ${JSON.stringify(command)}`); this.log.debug(error); } this.log.error(`Error executing command: ${error.message}`); response.push({ ids: [device.id], status: 'ERROR', debugString: error.message, }); } } } else { this.log.error(`Device not found: ${device.id}`); response.push({ ids: [device.id], status: 'OFFLINE', errorCode: 'deviceNotFound', }); } } } return response; }); } getStatus(service) { return __awaiter(this, void 0, void 0, function* () { return yield service.refreshCharacteristics(); }); } loadAccessories() { return __awaiter(this, void 0, void 0, function* () { return this.hapClient.getAllServices().then((services) => { if (this.config.debug && process.uptime() < 600) { try { const storagePath = this.api.user.storagePath() + '/homebridge-gsh-discovery.json'; this.log.warn(`Writing Discovery Response to ${storagePath}`); fs.writeFileSync(storagePath, JSON.stringify(services, null, 2)); } catch (e) { this.log.error(`Failed to write discovery response to file: ${e.message}`); } } services = services.filter(x => this.types[x.type] !== undefined); this.log.debug(`Loaded ${services.length} accessories from Homebridge - pre filter`); if (this.accessoryFilterInverse) { services = services.filter(x => this.accessoryFilter.includes(x.serviceName)); } else { services = services.filter(x => !this.accessoryFilter.includes(x.serviceName)); } services = services.filter(x => !this.accessorySerialFilter.includes(x.accessoryInformation['Serial Number'])); services = services.filter(x => { if (this.types[x.type].twoFactorRequired && !this.config.twoFactorAuthPin && !this.config.disablePinCodeRequirement) { this.log.warn(`Not registering ${x.serviceName} - Pin code has not been set and is required for secure ` + `${x.type} accessory types. See https://git.io/JUQWX`); return false; } else { return true; } }); services = services.map(service => { return Object.assign(Object.assign({}, service), { uniqueId: (0, node_crypto_1.createHash)('sha256') .update(`${service.instance.username}${service.aid}${service.iid}${service.uuid}`) .digest('hex') }); }); this.log.debug(`Returned ${services.length} accessories from Homebridge - post filter`); return services; }).catch((e) => { var _a; if (((_a = e.response) === null || _a === void 0 ? void 0 : _a.status) === 401) { this.log.warn('Homebridge must be running in insecure mode to view and control accessories from this plugin.'); } else { this.log.error(`Failed load accessories from Homebridge: ${e.message}`); } return []; }); }); } handleHapEvent(events) { return __awaiter(this, void 0, void 0, function* () { for (const event of events) { const index = this.services.findIndex(item => item.uniqueId === event.uniqueId); if (index === -1) { this.log.debug(`[handleHapEvent] Service not found in services list ${event}`); return; } else { this.services[index] = event; this.reportStateSubject.next(event.uniqueId); } } }); } processPendingStateReports(pendingStateReport) { return __awaiter(this, void 0, void 0, function* () { const states = {}; for (const uniqueId of pendingStateReport) { const service = this.services.find(x => x.uniqueId === uniqueId); states[service.uniqueId] = this.types[service.type].query(service); } return yield this.sendStateReport(states); }); } sendFullStateReport() { return __awaiter(this, void 0, void 0, function* () { const states = {}; if (!this.services.length) { return; } this.services.map((service) => { if (!this.types[service.type]) { return; } return states[service.uniqueId] = this.types[service.type].query(service); }); return yield this.sendStateReport(states); }); } sendStateReport(states, requestId) { return __awaiter(this, void 0, void 0, function* () { const payload = { requestId, type: 'report-state', body: states, }; this.log.debug('Sending State Report'); this.log.debug(JSON.stringify(payload, null, 2)); this.socket.sendJson(payload); }); } destroy() { return __awaiter(this, void 0, void 0, function* () { if (this.startTimeout) { clearTimeout(this.startTimeout); } if (this.discoveryTimeout) { clearTimeout(this.discoveryTimeout); } if (this.syncTimeout) { clearTimeout(this.syncTimeout); } if (this.hapClient) { this.hapClient.destroy(); } }); } } exports.Hap = Hap; //# sourceMappingURL=hap.js.map