import { IAccessory } from "./models/iAccessory"; 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 from "node-cron"; import { WizBulb } from "@watsonb8/wiz-lib/build/wizBulb"; import { colorTemperature2rgb, Pilot, RGB } from "@watsonb8/wiz-lib"; let Service: HAPNodeJS.Service; let Characteristic: HAPNodeJS.Characteristic; const SECONDS_IN_DAY = 86400000; const MINUTES_IN_MILLISECOND = 60000; const SECONDS_IN_HOUR = 3600; export interface IFluxProps { api: any; log: any; homebridge: any; hue: Api; wizBulbs: Array; config: IConfig; } export class FluxAccessory implements IAccessory { private _api: any; private _homebridge: any; private _log: any = {}; private _config: IConfig; private _isActive: boolean; //Service fields private _switchService: HAPNodeJS.Service; private _infoService: HAPNodeJS.Service; private _hue: Api; private _lights: Array = []; private _wizLights: Array = []; private _times: GetTimesResult; constructor(props: IFluxProps) { //Assign class variables this._log = props.log; this._api = props.api; this._config = props.config; Service = props.api.hap.Service; Characteristic = props.api.hap.Characteristic; this._homebridge = props.homebridge; this._isActive = false; this._wizLights = props.wizBulbs; 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._log("Updated sunset times"); }, { scheduled: true, } ).start(); this._hue = props.hue; this.name = this._config.name; this.platformAccessory = new this._homebridge.platformAccessory( this.name, this.generateUUID(), this._homebridge.hap.Accessory.Categories.SWITCH ); //@ts-ignore this._infoService = new Service.AccessoryInformation(); this._infoService.setCharacteristic( Characteristic.Manufacturer, "Brandon Watson" ); this._infoService.setCharacteristic(Characteristic.Model, "F.lux"); this._infoService.setCharacteristic( Characteristic.SerialNumber, "123-456-789" ); this._switchService = new Service.Switch(this.name, "fluxService"); this._switchService .getCharacteristic(Characteristic.On) //@ts-ignore .on("set", this.onSetEnabled) //@ts-ignore .on("get", this.onGetEnabled); } 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.update(); } else { this._isActive = false; } 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; } /** * Called by homebridge to gather services. */ public getServices = (): Array => { return [this._infoService, this._switchService!]; }; /** * Popuplates internal lights array using the configuration values */ private getLights = async (): Promise => { for (const value of this._config.lights) { //@ts-ignore const light: Light = await this._hue.lights.getLightByName(value); 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 | PromiseLike> = []; 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._log(`Error while setting lights: ${err}`); } } }); await Promise.all(promises); }; private setWizLights = async (rgb: RGB, fade: number): Promise => { 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. */ 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 update = async (): Promise => { this._isActive = true; while (this._isActive) { if (this._lights.length === 0) { await this.getLights(); } 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._log( `Adjusting light temp to ${newTemp}, ${JSON.stringify( hueRGB )}` ); } await Sleep(this._config.delay ? this._config.delay : 60000); } }; }