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"); let Service: HAPNodeJS.Service; let Characteristic: HAPNodeJS.Characteristic; export interface IFluxProps { api: any; log: any; homebridge: any; hue: Api, 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 _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._hue = props.hue; this.name = this._config.name; this._times = getTimes(new Date(), this._config.latitude, this._config.longitude); this.getLights(); 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) .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) => { return callback(null, 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): { red: number, green: number, blue: number } => { 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 { red: this.clamp(red, 0, 255), green: this.clamp(green, 0, 255), blue: 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 setLights = async (state: LightState) => { const promises: Array | PromiseLike> = []; this._lights.map(async (light: Light) => { try { await this._hue.lights.setLightState(light.id, state); } catch (err) { this._log(`Error while setting lights: ${err}`); } }); await Promise.all(promises); } /** * 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); }); } private update = async (): Promise => { this._isActive = true; while (this._isActive) { if (this._lights.length === 0) { await this.getLights(); } const now = new Date(Date.now()); const sunset = this._times.sunset; let startTime: Date; if (this._config.startTimeHour) { startTime = new Date(Date.now()); startTime.setHours(this._config.startTimeHour, this._config.startTimeMinute); } else { startTime = this._times.sunrise; } let stopTime: Date; if (this._config.stopTimeHour) { stopTime = new Date(Date.now()); stopTime.setHours(this._config.stopTimeHour, this._config.stopTimeMinute); } else { stopTime = this._times.dusk; } const startColorTemp = this._config.startColorTemp ? this._config.startColorTemp : 4000; const stopColorTemp = this._config.stopColorTemp ? this._config.stopColorTemp : 1900; const sunsetColorTemp = this._config.sunsetColorTemp ? this._config.sunsetColorTemp : 3000; let percentageComplete = 0; let newTemp = 0; //Adjust for next day times if (stopTime <= startTime) { //Stop time does not happen in the same day as start time if (startTime < now) { //stop time is tomorrow stopTime.setTime(stopTime.getTime() + 1 * 86400000); } } else if (now < startTime) { //Stop time was yesterday since the new start time is not reached stopTime.setTime(stopTime.getTime() - 1 * 86400000); } if ((startTime < now) && (now < sunset)) { //Before sunset; calculate temp based on TOD const tempRange = Math.abs(startColorTemp - stopColorTemp); const dayLength = (sunset.getTime() - startTime.getTime()) / 1000; const secondsFromStart = (now.getTime() - startTime.getTime()) / 1000; percentageComplete = secondsFromStart / dayLength; const tempOffset = tempRange * percentageComplete; if (startColorTemp > sunsetColorTemp) { newTemp = startColorTemp - tempOffset; } else { newTemp = startColorTemp + tempOffset; } } else { //After sunset; calculate temp based on TOD if (now < stopTime) { let sunsetTime; if ((stopTime < startTime) && stopTime.getDay() == sunset.getDay()) { sunsetTime = new Date(sunset.setTime(stopTime.getTime() + 1 * 86400000)); } else { sunsetTime = sunset; } const nightLength = (stopTime.getTime() - sunsetTime.getTime()) / 1000; const secondsFromSunset = (now.getTime() - sunsetTime.getTime()) / 100; percentageComplete = secondsFromSunset / nightLength; } else { percentageComplete = 1; } const tempRange = Math.abs(sunsetColorTemp - stopColorTemp); const tempOffset = tempRange * percentageComplete; if (startColorTemp > sunsetColorTemp) { newTemp = startColorTemp - tempOffset; } else { newTemp = startColorTemp + tempOffset; } } //Set lights const rgb = this.colorTempToRgb(newTemp); if (rgb) { const lightState = new LightState(); lightState .transitionInMillis(this._config.transition ? this._config.transition : 5000) .rgb(rgb.red ? rgb.red : 0, rgb.green ? rgb.green : 0, rgb.blue ? rgb.blue : 0); await this.setLights(lightState) this._log(`Adjusting light temp to ${newTemp}, ${JSON.stringify(rgb)}`) } await Sleep(this._config.delay ? this._config.delay : 60000); } } }