From 7a391cbdcdbea198f41b59490c81238a744302fd Mon Sep 17 00:00:00 2001 From: Luis Ramos Date: Fri, 29 Mar 2024 14:46:01 +0000 Subject: [PATCH 1/3] Add support for devices with multiple nodes In the eye of the Helki API, devices represent the client calling their remote API. Devices contain nodes. A node repesents a physical radiator/heater unit. Some heaters ship with a built-in controller device capable of talking to the API directly, in such cases, these devices contain a single node. Other heaters require connecting to a central controller device, which means you end up with a Device containing multiple nodes. This commit updates the device/node discovery logic to handle the latter scenario described above. This in theory will work for users despite of the heater setup they have. The accesory UUID is now seeded from the Node UID, previosuly was using the Device UID --- .vscode/settings.json | 4 +-- src/helki_client.ts | 1 + src/platform.ts | 76 ++++++++++++++++++++++++------------------- 3 files changed, 46 insertions(+), 35 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 31b20d1..8b6e306 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,8 @@ { "files.eol": "\n", "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "editor.rulers": [ 140 ], "eslint.enable": true -} \ No newline at end of file +} diff --git a/src/helki_client.ts b/src/helki_client.ts index 836c2f3..0b9901d 100644 --- a/src/helki_client.ts +++ b/src/helki_client.ts @@ -34,6 +34,7 @@ interface Node { addr: string; installed?: boolean; lost?: boolean; + uid: string; } interface GroupedDevices { diff --git a/src/platform.ts b/src/platform.ts index 4eef075..c9442d0 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -63,43 +63,53 @@ export class Technotherm implements DynamicPlatformPlugin { const home = groups.find(home => home.name === this.config.home); for (const group of groups) { - // Loop over the devices and register each one + // Loop over the devices in the group + // Sometimes a device contains a single radiator, sometimes multiple + // Each radiator is a node in the Helki API for (const device of group.devs) { - const uuid = this.api.hap.uuid.generate(device.dev_id); // Use dev_id to generate a UUID - const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid); + const nodes = await helki.getNodes(device.dev_id); - if (existingAccessory) { - // If the existing accessory is not in the configured home, remove it - if (home && existingAccessory?.context.home !== home.name) { - this.log.warn(`Removing accessory that does not match configured home "${home.name}: ${existingAccessory.displayName}"`); - try { - this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [existingAccessory]); - } catch (error: unknown) { - this.log.warn(`existing accessory: ${error}`); + if (nodes.length === 0) { + this.log.warn(`No nodes found for device: ${device.name}`); + continue; + } + + // Devices can have multiple nodes, register an accessory for each + for (const node of nodes) { + const accessoryName = node.name || device.name; + const accessoryUUID = this.api.hap.uuid.generate(node.uid); // Use dev_id to generate a UUID + const existingAccessory = this.accessories.find(accessory => accessory.UUID === accessoryUUID); + + if (existingAccessory) { + // If the existing accessory is not in the configured home, remove it + if (home && existingAccessory?.context.home !== home.name) { + this.log.warn(`Removing accessory that does not match configured home "${home.name}: ${existingAccessory.displayName}"`); + try { + this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [existingAccessory]); + } catch (error: unknown) { + this.log.warn(`existing accessory: ${error}`); + } + } else { + // Accessory exists, restore from cache + this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName); + new Radiator(this, existingAccessory, helki); } } else { - // Accessory exists, restore from cache - this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName); - new Radiator(this, existingAccessory, helki); - } - } else { - // Accessory doesn't exist, add new - const accessoryName = `${device.name}`; - const accessory = new this.api.platformAccessory(accessoryName, uuid); - const nodes = await helki.getNodes(device.dev_id); - const node = nodes[0]; - accessory.context.device = device; - accessory.context.node = node; - accessory.context.home = group.name; - if (home !== undefined && home.name === group.name) { - this.log.info('Adding new accessory:', accessoryName); - new Radiator(this, accessory, helki); - this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); - } - if (home === undefined) { - this.log.info('Adding new accessory:', accessoryName); - new Radiator(this, accessory, helki); - this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); + // Accessory doesn't exist, add new + const accessory = new this.api.platformAccessory(accessoryName, accessoryUUID); + accessory.context.device = device; + accessory.context.node = node; + accessory.context.home = group.name; + if (home !== undefined && home.name === group.name) { + this.log.info('Adding new accessory:', accessoryName); + new Radiator(this, accessory, helki); + this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); + } + if (home === undefined) { + this.log.info('Adding new accessory:', accessoryName); + new Radiator(this, accessory, helki); + this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); + } } } } From 671508f873638d69f68e3b2bd93004f14f6630d3 Mon Sep 17 00:00:00 2001 From: Luis Ramos Date: Fri, 29 Mar 2024 15:10:16 +0000 Subject: [PATCH 2/3] Add support for accessory renames Check that if a node name has been modified and if so, notify Homebridge so the accessory is updated after initial discovery --- src/platform.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/platform.ts b/src/platform.ts index c9442d0..fa730d8 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -90,6 +90,13 @@ export class Technotherm implements DynamicPlatformPlugin { this.log.warn(`existing accessory: ${error}`); } } else { + // If the node has been renamed, update the corresponding accessory + if (existingAccessory.displayName !== accessoryName) { + this.log.info(`Renaming accessory from ${existingAccessory.displayName} to ${accessoryName}`); + existingAccessory.displayName = accessoryName; + this.api.updatePlatformAccessories([existingAccessory]); + } + // Accessory exists, restore from cache this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName); new Radiator(this, existingAccessory, helki); From 850b7bdd163a0248c84b5bbd7cab7732e5b7cb34 Mon Sep 17 00:00:00 2001 From: Luis Ramos Date: Mon, 1 Apr 2024 00:14:12 +0100 Subject: [PATCH 3/3] Fallback UUID generation using node.uid to device.dev_id @duggan confirmed that node.uid is not defined on their end, so falling back to the original bahviour device.dev_id should handle this edge case whilst also maintaining backwards compatibility with the current implemention. --- src/helki_client.ts | 2 +- src/platform.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/helki_client.ts b/src/helki_client.ts index 0b9901d..96e4b5b 100644 --- a/src/helki_client.ts +++ b/src/helki_client.ts @@ -34,7 +34,7 @@ interface Node { addr: string; installed?: boolean; lost?: boolean; - uid: string; + uid?: string; } interface GroupedDevices { diff --git a/src/platform.ts b/src/platform.ts index fa730d8..e4dd57e 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -76,8 +76,10 @@ export class Technotherm implements DynamicPlatformPlugin { // Devices can have multiple nodes, register an accessory for each for (const node of nodes) { + // Use Node attributes for the accessory display name and UUID gen + // but fallback to Device attributes in case they're undefined (i.e. Devices with a single node) const accessoryName = node.name || device.name; - const accessoryUUID = this.api.hap.uuid.generate(node.uid); // Use dev_id to generate a UUID + const accessoryUUID = this.api.hap.uuid.generate(node.uid || device.dev_id); const existingAccessory = this.accessories.find(accessory => accessory.UUID === accessoryUUID); if (existingAccessory) {