Compare commits

..

29 Commits

Author SHA1 Message Date
d6b7f3b6e6 Removing drone yaml
All checks were successful
Build homebridge-flux / Version (push) Successful in 7s
Build homebridge-flux / Build (push) Successful in 14s
Build homebridge-flux / Publish Latest (push) Successful in 7s
Build homebridge-flux / Deploy (push) Successful in 12s
2024-06-06 10:32:41 -05:00
f69f2a3ca5 Adding ci action
All checks were successful
Build homebridge-flux / Build (push) Successful in 16s
Build homebridge-flux / Version (push) Successful in 7s
Build homebridge-flux / Publish Latest (push) Successful in 9s
Build homebridge-flux / Deploy (push) Successful in 13s
2024-06-06 10:25:45 -05:00
Brandon Watson
1d39c725af Adding separate configuration for wiz vs hue lights
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-30 12:14:10 -05:00
Brandon Watson
417f017f45 Using command line to publish libs
Some checks failed
continuous-integration/drone Build is passing
continuous-integration/drone/push Build is failing
WIP

WIP
2023-01-27 16:11:23 -06:00
Brandon Watson
9cf8ef3c60 Using gitea npm registry
Some checks failed
continuous-integration/drone Build is failing
WIP

WIP
2023-01-26 17:14:57 -06:00
Brandon Watson
e1ac0a3a5b Only control wiz lights if they are already on
All checks were successful
continuous-integration/drone Build is passing
2022-10-30 10:47:08 -05:00
Brandon Watson
e61ec0cc3c Stability fixes
Updating longitude type

Only updating wiz bulb if pilot is not undefined

Fixing issue where lights will turn off after receiving power

Fixing bug where wiz lights would go dark instead of maintaining the current brightness
2022-10-29 13:59:36 -05:00
Brandon Watson
abb66eb26f Bump wiz-lib
All checks were successful
continuous-integration/drone/push Build is passing
2022-09-07 22:10:50 -05:00
Brandon Watson
c378e46cb3 Making default cron schedule configurable
All checks were successful
continuous-integration/drone/push Build is passing
asdf
2022-09-07 22:04:17 -05:00
Brandon Watson
a9833729f7 Fixing bugs
All checks were successful
continuous-integration/drone/push Build is passing
- Only RGB lights
- Fixing on/off bug
2022-09-07 21:41:26 -05:00
Brandon Watson
51b82fc8d2 Updating config schema
All checks were successful
continuous-integration/drone/push Build is passing
asdf
2022-09-07 14:45:36 -05:00
Brandon Watson
feb5533419 Adding new features
All checks were successful
continuous-integration/drone/push Build is passing
- Adding ability to specify delay times per light
- Adding config.schema.json
2022-09-07 14:29:51 -05:00
Brandon Watson
8fbfc51276 Bump wiz-lib
All checks were successful
continuous-integration/drone/push Build is passing
Bump
2022-09-06 20:44:31 -05:00
Brandon Watson
c79b776ec5 Publishing bin dir
All checks were successful
continuous-integration/drone/push Build is passing
2022-09-06 20:33:25 -05:00
Brandon Watson
da83a94742 Squashed commit of the following:
All checks were successful
continuous-integration/drone/push Build is passing
commit 440a4d62a19a80917cc0fc2afea755640d2954d0
Author: Brandon Watson <brandon@watsonlabs.net>
Date:   Tue Sep 6 20:23:31 2022 -0500

    Fixing issue where button becomes unresponsive | Updating homebridge

commit d73dead5d4606e8cf1d38f7c283828e5e734538a
Author: Brandon Watson <brandon@watsonlabs.net>
Date:   Tue Sep 6 18:25:38 2022 -0500

    Successfully controlling wiz bulbs
2022-09-06 20:28:49 -05:00
Brandon Watson
c3a6882413 Updating build
All checks were successful
continuous-integration/drone/push Build is passing
2022-09-06 08:59:16 -05:00
Brandon Watson
f8320319a4 Restricting build to internal lan
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
2022-01-07 16:58:29 -05:00
Brandon Watson
779ffac227 Adding deployment steps
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-07 16:56:05 -05:00
Brandon Watson
55c65dac84 Fixing issue where flux was unresponsive
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-03 13:13:50 -05:00
Brandon Watson
8825a116fe Moving homebridge to dev deps
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-29 18:00:11 -05:00
Brandon Watson
a31ec7c282 Fixing publish step
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-29 17:43:55 -05:00
Brandon Watson
5e697021ed Bump version
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-29 17:42:48 -05:00
Brandon Watson
f935896cf2 Update homebridge version
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-23 15:34:34 -05:00
Brandon Watson
829e45b329 Fix build
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-23 15:32:41 -05:00
Brandon Watson
728d3a64a2 Update package versions
Some checks failed
continuous-integration/drone/push Build is failing
2021-12-22 21:06:48 -05:00
Brandon Watson
362a5e9d7c Rebuilding package lock
Some checks failed
continuous-integration/drone/push Build is failing
2021-12-22 21:03:10 -05:00
Brandon Watson
b5fe4a64b9 Updating registry
Some checks failed
continuous-integration/drone/push Build is failing
2021-12-22 20:58:24 -05:00
Brandon Watson
c80b01d3e2 Update drone.yml
Some checks reported errors
continuous-integration/drone Build was killed
2021-12-22 20:54:14 -05:00
Brandon Watson
87e6ff99f6 Adding typescript dev dep
Some checks failed
continuous-integration/drone/push Build is failing
2021-06-03 20:23:11 -04:00
17 changed files with 4002 additions and 1343 deletions

