426 lines
13 KiB
TypeScript
426 lines
13 KiB
TypeScript
import Api = require("node-hue-api/lib/api/Api");
|
|
import Light = require("node-hue-api/lib/model/Light");
|
|
import LightState = require("node-hue-api/lib/model/lightstate/LightState");
|
|
import { Sleep } from "./sleep";
|
|
import { IConfig } from "./models/iConfig";
|
|
//@ts-ignore
|
|
import { GetTimesResult, getTimes } from "suncalc";
|
|
import HueError = require("node-hue-api/lib/HueError");
|
|
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;
|
|
const SECONDS_IN_HOUR = 3600;
|
|
|
|
export interface IFluxProps {
|
|
platform: Platform;
|
|
accessory: PlatformAccessory;
|
|
hue: Api;
|
|
wizBulbs: Array<WizBulb>;
|
|
config: IConfig;
|
|
}
|
|
|
|
export class FluxAccessory {
|
|
private readonly _platform: Platform;
|
|
private readonly _accessory: PlatformAccessory;
|
|
private _config: IConfig;
|
|
private _isActive: boolean;
|
|
private _hueRGB: RGB;
|
|
private _wizRGB: RGB;
|
|
private _fade: number;
|
|
private _cron: string;
|
|
|
|
//Service fields
|
|
private _switchService;
|
|
|
|
private _hue: Api;
|
|
|
|
private _lights: Array<Light> = [];
|
|
private _wizLights: Array<WizBulb> = [];
|
|
|
|
private _times: GetTimesResult;
|
|
private _tasks: Array<ScheduledTask> = [];
|
|
|
|
constructor(props: IFluxProps) {
|
|
//Assign class variables
|
|
this._platform = props.platform;
|
|
this._accessory = props.accessory;
|
|
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._cron = this._config.cron ?? "*/30 * * * * *";
|
|
|
|
this._times = getTimes(
|
|
new Date(),
|
|
this._config.latitude,
|
|
this._config.longitude
|
|
);
|
|
|
|
//Schedule job to refresh times
|
|
cron.schedule(
|
|
"0 12 * * *",
|
|
() => {
|
|
this._times = getTimes(
|
|
new Date(),
|
|
this._config.latitude,
|
|
this._config.longitude
|
|
);
|
|
this._platform.log.info("Updated sunset times");
|
|
},
|
|
{
|
|
scheduled: true,
|
|
}
|
|
).start();
|
|
|
|
//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)!
|
|
.setCharacteristic(
|
|
this._platform.api.hap.Characteristic.Manufacturer,
|
|
"Brandon Watson"
|
|
)
|
|
.setCharacteristic(
|
|
this._platform.api.hap.Characteristic.Model,
|
|
"F.lux"
|
|
)
|
|
.setCharacteristic(
|
|
this._platform.api.hap.Characteristic.SerialNumber,
|
|
"123-456-789"
|
|
);
|
|
|
|
const switchUUID = this._platform.api.hap.uuid.generate(
|
|
`${this._accessory.displayName} Switch`
|
|
);
|
|
|
|
this._switchService =
|
|
this._accessory.getService(this._platform.api.hap.Service.Switch) ||
|
|
this._accessory.addService(
|
|
this._platform.api.hap.Service.Switch,
|
|
this._accessory.displayName,
|
|
switchUUID
|
|
);
|
|
|
|
this._switchService
|
|
.getCharacteristic(this._platform.api.hap.Characteristic.On)
|
|
//@ts-ignore
|
|
.on("set", this.onSetEnabled)
|
|
//@ts-ignore
|
|
.on("get", this.onGetEnabled);
|
|
|
|
// this.test();
|
|
}
|
|
|
|
public name: string = "Flux";
|
|
|
|
public platformAccessory: any;
|
|
|
|
/**
|
|
* Handler for switch set event
|
|
* @param callback The callback function to call when complete
|
|
*/
|
|
private onSetEnabled = async (
|
|
activeState: boolean,
|
|
callback: (error?: Error | null | undefined) => void
|
|
) => {
|
|
if (activeState) {
|
|
this._times = getTimes(
|
|
new Date(),
|
|
this._config.latitude,
|
|
this._config.longitude
|
|
);
|
|
this._isActive = true;
|
|
this.enable();
|
|
} else {
|
|
this._isActive = false;
|
|
this.disable();
|
|
}
|
|
return callback();
|
|
};
|
|
|
|
/**
|
|
* Handler for switch get event
|
|
* @param callback The callback function to call when complete
|
|
*/
|
|
private onGetEnabled = (
|
|
callback: (error: Error | null, value: boolean) => void
|
|
): void => {
|
|
callback(null, this._isActive);
|
|
// return this._isActive;
|
|
};
|
|
|
|
/**
|
|
* Populates internal lights array using the configuration values
|
|
*/
|
|
private getLights = async (): Promise<void> => {
|
|
for (const value of this._config.hueLights) {
|
|
//@ts-ignore
|
|
const light: Light = await this._hue.lights.getLightByName(
|
|
value.name
|
|
);
|
|
this._lights.push(light);
|
|
}
|
|
};
|
|
|
|
private isHueError = (object: any): object is HueError => {
|
|
return "_hueError" in object;
|
|
};
|
|
|
|
/**
|
|
* Gets adjusted color temperature.
|
|
*/
|
|
private getTempOffset = (
|
|
startTemp: number,
|
|
endTemp: number,
|
|
startTime: Date,
|
|
endTime: Date
|
|
) => {
|
|
const now = this.getNow().getTime();
|
|
const percentComplete =
|
|
(now - startTime.getTime()) /
|
|
(endTime.getTime() - startTime.getTime());
|
|
const tempRange = Math.abs(startTemp - endTemp);
|
|
const tempOffset = tempRange * percentComplete;
|
|
return startTemp - tempOffset;
|
|
};
|
|
|
|
/**
|
|
* Get the current time. Use test time if present.
|
|
*/
|
|
private getNow() {
|
|
if (this._config.testNowDateString) {
|
|
return new Date(this._config.testNowDateString);
|
|
} else {
|
|
return new Date();
|
|
}
|
|
}
|
|
|
|
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 hueStartColorTemp = this._config.hueCeilingColorTemp ?? 4000;
|
|
const hueSunsetColorTemp = this._config.hueSunsetColorTemp ?? 2800;
|
|
const hueFloorColorTemp = this._config.hueFloorColorTemp ?? 1900;
|
|
|
|
const wizStartColorTemp = this._config.wizCeilingColorTemp ?? 4000;
|
|
const wizSunsetColorTemp = this._config.wizSunsetColorTemp ?? 2800;
|
|
const wizFloorColorTemp = this._config.wizFloorColorTemp ?? 1900;
|
|
|
|
let newHueTemp = this._config.hueCeilingColorTemp;
|
|
let newWizTemp = this._config.wizCeilingColorTemp;
|
|
|
|
if (start < now && now < sunsetStart) {
|
|
newHueTemp = this.getTempOffset(
|
|
hueStartColorTemp,
|
|
hueSunsetColorTemp,
|
|
start,
|
|
sunsetStart
|
|
);
|
|
|
|
newWizTemp = this.getTempOffset(
|
|
wizStartColorTemp,
|
|
wizSunsetColorTemp,
|
|
start,
|
|
sunsetStart
|
|
);
|
|
} else if (sunsetStart < now && now < sunsetEnd) {
|
|
newHueTemp = this._config.hueSunsetColorTemp;
|
|
newWizTemp = this._config.wizSunsetColorTemp;
|
|
} else if (sunsetEnd < now && now < nightStart) {
|
|
newHueTemp = this.getTempOffset(
|
|
hueSunsetColorTemp,
|
|
hueFloorColorTemp,
|
|
sunsetEnd,
|
|
nightStart
|
|
);
|
|
|
|
newWizTemp = this.getTempOffset(
|
|
wizSunsetColorTemp,
|
|
wizFloorColorTemp,
|
|
sunsetEnd,
|
|
nightStart
|
|
);
|
|
} else if (nightStart < now && now < sunrise) {
|
|
newHueTemp = this._config.hueFloorColorTemp;
|
|
newWizTemp = this._config.wizFloorColorTemp;
|
|
}
|
|
|
|
//Set RGB
|
|
this._hueRGB = colorTempToRgb(newHueTemp);
|
|
this._wizRGB = colorTemperature2rgb(newWizTemp);
|
|
};
|
|
|
|
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 ?? this._cron;
|
|
this._platform.log.info(
|
|
`Scheduling task for ${light?.name}: ${schedule}`
|
|
);
|
|
return cron.schedule(
|
|
schedule,
|
|
async () => {
|
|
await this.updateHueLight(
|
|
light,
|
|
hueLightConfig?.on ?? false
|
|
);
|
|
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 ?? this._cron;
|
|
this._platform.log.info(
|
|
`Scheduling task for ${wizBulb.getMac()}: ${schedule}`
|
|
);
|
|
return cron.schedule(
|
|
schedule,
|
|
async () => {
|
|
await this.updateWizLight(
|
|
wizBulb,
|
|
wizLightConfig?.on ?? false
|
|
);
|
|
this._platform.log.info("Updated hues");
|
|
},
|
|
{
|
|
scheduled: false,
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
private updateWizLight = async (
|
|
wizBulb: WizBulb,
|
|
on: Boolean
|
|
): Promise<void> => {
|
|
let pilot;
|
|
try {
|
|
pilot = await wizBulb.get();
|
|
} catch (err: any) {
|
|
this._platform.log.error(err.message);
|
|
}
|
|
if (pilot && pilot.state) {
|
|
this._platform.log.info(`Adjusting wiz bulb: ${wizBulb.getMac()}`);
|
|
|
|
await wizBulb.set(
|
|
this._wizRGB,
|
|
on ? 100 : pilot.dimming,
|
|
this._fade
|
|
);
|
|
}
|
|
};
|
|
|
|
private updateHueLight = async (
|
|
hueLight: Light | undefined,
|
|
on: Boolean
|
|
): 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);
|
|
|
|
if (on) {
|
|
lightState.brightness(100).on(true);
|
|
}
|
|
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());
|
|
}
|
|
|
|
private test = async () => {
|
|
for (let i = 2500; i > 0; i--) {
|
|
this._platform.log.info(`i: ${i}`);
|
|
for (const wizBulb of this._wizLights) {
|
|
let pilot;
|
|
try {
|
|
pilot = await wizBulb.get();
|
|
} catch (err: any) {
|
|
this._platform.log.error(err.message);
|
|
}
|
|
this._platform.log.info(
|
|
`Adjusting wiz bulb: ${wizBulb.getMac()}`
|
|
);
|
|
wizBulb.set(colorTemperature2rgb(i), 100, this._fade);
|
|
}
|
|
await Sleep(100);
|
|
}
|
|
};
|
|
}
|