Compare commits

...

15 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 440a4d62a1
Author: Brandon Watson <brandon@watsonlabs.net>
Date:   Tue Sep 6 20:23:31 2022 -0500

    Fixing issue where button becomes unresponsive | Updating homebridge

commit d73dead5d4
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
16 changed files with 3415 additions and 1320 deletions

View File

@ -1,130 +0,0 @@
kind: pipeline
type: docker
name: default
node:
lan: internal
steps:
- name: build
image: node
commands:
- npm install
- npm run build
- name: version
image: node
commands:
- export version=`node -p "require('./package.json').version"`
- export commit=`echo $DRONE_COMMIT | cut -c1-5`
- npm version prerelease --preid=$commit --git-tag-version=false --allow-same-version=true
when:
event:
exclude:
- tag
- pull_request
branch:
include:
- master
- name: publish pre
image: plugins/npm:1.0.0
settings:
username:
from_secret: npm_username
password:
from_secret: npm_password
email: b.watson@watsonlabs.net
registry: "http://10.44.1.6:4873/"
when:
event:
exclude:
- tag
- pull_request
branch:
include:
- master
- name: publish tagged version
image: plugins/npm:1.0.0
settings:
username:
from_secret: npm_username
password:
from_secret: npm_password
email: b.watson@watsonlabs.net
registry: "http://10.44.1.6:4873/"
when:
event:
- tag
exclude:
- pull_request
- name: remove old package
image: appleboy/drone-ssh
environment:
SSH_USER:
from_secret: ssh_user
settings:
host: homebridge.me
envs:
- SSH_USER
username:
from_secret: ssh_user
key:
from_secret: ssh_key
port: 22
script:
- rm -r /home/$SSH_USER/.npm-global/lib/node_modules/@watsonb8/homebridge-flux
when:
event:
- tag
exclude:
- pull_request
- name: deploy
image: appleboy/drone-ssh
settings:
host: homebridge.me
username:
from_secret: ssh_user
key:
from_secret: ssh_key
port: 22
script:
- npm install -g @watsonb8/homebridge-flux --registry http://10.44.1.6:4873
when:
event:
- tag
exclude:
- pull_request
- name: restart homebridge
image: appleboy/drone-ssh
settings:
host: homebridge.me
username:
from_secret: elevated_ssh_user
key:
from_secret: ssh_key
port: 22
script:
- systemctl restart homebridge
when:
event:
- tag
exclude:
- pull_request
- name: Notify
image: drillster/drone-email
settings:
host: 10.44.1.13
username: srvGitea
password:
from_secret: smtp_password
from: drone@watsonlabs.net
skip_verify: true
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
}

35
.vscode/launch.json vendored
View File

@ -1,19 +1,20 @@
{ {
// Use IntelliSense to learn about possible attributes. // Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes. // Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"name": "Launch Program", "name": "Launch Program",
"preLaunchTask": "build", "preLaunchTask": "build",
"program": "/Users/brandonwatson/.nvm/versions/node/v14.15.0/lib/node_modules/homebridge/bin/homebridge", "program": "/Users/brandonwatson/.nvm/versions/node/v14.15.0/lib/node_modules/homebridge/bin/homebridge",
"env": { "env": {
"HOMEBRIDGE_OPTS": "/Users/brandonwatson/.homebridge" "HOMEBRIDGE_OPTS": "/Users/brandonwatson/.homebridge",
}, "LOG_LEVEL": "debug"
"sourceMaps": true },
} "sourceMaps": true
] }
]
} }

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
}

