From 106a1280dd999173836fd90de48f6329c515d45c Mon Sep 17 00:00:00 2001 From: watsonb8 Date: Sat, 11 Apr 2020 15:49:07 -0400 Subject: [PATCH] Working. Still need some testing --- package-lock.json | 10 +++ package.json | 4 +- src/fluxAccessory.ts | 200 ++++++++++++++++++++++++++++++++++++++---- src/index.ts | 7 +- src/models/iConfig.ts | 23 ++--- src/scheduler.ts | 4 +- tsconfig.json | 2 +- 7 files changed, 218 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3789650..db2693d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,11 @@ "integrity": "sha512-eWQGP3qtxwL8FGneRrC5DwrJLGN4/dH1clNTuLfN81HCrxVtxRjygDTUoZJ5ASlDEeo0ppYFQjQIlXhtXpOn6g==", "dev": true }, + "@types/suncalc": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@types/suncalc/-/suncalc-1.8.0.tgz", + "integrity": "sha512-1Bx7KgoCLP8LuKaY9whWiX0Y8JMEB9gmZHNJigainwFuv3gEkZvTx0AGNvnA5nSu1daQcJDKScm9tNpW/ZjpjA==" + }, "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", @@ -461,6 +466,11 @@ "ansi-regex": "^2.0.0" } }, + "suncalc": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/suncalc/-/suncalc-1.8.0.tgz", + "integrity": "sha1-HZiYEJVjB4dQ9JlKlZ5lTYdqy/U=" + }, "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", diff --git a/package.json b/package.json index e46919a..f81b01e 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,10 @@ "author": "Brandon Watson", "license": "ISC", "dependencies": { + "@types/suncalc": "^1.8.0", "homebridge": "^0.4.53", - "node-hue-api": "^4.0.5" + "node-hue-api": "^4.0.5", + "suncalc": "^1.8.0" }, "devDependencies": { "@types/node": "^13.11.1" diff --git a/src/fluxAccessory.ts b/src/fluxAccessory.ts index b81161f..78fbb3a 100644 --- a/src/fluxAccessory.ts +++ b/src/fluxAccessory.ts @@ -3,7 +3,12 @@ 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 } from "./scheduler"; +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; @@ -12,13 +17,15 @@ export interface IFluxProps { api: any; log: any; homebridge: any; - hue: Api + 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; @@ -31,17 +38,26 @@ export class FluxAccessory implements IAccessory { 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(60000, 60000, this._log); + this._scheduler = new Scheduler(this._config.delay ? this._config.delay : 60000, 60000, this._log); + this._scheduler.addTask({ delegate: this.updateDelegate, title: "Update" }) this._isEnabled = 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); @@ -71,7 +87,11 @@ export class FluxAccessory implements IAccessory { * @param callback The callback function to call when complete */ private onSetEnabled = async (activeState: boolean, callback: (error?: Error | null | undefined) => void) => { - + if (activeState) { + this._scheduler.start(); + } else { + this._scheduler.stop(); + } return callback(); } @@ -80,7 +100,7 @@ export class FluxAccessory implements IAccessory { * @param callback The callback function to call when complete */ private onGetEnabled = (callback: (error: Error | null, value: boolean) => void) => { - return callback(null, this._isEnabled); + return callback(null, this._scheduler.IsStarted); } @@ -91,18 +111,73 @@ export class FluxAccessory implements IAccessory { return [this._infoService, this._switchService!]; } - /** - * Helper function to convert a hex string to rgb - * @param hex hex string starting with "#" + * Popuplates internal lights array using the configuration values */ - private hexToRgb = (hex: string): { red: number, green: number, blue: number } | null => { - var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - return result ? { - red: parseInt(result[1], 16), - green: parseInt(result[2], 16), - blue: parseInt(result[3], 16) - } : null; + 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}`); + } + } } /** @@ -120,4 +195,99 @@ export class FluxAccessory implements IAccessory { }); } + 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; + } } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index ac0f963..9fc3c52 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { IConfig, ISequence } from "./models/iConfig"; +import { IConfig } from "./models/iConfig"; import { v3 } from 'node-hue-api'; import LocalBootstrap = require("node-hue-api/lib/api/http/LocalBootstrap"); import Api = require("node-hue-api/lib/api/Api"); @@ -36,7 +36,7 @@ class FluxPlatform { this.log = log; this.api = api; this.config = config; - this.log('INFO - Registering Hue Chase platform'); + this.log('INFO - Registering Flux platform'); this.api.on('didFinishLaunching', this.didFinishLaunching.bind(this)); } @@ -81,7 +81,7 @@ class FluxPlatform { * Happens after constructor */ private didFinishLaunching() { - this.log(`INFO - Done registering Hue Chase platform`); + this.log(`INFO - Done registering Flux platform`); } /** @@ -97,6 +97,7 @@ class FluxPlatform { log: this.log, homebridge: Homebridge, hue: this.hue!, + config: this.config })); callback(this.accessoryList); diff --git a/src/models/iConfig.ts b/src/models/iConfig.ts index db14cdb..38bfcc7 100644 --- a/src/models/iConfig.ts +++ b/src/models/iConfig.ts @@ -1,16 +1,19 @@ -export interface ISequence { - name: string; - transitionTime: number; - matchAllLights?: boolean; - colors: Array; - lights: Array; -} - export interface IConfig { platform: string; ipAddress: string; userName?: string; clientKey?: string; - configLocation: string; - sequences: Array + latitude: number; + longitude: number; + lights: Array; + name: string; + startTimeHour?: number; + startTimeMinute?: number; + stopTimeHour?: number; + stopTimeMinute?: number; + startColorTemp?: number; + stopColorTemp?: number; + sunsetColorTemp?: number; + transition?: number; + delay?: number; } \ No newline at end of file diff --git a/src/scheduler.ts b/src/scheduler.ts index af1fbbe..1001b6c 100644 --- a/src/scheduler.ts +++ b/src/scheduler.ts @@ -86,8 +86,8 @@ export class Scheduler extends EventEmitter { continue; } } - } - await Sleep(this._delay); + await Sleep(this._delay); + } } } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 7c8c5ee..fce8d3c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,7 @@ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ // "declaration": true, /* Generates corresponding '.d.ts' file. */ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ - // "sourceMap": true, /* Generates corresponding '.map' file. */ + "sourceMap": true, /* Generates corresponding '.map' file. */ // "outFile": "./", /* Concatenate and emit output to single file. */ "outDir": "./bin", /* Redirect output structure to the directory. */ // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */