From 4890a5b0792bb30d683ca5416382f5757a9762fa Mon Sep 17 00:00:00 2001 From: Jasper Snowolf Date: Tue, 24 Dec 2024 01:00:12 -0500 Subject: [PATCH] Adding device failover --- src/platform.ts | 2 +- src/platformAccessory.ts | 49 ++++++-------- src/types.ts | 13 ++-- src/util/network.ts | 139 ++++++++++++++++++--------------------- 4 files changed, 89 insertions(+), 114 deletions(-) diff --git a/src/platform.ts b/src/platform.ts index 9ed895d..ca98215 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -79,7 +79,7 @@ export class WizSceneControllerPlatform implements DynamicPlatformPlugin { // store a copy of the device object in the `accessory.context` // the `context` property can be used to store any data about the accessory you may need - accessory.context.device = accessoryGroup; + accessory.context.accessoryGroup = accessoryGroup; // create the accessory handler for the newly create accessory // this is imported from `platformAccessory.ts` diff --git a/src/platformAccessory.ts b/src/platformAccessory.ts index 69dc57f..7a9b36c 100644 --- a/src/platformAccessory.ts +++ b/src/platformAccessory.ts @@ -1,7 +1,8 @@ -import { Service, PlatformAccessory, CharacteristicValue, CharacteristicGetCallback, HAPStatus } from 'homebridge'; +/* eslint-disable max-len */ +import { Service, PlatformAccessory, CharacteristicValue, HAPStatus } from 'homebridge'; import { WizSceneControllerPlatform } from './platform'; -import { getLightSetting, setLightSetting } from './util/network'; +import { getAccessoryGroupSetting, setLightSetting } from './util/network'; import { SCENES } from './scenes'; import { LightSetting } from './types'; @@ -36,31 +37,37 @@ export class WizSceneController { this.tvService = this.accessory.getService(this.platform.Service.Television) || this.accessory.addService(this.platform.Service.Television); - this.tvService.setCharacteristic(this.platform.Characteristic.ConfiguredName, accessory.context.device.groupName); + this.tvService.setCharacteristic(this.platform.Characteristic.ConfiguredName, accessory.context.accessoryGroup.groupName); // Is on/off this.tvService.getCharacteristic(this.platform.Characteristic.Active) - .on('get', callback => getLightSetting( + .on('get', callback => getAccessoryGroupSetting( this.platform, - this.accessory.context.device.accessories[0], + this.accessory.context.accessoryGroup, (lightSetting?: LightSetting, hapStatus?: HAPStatus) => callback(hapStatus ? hapStatus : 0, Number(lightSetting?.state)))) .onSet(this.setOn.bind(this)); // What is the current Scene? this.tvService.getCharacteristic(this.platform.Characteristic.ActiveIdentifier) - .on('get', callback => getLightSetting( + .on('get', callback => getAccessoryGroupSetting( this.platform, - this.accessory.context.device.accessories[0], + this.accessory.context.accessoryGroup, (lightSetting?: LightSetting, hapStatus?: HAPStatus) => callback(hapStatus ? hapStatus : 0, lightSetting?.sceneId))) - .onSet(sceneId => setLightSetting(this.platform, this.accessory.context.device.accessories, SCENES[Number(sceneId)].lightSetting)); + .onSet(sceneId => setLightSetting( + this.platform, + this.accessory.context.accessoryGroup.accessories, + SCENES[Number(sceneId)].lightSetting)); // Brightness this.tvService.getCharacteristic(this.platform.Characteristic.Brightness) - .on('get', callback => getLightSetting( + .on('get', callback => getAccessoryGroupSetting( this.platform, - this.accessory.context.device.accessories[0], + this.accessory.context.accessoryGroup, (lightSetting?: LightSetting, hapStatus?: HAPStatus) => callback(hapStatus ? hapStatus : 0, lightSetting?.dimming))) - .onSet(brightness => setLightSetting(this.platform, this.accessory.context.device.accessories, { dimming: Number(brightness) })); + .onSet(brightness => setLightSetting( + this.platform, + this.accessory.context.accessoryGroup.accessories, + { dimming: Number(brightness) })); // Initialize Scenes const configuredScenes: string[] = this.platform.config.scenes; @@ -107,32 +114,14 @@ export class WizSceneController { }); } - getOn(callback: CharacteristicGetCallback) { - getLightSetting( - this.platform, - this.accessory.context.device.accessories[0], - (lightSetting?: LightSetting, hapStatus?: HAPStatus) => callback(hapStatus ? hapStatus : 0, Number(lightSetting?.state))); - } - /** * Handle "SET" requests from HomeKit * These are sent when the user changes the state of an accessory, for example, turning on a Light bulb. */ async setOn(value: CharacteristicValue) { - setLightSetting(this.platform, this.accessory.context.device.accessories, {state: Boolean(value)}); + setLightSetting(this.platform, this.accessory.context.accessoryGroup.accessories, {state: Boolean(value)}); this.platform.log.debug('Set Characteristic On ->', value); } - /** - * Handle "SET" requests from HomeKit - * These are sent when the user changes the state of an accessory, for example, changing the Brightness - */ - async setBrightness(value: CharacteristicValue) { - // implement your own code to set the brightness - this.exampleStates.Brightness = value as number; - - this.platform.log.debug('Set Characteristic Brightness -> ', value); - } - } diff --git a/src/types.ts b/src/types.ts index ebf6d35..1e20d3e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,15 +1,10 @@ import { PlatformConfig } from 'homebridge'; -export interface Config extends PlatformConfig { - port?: number; - enableScenes?: boolean; - lastStatus?: boolean; - broadcast?: string; - address?: string; - devices?: { host?: string; mac?: string; name?: string }[]; - ignoredDevices?: { host?: string; mac?: string }[]; - refreshInterval?: number; +export interface AccessoryGroup { + groupName: string; + accessories: Device[]; } + export interface Device { name: string; macAddress: string; diff --git a/src/util/network.ts b/src/util/network.ts index 106ddb8..6211e6b 100644 --- a/src/util/network.ts +++ b/src/util/network.ts @@ -2,7 +2,7 @@ import dgram from 'dgram'; import getMac from 'getmac'; import internalIp from 'internal-ip'; -import { Device, LightResponse, LightRegistrationResposne, LightSetting } from '../types'; +import { Device, LightResponse, LightRegistrationResposne, LightSetting, AccessoryGroup } from '../types'; import { WizSceneControllerPlatform } from '../platform'; import { makeLogger } from './logger'; import { HAPStatus } from 'homebridge'; @@ -26,25 +26,43 @@ const deviceIpMap: Map = new Map(); const requestQueue: { [ipAddress: string]: { [method: string]: { + accessoryGroupName: string; timeout: NodeJS.Timeout; callbacks: ((lightSetting?: LightSetting, hapStatus?: HAPStatus) => void)[]; }; }; } = {}; +const accessoryRequestMap: Map = new Map(); + function getDeviceIpAddress(device: Device): string | undefined { return device.ipAddress ? device.ipAddress : deviceIpMap.get(device.macAddress); } -export function getLightSetting( - platform: WizSceneControllerPlatform, device: Device, onSuccess: (lightSetting?: LightSetting, hapStatus?: HAPStatus) => void): void { +export function getAccessoryGroupSetting( + platform: WizSceneControllerPlatform, + accessoryGroup: AccessoryGroup, + callback: (lightSetting?: LightSetting, hapStatus?: HAPStatus) => void): + void { + const requestsSent: boolean[] = accessoryGroup.accessories + .map(accessory => getLightSetting(accessoryGroup.groupName, platform, accessory, callback)); + + if (!requestsSent.includes(true)) { + callback(undefined, -70409); + } +} +export function getLightSetting( + accessoryGroupName: string, + platform: WizSceneControllerPlatform, + device: Device, + callback: (lightSetting?: LightSetting, hapStatus?: HAPStatus) => void): + boolean { const deviceIpAddress = getDeviceIpAddress(device); if (!deviceIpAddress) { platform.log.error(`No device ip address found for device: ${JSON.stringify(device)}`); - onSuccess(undefined, -70409); - return; + return false; } platform.log.debug('Senging UDP request to: ' + deviceIpAddress); @@ -68,21 +86,33 @@ export function getLightSetting( } const timeout = setTimeout(() => { - platform.log.warn(`Request timeout to device name/mac/ip: ${device.name}/${device.macAddress}/${deviceIpAddress}`); + platform.log.warn( + // eslint-disable-next-line max-len + `Request timeout to Accessory Group name/device name/mac/ip: ${accessoryGroupName}/${device.name}/${device.macAddress}/${deviceIpAddress}`, + ); const callbacks = requestQueue[deviceIpAddress]['getPilot']?.callbacks; if (callbacks) { callbacks.forEach(callback => callback(undefined, -70409)); } - }, 500); + }, 2000); if (!requestQueue[deviceIpAddress]['getPilot']) { - requestQueue[deviceIpAddress]['getPilot'] = { timeout, callbacks: []}; + requestQueue[deviceIpAddress]['getPilot'] = { accessoryGroupName, timeout, callbacks: []}; } else { clearTimeout(requestQueue[deviceIpAddress]['getPilot'].timeout); requestQueue[deviceIpAddress]['getPilot'].timeout = timeout; } - requestQueue[deviceIpAddress]['getPilot'].callbacks.push(onSuccess); + requestQueue[deviceIpAddress]['getPilot'].callbacks.push(callback); + + if (accessoryRequestMap.has(accessoryGroupName)) { + if (!accessoryRequestMap.get(accessoryGroupName)?.includes(deviceIpAddress)) { + accessoryRequestMap.get(accessoryGroupName)?.push(deviceIpAddress); + } + } else { + accessoryRequestMap.set(accessoryGroupName, [deviceIpAddress]); + } + return true; } export function setLightSetting( @@ -116,62 +146,6 @@ export function setLightSetting( }); } -const setPilotQueue: { [key: string]: ((error: Error | null) => void)[] } = {}; -export function setPilot( - platform: WizSceneControllerPlatform, - device: Device, - lightSetting: LightSetting, - callback: (error: Error | null) => void, -) { - - const deviceIpAddress = getDeviceIpAddress(device); - - if (!deviceIpAddress) { - const errorMessage = `No device ip address found for device: ${JSON.stringify(device)}`; - platform.log.error(errorMessage); - callback({ name: 'no_ip_error', message: errorMessage }); - return; - } - - if (platform.config.lastStatus) { - // Keep only the settings that cannot change the bulb color - Object.keys(lightSetting).forEach((key: string) => { - if (['sceneId', 'speed', 'temp', 'dimming', 'r', 'g', 'b'].includes(key)) { - delete lightSetting[key as keyof typeof lightSetting]; - } - }); - } - const msg = JSON.stringify({ - method: 'setPilot', - env: 'pro', - params: Object.assign( - { - mac: deviceIpAddress, - src: 'udp', - }, - lightSetting, - ), - }); - if (deviceIpAddress in setPilotQueue) { - setPilotQueue[deviceIpAddress].push(callback); - } else { - setPilotQueue[deviceIpAddress] = [callback]; - } - platform.log.debug(`[SetPilot][${deviceIpAddress}:${BROADCAST_PORT}] ${msg}`); - platform.socket.send(msg, BROADCAST_PORT, deviceIpAddress, (error: Error | null) => { - if (error !== null && deviceIpAddress in setPilotQueue) { - platform.log.debug( - `[Socket] Failed to send setPilot response to ${ - deviceIpAddress - }: ${error.toString()}`, - ); - const callbacks = setPilotQueue[deviceIpAddress]; - delete setPilotQueue[deviceIpAddress]; - callbacks.map((f) => f(error)); - } - }); -} - export function createSocket(platform: WizSceneControllerPlatform) { const log = makeLogger(platform, 'Socket'); @@ -193,16 +167,33 @@ export function createSocket(platform: WizSceneControllerPlatform) { handleRegistration(platform, lightResponse as LightRegistrationResposne, rinfo.address); } else if (lightResponse.method === 'getPilot') { const methodsForDevice = requestQueue[rinfo.address]; - const callbackbacksForMethod = methodsForDevice ? methodsForDevice[lightResponse.method]?.callbacks : null; - clearTimeout(methodsForDevice?.[lightResponse.method]?.timeout); - - if (callbackbacksForMethod && callbackbacksForMethod.length > 0) { - const lightSetting: LightSetting = JSON.parse(decryptedMsg).result; - platform.log.debug('Received lighting setting for ' + rinfo.address + ' is: ' + JSON.stringify(lightSetting)); - platform.log.debug('Flushing all callbacks for:', rinfo.address, lightResponse.method); - callbackbacksForMethod.forEach(callback => callback(lightSetting)); - delete methodsForDevice[lightResponse.method]; + + if (!methodsForDevice) { + return; + } + + const methodContext = methodsForDevice[lightResponse.method]; + + if (!methodContext) { + return; } + + clearTimeout(methodContext.timeout); + + const accessoryGroupName = methodContext.accessoryGroupName; + const lightSetting: LightSetting = JSON.parse(decryptedMsg).result; + platform.log.debug(`Received lighting setting for: ${accessoryGroupName} ${rinfo.address} is: ${JSON.stringify(lightSetting)}`); + platform.log.debug(`Calling all callbacks for: ${accessoryGroupName} ${rinfo.address} ${lightResponse.method}`); + methodContext.callbacks.forEach(callback => callback(lightSetting)); + + platform.log.debug(`Deleting all pending callbacks for Accessory Group: ${accessoryGroupName}`); + accessoryRequestMap.get(accessoryGroupName)?.forEach(ipAddress => { + platform.log.debug(`Deleting method context for: ${ipAddress} ${lightResponse.method}`); + clearTimeout(requestQueue[ipAddress][lightResponse.method].timeout); + delete requestQueue[ipAddress][lightResponse.method]; + }); + + delete methodsForDevice[lightResponse.method]; } });