homebridge-gsh
Version:
Google Smart Home
351 lines • 16 kB
JavaScript
"use strict";
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 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) {
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));
}, 5000);
};
this.config = config;
this.socket = socket;
this.log = log;
this.pin = pin;
this.accessoryFilter = config.accessoryFilter || [];
this.accessoryFilterInverse = config.accessoryFilterInverse || false;
this.accessorySerialFilter = config.accessorySerialFilter || [];
this.instanceBlacklist = config.instanceDenylist || [];
this.log.debug('Waiting 15 seconds before starting instance discovery...');
this.startTimeout = setTimeout(() => {
this.discover();
}, 15000);
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) {
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) => {
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