View File

@ -1,41 +0,0 @@
kind: pipeline
type: docker
name: default
clone:
disable: true
steps:
- name: clone
image: alpine/git
commands:
- git clone https://gitea.watsonlabs.net/watsonb8/homebridge-flux.git .
- git checkout $DRONE_COMMIT
- name: build
image: node
commands:
- npm install
- npm run build
- name: publish
image: plugins/npm
settings:
username: admin
password:
from_secret: npm_password
email: brandon@watsonlabs.net
registry: "http://linuxhost.me:4873/"
when:
event:
- tag
notify:
image: drillster/drone-email
host: smtp.watsonlabs.net
username: srvGitea
password:
from_secret: smtp_password
from: drone@watsonlabs.net
when:
status: [failure]

85
.gitea/workflows/ci.yaml Normal file
View File

@ -0,0 +1,85 @@
name: Build homebridge-flux
on:
workflow_dispatch:
push:
branches:
- master
jobs:
build:
name: Build
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- run: |
npm ci
npm run build
version:
name: Version
outputs:
version: ${{ steps.get_version.outputs.version }}
commit: ${{ steps.get_version.outputs.commit }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- id: get_version
name: Set Version
run: |
export version=`node -p "require('./package.json').version"`
export commit=`echo $GITHUB_SHA | cut -c1-5`
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "commit=$commit" >> "$GITHUB_OUTPUT"
publish_tagged:
name: Publish Latest
needs:
- build
- version
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm config set @watsonb8:registry https://gitea.watsonlabs.net/api/packages/watsonb8/npm/
- name: Publish
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
COMMIT: ${{ needs.version.outputs.commit }}
run: |
npm config set -- '//gitea.watsonlabs.net/api/packages/watsonb8/npm/:_authToken' "$NPM_TOKEN"
npm version prerelease --preid="$COMMIT" --git-tag-version=false --allow-same-version=true
npm publish
deploy:
runs-on:
- ubuntu-latest
- internal
name: Deploy
needs:
- publish_tagged
- version
steps:
- name: Set up SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.ELEVATED_HOMEBRIDGE_SSH_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -p 22 homebridge.me >> ~/.ssh/known_hosts
sudo apt update
sudo apt install sshpass
- name: Remove old Package
run: |
sshpass -p '${{ secrets.ELEVATED_HOMEBRIDGE_PASSWORD }}' ssh -v -o StrictHostKeyChecking=no ${{ secrets.ELEVATED_HOMEBRIDGE_USER }}@${{ secrets.HOMEBRIDGE_HOST }} <<'ENDSSH'
rm -r /home/${{ secrets.HOMEBRIDGE_USER }}/.npm-global/lib/node_modules/@watsonb8/homebridge-flux
ENDSSH
- name: Deploy
env:
COMMIT: ${{ needs.version.outputs.commit }}
run: |
sshpass -p '${{ secrets.ELEVATED_HOMEBRIDGE_PASSWORD }}' ssh -v -o StrictHostKeyChecking=no ${{ secrets.ELEVATED_HOMEBRIDGE_USER }}@${{ secrets.HOMEBRIDGE_HOST }} <<'ENDSSH'
npm install -g @watsonb8/homebridge-flux@$COMMIT
ENDSSH

4
.prettierrc Normal file
View File

@ -0,0 +1,4 @@
{
"tabWidth": 4,
"useTabs": false
}

10
.vscode/launch.json vendored
View File

@ -8,9 +8,13 @@
"type": "node",
"request": "launch",
"name": "Launch Program",
"preLaunchTask": "build and install",
"program": "/Users/brandonwatson/.npm-global/bin/homebridge",
"sourceMaps": true,
"preLaunchTask": "build",
"program": "/Users/brandonwatson/.nvm/versions/node/v14.15.0/lib/node_modules/homebridge/bin/homebridge",
"env": {
"HOMEBRIDGE_OPTS": "/Users/brandonwatson/.homebridge",
"LOG_LEVEL": "debug"
},
"sourceMaps": true
}
]
}

1
.vscode/tasks.json vendored
View File