2804
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,16 @@
{ {
"name": "@watsonb8/homebridge-flux", "name": "@watsonb8/homebridge-flux",
"version": "1.1.3", "version": "1.2.0",
"description": "", "description": "",
"main": "bin/index.js", "main": "bin/index.js",
"publishConfig": { "publishConfig": {
"registry": "http://10.44.1.6:4873/" "registry": "https://gitea.watsonlabs.net"
}, },
"files": [
"bin",
"src",
"config.schema.json"
],
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
@ -21,7 +26,7 @@
"typescript" "typescript"
], ],
"engines": { "engines": {
"homebridge": ">=0.4.21", "homebridge": ">=1.1.6",
"node": ">=7.6.0" "node": ">=7.6.0"
}, },
"author": "Brandon Watson", "author": "Brandon Watson",
@ -29,13 +34,14 @@
"dependencies": { "dependencies": {
"@types/node-cron": "^2.0.3", "@types/node-cron": "^2.0.3",
"@types/suncalc": "^1.8.0", "@types/suncalc": "^1.8.0",
"@watsonb8/wiz-lib": "^1.0.1-ae175.0",
"node-cron": "^2.0.3", "node-cron": "^2.0.3",
"node-hue-api": "^4.0.5", "node-hue-api": "^4.0.5",
"suncalc": "^1.8.0" "suncalc": "^1.8.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^13.11.1", "@types/node": "^13.11.1",
"homebridge": "^1.3.9", "homebridge": "^1.5.0",
"typescript": "^4.5.4" "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 Api = require("node-hue-api/lib/api/Api");
import Light = require("node-hue-api/lib/model/Light"); import Light = require("node-hue-api/lib/model/Light");
import LightState = require("node-hue-api/lib/model/lightstate/LightState"); import LightState = require("node-hue-api/lib/model/lightstate/LightState");
@ -7,348 +6,420 @@ import { IConfig } from "./models/iConfig";
//@ts-ignore //@ts-ignore
import { GetTimesResult, getTimes } from "suncalc"; import { GetTimesResult, getTimes } from "suncalc";
import HueError = require("node-hue-api/lib/HueError"); 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 SECONDS_IN_DAY = 86400000;
const MINUTES_IN_MILLISECOND = 60000; const MINUTES_IN_MILLISECOND = 60000;
const SECONDS_IN_HOUR = 3600; const SECONDS_IN_HOUR = 3600;
export interface IFluxProps { export interface IFluxProps {
api: any; platform: Platform;
log: any; accessory: PlatformAccessory;
homebridge: any; hue: Api;
hue: Api; wizBulbs: Array<WizBulb>;
config: IConfig; config: IConfig;
} }
export class FluxAccessory implements IAccessory { export class FluxAccessory {
private _api: any; private readonly _platform: Platform;
private _homebridge: any; private readonly _accessory: PlatformAccessory;
private _log: any = {}; private _config: IConfig;
private _config: IConfig; private _isActive: boolean;
private _isActive: boolean; private _hueRGB: RGB;
private _wizRGB: RGB;
private _fade: number;
private _cron: string;
//Service fields //Service fields
private _switchService: HAPNodeJS.Service; private _switchService;
private _infoService: HAPNodeJS.Service;
private _hue: Api; private _hue: Api;
private _lights: Array<Light> = []; private _lights: Array<Light> = [];
private _wizLights: Array<WizBulb> = [];
private _times: GetTimesResult; private _times: GetTimesResult;
private _tasks: Array<ScheduledTask> = [];
constructor(props: IFluxProps) { constructor(props: IFluxProps) {
//Assign class variables //Assign class variables
this._log = props.log; this._platform = props.platform;
this._api = props.api; this._accessory = props.accessory;
this._config = props.config; this._config = props.config;
Service = props.api.hap.Service; this._isActive = false;
Characteristic = props.api.hap.Characteristic; this._wizLights = props.wizBulbs;
this._homebridge = props.homebridge; this._hue = props.hue;
this._isActive = false; 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( 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(), new Date(),
this._config.latitude, this._config.latitude,
this._config.longitude this._config.longitude
); );
this._log("Updated sunset times");
},
{
scheduled: true,
}
)
.start();
this._hue = props.hue; //Schedule job to refresh times
this.name = this._config.name; 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.platformAccessory = new this._homebridge.platformAccessory( //Schedule job to refresh hues every minute
this.name, this.updateRGB();
this.generateUUID(), cron.schedule(
this._homebridge.hap.Accessory.Categories.SWITCH "* * * * *",
); () => {
this.updateRGB();
this._platform.log.info("Updated hues");
},
{
scheduled: true,
}
).start();
//@ts-ignore this.scheduleLights();
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._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 const switchUUID = this._platform.api.hap.uuid.generate(
.getCharacteristic(Characteristic.On) `${this._accessory.displayName} Switch`
//@ts-ignore );
.on("set", this.onSetEnabled)
//@ts-ignore
.on("get", this.onGetEnabled);
}
public name: string = "Flux"; this._switchService =
this._accessory.getService(this._platform.api.hap.Service.Switch) ||
this._accessory.addService(
this._platform.api.hap.Service.Switch,
this._accessory.displayName,
switchUUID
);
public platformAccessory: any; this._switchService
.getCharacteristic(this._platform.api.hap.Characteristic.On)
//@ts-ignore
.on("set", this.onSetEnabled)
//@ts-ignore
.on("get", this.onGetEnabled);
/** // this.test();
* 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();
};
/** public name: string = "Flux";
* 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;
}
/** public platformAccessory: any;
* Called by homebridge to gather services.
*/
public getServices = (): Array<HAPNodeJS.Service> => {
return [this._infoService, this._switchService!];
};
/** /**
* Popuplates internal lights array using the configuration values * Handler for switch set event
*/ * @param callback The callback function to call when complete
private getLights = async (): Promise<void> => { */
for (const value of this._config.lights) { private onSetEnabled = async (
//@ts-ignore activeState: boolean,
const light: Light = await this._hue.lights.getLightByName(value); callback: (error?: Error | null | undefined) => void
this._lights.push(light); ) => {
} if (activeState) {
}; this._times = getTimes(
new Date(),
private colorTempToRgb = ( this._config.latitude,
kelvin: number this._config.longitude
): { red: number; green: number; blue: number } => { );
var temp = kelvin / 100; this._isActive = true;
var red, green, blue; this.enable();
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 { } else {
this._log(`Error while setting lights: ${err}`); this._isActive = false;
this.disable();
} }
} return callback();
}); };
await Promise.all(promises); /**
}; * 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;
};
/** /**
* Helper function to generate a UUID * Populates internal lights array using the configuration values
*/ */
private generateUUID(): string { private getLights = async (): Promise<void> => {
// Public Domain/MIT for (const value of this._config.hueLights) {
var d = new Date().getTime(); //@ts-ignore
if ( const light: Light = await this._hue.lights.getLightByName(
typeof performance !== "undefined" && value.name
typeof performance.now === "function" );
) { this._lights.push(light);
d += performance.now(); //use high-precision timer if available }
};
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();
}
} }
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 updateRGB = (): void => {
* Gets adjusted color temperature. const now = this.getNow();
*/ //Pad start time by an hour before sunset
private getTempOffset = ( const start = new Date(
startTemp: number, this._times.sunset.getTime() - 60 * MINUTES_IN_MILLISECOND
endTemp: number, );
startTime: Date, const sunsetStart = this._times.sunsetStart;
endTime: Date const sunsetEnd = new Date(
) => { this._times.sunset.getTime() + this._config.sunsetDuration
const now = this.getNow().getTime(); );
const percentComplete = const nightStart = new Date(
(now - startTime.getTime()) / (endTime.getTime() - startTime.getTime()); sunsetEnd.getTime() + 60 * MINUTES_IN_MILLISECOND
const tempRange = Math.abs(startTemp - endTemp); );
const tempOffset = tempRange * percentComplete; const sunrise = new Date(
return startTemp - tempOffset; this._times.sunrise.getTime() + 1 * SECONDS_IN_DAY
}; );
/** const hueStartColorTemp = this._config.hueCeilingColorTemp ?? 4000;
* Get the current time. Use test time if present. const hueSunsetColorTemp = this._config.hueSunsetColorTemp ?? 2800;
*/ const hueFloorColorTemp = this._config.hueFloorColorTemp ?? 1900;
private getNow() {
if (this._config.testNowDateString) { const wizStartColorTemp = this._config.wizCeilingColorTemp ?? 4000;
return new Date(this._config.testNowDateString); const wizSunsetColorTemp = this._config.wizSunsetColorTemp ?? 2800;
} else { const wizFloorColorTemp = this._config.wizFloorColorTemp ?? 1900;
return new Date();
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()];
};
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,
}
);
});
} }
}
private update = async (): Promise<void> => { private getWizTasks(): Array<ScheduledTask> {
this._isActive = true; return this._wizLights.map((wizBulb) => {
while (this._isActive) { let wizLightConfig = this._config.wizLights.find(
if (this._lights.length === 0) { (x) => x.ip == wizBulb.getIp()
await this.getLights(); );
} 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,
}
);
});
}
const now = this.getNow(); private updateWizLight = async (
//Pad start time by an hour before sunset wizBulb: WizBulb,
const start = new Date( on: Boolean
this._times.sunset.getTime() - 60 * MINUTES_IN_MILLISECOND ): Promise<void> => {
); let pilot;
const sunsetStart = this._times.sunsetStart; try {
const sunsetEnd = new Date( pilot = await wizBulb.get();
this._times.sunset.getTime() + this._config.sunsetDuration } catch (err: any) {
); this._platform.log.error(err.message);
const nightStart = new Date( }
sunsetEnd.getTime() + 60 * MINUTES_IN_MILLISECOND if (pilot && pilot.state) {
); this._platform.log.info(`Adjusting wiz bulb: ${wizBulb.getMac()}`);
const sunrise = new Date(
this._times.sunrise.getTime() + 1 * SECONDS_IN_DAY
);
const startColorTemp = this._config.ceilingColorTemp await wizBulb.set(
? this._config.ceilingColorTemp this._wizRGB,
: 4000; on ? 100 : pilot.dimming,
const sunsetColorTemp = this._config.sunsetColorTemp this._fade
? this._config.sunsetColorTemp );
: 2800; }
const floorColorTemp = this._config.floorColorTemp };
? this._config.floorColorTemp
: 1900;
let newTemp = 0; private updateHueLight = async (
hueLight: Light | undefined,
on: Boolean
): Promise<void> => {
if (!hueLight) {
return;
}
if (start < now && now < sunsetStart) { this._platform.log.info(`Adjusting wiz bulb: ${hueLight.name}`);
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;
}
//Set lights
const rgb = this.colorTempToRgb(newTemp);
if (rgb && newTemp !== 0) {
const lightState = new LightState(); const lightState = new LightState();
lightState lightState
.transitionInMillis( .transitionInMillis(this._fade)
this._config.transition ? this._config.transition : 5000 .rgb(this._hueRGB.r ?? 0, this._hueRGB.g ?? 0, this._hueRGB.b ?? 0);
)
.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); 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);
}
};
} }

