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; 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 = []; private _wizLights: Array = []; private _times: GetTimesResult; private _tasks: Array = []; 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 => { 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 => { if (this._lights.length === 0) { await this.getLights(); } this._tasks = [...this.getHueTasks(), ...this.getWizTasks()]; }; private getHueTasks(): Array { 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 { 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 => { 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 => { 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); } }; }