From e8e60b6118a27ebc06ccbca2d9e4c7335f85653a Mon Sep 17 00:00:00 2001 From: Northern Man <19808920+NorthernMan54@users.noreply.github.com> Date: Thu, 28 Nov 2024 18:59:34 -0500 Subject: [PATCH] WIP --- package-lock.json | 8 +- package.json | 2 +- src/HapDeviceRoutes.js | 2 +- src/hbConfigNode.js | 99 ++++++++++------------- src/hbControlNode.js | 1 + src/hbStatusNode.js | 18 +++-- test/node-red/flows.json | 166 +++++++++++++++++++++++++++++++++++++-- 7 files changed, 221 insertions(+), 75 deletions(-) diff --git a/package-lock.json b/package-lock.json index be45d4f..e70826a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.12", "license": "ISC", "dependencies": { - "@homebridge/hap-client": "2.0.5-beta.5", + "@homebridge/hap-client": "2.0.5-beta.7", "better-queue": ">=3.8.12", "debug": "^4.3.5" }, @@ -43,9 +43,9 @@ } }, "node_modules/@homebridge/hap-client": { - "version": "2.0.5-beta.5", - "resolved": "https://registry.npmjs.org/@homebridge/hap-client/-/hap-client-2.0.5-beta.5.tgz", - "integrity": "sha512-+XQC7Qs8RYhIH6t5YUhzrgDB24XV+Eozxjo7NUN2QdqCjqO+6H1YGci15ZE9KN0pYyE69yPRFjCP6AIKaJKiuQ==", + "version": "2.0.5-beta.7", + "resolved": "https://registry.npmjs.org/@homebridge/hap-client/-/hap-client-2.0.5-beta.7.tgz", + "integrity": "sha512-gvT3GLPBbdmopQ32j1HiRm13qBdF2aiKiJjrBJq/o6AuiX9UCeoCcYQVY0qWS8wQKxDIc9oevwuMEWY3vXQV3g==", "license": "MIT", "dependencies": { "axios": "1.7.7", diff --git a/package.json b/package.json index ffea2a3..b90d145 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "dependencies": { "better-queue": ">=3.8.12", "debug": "^4.3.5", - "@homebridge/hap-client": "2.0.5-beta.5" + "@homebridge/hap-client": "2.0.5-beta.7" }, "author": "NorthernMan54", "license": "ISC", diff --git a/src/HapDeviceRoutes.js b/src/HapDeviceRoutes.js index d2fdc21..592cdde 100644 --- a/src/HapDeviceRoutes.js +++ b/src/HapDeviceRoutes.js @@ -30,7 +30,7 @@ class HapDeviceRoutes { getDeviceById(req, res, key) { const devices = this.RED.nodes.getNode(req.params.id)?.[key]; if (devices) { - debug(`${key} devices`, devices.length); + // debug(`${key} devices`, devices.length); res.send(devices); } else { res.status(404).send(); diff --git a/src/hbConfigNode.js b/src/hbConfigNode.js index 09c4260..39a3ef5 100644 --- a/src/hbConfigNode.js +++ b/src/hbConfigNode.js @@ -8,6 +8,7 @@ class HBConfigNode { if (!config.jest) { RED.nodes.createNode(this, config); + // Initialize properties this.username = config.username; this.macAddress = config.macAddress || ''; this.users = {}; @@ -19,6 +20,7 @@ class HBConfigNode { this.monitorNodes = []; this.log = new Log(console, true); + // Initialize queue this.reqisterQueue = new Queue(this._register.bind(this), { concurrent: 1, autoResume: false, @@ -29,27 +31,25 @@ class HBConfigNode { }); this.reqisterQueue.pause(); + // Initialize HAP client this.hapClient = new HapClient({ config: { debug: false }, pin: config.username, logger: this.log, }); - this.waitForNoMoreDiscoveries(); this.hapClient.on('instance-discovered', this.waitForNoMoreDiscoveries); - + this.waitForNoMoreDiscoveries(); this.on('close', this.close.bind(this)); } } waitForNoMoreDiscoveries = () => { - if (this.discoveryTimeout) { - clearTimeout(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.hapClient.on('instance-discovered', async (instance) => { debug('instance-discovered', instance); await this.monitorDevices(); }); this.handleReady(); }, 5000); }; @@ -58,10 +58,9 @@ class HBConfigNode { this.hbDevices = await this.hapClient.getAllServices(); this.evDevices = this.toList({ perms: 'ev' }); this.ctDevices = this.toList({ perms: 'pw' }); - console.log('evDevices', this.evDevices); - console.log('ctDevices', this.ctDevices); + this.log.info(`Devices initialized: evDevices: ${this.evDevices.length}, ctDevices: ${this.ctDevices.length}`); this.handleDuplicates(this.evDevices); - debug('Queue', this.reqisterQueue.getStats()); + debug('Queue stats:', this.reqisterQueue.getStats()); this.reqisterQueue.resume(); } @@ -92,23 +91,19 @@ class HBConfigNode { const seenFullNames = new Set(); const seenUniqueIds = new Set(); - for (const endpoint of list) { - if (seenFullNames.has(endpoint.fullName)) { + list.forEach(endpoint => { + if (!seenFullNames.add(endpoint.fullName)) { console.warn('WARNING: Duplicate device name', endpoint.fullName); - } else { - seenFullNames.add(endpoint.fullName); } - if (seenUniqueIds.has(endpoint.uniqueId)) { - console.error('ERROR: Parsing failed, duplicate uniqueID.', endpoint.fullName); - } else { - seenUniqueIds.add(endpoint.uniqueId); + if (!seenUniqueIds.add(endpoint.uniqueId)) { + console.error('ERROR: Duplicate uniqueId detected.', endpoint.fullName); } - } + }); } register(clientNode) { - debug('Register %s -> %s', clientNode.type, clientNode.name); + debug('Register: %s type: %s', clientNode.type, clientNode.name); this.clientNodes[clientNode.id] = clientNode; this.reqisterQueue.push(clientNode); clientNode.status({ fill: 'yellow', shape: 'ring', text: 'connecting' }); @@ -116,64 +111,52 @@ class HBConfigNode { async _register(clientNodes, cb) { for (const clientNode of clientNodes) { - debug('_Register %s -> %s', clientNode.type, clientNode.name); - clientNode.hbDevice = this.hbDevices.find(service => { - const deviceUnique = `${service.instance.name}${service.instance.username}${service.accessoryInformation.Manufacturer}${service.serviceName}${service.uuid.slice(0, 8)}`; - if (clientNode.device === deviceUnique) { - clientNode.status({ fill: 'green', shape: 'dot', text: 'connected' }); - clientNode.emit('hbReady', service); + debug('_Register: %s type: %s', clientNode.type, clientNode.name); + const matchedDevice = this.hbDevices.find(service => + clientNode.device === `${service.instance.name}${service.instance.username}${service.accessoryInformation.Manufacturer}${service.serviceName}${service.uuid.slice(0, 8)}` + ); + + if (matchedDevice) { + clientNode.hbDevice = matchedDevice; + clientNode.status({ fill: 'green', shape: 'dot', text: 'connected' }); + clientNode.emit('hbReady', matchedDevice); + if (clientNode.config.type === 'hb-status') { + this.monitorNodes[clientNode.device] = matchedDevice; } - return clientNode.device === deviceUnique; - }); - - if (clientNode.config.type === 'hb-status') { - this.monitorNodes[clientNode.device] = clientNode.hbDevice; - } - - if (!clientNode.hbDevice) { - console.error('ERROR: _register - HB Device Missing', clientNode.name); + } else { + console.error('ERROR: Device registration failed', clientNode.name); } } + await this.monitorDevices(); + + cb(null); + } + + async monitorDevices() { if (Object.keys(this.monitorNodes).length) { this.monitor = await this.hapClient.monitorCharacteristics(Object.values(this.monitorNodes)); this.monitor.on('service-update', (services) => { - for (const service of services) { - const eventNodes = Object.values(this.clientNodes).filter( - clientNode => - clientNode.config.device === `${service.instance.name}${service.instance.username}${service.accessoryInformation.Manufacturer}${service.serviceName}${service.uuid.slice(0, 8)}` + services.forEach(service => { + const eventNodes = Object.values(this.clientNodes).filter(clientNode => + clientNode.config.device === `${service.instance.name}${service.instance.username}${service.accessoryInformation.Manufacturer}${service.serviceName}${service.uuid.slice(0, 8)}` ); - - eventNodes.forEach((eventNode) => { - if (eventNode._events && typeof eventNode.emit === 'function') { - eventNode.emit('hbEvent', service); - } - }); - } + eventNodes.forEach(eventNode => eventNode.emit('hbEvent', service)); + }); }); } - cb(null); } - /* - deregister(clientNode) { - clientNode.status({ text: 'disconnected', shape: 'ring', fill: 'red' }); - delete this.clientNodes[clientNode.id]; - } - */ close() { - if (this.hapClient) { - this.hapClient.destroy(); - } + this.hapClient?.destroy(); } - } -// Cameras have multiple AID's of 1.... + +// Filter unique devices by AID, service name, username, and port const filterUnique = (data) => { const seen = new Set(); return data.filter(item => { const uniqueKey = `${item.aid}-${item.serviceName}-${item.instance.username}-${item.instance.port}`; - console.log(uniqueKey, seen.has(uniqueKey)) if (seen.has(uniqueKey)) return false; seen.add(uniqueKey); return true; diff --git a/src/hbControlNode.js b/src/hbControlNode.js index 95123e1..daf1f5d 100644 --- a/src/hbControlNode.js +++ b/src/hbControlNode.js @@ -54,6 +54,7 @@ class HbControlNode extends hbBaseNode { const result = await this.hbDevice.setCharacteristicByType(key, message.payload[key]); results.push({ [result.type]: result.value }); } catch (error) { + console.log(error) this.error(`Failed to set value for "${key}": ${error.message}`); results.push({ [key]: `Error: ${error.message}` }); fill = 'red'; diff --git a/src/hbStatusNode.js b/src/hbStatusNode.js index fe9fef7..076814d 100644 --- a/src/hbStatusNode.js +++ b/src/hbStatusNode.js @@ -15,13 +15,19 @@ class HbStatusNode extends HbBaseNode { } const result = await this.hbDevice.refreshCharacteristics(); - this.status({ - text: this.statusText(JSON.stringify(await this.hbDevice.values)), - shape: 'dot', - fill: 'green' - }); + if (result) { + this.status({ + text: this.statusText(JSON.stringify(await this.hbDevice.values)), + shape: 'dot', + fill: 'green' + }); + + send(Object.assign(message, this.createMessage(result))); + } else { + this.status({ fill: "red", shape: "ring", text: "disconnected" }); + this.error("No response from device", this.name); + } - send(Object.assign(message, this.createMessage(result))); } } diff --git a/test/node-red/flows.json b/test/node-red/flows.json index 5b95d49..ae1a71c 100644 --- a/test/node-red/flows.json +++ b/test/node-red/flows.json @@ -243,7 +243,7 @@ "payload": "{\"On\": true}", "payloadType": "json", "x": 110, - "y": 280, + "y": 300, "wires": [ [ "6703815a8874b156" @@ -436,14 +436,14 @@ "id": "12ce98441601c981", "type": "hb-event", "z": "caef1e7b5b399e80", - "name": "Driveway 8E52", + "name": "Driveway", "Homebridge": "ECI-T24F2", "Manufacturer": "HikVision", - "Service": "CameraRTPStreamManagement", - "device": "ECI-T24F2CB:6F:94:DD:43:77HikVisionDriveway 8E5200000110", + "Service": "MotionSensor", + "device": "ECI-T24F2CB:6F:94:DD:43:77HikVisionDriveway00000085", "conf": "557aec8e8c47e61e", "sendInitialState": true, - "x": 340, + "x": 320, "y": 520, "wires": [ [ @@ -603,5 +603,161 @@ "wires": [ [] ] + }, + { + "id": "4b787bfb023ccf23", + "type": "hb-event", + "z": "caef1e7b5b399e80", + "name": "Backyard", + "Homebridge": "Default Model", + "Manufacturer": "NRCHKB", + "Service": "TemperatureSensor", + "device": "Default Model69:62:B7:AE:38:D4NRCHKBBackyard0000008A", + "conf": "557aec8e8c47e61e", + "sendInitialState": true, + "x": 160, + "y": 660, + "wires": [ + [ + "c68bef6563c5d07e" + ] + ] + }, + { + "id": "c68bef6563c5d07e", + "type": "debug", + "z": "caef1e7b5b399e80", + "name": "debug 7", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": true, + "complete": "payload", + "targetType": "msg", + "statusVal": "payload", + "statusType": "auto", + "x": 620, + "y": 740, + "wires": [] + }, + { + "id": "3c36252a6f45eed9", + "type": "hb-status", + "z": "caef1e7b5b399e80", + "name": "Backyard Tree", + "Homebridge": "Default Model", + "Manufacturer": "NRCHKB", + "Service": "TemperatureSensor", + "device": "Default Model69:62:B7:AE:38:D4NRCHKBBackyard Tree0000008A", + "conf": "557aec8e8c47e61e", + "x": 360, + "y": 800, + "wires": [ + [ + "c68bef6563c5d07e" + ] + ] + }, + { + "id": "3b7537939b63eee8", + "type": "inject", + "z": "caef1e7b5b399e80", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "60", + "crontab": "", + "once": true, + "onceDelay": "60", + "topic": "", + "payload": "", + "payloadType": "date", + "x": 150, + "y": 800, + "wires": [ + [ + "3c36252a6f45eed9" + ] + ] + }, + { + "id": "aabce5ff7fbbeec6", + "type": "status", + "z": "caef1e7b5b399e80", + "name": "", + "scope": [ + "3d7babac3a298e60", + "452e3e6171aa7a25", + "0ed3cd7e0d60beda", + "6703815a8874b156", + "82638dac6ac32bb1", + "6216377792cba653", + "12ce98441601c981", + "4b787bfb023ccf23", + "3c36252a6f45eed9" + ], + "x": 1020, + "y": 380, + "wires": [ + [ + "94f051597d18eaa3" + ] + ] + }, + { + "id": "94f051597d18eaa3", + "type": "debug", + "z": "caef1e7b5b399e80", + "name": "debug 8", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": true, + "complete": "true", + "targetType": "full", + "statusVal": "status.source.name", + "statusType": "msg", + "x": 1180, + "y": 380, + "wires": [] + }, + { + "id": "296297c79b544f59", + "type": "catch", + "z": "caef1e7b5b399e80", + "name": "", + "scope": null, + "uncaught": false, + "x": 1020, + "y": 300, + "wires": [ + [ + "0073218b1ebc53f1" + ] + ] + }, + { + "id": "0073218b1ebc53f1", + "type": "debug", + "z": "caef1e7b5b399e80", + "name": "debug 9", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": true, + "complete": "payload", + "targetType": "msg", + "statusVal": "payload", + "statusType": "auto", + "x": 1200, + "y": 300, + "wires": [] } ] \ No newline at end of file