View File

@ -1,105 +1,11 @@
import { IConfig } from "./models/iConfig"; import { API } from "homebridge";
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";
let Accessory: any; import { PLATFORM_NAME } from "./settings";
let Homebridge: any; import { Platform } from "./platform";
/** /**
* Main entry. * This method registers the platform with Homebridge
* @param homebridge
*/ */
export default function (homebridge: any) { export = (api: API) => {
Homebridge = homebridge; api.registerPlatform(PLATFORM_NAME, Platform);
Accessory = homebridge.platformAccessory;
homebridge.registerPlatform(
'homebridge-flux',
'Flux',
FluxPlatform,
true
);
}; };
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: any) {
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 * 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 * The name of the enable switch in homekit
@ -27,17 +34,32 @@ export interface IConfig {
/** /**
* The color temperature at the start of sunset transition * The color temperature at the start of sunset transition
*/ */
ceilingColorTemp: number; hueCeilingColorTemp: number;
/** /**
* The color temp during the night * The color temp during the night
*/ */
floorColorTemp: number; hueFloorColorTemp: number;
/** /**
* The color temp at sunet * 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. * The time in milliseconds the lights should remain at sunset temperature.
@ -52,10 +74,10 @@ export interface IConfig {
/** /**
* The number of milliseconds to wait btw updates * The number of milliseconds to wait btw updates
*/ */
delay?: number; cron?: string;
/** /**
* The current formatted date and time to use with testing * The current formatted date and time to use with testing
*/ */
testNowDateString?: string; testNowDateString?: string;
} }

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": { "compilerOptions": {
/* Basic Options */ /* Basic Options */
// "incremental": true, /* Enable incremental compilation */ // "incremental": true, /* Enable incremental compilation */
"target": "ESNext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' 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'. */ "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
// "lib": [], /* Specify library files to be included in the compilation. */ // "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */ // "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */ // "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */ // "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each 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. */ // "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./bin", /* Redirect output structure to the directory. */ "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. */ "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
// "composite": true, /* Enable project compilation */ // "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */ // "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'. */ // "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'). */ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */ /* 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. */ // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */ // "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */ // "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. */ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */ /* 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. */ // "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'. */ // "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. */ // "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. */ // "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. */ // "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. */ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */ /* Source Map Options */
@ -55,6 +55,7 @@
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */ /* Advanced Options */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
"skipLibCheck": false
} }
} }