Adding new features
All checks were successful
continuous-integration/drone/push Build is passing

- Adding ability to specify delay times per light
- Adding config.schema.json
This commit is contained in:
Brandon Watson 2022-09-07 14:29:51 -05:00
parent 8fbfc51276
commit feb5533419
7 changed files with 368 additions and 189 deletions

119
config.schema.json Normal file
View File

@ -0,0 +1,119 @@
{
"pluginAlias": "Flux",
"pluginType": "platform",
"singular": true,
"schema": {
"type": "object",
"properties": {
"ipAddress": {
"title": "IP Address",
"type": "string",
"required": true
},
"userName": {
"title": "User Name",
"type": "string",
"required": false
},
"clientKey": {
"title": "Client Key",
"type": "string",
"required": false
},
"latitude": {
"title": "Latitude",
"type": "number",
"required": true
},
"longitude": {
"title": "Longitude",
"type": "string",
"required": true
},
"hueLights": {
"title": "Hue Lights",
"type": "array",
"uniqueItems": true,
"items": {
"type": "object",
"properties": {
"ip": {
"title": "Name",
"type": "string",
"required": true
},
"cron": {
"title": "Poll Delay in MS",
"type": "string",
"required": false
}
}
},
"required": true
},
"wizLights": {
"title": "Wiz Lights",
"type": "array",
"uniqueItems": true,
"items": {
"type": "object",
"properties": {
"ip": {
"title": "Ip Address",
"type": "string",
"required": true
},
"mac": {
"title": "Mac Address",
"type": "string",
"required": true
},
"cron": {
"title": "Poll Delay in MS",
"type": "string",
"required": false
}
}
},
"required": false
},
"wizDiscoveryEnabled": {
"title": "Wiz Discovery Enabled",
"type": "boolean",
"required": true
},
"ceilingColorTemp": {
"title": "Ceiling Color Temperature",
"type": "number",
"required": true
},
"sunsetColorTemp": {
"title": "Sunset Color Temperature",
"type": "number",
"required": true
},
"floorColorTemp": {
"title": "Floor Color Temperature",
"type": "number",
"required": true
},
"sunsetDuration": {
"title": "Sunset Duration",
"type": "number",
"required": true
},
"transition": {
"title": "Transition Time",
"type": "number",
"required": true
},
"delay": {
"title": "Poll Delay in MS",
"type": "number",
"required": true
}
}
},
"form": null,
"display": null
}

12
package-lock.json generated
View File

