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

View File

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

View File

@ -6,11 +6,12 @@ import { IConfig } from "./models/iConfig";
//@ts-ignore
import { GetTimesResult, getTimes } from "suncalc";
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 { colorTemperature2rgb, RGB } from "@watsonb8/wiz-lib";
import { PlatformAccessory } from "homebridge";
import { Platform } from "./platform";
import { colorTempToRgb } from "./util/colorUtil";
const SECONDS_IN_DAY = 86400000;
const MINUTES_IN_MILLISECOND = 60000;
@ -29,6 +30,9 @@ export class FluxAccessory {
private readonly _accessory: PlatformAccessory;
private _config: IConfig;
private _isActive: boolean;
private _hueRGB: RGB;
private _wizRGB: RGB;
private _fade: number;
//Service fields
private _switchService;
@ -39,6 +43,7 @@ export class FluxAccessory {
private _wizLights: Array<WizBulb> = [];
private _times: GetTimesResult;
private _tasks: Array<ScheduledTask> = [];
constructor(props: IFluxProps) {
//Assign class variables
@ -47,6 +52,11 @@ export class FluxAccessory {
this._config = props.config;
this._isActive = false;
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(
new Date(),
@ -70,8 +80,20 @@ export class FluxAccessory {
}
).start();
this._hue = props.hue;
this.name = this._config.name;
//Schedule job to refresh hues every minute
this.updateRGB();
cron.schedule(
"* * * * *",
() => {
this.updateRGB();
this._platform.log.info("Updated hues");
},
{
scheduled: true,
}
).start();
this.scheduleLights();
this._accessory
.getService(this._platform.api.hap.Service.AccessoryInformation)!
@ -126,9 +148,11 @@ export class FluxAccessory {
this._config.latitude,
this._config.longitude
);
this.update();
this._isActive = true;
this.enable();
} else {
this._isActive = false;
this.disable();
}
return callback();
};
@ -148,112 +172,19 @@ export class FluxAccessory {
* Populates internal lights array using the configuration values
*/
private getLights = async (): Promise<void> => {
for (const value of this._config.lights) {
for (const value of this._config.hueLights) {
//@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);
}
};
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 => {
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.
*/
@ -283,84 +214,142 @@ export class FluxAccessory {
}
}
private update = async (): Promise<void> => {
this._isActive = true;
while (this._isActive) {
if (this._lights.length === 0) {
await this.getLights();
private updateRGB = (): void => {
const now = this.getNow();
//Pad start time by an hour before sunset
const start = new Date(
this._times.sunset.getTime() - 60 * MINUTES_IN_MILLISECOND
);
const sunsetStart = this._times.sunsetStart;
const sunsetEnd = new Date(
this._times.sunset.getTime() + this._config.sunsetDuration
);
const nightStart = new Date(
sunsetEnd.getTime() + 60 * MINUTES_IN_MILLISECOND
);
const sunrise = new Date(
this._times.sunrise.getTime() + 1 * SECONDS_IN_DAY
);
const startColorTemp = this._config.ceilingColorTemp ?? 4000;
const sunsetColorTemp = this._config.sunsetColorTemp ?? 2800;
const floorColorTemp = this._config.floorColorTemp ?? 1900;
let newTemp = 0;
if (start < now && now < sunsetStart) {
newTemp = this.getTempOffset(
startColorTemp,
sunsetColorTemp,
start,
sunsetStart
);
} else if (sunsetStart < now && now < sunsetEnd) {
newTemp = this._config.sunsetColorTemp;
} else if (sunsetEnd < now && now < nightStart) {
newTemp = this.getTempOffset(
sunsetColorTemp,
floorColorTemp,
sunsetEnd,
nightStart
);
} else if (nightStart < now && now < sunrise) {
newTemp = this._config.floorColorTemp;
}
//Set RGB
this._hueRGB = colorTempToRgb(newTemp);
this._wizRGB = colorTemperature2rgb(newTemp);
};
private scheduleLights = async (): Promise<void> => {
if (this._lights.length === 0) {
await this.getLights();
}
this._tasks = [...this.getHueTasks(), ...this.getWizTasks()];
};
private getHueTasks(): Array<ScheduledTask> {
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(
`Scheduling task for ${light?.name}: ${schedule}`
);
return cron.schedule(
schedule,
async () => {
await this.updateHueLight(light);
this._platform.log.info("Updated hues");
},
{
scheduled: false,
}
);
});
}
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}`);
}
const now = this.getNow();
//Pad start time by an hour before sunset
const start = new Date(
this._times.sunset.getTime() - 60 * MINUTES_IN_MILLISECOND
);
const sunsetStart = this._times.sunsetStart;
const sunsetEnd = new Date(
this._times.sunset.getTime() + this._config.sunsetDuration
);
const nightStart = new Date(
sunsetEnd.getTime() + 60 * MINUTES_IN_MILLISECOND
);
const sunrise = new Date(
this._times.sunrise.getTime() + 1 * SECONDS_IN_DAY
);
const startColorTemp = this._config.ceilingColorTemp
? this._config.ceilingColorTemp
: 4000;
const sunsetColorTemp = this._config.sunsetColorTemp
? this._config.sunsetColorTemp
: 2800;
const floorColorTemp = this._config.floorColorTemp
? this._config.floorColorTemp
: 1900;
let newTemp = 0;
if (start < now && now < sunsetStart) {
newTemp = this.getTempOffset(
startColorTemp,
sunsetColorTemp,
start,
sunsetStart
);
} else if (sunsetStart < now && now < sunsetEnd) {
newTemp = this._config.sunsetColorTemp;
} else if (sunsetEnd < now && now < nightStart) {
newTemp = this.getTempOffset(
sunsetColorTemp,
floorColorTemp,
sunsetEnd,
nightStart
);
} else if (nightStart < now && now < sunrise) {
newTemp = this._config.floorColorTemp;
}
//Set lights
const hueRGB = this.colorTempToRgb(newTemp);
const wizRGB = colorTemperature2rgb(newTemp);
if (hueRGB && newTemp !== 0) {
const lightState = new LightState();
lightState
.transitionInMillis(
this._config.transition ? this._config.transition : 5000
)
.rgb(hueRGB.r ?? 0, hueRGB.g ?? 0, hueRGB.b ?? 0);
await this.setHueLights(lightState);
await this.setWizLights(
wizRGB,
this._config.transition ? this._config.transition / 1000 : 5
);
this._platform.log.info(
`Adjusting light temp to ${newTemp}, ${JSON.stringify(
hueRGB
)}`
);
}
await Sleep(this._config.delay ? this._config.delay : 60000);
}
};
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
*/
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
@ -58,4 +65,4 @@ export interface IConfig {
* The current formatted date and time to use with testing
*/
testNowDateString?: string;
}
}

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 {
API,
Characteristic,
@ -19,6 +20,7 @@ import { Sleep } from "./sleep";
export class Platform implements DynamicPlatformPlugin {
private hue: Api | undefined;
private wiz: Discover;
private accessory: PlatformAccessory | undefined = undefined;
private config: IConfig;
constructor(
@ -27,6 +29,7 @@ export class Platform implements DynamicPlatformPlugin {
public readonly api: API
) {
this.config = config as unknown as IConfig;
this.wiz = new Discover();
this.log.info("INFO - Registering Flux platform");
this.api.on("didFinishLaunching", this.didFinishLaunching.bind(this));
}
@ -77,7 +80,26 @@ export class Platform implements DynamicPlatformPlugin {
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 () => {

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;
};