@ -6,6 +6,7 @@
{
"type": "npm",
"script": "build",
"label": "build",
"problemMatcher": []
},
{

154
config.schema.json Normal file
View File

@ -0,0 +1,154 @@
{
"pluginAlias": "Flux",
"pluginType": "platform",
"singular": true,
"schema": {
"type": "object",
"properties": {
"name": {
"title": "Switch Name",
"type": "string",
"required": true
},
"ipAddress": {
"title": "IP Address",
"type": "string",
"required": true
},
"userName": {
"title": "User Name",
"type": "string",
"required": false
},
"clientKey": {
"title": "Client Key",
"type": "string",
"required": false
},
"latitude": {
"title": "Latitude",
"type": "number",
"required": true
},
"longitude": {
"title": "Longitude",
"type": "number",
"required": true
},
"testNowDateString": {
"title": "Test Date Time",
"type": "string",
"required": false
},
"hueLights": {
"title": "Hue Lights",
"type": "array",
"uniqueItems": true,
"items": {
"type": "object",
"properties": {
"name": {
"title": "Name",
"type": "string",
"required": true
},
"cron": {
"title": "Cron Schedule",
"type": "string",
"required": false
},
"on": {
"title": "On",
"type": "boolean",
"required": false
}
}
},
"required": true
},
"wizLights": {
"title": "Wiz Lights",
"type": "array",
"uniqueItems": true,
"items": {
"type": "object",
"properties": {
"ip": {
"title": "Ip Address",
"type": "string",
"required": true
},
"mac": {
"title": "Mac Address",
"type": "string",
"required": true
},
"cron": {
"title": "Cron Schedule",
"type": "string",
"required": false
},
"on": {
"title": "On",
"type": "boolean",
"required": false
}
}
},
"required": false
},
"wizDiscoveryEnabled": {
"title": "Wiz Discovery Enabled",
"type": "boolean",
"required": true
},
"hueCeilingColorTemp": {
"title": "Hue Ceiling Color Temperature",
"type": "number",
"required": true
},
"hueSunsetColorTemp": {
"title": "Hue Sunset Color Temperature",
"type": "number",
"required": true
},
"hueFloorColorTemp": {
"title": "Hue Floor Color Temperature",
"type": "number",
"required": true
},
"wizCeilingColorTemp": {
"title": "Wiz Ceiling Color Temperature",
"type": "number",
"required": true
},
"wizSunsetColorTemp": {
"title": "Wiz Sunset Color Temperature",
"type": "number",
"required": true
},
"wizFloorColorTemp": {
"title": "Wiz Floor Color Temperature",
"type": "number",
"required": true
},
"sunsetDuration": {
"title": "Sunset Duration",
"type": "number",
"required": true
},
"transition": {
"title": "Transition Time",
"type": "number",
"required": true
},
"cron": {
"title": "Default Cron",
"type": "string",
"required": false
}
}
},
"form": null,
"display": null
}

3225
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,16 @@
{
"name": "homebridge-flux",
"version": "1.0.0",
"name": "@watsonb8/homebridge-flux",
"version": "1.2.0",
"description": "",
"main": "bin/index.js",
"publishConfig": {
"registry": "https://gitea.watsonlabs.net"
},
"files": [
"bin",
"src",
"config.schema.json"
],
"scripts": {
"build": "tsc",
"test": "echo \"Error: no test specified\" && exit 1"
@ -18,7 +26,7 @@
"typescript"
],
"engines": {
"homebridge": ">=0.4.21",
"homebridge": ">=1.1.6",
"node": ">=7.6.0"
},
"author": "Brandon Watson",
@ -26,12 +34,14 @@
"dependencies": {
"@types/node-cron": "^2.0.3",
"@types/suncalc": "^1.8.0",
"homebridge": "^0.4.53",
"@watsonb8/wiz-lib": "^1.0.1-ae175.0",
"node-cron": "^2.0.3",
"node-hue-api": "^4.0.5",
"suncalc": "^1.8.0"
},
"devDependencies": {
"@types/node": "^13.11.1"
"@types/node": "^13.11.1",
"homebridge": "^1.5.0",
"typescript": "^4.5.4"
}
}

View File

@ -1,4 +1,3 @@
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");
@ -7,79 +6,132 @@ 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 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";
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,
platform: Platform;
accessory: PlatformAccessory;
hue: Api;
wizBulbs: Array<WizBulb>;
config: IConfig;
}
export class FluxAccessory implements IAccessory {
private _api: any;
private _homebridge: any;
private _log: any = {};
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: HAPNodeJS.Service;
private _infoService: HAPNodeJS.Service;
private _switchService;
private _hue: Api;
private _lights: Array<Light> = [];
private _wizLights: Array<WizBulb> = [];
private _times: GetTimesResult;
private _tasks: Array<ScheduledTask> = [];
constructor(props: IFluxProps) {
//Assign class variables
this._log = props.log;
this._api = props.api;
this._platform = props.platform;
this._accessory = props.accessory;
this._config = props.config;
Service = props.api.hap.Service;
Characteristic = props.api.hap.Characteristic;
this._homebridge = props.homebridge;
this._isActive = false;
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._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.platformAccessory = new this._homebridge.platformAccessory(this.name, this.generateUUID(), this._homebridge.hap.Accessory.Categories.SWITCH);
this._times = getTimes(
new Date(),
this._config.latitude,
this._config.longitude
);
//@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");
//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();
this._switchService = new Service.Switch(
this.name,
'fluxService'
//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"
);
this._switchService.getCharacteristic(Characteristic.On)
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";
@ -90,126 +142,70 @@ export class FluxAccessory implements IAccessory {
* 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) => {
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();
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) => {
return callback(null, this._isActive);
}
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<HAPNodeJS.Service> => {
return [this._infoService, this._switchService!];
}
/**
* Popuplates internal lights array using the configuration values
* Populates internal lights array using the configuration values
*/
private getLights = async (): Promise<void> => {
for (const value of this._config.lights) {
for (const value of this._config.hueLights) {
//@ts-ignore
const light: Light = await this._hue.lights.getLightByName(value)
const light: Light = await this._hue.lights.getLightByName(
value.name
);
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 isHueError = (object: any): object is HueError => {
return '_hueError' in object;
}
private setLights = async (state: LightState) => {
const promises: Array<Promise<unknown> | PromiseLike<unknown>> = [];
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);
}
/**
* 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);
});
}
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()));
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
const tempOffset = tempRange * percentComplete;
return startTemp - tempOffset;
}
};
/**
* Get the current time. Use test time if present.
@ -222,49 +218,208 @@ export class FluxAccessory implements IAccessory {
}
}
private update = async (): Promise<void> => {
this._isActive = true;
while (this._isActive) {
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<void> => {
if (this._lights.length === 0) {
await this.getLights();
}
this._tasks = [...this.getHueTasks(), ...this.getWizTasks()];
};
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;
private getHueTasks(): Array<ScheduledTask> {
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,
}
);
});
}
//Set lights
const rgb = this.colorTempToRgb(newTemp);
if (rgb && newTemp !== 0) {
private getWizTasks(): Array<ScheduledTask> {
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<void> => {
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<void> => {
if (!hueLight) {
return;
}
this._platform.log.info(`Adjusting wiz bulb: ${hueLight.name}`);
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)}`)
.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());
}
await Sleep(this._config.delay ? this._config.delay : 60000);
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);
}
};
}

View File

@ -1,105 +1,11 @@
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");
import { Sleep } from "./sleep";
import { IAccessory } from "./models/iAccessory";
import { FluxAccessory } from "./fluxAccessory";
import { API } from "homebridge";
let Accessory: any;
let Homebridge: any;
import { PLATFORM_NAME } from "./settings";
import { Platform } from "./platform";
/**
* Main entry.
* @param homebridge
* This method registers the platform with Homebridge
*/
export default function (homebridge: any) {
Homebridge = homebridge;
Accessory = homebridge.platformAccessory;
homebridge.registerPlatform(
'homebridge-flux',
'Flux',
FluxPlatform,
true
);
export = (api: API) => {
api.registerPlatform(PLATFORM_NAME, Platform);
};
class FluxPlatform {
log: any = {};
api: any;
accessoryList: Array<IAccessory> = [];
config: IConfig;
hue: Api | undefined;
constructor(log: any, config: any, api: any) {
this.log = log;
this.api = api;
this.config = config;
this.log('INFO - Registering Flux platform');
this.api.on('didFinishLaunching', this.didFinishLaunching.bind(this));
}
private connectHue = async () => {
if (!this.config) {
return;
}
if (this.config.userName && this.config.clientKey) {
this.hue = await v3.api.createLocal(this.config.ipAddress).connect(this.config.userName, this.config.clientKey, undefined);
this.log("Using existing connection info");
} else {
const unauthenticatedApi = await v3.api.createLocal(this.config.ipAddress).connect(undefined, undefined, undefined);
let createdUser;
let connected = false
while (!connected) {
try {
this.log("Creating hue user. Push link button")
createdUser = await unauthenticatedApi.users.createUser("homebridge", "HueChase");
this.hue = await v3.api.createLocal(this.config.ipAddress).connect(createdUser.username, createdUser.clientKey, undefined);
this.log("Connected to Hue Bridge");
this.log(`UserName: ${createdUser.username}, ClientKey: ${createdUser.clientkey}`)
connected = true;
} catch (err) {
if (err.getHueErrorType() === 101) {
this.log('The Link button on the bridge was not pressed. Please press the Link button and try again.');
Sleep(5000);
} else {
this.log(`Unexpected Error: ${err.message}`);
break;
}
}
}
}
}
/**
* Handler for didFinishLaunching
* Happens after constructor
*/
private didFinishLaunching() {
this.log(`INFO - Done registering Flux platform`);
}
/**
* Called by homebridge to gather accessories.
* @param callback
*/
public accessories = async (callback: (accessories: Array<IAccessory>) => void) => {
//Connect to hue bridge
await this.connectHue();
this.accessoryList.push(new FluxAccessory({
api: this.api,
log: this.log,
homebridge: Homebridge,
hue: this.hue!,
config: this.config
}));
callback(this.accessoryList);
}
}

View File

@ -1,456 +0,0 @@
declare namespace HAPNodeJS {
export interface uuid {
generate(data: string): string;
isValid(UUID: string): boolean;
unparse(bug: string, offset: number): string;
}
type EventService = "characteristic-change" | "service-configurationChange"
export interface IEventEmitterAccessory {
addListener(event: EventService, listener: Function): this;
on(event: EventService, listener: Function): this;
once(event: EventService, listener: Function): this;
removeListener(event: EventService, listener: Function): this;
removeAllListeners(event?: EventService): this;
setMaxListeners(n: number): this;
getMaxListeners(): number;
listeners(event: EventService): Function[];
emit(event: EventService, ...args: any[]): boolean;
listenerCount(type: string): number;
}
export interface Service extends IEventEmitterAccessory {
new(displayName: string, UUID: string, subtype: string): Service;
displayName: string;
UUID: string;
subtype: string;
iid: string;
characteristics: Characteristic[];
optionalCharacteristics: Characteristic[];
addCharacteristic(characteristic: Characteristic | Function): Characteristic;
removeCharacteristic(characteristic: Characteristic): void;
getCharacteristic(name: string | Function): Characteristic;
testCharacteristic(name: string | Function): boolean;
setCharacteristic(name: string | Function, value: CharacteristicValue): Service;
updateCharacteristic(name: string | Function, value: CharacteristicValue): Service;
addOptionalCharacteristic(characteristic: Characteristic | Function): void;
getCharacteristicByIID(iid: string): Characteristic;
toHAP(opt: any): JSON;
AccessoryInformation: PredefinedService;
AirPurifier: PredefinedService;
AirQualitySensor: PredefinedService;
BatteryService: PredefinedService;
BridgeConfiguration: PredefinedService;
BridgingState: PredefinedService;
CameraControl: PredefinedService;
CameraRTPStreamManagement: PredefinedService;
CarbonDioxideSensor: PredefinedService;
CarbonMonoxideSensor: PredefinedService;
ContactSensor: PredefinedService;
Door: PredefinedService;
Doorbell: PredefinedService;
Fan: PredefinedService;
Fanv2: PredefinedService;
Faucet: PredefinedService;
FilterMaintenance: PredefinedService;
GarageDoorOpener: PredefinedService;
HeaterCooler: PredefinedService;
HumidifierDehumidifier: PredefinedService;
HumiditySensor: PredefinedService;
InputSource: PredefinedService;
IrrigationSystem: PredefinedService;
LeakSensor: PredefinedService;
LightSensor: PredefinedService;
Lightbulb: PredefinedService;
LockManagement: PredefinedService;
LockMechanism: PredefinedService;
Microphone: PredefinedService;
MotionSensor: PredefinedService;
OccupancySensor: PredefinedService;
Outlet: PredefinedService;
Pairing: PredefinedService;
ProtocolInformation: PredefinedService;
Relay: PredefinedService;
SecuritySystem: PredefinedService;
ServiceLabel: PredefinedService;
Slat: PredefinedService;
SmokeSensor: PredefinedService;
Speaker: PredefinedService;
StatefulProgrammableSwitch: PredefinedService;
StatelessProgrammableSwitch: PredefinedService;
Switch: PredefinedService;
Television: PredefinedService;
TelevisionSpeaker: PredefinedService;
TemperatureSensor: PredefinedService;
Thermostat: PredefinedService;
TimeInformation: PredefinedService;
TunneledBTLEAccessoryService: PredefinedService;
Valve: PredefinedService;
Window: PredefinedService;
WindowCovering: PredefinedService;
}
export interface PredefinedService {
new(displayName: string, subtype: string): Service;
}
export interface CameraSource {
}
type EventAccessory = "service-configurationChange" | "service-characteristic-change" | "identify"
export interface IEventEmitterAccessory {
addListener(event: EventAccessory, listener: Function): this;
on(event: EventAccessory, listener: Function): this;
once(event: EventAccessory, listener: Function): this;
removeListener(event: EventAccessory, listener: Function): this;
removeAllListeners(event?: EventAccessory): this;
setMaxListeners(n: number): this;
getMaxListeners(): number;
listeners(event: EventAccessory): Function[];
emit(event: EventAccessory, ...args: any[]): boolean;
listenerCount(type: string): number;
}
export interface CharacteristicProps {
format: Characteristic.Formats;
unit: Characteristic.Units,
minValue: number,
maxValue: number,
minStep: number,
perms: Characteristic.Perms[]
}
type EventCharacteristic = "get" | "set"
type CharacteristicValue = boolean | string | number
export type CharacteristicGetCallback<T = CharacteristicValue> = (error: Error | null, value: T) => void
export type CharacteristicSetCallback = (error?: Error | null) => void
export type CharacteristicCallback = CharacteristicGetCallback | CharacteristicSetCallback
export interface IEventEmitterCharacteristic {
addListener(event: EventCharacteristic, listener: CharacteristicCallback): this;
on(event: EventCharacteristic, listener: CharacteristicCallback): this;
once(event: EventCharacteristic, listener: CharacteristicCallback): this;
removeListener(event: EventCharacteristic, listener: CharacteristicCallback): this;
removeAllListeners(event?: EventCharacteristic): this;
setMaxListeners(n: number): this;
getMaxListeners(): number;
listeners(event: EventCharacteristic): CharacteristicCallback[];
emit(event: EventCharacteristic, ...args: any[]): boolean;
listenerCount(type: string): number;
}
export interface Characteristic extends IEventEmitterCharacteristic {
new(displayName: string, UUID: string, props?: CharacteristicProps): Characteristic;
Formats: typeof Characteristic.Formats;
Units: typeof Characteristic.Units;
Perms: typeof Characteristic.Perms;
setProps(props: CharacteristicProps): Characteristic
getValue(callback?: CharacteristicGetCallback, context?: any, connectionID?: string): void;
setValue(newValue: CharacteristicValue, callback?: CharacteristicSetCallback, context?: any, connectionID?: string): Characteristic;
updateValue(newValue: CharacteristicValue, callback?: () => void, context?: any): Characteristic;
getDefaultValue(): CharacteristicValue;
toHAP(opt: any): JSON;
AccessoryFlags: Characteristic;
AccessoryIdentifier: Characteristic;
Active: Characteristic;
ActiveIdentifier: Characteristic;
AdministratorOnlyAccess: Characteristic;
AirParticulateDensity: Characteristic;
AirParticulateSize: Characteristic;
AirQuality: Characteristic;
AppMatchingIdentifier: Characteristic;
AudioFeedback: Characteristic;
BatteryLevel: Characteristic;
Brightness: Characteristic;
CarbonDioxideDetected: Characteristic;
CarbonDioxideLevel: Characteristic;
CarbonDioxidePeakLevel: Characteristic;
CarbonMonoxideDetected: Characteristic;
CarbonMonoxideLevel: Characteristic;
CarbonMonoxidePeakLevel: Characteristic;
Category: Characteristic;
ChargingState: Characteristic;
ClosedCaptions: Characteristic;
ColorTemperature: Characteristic;
ConfigureBridgedAccessory: Characteristic;
ConfigureBridgedAccessoryStatus: Characteristic;
ConfiguredName: Characteristic;
ContactSensorState: Characteristic;
CoolingThresholdTemperature: Characteristic;
CurrentAirPurifierState: Characteristic;
CurrentAmbientLightLevel: Characteristic;
CurrentDoorState: Characteristic;
CurrentFanState: Characteristic;
CurrentHeaterCoolerState: Characteristic;
CurrentHeatingCoolingState: Characteristic;
CurrentHorizontalTiltAngle: Characteristic;
CurrentHumidifierDehumidifierState: Characteristic;
CurrentMediaState: Characteristic;
CurrentPosition: Characteristic;
CurrentRelativeHumidity: Characteristic;
CurrentSlatState: Characteristic;
CurrentTemperature: Characteristic;
CurrentTiltAngle: Characteristic;
CurrentTime: Characteristic;
CurrentVerticalTiltAngle: Characteristic;
CurrentVisibilityState: Characteristic;
DayoftheWeek: Characteristic;
DigitalZoom: Characteristic;
DiscoverBridgedAccessories: Characteristic;
DiscoveredBridgedAccessories: Characteristic;
DisplayOrder: Characteristic;
FilterChangeIndication: Characteristic;
FilterLifeLevel: Characteristic;
FirmwareRevision: Characteristic;
HardwareRevision: Characteristic;
HeatingThresholdTemperature: Characteristic;
HoldPosition: Characteristic;
Hue: Characteristic;
Identifier: Characteristic;
Identify: Characteristic;
ImageMirroring: Characteristic;
ImageRotation: Characteristic;
InUse: Characteristic;
InputDeviceType: Characteristic;
InputSourceType: Characteristic;
IsConfigured: Characteristic;
LeakDetected: Characteristic;
LinkQuality: Characteristic;
LockControlPoint: Characteristic;
LockCurrentState: Characteristic;
LockLastKnownAction: Characteristic;
LockManagementAutoSecurityTimeout: Characteristic;
LockPhysicalControls: Characteristic;
LockTargetState: Characteristic;
Logs: Characteristic;
Manufacturer: Characteristic;
Model: Characteristic;
MotionDetected: Characteristic;
Mute: Characteristic;
Name: Characteristic;
NightVision: Characteristic;
NitrogenDioxideDensity: Characteristic;
ObstructionDetected: Characteristic;
OccupancyDetected: Characteristic;
On: Characteristic;
OpticalZoom: Characteristic;
OutletInUse: Characteristic;
OzoneDensity: Characteristic;
PM10Density: Characteristic;
PM2_5Density: Characteristic;
PairSetup: Characteristic;
PairVerify: Characteristic;
PairingFeatures: Characteristic;
PairingPairings: Characteristic;
PictureMode: Characteristic;
PositionState: Characteristic;
PowerModeSelection: Characteristic;
ProgramMode: Characteristic;
ProgrammableSwitchEvent: Characteristic;
ProgrammableSwitchOutputState: Characteristic;
Reachable: Characteristic;
RelativeHumidityDehumidifierThreshold: Characteristic;
RelativeHumidityHumidifierThreshold: Characteristic;
RelayControlPoint: Characteristic;
RelayEnabled: Characteristic;
RelayState: Characteristic;
RemainingDuration: Characteristic;
RemoteKey: Characteristic;
ResetFilterIndication: Characteristic;
RotationDirection: Characteristic;
RotationSpeed: Characteristic;
Saturation: Characteristic;
SecuritySystemAlarmType: Characteristic;
SecuritySystemCurrentState: Characteristic;
SecuritySystemTargetState: Characteristic;
SelectedRTPStreamConfiguration: Characteristic;
SerialNumber: Characteristic;
ServiceLabelIndex: Characteristic;
ServiceLabelNamespace: Characteristic;
SetDuration: Characteristic;
SetupEndpoints: Characteristic;
SlatType: Characteristic;
SleepDiscoveryMode: Characteristic;
SmokeDetected: Characteristic;
SoftwareRevision: Characteristic;
StatusActive: Characteristic;
StatusFault: Characteristic;
StatusJammed: Characteristic;
StatusLowBattery: Characteristic;
StatusTampered: Characteristic;
StreamingStatus: Characteristic;
SulphurDioxideDensity: Characteristic;
SupportedAudioStreamConfiguration: Characteristic;
SupportedRTPConfiguration: Characteristic;
SupportedVideoStreamConfiguration: Characteristic;
SwingMode: Characteristic;
TargetAirPurifierState: Characteristic;
TargetAirQuality: Characteristic;
TargetDoorState: Characteristic;
TargetFanState: Characteristic;
TargetHeaterCoolerState: Characteristic;
TargetHeatingCoolingState: Characteristic;
TargetHorizontalTiltAngle: Characteristic;
TargetHumidifierDehumidifierState: Characteristic;
TargetMediaState: Characteristic;
TargetPosition: Characteristic;
TargetRelativeHumidity: Characteristic;
TargetSlatState: Characteristic;
TargetTemperature: Characteristic;
TargetTiltAngle: Characteristic;
TargetVerticalTiltAngle: Characteristic;
TargetVisibilityState: Characteristic;
TemperatureDisplayUnits: Characteristic;
TimeUpdate: Characteristic;
TunnelConnectionTimeout: Characteristic;
TunneledAccessoryAdvertising: Characteristic;
TunneledAccessoryConnected: Characteristic;
TunneledAccessoryStateNumber: Characteristic;
VOCDensity: Characteristic;
ValveType: Characteristic;
Version: Characteristic;
Volume: Characteristic;
VolumeControlType: Characteristic;
VolumeSelector: Characteristic;
WaterLevel: Characteristic;
}
module Characteristic {
export enum Formats {
BOOL,
INT,
FLOAT,
STRING,
ARRAY, // unconfirmed
DICTIONARY, // unconfirmed
UINT8,
UINT16,
UINT32,
UINT64,
DATA, // unconfirmed
TLV8
}
export enum Units {
// HomeKit only defines Celsius, for Fahrenheit, it requires iOS app to do the conversion.
CELSIUS,
PERCENTAGE,
ARC_DEGREE,
LUX,
SECONDS
}
export enum Perms {
READ,
WRITE,
NOTIFY,
HIDDEN
}
}
export interface PublishInfo {
port: number;
username: string;
pincode: string;
category: number;
}
export interface Accessory extends IEventEmitterAccessory {
new(displayName: string, UUID: string): Accessory;
displayName: string;
username: string;
pincode: string;
UUID: string;
aid: string;
bridged: boolean;
bridgedAccessories: Accessory[];
reachable: boolean;
category: Accessory.Categories;
services: Service[];
cameraSource: CameraSource;
Categories: typeof Accessory.Categories
addService(service: Service | Function): Service;
removeService(service: Service): void;
getService(name: string | Function): Service;
updateReachability(reachable: boolean): void;
addBridgedAccessory(accessory: Accessory, deferUpdate: boolean): Accessory;
addBridgedAccessories(accessories: Accessory[]): void
removeBridgedAccessory(accessory: Accessory, deferUpdate: boolean): void;
removeBridgedAccessories(accessories: Accessory[]): void;
getCharacteristicByIID(iid: string): Characteristic;
getBridgedAccessoryByAID(aid: string): Accessory;
findCharacteristic(aid: string, iid: string): Accessory;
configureCameraSource(cameraSource: CameraSource): void;
toHAP(opt: any): JSON;
publish(info: PublishInfo, allowInsecureRequest: boolean): void;
destroy(): void;
setupURI(): string;
}
module Accessory {
export enum Categories {
OTHER = 1,
BRIDGE = 2,
FAN = 3,
GARAGE_DOOR_OPENER = 4,
LIGHTBULB = 5,
DOOR_LOCK = 6,
OUTLET = 7,
SWITCH = 8,
THERMOSTAT = 9,
SENSOR = 10,
ALARM_SYSTEM = 11,
SECURITY_SYSTEM = 11,
DOOR = 12,
WINDOW = 13,
WINDOW_COVERING = 14,
PROGRAMMABLE_SWITCH = 15,
RANGE_EXTENDER = 16,
CAMERA = 17,
IP_CAMERA = 17,
VIDEO_DOORBELL = 18,
AIR_PURIFIER = 19,
AIR_HEATER = 20,
AIR_CONDITIONER = 21,
AIR_HUMIDIFIER = 22,
AIR_DEHUMIDIFIER = 23,
APPLE_TV = 24,
SPEAKER = 26,
AIRPORT = 27,
SPRINKLER = 28,
FAUCET = 29,
SHOWER_HEAD = 30,
TELEVISION = 31,
TARGET_CONTROLLER = 32
}
}
export interface HAPNodeJS {
init(storagePath?: string): void,
uuid: uuid,
Accessory: Accessory,
Service: Service,
Characteristic: Characteristic
}
}
declare var hapNodeJS: HAPNodeJS.HAPNodeJS;
declare module "hap-nodejs" {
export = hapNodeJS;
}

View File

@ -1,14 +0,0 @@
/**
* Interface to describe homebridge required elements.
*/
export interface IAccessory {
/**
* Required by homebridge.
*/
name: string,
/**
* Called by homebridge to gather services.
*/
getServices(): Array<HAPNodeJS.Service>,
}

View File

@ -17,7 +17,14 @@ export interface IConfig {
/**
* The list of lights to affect
*/
lights: Array<string>;
hueLights: Array<{ name: string; cron?: string; on?: boolean }>;
/**
* The list of wiz lights to affect
*/
wizLights: Array<{ ip: string; mac: string; cron?: string; on?: boolean }>;
wizDiscoveryEnabled: boolean;
/**
* The name of the enable switch in homekit
@ -27,17 +34,32 @@ export interface IConfig {
/**
* The color temperature at the start of sunset transition
*/
ceilingColorTemp: number;
hueCeilingColorTemp: number;
/**
* The color temp during the night
*/
floorColorTemp: number;
hueFloorColorTemp: number;
/**
* The color temp at sunet
*/
sunsetColorTemp: number;
hueSunsetColorTemp: number;
/**
* The color temperature at the start of sunset transition
*/
wizCeilingColorTemp: number;
/**
* The color temp during the night
*/
wizFloorColorTemp: number;
/**
* The color temp at sunet
*/
wizSunsetColorTemp: number;
/**
* The time in milliseconds the lights should remain at sunset temperature.
@ -52,7 +74,7 @@ export interface IConfig {
/**
* The number of milliseconds to wait btw updates
*/
delay?: number;
cron?: string;
/**
* The current formatted date and time to use with testing

157
src/platform.ts Normal file
View File

@ -0,0 +1,157 @@
import { Discover } from "@watsonb8/wiz-lib/build/discovery";
import { WizBulb } from "@watsonb8/wiz-lib/build/wizBulb";
import {
API,
DynamicPlatformPlugin,
Logger,
PlatformAccessory,
PlatformConfig,
UnknownContext,
} from "homebridge";
import { v3 } from "node-hue-api";
import Api from "node-hue-api/lib/api/Api";
import { FluxAccessory } from "./fluxAccessory";
import { IConfig } from "./models/iConfig";
import { PLATFORM_NAME, PLUGIN_NAME } from "./settings";
import { Sleep } from "./sleep";
export class Platform implements DynamicPlatformPlugin {
private hue: Api | undefined;
private wiz: Discover;
private accessory: PlatformAccessory | undefined = undefined;
private config: IConfig;
constructor(
public readonly log: Logger,
config: PlatformConfig,
public readonly api: API
) {
this.config = config as unknown as IConfig;
this.wiz = new Discover();
this.log.info("INFO - Registering Flux platform");
this.api.on("didFinishLaunching", this.didFinishLaunching.bind(this));
}
async configureAccessory(accessory: PlatformAccessory<UnknownContext>) {
this.log.info("Loading accessory from cache:", accessory.displayName);
this.accessory = accessory;
}
/**
* Handler for didFinishLaunching
* Happens after constructor
*/
private async didFinishLaunching() {
this.log.info(`INFO - Done registering Flux platform`);
await this.connectHue();
const wizBulbs = await this.connectWiz();
this.log.info("Registering accessory: " + this.config.name);
const uuid = this.api.hap.uuid.generate(this.config.name);
// Load accessory if not cached
if (!this.accessory) {
this.accessory = new this.api.platformAccessory(
this.config.name,
uuid
);
this.accessory.context["DeviceName"] = this.config.name;
this.accessory.context["Type"] = typeof "Flux";
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [
this.accessory,
]);
}
new FluxAccessory({
platform: this,
accessory: this.accessory,
hue: this.hue!,
wizBulbs: wizBulbs ?? [],
config: this.config,
});
}
private connectWiz = async () => {
if (!this.config) {
return;
}
let bulbs: Array<WizBulb> = await this.wiz.createWizBulbs(
this.config.wizLights
);
if (this.config.wizDiscoveryEnabled) {
let discoveredBulbs: Array<WizBulb> = await this.wiz.discover();
let filtered = [];
for (const bulb of discoveredBulbs) {
if (
!bulbs.some(
(manualBulb) => manualBulb.getIp() === bulb.getIp()
) &&
bulb.isRGB()
) {
filtered.push(bulb);
}
}
bulbs.push(...filtered);
}
return bulbs;
};
private connectHue = async () => {
if (!this.config) {
return;
}
if (this.config.userName && this.config.clientKey) {
this.hue = await v3.api
.createLocal(this.config.ipAddress)
.connect(
this.config.userName,
this.config.clientKey,
undefined
);
this.log.info("Using existing connection info");
} else {
const unauthenticatedApi = await v3.api
.createLocal(this.config.ipAddress)
.connect(undefined, undefined, undefined);
let createdUser;
let connected = false;
while (!connected) {
try {
this.log.info("Creating hue user. Push link button");
createdUser = await unauthenticatedApi.users.createUser(
"homebridge",
"HueChase"
);
this.hue = await v3.api
.createLocal(this.config.ipAddress)
.connect(
createdUser.username,
createdUser.clientKey,
undefined
);
this.log.info("Connected to Hue Bridge");
this.log.info(
`UserName: ${createdUser.username}, ClientKey: ${createdUser.clientkey}`
);
connected = true;
} catch (err: any) {
if (err.getHueErrorType() === 101) {
this.log.info(
"The Link button on the bridge was not pressed. Please press the Link button and try again."
);
Sleep(5000);
} else {
this.log.info(`Unexpected Error: ${err.message}`);
break;
}
}
}
}
};
}

9
src/settings.ts Normal file
View File

@ -0,0 +1,9 @@
/**
* This is the name of the platform that users will use to register the plugin in the Homebridge config.json
*/
export const PLATFORM_NAME = "Flux";
/**
* This must match the name of your plugin as defined the package.json
*/
export const PLUGIN_NAME = "@watsonb8/homebridge-flux";

41
src/util/colorUtil.ts Normal file
View File

@ -0,0 +1,41 @@
import { RGB } from "@watsonb8/wiz-lib";
export const 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: clamp(red, 0, 255),
g: clamp(green, 0, 255),
b: clamp(blue, 0, 255),
};
};
const clamp = (x: number, min: number, max: number) => {
if (x < min) {
return min;
}
if (x > max) {
return max;
}
return x;
};

View File

@ -2,18 +2,18 @@
"compilerOptions": {
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "ESNext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"target": "ESNext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "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. */
"outDir": "./bin" /* Redirect output structure to the directory. */,
"rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
@ -22,7 +22,7 @@
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
"strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
@ -36,14 +36,14 @@
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
"types": [] /* Type declaration files to be included in compilation. */,
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
@ -55,6 +55,7 @@
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
"skipLibCheck": false
}
}