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 { Scheduler, Delegate } from "./scheduler"; import { IConfig } from "./models/iConfig"; //@ts-ignore import { getSunset, getSunRise } from 'sunrise-sunset'; 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; //Service fields private _switchService: HAPNodeJS.Service; private _infoService: HAPNodeJS.Service; private _hue: Api; private _lights: Array = []; private _scheduler: Scheduler; 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._scheduler = new Scheduler(this._config.delay ? this._config.delay : 60000, 60000, this._log); this._scheduler.addTask({ delegate: this.updateDelegate, title: "Update" }) 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._scheduler.start(); } else { this._scheduler.stop(); } 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._scheduler.IsStarted); } /** * 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 () => { //Get lights const lightPromises: Array> = this._config.lights.map(async (value: string) => { //@ts-ignore const light: Light = await this._hue.lights.getLightByName(value) this._lights.push(light); }); await Promise.all(lightPromises); } 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 isHueError = (object: any): object is HueError => { return '_hueError' in object; } private setLights = async (state: LightState) => { try { await Promise.all( this._lights.map(async (value: Light) => { await this._hue.lights.setLightState(value.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}`); } } } /** * 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 updateDelegate: Delegate = async (): Promise => { 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 && rgb.red && rgb.blue && rgb.green) { const lightState = new LightState(); lightState .transitionInMillis(this._config.transition ? this._config.transition : 5) .rgb(rgb.red, rgb.green, rgb.blue); await this.setLights(lightState) this._log(`Adjusting light temp to ${newTemp}, ${JSON.stringify(rgb)}`) } return; } }