Skip to content

Commit

Permalink
Adding device failover
Browse files Browse the repository at this point in the history
  • Loading branch information
JasperSnowolf committed Dec 24, 2024
1 parent 5f3d9e8 commit 4890a5b
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 114 deletions.
2 changes: 1 addition & 1 deletion src/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
49 changes: 19 additions & 30 deletions src/platformAccessory.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

}
13 changes: 4 additions & 9 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import { PlatformConfig } from 'homebridge';

Check warning on line 1 in src/types.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

'PlatformConfig' is defined but never used

Check warning on line 1 in src/types.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

'PlatformConfig' is defined but never used

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;
Expand Down
139 changes: 65 additions & 74 deletions src/util/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -26,25 +26,43 @@ const deviceIpMap: Map<string, string> = new Map<string, string>();
const requestQueue: {
[ipAddress: string]: {
[method: string]: {
accessoryGroupName: string;
timeout: NodeJS.Timeout;
callbacks: ((lightSetting?: LightSetting, hapStatus?: HAPStatus) => void)[];
};
};
} = {};

const accessoryRequestMap: Map<string, string[]> = new Map<string, string[]>();

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);
Expand All @@ -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(
Expand Down Expand Up @@ -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');

Expand All @@ -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];
}
});

Expand Down

0 comments on commit 4890a5b

Please sign in to comment.