@ -119,9 +119,9 @@
"integrity": "sha512-XLD/llTSB6EBe3thkN+/I0L+yCTB6sjrcVovQdx2Cnl6N6bTzHmwe/J8mWnsXFgxLrj/emzdv8IR4evKYG2qxQ==" "integrity": "sha512-XLD/llTSB6EBe3thkN+/I0L+yCTB6sjrcVovQdx2Cnl6N6bTzHmwe/J8mWnsXFgxLrj/emzdv8IR4evKYG2qxQ=="
}, },
"node_modules/@watsonb8/wiz-lib": { "node_modules/@watsonb8/wiz-lib": {
"version": "1.0.1-1c687.0", "version": "1.0.1-9548b.0",
"resolved": "http://10.44.1.6:4873/@watsonb8%2fwiz-lib/-/wiz-lib-1.0.1-1c687.0.tgz", "resolved": "http://10.44.1.6:4873/@watsonb8%2fwiz-lib/-/wiz-lib-1.0.1-9548b.0.tgz",
"integrity": "sha1-dag89Ke1YURYSIwyWkQM/lCbvPI=", "integrity": "sha1-PL/Phi0KQ5DJGRspSHA+i1lprp4=",
"dependencies": { "dependencies": {
"dgram": "^1.0.1", "dgram": "^1.0.1",
"getmac": "^5.20.0", "getmac": "^5.20.0",
@ -1967,9 +1967,9 @@
"integrity": "sha512-XLD/llTSB6EBe3thkN+/I0L+yCTB6sjrcVovQdx2Cnl6N6bTzHmwe/J8mWnsXFgxLrj/emzdv8IR4evKYG2qxQ==" "integrity": "sha512-XLD/llTSB6EBe3thkN+/I0L+yCTB6sjrcVovQdx2Cnl6N6bTzHmwe/J8mWnsXFgxLrj/emzdv8IR4evKYG2qxQ=="
}, },
"@watsonb8/wiz-lib": { "@watsonb8/wiz-lib": {
"version": "1.0.1-1c687.0", "version": "1.0.1-9548b.0",
"resolved": "http://10.44.1.6:4873/@watsonb8%2fwiz-lib/-/wiz-lib-1.0.1-1c687.0.tgz", "resolved": "http://10.44.1.6:4873/@watsonb8%2fwiz-lib/-/wiz-lib-1.0.1-9548b.0.tgz",
"integrity": "sha1-dag89Ke1YURYSIwyWkQM/lCbvPI=", "integrity": "sha1-PL/Phi0KQ5DJGRspSHA+i1lprp4=",
"requires": { "requires": {
"dgram": "^1.0.1", "dgram": "^1.0.1",
"getmac": "^5.20.0", "getmac": "^5.20.0",

View File

@ -8,7 +8,8 @@
}, },
"files": [ "files": [
"bin", "bin",
"src" "src",
"config.schema.json"
], ],
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",

View File

@ -6,11 +6,12 @@ import { IConfig } from "./models/iConfig";
//@ts-ignore //@ts-ignore
import { GetTimesResult, getTimes } from "suncalc"; import { GetTimesResult, getTimes } from "suncalc";
import HueError = require("node-hue-api/lib/HueError"); import HueError = require("node-hue-api/lib/HueError");
import cron from "node-cron"; import cron, { ScheduledTask } from "node-cron";
import { WizBulb } from "@watsonb8/wiz-lib/build/wizBulb"; import { WizBulb } from "@watsonb8/wiz-lib/build/wizBulb";
import { colorTemperature2rgb, RGB } from "@watsonb8/wiz-lib"; import { colorTemperature2rgb, RGB } from "@watsonb8/wiz-lib";
import { PlatformAccessory } from "homebridge"; import { PlatformAccessory } from "homebridge";
import { Platform } from "./platform"; import { Platform } from "./platform";
import { colorTempToRgb } from "./util/colorUtil";
const SECONDS_IN_DAY = 86400000; const SECONDS_IN_DAY = 86400000;
const MINUTES_IN_MILLISECOND = 60000; const MINUTES_IN_MILLISECOND = 60000;
@ -29,6 +30,9 @@ export class FluxAccessory {
private readonly _accessory: PlatformAccessory; private readonly _accessory: PlatformAccessory;
private _config: IConfig; private _config: IConfig;
private _isActive: boolean; private _isActive: boolean;
private _hueRGB: RGB;
private _wizRGB: RGB;
private _fade: number;
//Service fields //Service fields
private _switchService; private _switchService;
@ -39,6 +43,7 @@ export class FluxAccessory {
private _wizLights: Array<WizBulb> = []; private _wizLights: Array<WizBulb> = [];
private _times: GetTimesResult; private _times: GetTimesResult;
private _tasks: Array<ScheduledTask> = [];
constructor(props: IFluxProps) { constructor(props: IFluxProps) {
//Assign class variables //Assign class variables
@ -47,6 +52,11 @@ export class FluxAccessory {
this._config = props.config; this._config = props.config;
this._isActive = false; this._isActive = false;
this._wizLights = props.wizBulbs; this._wizLights = props.wizBulbs;
this._hue = props.hue;
this.name = this._config.name;
this._hueRGB = { r: 0, g: 0, b: 0 };
this._wizRGB = { r: 0, g: 0, b: 0 };
this._fade = this._config.transition ?? 30000;
this._times = getTimes( this._times = getTimes(
new Date(), new Date(),
@ -70,8 +80,20 @@ export class FluxAccessory {
} }
).start(); ).start();
this._hue = props.hue; //Schedule job to refresh hues every minute
this.name = this._config.name; this.updateRGB();
cron.schedule(
"* * * * *",
() => {
this.updateRGB();
this._platform.log.info("Updated hues");
},
{
scheduled: true,
}
).start();
this.scheduleLights();
this._accessory this._accessory
.getService(this._platform.api.hap.Service.AccessoryInformation)! .getService(this._platform.api.hap.Service.AccessoryInformation)!
@ -126,9 +148,11 @@ export class FluxAccessory {
this._config.latitude, this._config.latitude,
this._config.longitude this._config.longitude
); );
this.update(); this._isActive = true;
this.enable();
} else { } else {
this._isActive = false; this._isActive = false;
this.disable();
} }
return callback(); return callback();
}; };
@ -148,112 +172,19 @@ export class FluxAccessory {
* Populates internal lights array using the configuration values * Populates internal lights array using the configuration values
*/ */
private getLights = async (): Promise<void> => { private getLights = async (): Promise<void> => {
for (const value of this._config.lights) { for (const value of this._config.hueLights) {
//@ts-ignore //@ts-ignore
const light: Light = await this._hue.lights.getLightByName(value); const light: Light = await this._hue.lights.getLightByName(
value.name
);
this._lights.push(light); this._lights.push(light);
} }
}; };
private colorTempToRgb = (kelvin: number): RGB => {
var temp = kelvin / 100;
var red, green, blue;
if (temp <= 66) {
red = 255;
green = temp;
green = 99.4708025861 * Math.log(green) - 161.1195681661;
if (temp <= 19) {
blue = 0;
} else {
blue = temp - 10;
blue = 138.5177312231 * Math.log(blue) - 305.0447927307;
}
} else {
red = temp - 60;
red = 329.698727446 * Math.pow(red, -0.1332047592);
green = temp - 60;
green = 288.1221695283 * Math.pow(green, -0.0755148492);
blue = 255;
}
return {
r: this.clamp(red, 0, 255),
g: this.clamp(green, 0, 255),
b: this.clamp(blue, 0, 255),
};
};
private clamp(x: number, min: number, max: number) {
if (x < min) {
return min;
}
if (x > max) {
return max;
}
return x;
}
private isHueError = (object: any): object is HueError => { private isHueError = (object: any): object is HueError => {
return "_hueError" in object; return "_hueError" in object;
}; };
private setHueLights = async (state: LightState) => {
const promises: Array<Promise<unknown> | PromiseLike<unknown>> = [];
this._lights.map(async (light: Light) => {
try {
await this._hue.lights.setLightState(light.id, state);
} catch (err) {
if (
this.isHueError(err) &&
err.message ===
"parameter, xy, is not modifiable. Device is set to off."
) {
//Eat this
} else {
this._platform.log.info(
`Error while setting lights: ${err}`
);
}
}
});
await Promise.all(promises);
};
private setWizLights = async (rgb: RGB, fade: number): Promise<void> => {
await Promise.all(
this._wizLights.map(async (bulb) => {
const pilot = await bulb.get();
bulb.set(rgb, pilot?.dimming, fade);
})
);
return;
};
/**
* Helper function to generate a UUID
*/
private generateUUID(): string {
// Public Domain/MIT
var d = new Date().getTime();
if (
typeof performance !== "undefined" &&
typeof performance.now === "function"
) {
d += performance.now(); //use high-precision timer if available
}
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
/[xy]/g,
function (c) {
var r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
}
);
}
/** /**
* Gets adjusted color temperature. * Gets adjusted color temperature.
*/ */
@ -283,13 +214,7 @@ export class FluxAccessory {
} }
} }
private update = async (): Promise<void> => { private updateRGB = (): void => {
this._isActive = true;
while (this._isActive) {
if (this._lights.length === 0) {
await this.getLights();
}
const now = this.getNow(); const now = this.getNow();
//Pad start time by an hour before sunset //Pad start time by an hour before sunset
const start = new Date( const start = new Date(
@ -306,15 +231,9 @@ export class FluxAccessory {
this._times.sunrise.getTime() + 1 * SECONDS_IN_DAY this._times.sunrise.getTime() + 1 * SECONDS_IN_DAY
); );
const startColorTemp = this._config.ceilingColorTemp const startColorTemp = this._config.ceilingColorTemp ?? 4000;
? this._config.ceilingColorTemp const sunsetColorTemp = this._config.sunsetColorTemp ?? 2800;
: 4000; const floorColorTemp = this._config.floorColorTemp ?? 1900;
const sunsetColorTemp = this._config.sunsetColorTemp
? this._config.sunsetColorTemp
: 2800;
const floorColorTemp = this._config.floorColorTemp
? this._config.floorColorTemp
: 1900;
let newTemp = 0; let newTemp = 0;
@ -338,29 +257,99 @@ export class FluxAccessory {
newTemp = this._config.floorColorTemp; newTemp = this._config.floorColorTemp;
} }
//Set lights //Set RGB
const hueRGB = this.colorTempToRgb(newTemp); this._hueRGB = colorTempToRgb(newTemp);
const wizRGB = colorTemperature2rgb(newTemp); this._wizRGB = colorTemperature2rgb(newTemp);
if (hueRGB && newTemp !== 0) { };
const lightState = new LightState();
lightState private scheduleLights = async (): Promise<void> => {
.transitionInMillis( if (this._lights.length === 0) {
this._config.transition ? this._config.transition : 5000 await this.getLights();
) }
.rgb(hueRGB.r ?? 0, hueRGB.g ?? 0, hueRGB.b ?? 0); this._tasks = [...this.getHueTasks(), ...this.getWizTasks()];
await this.setHueLights(lightState); };
await this.setWizLights(
wizRGB, private getHueTasks(): Array<ScheduledTask> {
this._config.transition ? this._config.transition / 1000 : 5 return this._config.hueLights.map((hueLightConfig) => {
); let light = this._lights.find((x) => x.name == hueLightConfig.name);
let schedule: string = hueLightConfig.cron ?? "*/6 * * * *";
this._platform.log.info( this._platform.log.info(
`Adjusting light temp to ${newTemp}, ${JSON.stringify( `Scheduling task for ${light?.name}: ${schedule}`
hueRGB
)}`
); );
return cron.schedule(
schedule,
async () => {
await this.updateHueLight(light);
this._platform.log.info("Updated hues");
},
{
scheduled: false,
}
);
});
} }
await Sleep(this._config.delay ? this._config.delay : 60000); private getWizTasks(): Array<ScheduledTask> {
return this._wizLights.map((wizBulb) => {
let wizLightConfig = this._config.wizLights.find(
(x) => x.ip == wizBulb.getIp()
);
let schedule: string = wizLightConfig?.cron ?? "*/6 * * * *";
this._platform.log.info(
`Scheduling task for ${wizBulb.getMac()}: ${schedule}`
);
return cron.schedule(
schedule,
async () => {
await this.updateWizLight(wizBulb);
this._platform.log.info("Updated hues");
},
{
scheduled: false,
}
);
});
}
private updateWizLight = async (wizBulb: WizBulb): Promise<void> => {
const pilot = await wizBulb.get();
this._platform.log.info(`Adjusting wiz bulb: ${wizBulb.getMac()}`);
wizBulb.set(this._wizRGB, pilot?.dimming, this._fade);
};
private updateHueLight = async (
hueLight: Light | undefined
): Promise<void> => {
if (!hueLight) {
return;
}
this._platform.log.info(`Adjusting wiz bulb: ${hueLight.name}`);
const lightState = new LightState();
lightState
.transitionInMillis(this._fade)
.rgb(this._hueRGB.r ?? 0, this._hueRGB.g ?? 0, this._hueRGB.b ?? 0);
try {
await this._hue.lights.setLightState(hueLight.id, lightState);
} catch (err) {
if (
this.isHueError(err) &&
err.message ===
"parameter, xy, is not modifiable. Device is set to off."
) {
//Eat this
} else {
this._platform.log.info(`Error while setting lights: ${err}`);
}
} }
}; };
private enable() {
this._tasks.forEach((task) => task.start());
}
private disable() {
this._tasks.forEach((task) => task.stop());
}
} }

