UNPKG

homebridge-homekit-proxy

Version:

Homebridge Homekit Proxy allows you to control HomeKit-enabled Devices directly from within HomeBridege. (Based on homebridge-homekit-controller by MartinPham)

634 lines 29.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.HKClient = void 0; const settings_1 = require("./settings"); const HttpWrapper_1 = require("./HttpWrapper"); function copyProps(source, char) { const props = { perms: [], format: '', }; if (source.minValue !== undefined) { props.minValue = source.minValue; } if (source.maxValue !== undefined) { props.maxValue = source.maxValue; } if (source.minStep !== undefined) { props.minStep = source.minStep; } if (source.format !== undefined) { props.format = source.format; } if (source.perms !== undefined) { props.perms = source.perms; } if (source.unit !== undefined) { props.unit = source.unit; } if (source.description !== undefined) { props.description = source.description; } if (source.maxLen !== undefined) { props.maxLen = source.maxLen; } if (source.maxDataLen !== undefined) { props.maxDataLen = source.maxDataLen + 4; } if (source.validValues !== undefined) { props.validValues = source.validValues; } if (source.validValueRanges !== undefined) { props.validValueRanges = source.validValueRanges; } if (source.adminOnlyAccess !== undefined) { props.adminOnlyAccess = source.adminOnlyAccess; } char.setProps(props); } class HKClient { constructor(serviceConfig, parent) { this.didFinishWasDefered = false; this.accesoriesWereLoaded = false; this.didFinishStartup = false; this.parent = parent; this.serviceConfig = serviceConfig; this.name = serviceConfig.name ? serviceConfig.name : 'Unnamed Accessory'; this.deviceID = this.name; //data this.supportedAccessories = []; if (serviceConfig.uniquePrefix) { this.uuid = parent.api.hap.uuid.generate(`${serviceConfig.uniquePrefix}.${this.name}`); } else { this.uuid = parent.api.hap.uuid.generate(this.name); } this.fakeGato = { interval: 0, timer: undefined, }; const self = this; this.client = new HttpWrapper_1.HttpWrapper(this.parent.log, this.serviceConfig.id, this.serviceConfig.address, this.serviceConfig.port, this.serviceConfig.pairingData); this.client .getAccessories() .then((inData) => { this._preloadValues(inData) .then(() => { //looks like this Gatt-Result is the actual return type for httpClient as well self._loadDevices(inData); self.accesoriesWereLoaded = true; if (this.serviceConfig.logFoundServices) { this.logSourceServices(); } if (this.didFinishWasDefered) { this.didFinishWasDefered = false; this.didFinishLaunching(); } }) .catch((e) => { self.parent.log.error('Error', e); }); }) .catch((e) => { self.parent.log.error('Error', e); }); parent.api.on("didFinishLaunching" /* DID_FINISH_LAUNCHING */, () => { if (self.accesoriesWereLoaded) { self.didFinishLaunching(); } else { this.didFinishWasDefered = true; } }); if (serviceConfig.enableHistory) { if (serviceConfig.historyInterval) { this.fakeGato.interval = Math.max(30, Math.min(600, serviceConfig.historyInterval)); } else { this.fakeGato.interval = 600; } } if (serviceConfig.proxyAll === undefined) { serviceConfig.proxyAll = false; } } async _preloadValues(data) { const list = data.accessories .map((a) => { return a.services.map((s) => { const r = s.characteristics .filter((c) => c.value === null) .map((c) => { const cany = c; cany.cname = `${a.aid}.${c.iid}`; cany.hadResonse = false; return c; }); return r; }); }) .flat(2); const chars = list.map((c) => c.cname); if (chars.length > 0) { const inData = await this.con().getCharacteristics(chars, { meta: false, perms: false, type: false, ev: false, }); //console.log(inData) const newValues = inData.characteristics.filter((d) => d.value !== undefined && d.value != null); this.parent.log.debug('Preloaded Values'); this.parent.log.debug(newValues); data.accessories.forEach((a) => a.services.forEach((s) => s.characteristics.forEach((c) => { const val = newValues.find((v) => v.aid == a.aid && c.iid == v.iid); if (val !== undefined) { c.value = val.value; } }))); } } _loadDevices(data) { this.supportedAccessories = data.accessories.map((acc) => { const aRes = { fakeGato: { room: {}, motion: {}, history: [], }, services: acc.services.map((s) => { //console.log('Service:', s) const name = s.characteristics.filter((c) => c.type === '23' || c.type === '00000023' || c.type === this.parent.api.hap.Characteristic.Name.UUID); let displayName = undefined; if (name.length > 0) { displayName = name[0].value; } if (displayName === undefined || displayName === null) { displayName = `c-${s.iid}`; } const cl = this.parent.supported.classForService(s.type, this.serviceConfig.proxyAll ? s : undefined); const sRes = { create: cl, uuid: cl !== undefined ? cl.UUID : s.type, displayName: displayName, uname: `${this.uuid}.${acc.aid}.${s.iid}`, iid: s.iid, characteristics: s.characteristics .map((c) => { const ccl = this.parent.supported.classForChar(c.type, this.serviceConfig.proxyAll ? c : undefined); const cRes = { create: ccl, uuid: ccl !== undefined ? ccl.UUID : c.type, iid: c.iid === undefined ? 0 : c.iid, uname: `${this.uuid}.${acc.aid}.${s.iid}.${c.iid}`, cname: `${acc.aid}.${c.iid}`, value: c.value, source: c, connect: undefined, allowValueUpdates: true, }; return cRes; }) .filter((c) => { if (c.create === undefined) { this.parent.log.warn(`${this.name}: Missing Chracteristic in ${s.type} with type ${c.source.type}`); if (this.serviceConfig.logFoundServices) { this.parent.log.warn(JSON.stringify(c.source, null, 4)); } return false; } return true; }), source: s, }; return sRes; }), }; return aRes; }); } logSourceServices() { this.parent.log.info(this.supportedAccessories .map((a, i) => { const services = a.services .map((s) => { var _a; let name = '?'; if (s.create) { const ss = new s.create('dn', 'st'); name = ss.displayName; } const chars = s.characteristics .map((c) => { var _a; let name = '?'; let props = ''; if (c.create) { const cc = new c.create(); name = cc.displayName; props = JSON.stringify(cc.props); } return `${name}, ${(_a = c.create) === null || _a === void 0 ? void 0 : _a.UUID}: ${c.value}\n (${props})}`; }) .join('\n - '); return `Service ${name}, ${s.source.type}, ${(_a = s.create) === null || _a === void 0 ? void 0 : _a.UUID}\n - ${chars}`; }) .join('\n - '); return `Accessory ${i + 1}:\n - ${services}`; }) .join('\n')); } con() { return this.client; } _updateCharacteristicValue(char, c) { if (!c.allowValueUpdates) { return; } const self = this; self.parent.log.debug(`${self.name} - send get ${char.displayName} (${c.cname})`); this.con() .getCharacteristics([c.cname], { meta: false, perms: false, type: false, ev: false, }) .then((inData) => { const data = inData; const old = c.value; c.value = this.checkValue(data.characteristics[0].value, char.props); self.parent.log.debug(`${self.name} - received ${char.displayName}=${c.value} (was ${old}, ${c.cname})`); if (c.value != null) { char.updateValue(c.value); } if (char.chain && char.chainValue) { this.parent.log.debug(` ==> Chain received value from ${char.displayName} to ${char.chain.displayName} (Value=${char.chainValue()})`); char.chain.updateValue(char.chainValue()); } }) .catch((e) => { self.parent.log.error(`${self.name} - get error ${char.displayName}=${c.value} (${c.cname}, ${e})`); console.error(e); }); } checkValue(value, props) { let didChange = true; if (value === null || value === undefined) { value = 0; didChange = false; } if (props.minValue !== undefined) { didChange = true; value = Math.max(props.minValue, value); } if (props.maxValue !== undefined) { didChange = true; value = Math.min(props.maxValue, value); } if (props.format === "data" /* DATA */ && props.maxDataLen) { didChange = true; } if (props.format === "string" /* STRING */) { didChange = true; value = '' + value; } if (!didChange) { return null; } return value; } _setCharacteristicValue(char, value, c) { value = this.checkValue(value, char.props); if (value == null) { return; } const data = {}; const self = this; data[c.cname] = value; self.parent.log.debug(`${self.name} - send set ${char.displayName}=${c.value} (${c.cname})`); this.con() .setCharacteristics(data) .then(() => { c.value = value; self.parent.log.info(`${self.name} - wrote ${char.displayName}=${c.value} (${c.cname})`); }) .catch((e) => { self.parent.log.error(`${self.name} - set failed ${char.displayName}=${c.value} (${c.cname}, ${e})`); }); } initAccessoryService(sData, service) { const self = this; sData.characteristics.forEach((c) => { c.allowValueUpdates = true; if (c.create) { if (c.create.UUID === this.parent.api.hap.Characteristic.SerialNumber.UUID && this.serviceConfig.uniquePrefix) { this.deviceID = c.value; c.allowValueUpdates = false; c.value = `${this.serviceConfig.uniquePrefix}-${this.deviceID}`; } else if (c.create.UUID === this.parent.api.hap.Characteristic.Name.UUID && this.serviceConfig.name) { c.allowValueUpdates = false; c.value = this.serviceConfig.name; } if (c.source.value !== undefined) { const value = this.checkValue(c.value, c.source); if (value != null) { service.setCharacteristic(c.create, value); } } const char = service.characteristics.find((cc) => cc.UUID === c.uuid); //const char = service.getCharacteristic(c.create as any) as ExtendedCharacteristic if (char === undefined) { this.parent.log.error('Unknown Characteristic: ', c.cname, service.UUID, service.characteristics.map((c) => `${c.iid}-${c.displayName}`).join(',')); return; } c.connect = char; copyProps(c.source, char); if (c.source.perms && c.source.perms.indexOf('pr') >= 0) { if (char.hasOnGet) { console.log('ALREADY HAS a GET WATCH', c.cname, c.uuid); } else { console.log('ADDING GET WATCH', c.cname, c.uuid); } char.hasOnGet = true; char.on('get', (callback) => { c.value = this.checkValue(c.value, char.props); //console.log('GET GET GET', c.cname, callback) callback(null, c.value); self._updateCharacteristicValue(char, c); }); } if ((c.source.perms && c.source.perms.indexOf('pw') >= 0) || c.source.perms.indexOf('tw') >= 0) { char.on('set', (value, callback) => { self._setCharacteristicValue(char, value, c); //console.log('SET SET SET') if (c.source.perms && c.source.perms.indexOf('wr') >= 0) { callback(null, value); } else { callback(null, null); } }); } if (c.allowValueUpdates && c.source.perms && c.source.perms.indexOf('ev') >= 0 && (c.source.ev === undefined || c.source.ev)) { const cl = this.con(); cl.on(c, (data) => { if (data.value !== undefined && data.value !== null) { this.parent.log.debug(`${this.name} - Got Event for ${char.displayName} (${c.cname}). Changed value ${c.value} => ${data.value}`); c.value = this.checkValue(data.value, char.props); if (c.value != null) { char.updateValue(data.value); } if (char.chain && char.chainValue) { this.parent.log.debug(` ==> Chain event from ${char.displayName} to ${char.chain.displayName} (Value=${char.chainValue()})`); char.chain.updateValue(char.chainValue()); } } }); this.parent.log.debug(`${char.displayName} (${c.cname}) Has Events, trying to subscribe`); cl.subscribeCharacteristics([c.cname]) .then((conn) => { this.parent.log.debug(`${this.name}, ${char.displayName} (${c.cname}) Subscribed to Events`); c.connection = conn; }) .catch((e) => { this.parent.log.error(`${char.displayName} failed to Subscribe`, e); }); } } else { this.parent.log.warn(`Unable to create Characteristic ${c.uname} of type ${c.source.type}`); } }); } _serviceCreator(sData, accessory, allServices) { if (sData.create) { const hasSameService = allServices.filter((s) => s.uuid === sData.uuid).length > 1; const subtype = sData.displayName !== undefined && sData.displayName !== null ? sData.displayName : `${sData.iid}`; const serviceGeneral = accessory.getService(sData.create); //this method does some strange compating (uuid with displayname), better implement our own search... //let service: Service | undefined = accessory.getServiceById(sData.uuid, subtype) let service = hasSameService ? accessory.services.find((s) => s.UUID === sData.uuid && s.subtype === subtype) : serviceGeneral; //console.log(serviceGeneral !== undefined, service !== undefined) //some services are predefined without an iid, so we keep them // if (service === undefined && serviceGeneral !== undefined) { // const aService = new sData.create(subtype) // console.log('IID', serviceGeneral.iid, sData.iid) // if (serviceGeneral.iid === null || sData.iid === null) { // service = serviceGeneral // } // } if (service !== undefined) { this.parent.log.debug(`REUSING SERVICE', uuid=${service.UUID}, st=${subtype}, nst=${service.subtype}, niid=${service.iid}, iid=${sData.iid}`); accessory.removeService(service); service = undefined; } if (service === undefined) { let newService = new sData.create(sData.displayName, subtype); if (newService.UUID !== sData.uuid) { console.log(`nuuid=${newService.UUID}, uuid=${sData.uuid}`); newService = new sData.create(sData.displayName, sData.uuid); } this.parent.log.debug(`NEW SERVICE, uuid=${sData.uuid} nuuid=${newService.UUID}, st=${subtype}, nst=${newService.subtype}, niid=${newService.iid}, dn=${newService.displayName}`); try { service = accessory.addService(newService); } catch (e) { this.parent.log.error(`Unable to add new version of service UUID=${newService.UUID}, subtype=${subtype}`); this.parent.log.error(e); } } else { //this.parent.log.debug(`REUSING SERVICE', uuid=${service.UUID}, st=${subtype}, nst=${service.subtype}, niid=${service.iid}, iid=${sData.iid}`) } if (service) { this.initAccessoryService(sData, service); return; } } this.parent.log.warn(`Unable to create Service ${sData.uname} of type ${sData.source.type}`); } loadOrCreate(accessoryServices) { const self = this; const configuredAcc = this.parent.accessories.find((accessory) => accessory.UUID === this.uuid); // check the accessory was not restored from cache if (configuredAcc === undefined) { // create a new accessory const accessory = new this.parent.api.platformAccessory(this.name, this.uuid); this.parent.log.debug(`Building new Accessory ${accessory.displayName} (${accessory.UUID})`); accessoryServices.services.map((sData) => self._serviceCreator(sData, accessory, accessoryServices.services)); //console.log(accessoryServices.services) this.addAditionalServices(accessoryServices, accessory); // register the accessory try { this.parent.api.registerPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]); } catch (e) { this.parent.log.error(e); } } else { this.parent.log.debug(`Reconfiguring existing Accesory ${configuredAcc.displayName} (${configuredAcc.UUID})`); /*const services = */ accessoryServices.services.map((sData) => self._serviceCreator(sData, configuredAcc, accessoryServices.services)); //console.log('-----------------------') //console.log(accessoryServices.services) this.addAditionalServices(accessoryServices, configuredAcc); configuredAcc._wasUsed = true; } } didFinishLaunching() { const self = this; if (this.fakeGato.interval > 0) { this.amendFakeGato(); } this.supportedAccessories.forEach((acc) => self.loadOrCreate(acc)); this.didFinishStartup = true; this.parent.clientFinishedLaunching(self); if (this.fakeGato.interval > 0) { this.parent.log(`Enabeling History Log for '${this.name}' every ${this.fakeGato.interval} seconds`); this.fakegatoEvent(); this.fakeGato.timer = setInterval(self.fakegatoEvent.bind(self), this.fakeGato.interval * 1000); } } addAditionalServices(accessoryServices, accesory) { this.addFakeGatoService(accessoryServices.fakeGato, accesory); } _findTempService(has) { return has(this.parent.api.hap.Service.TemperatureSensor, this.parent.api.hap.Characteristic.CurrentTemperature); } _findHumidityService(has) { return has(this.parent.api.hap.Service.HumiditySensor, this.parent.api.hap.Characteristic.CurrentRelativeHumidity); } _findPPMService(has) { return [ has(this.parent.api.hap.Service.CarbonDioxideSensor, this.parent.api.hap.Characteristic.CarbonDioxideLevel), has(this.parent.api.hap.Service.AirQualitySensor, this.parent.api.hap.Characteristic.PM2_5Density), has(this.parent.api.hap.Service.AirQualitySensor, this.parent.api.hap.Characteristic.PM10Density), has(this.parent.api.hap.Service.CarbonMonoxideSensor, this.parent.api.hap.Characteristic.CarbonMonoxideLevel), ].find((i) => i !== undefined); } _findMotionService(has) { return has(this.parent.api.hap.Service.MotionSensor, this.parent.api.hap.Characteristic.MotionDetected); } amendFakeGato() { const self = this; if (this.fakeGato.interval <= 0) { return; } //console.log('COUNT', this.supportedAccessories.length) this.supportedAccessories.forEach((acc) => { //console.log('ACC', idx) const has = function (type, cType) { const s = acc.services.find((s) => s.create && s.create.UUID === type.UUID); if (s === undefined) { return undefined; } return s.characteristics.find((c) => c.create && c.create.UUID == cType.UUID); }; //find fitting services acc.fakeGato = { room: { temp: self._findTempService(has), humidity: self._findHumidityService(has), ppm: self._findPPMService(has), }, motion: { status: self._findMotionService(has), }, history: [], }; //console.log(idx, acc.fakeGato) //Add history Providers acc.fakeGato.history = []; if (acc.fakeGato.room.temp && acc.fakeGato.room.humidity && acc.fakeGato.room.ppm) { acc.fakeGato.history.push({ type: 'room', data: acc.fakeGato.room, logService: undefined }); } else if (acc.fakeGato.room.temp && acc.fakeGato.room.humidity) { acc.fakeGato.history.push({ type: 'weather', data: { temp: acc.fakeGato.room.temp, humidity: acc.fakeGato.room.humidity }, logService: undefined }); } else if (acc.fakeGato.motion.status) { acc.fakeGato.history.push({ type: 'motion', data: acc.fakeGato.motion, logService: undefined }); } }); } addFakeGatoService(fakeGato, accessory) { //add the fakeGato service if needed if (fakeGato && this.fakeGato.interval > 0) { const _accessory = accessory; _accessory.log = this.parent.log; fakeGato.history.forEach((h) => { const airQualityService = accessory.getService(this.parent.api.hap.Service.AirQualitySensor); if (airQualityService && h.type === 'room' && h.data.ppm) { let airQualityC = airQualityService.getCharacteristic(this.parent.supported.EveAirQuality); const valueGetter = () => { var _a, _b; if (h.data.ppm) { if (((_a = h.data.ppm.connect) === null || _a === void 0 ? void 0 : _a.UUID) === this.parent.api.hap.Characteristic.PM2_5Density.UUID || ((_b = h.data.ppm.connect) === null || _b === void 0 ? void 0 : _b.UUID) === this.parent.api.hap.Characteristic.PM10Density.UUID) { //sems to be ug/m3 not ppm => convert like https://github.com/simont77/fakegato-history/issues/107 return Math.max(450, h.data.ppm.value / 4.57); } else { return Math.max(450, h.data.ppm.value); } } return 0; }; if (airQualityC === undefined) { this.parent.log.debug(`Adding Eve PPM Characteristic to '${this.name}', ${this.deviceID}, ${this.serviceConfig.uniquePrefix ? this.serviceConfig.uniquePrefix : ''} = ${valueGetter()}`); airQualityService.setCharacteristic(this.parent.supported.EveAirQuality, valueGetter()); airQualityC = airQualityService.getCharacteristic(this.parent.supported.EveAirQuality); } else { this.parent.log.debug(`Updating initial value for Eve PPM Characteristic to ${valueGetter()}`); airQualityC.updateValue(valueGetter()); } if (h.data.ppm.connect) { h.data.ppm.connect.chain = airQualityC; h.data.ppm.connect.chainValue = valueGetter; } } h.logService = accessory.getService(this.parent.supported.FakeGatoService.UUID); if (h.logService === undefined) { this.parent.log.debug(`Adding FakeGatoService '${h.type}' to '${this.name}', ${this.deviceID}, ${this.serviceConfig.uniquePrefix ? this.serviceConfig.uniquePrefix : ''}`); h.logService = new this.parent.supported.FakeGatoService(h.type, accessory, { size: 4000, disableTimer: true }); } }); } } fakegatoEvent() { const self = this; this.parent.log.info('Timer - ', this.name); this.supportedAccessories .filter((acc) => acc.fakeGato && acc.fakeGato.history.length > 0) .forEach((acc) => { acc.fakeGato.history.forEach((h) => { const data = { time: Math.round(new Date().valueOf() / 1000), }; Object.keys(h.data).forEach((k) => { const c = h.data[k]; if (c) { if (k === 'ppm') { data[k] = Math.max(450, c.value); } else { data[k] = c.value; } //no auto updates, so trigger manual reads //if (c.source.perms && (c.source.perms.indexOf('ev')<0)) if (c.connect) { this.parent.log.info('FakeGato Triggered Value read', c.cname); self._updateCharacteristicValue(c.connect, c); } } }); this.parent.log.info('Writing FakeGato', h.type, data); h.logService.addEntry(data); }); }); } } exports.HKClient = HKClient; //# sourceMappingURL=HKClient.js.map