View File

@ -17,7 +17,14 @@ export interface IConfig {
/** /**
* The list of lights to affect * The list of lights to affect
*/ */
lights: Array<string>; hueLights: Array<{ name: string; cron?: string }>;
/**
* The list of wiz lights to affect
*/
wizLights: Array<{ ip: string; mac: string; cron?: string }>;
wizDiscoveryEnabled: boolean;
/** /**
* The name of the enable switch in homekit * The name of the enable switch in homekit

View File

@ -1,4 +1,5 @@
import discover from "@watsonb8/wiz-lib/build/discovery"; import { Discover } from "@watsonb8/wiz-lib/build/discovery";
import { WizBulb } from "@watsonb8/wiz-lib/build/wizBulb";
import { import {
API, API,
Characteristic, Characteristic,
@ -19,6 +20,7 @@ import { Sleep } from "./sleep";
export class Platform implements DynamicPlatformPlugin { export class Platform implements DynamicPlatformPlugin {
private hue: Api | undefined; private hue: Api | undefined;
private wiz: Discover;
private accessory: PlatformAccessory | undefined = undefined; private accessory: PlatformAccessory | undefined = undefined;
private config: IConfig; private config: IConfig;
constructor( constructor(
@ -27,6 +29,7 @@ export class Platform implements DynamicPlatformPlugin {
public readonly api: API public readonly api: API
) { ) {
this.config = config as unknown as IConfig; this.config = config as unknown as IConfig;
this.wiz = new Discover();
this.log.info("INFO - Registering Flux platform"); this.log.info("INFO - Registering Flux platform");
this.api.on("didFinishLaunching", this.didFinishLaunching.bind(this)); this.api.on("didFinishLaunching", this.didFinishLaunching.bind(this));
} }
@ -77,7 +80,26 @@ export class Platform implements DynamicPlatformPlugin {
return; return;
} }
return await discover(); let bulbs: Array<WizBulb> = await this.wiz.createWizBulbs(
this.config.wizLights
);
if (this.config.wizDiscoveryEnabled) {
let discoveredBulbs: Array<WizBulb> = await this.wiz.discover();
let filtered = [];
for (const bulb of discoveredBulbs) {
if (
!bulbs.some(
(manualBulb) => manualBulb.getIp() === bulb.getIp()
)
) {
filtered.push(bulb);
}
}
bulbs.push(...filtered);
}
return bulbs;
}; };
private connectHue = async () => { private connectHue = async () => {

41
src/util/colorUtil.ts Normal file
View File

@ -0,0 +1,41 @@
import { RGB } from "@watsonb8/wiz-lib";
export const colorTempToRgb = (kelvin: number): RGB => {
var temp = kelvin / 100;
var red, green, blue;
if (temp <= 66) {
red = 255;
green = temp;
green = 99.4708025861 * Math.log(green) - 161.1195681661;
if (temp <= 19) {
blue = 0;
} else {
blue = temp - 10;
blue = 138.5177312231 * Math.log(blue) - 305.0447927307;
}
} else {
red = temp - 60;
red = 329.698727446 * Math.pow(red, -0.1332047592);
green = temp - 60;
green = 288.1221695283 * Math.pow(green, -0.0755148492);
blue = 255;
}
return {
r: clamp(red, 0, 255),
g: clamp(green, 0, 255),
b: clamp(blue, 0, 255),
};
};
const clamp = (x: number, min: number, max: number) => {
if (x < min) {
return min;
}
if (x > max) {
return max;
}
return x;
};