diff --git a/README.md b/README.md index 3e5067a..40ec6d9 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Unofficial [Homebridge](http://github.com/nfarina/homebridge) plugin for [Twinkl Use Siri or HomeKit automation to: - Turn lights on or off - Change brightness +- Change the color A simple command line tool is also included. @@ -23,6 +24,7 @@ A simple command line tool is also included. | Value | Default | Description | |--------------------------------|---------------|-------------------------------------------------------------| | allowBrightnessControl | true | Adds a brightness control instead of a simple on/off switch | +| allowColorControl | true | Adds a color picker/wheel to the brightness control slider | | removeUnreachableDeviceMinutes | 0 | When to remove unreachable devices (0 meaning never) | The options can be configured using the UI or manually in a config.json. @@ -37,6 +39,7 @@ The options can be configured using the UI or manually in a config.json. "platforms": [{ "platform": "Twinkly", "allowBrightnessControl": true, + "allowColorControl": true, "removeUnreachableDeviceMinutes": 0 }] } @@ -56,6 +59,7 @@ You'll need to find the IP address of each light using the Twinkly app. It might | name | (required) | The name for light as it will appear in HomeKit | | ip | (required) | The IP address of the lights. | | allowBrightnessControl | true | Adds a brightness control instead of a simple on/off switch | +| allowColorControl | true | Adds a color picker/wheel to the brightness control slider | The options can be configured using the UI or manually in a config.json. Multiple lights are can be added as individual accessories. @@ -72,6 +76,7 @@ Multiple lights are can be added as individual accessories. "name": "Christmas Lights", "ip": "192.168.4.1", "allowBrightnessControl": true + "allowColorControl": true }] } ``` diff --git a/config.schema.json b/config.schema.json index 7f8cf4b..49b3507 100644 --- a/config.schema.json +++ b/config.schema.json @@ -12,6 +12,11 @@ "type": "boolean", "description": "Shows a brightness slider. Otherwise a simple on/off switch will be shown." }, + "allowColorControl": { + "title": "Allow color control", + "type": "boolean", + "description": "Shows a color controls." + }, "removeUnreachableDeviceMinutes": { "title": "Remove unreachable devices after", "type": "integer", diff --git a/index.js b/index.js index 047194f..4a03544 100644 --- a/index.js +++ b/index.js @@ -12,7 +12,8 @@ let hap, Service, Characteristic; // "accessory": "Twinkly", // "name": "Christmas Tree", // "ip": "192.168.4.1", -// "allowBrightnessControl": true +// "allowBrightnessControl": true, +// "allowColorControl": true // }] class TwinklyHomebridge extends Twinkly { @@ -30,6 +31,8 @@ class TwinklyHomebridge extends Twinkly { this.name = name; this.isBrightnessControlEnabled = config["allowBrightnessControl"]; if (this.isBrightnessControlEnabled === undefined) { this.isBrightnessControlEnabled = true; } + this.isColorControlEnabled = config["allowColorControl"]; + if (this.isColorControlEnabled === undefined) { this.isColorControlEnabled = false; } } getServices() { @@ -48,6 +51,15 @@ class TwinklyHomebridge extends Twinkly { value => this.setBrightness(value)); } + if (this.isColorControlEnabled) { + this.registerCharacteristic(lightService, Characteristic.Hue, + () => this.getHue(), + value => this.setHue(value)); + this.registerCharacteristic(lightService, Characteristic.Saturation, + () => this.getSaturation(), + value => this.setSaturation(value)); + } + return [lightService, informationService]; } @@ -79,6 +91,7 @@ function wrap(promise, callback, isSet = false) { // "platforms": [{ // "platform": "Twinkly", // "allowBrightnessControl": true, +// "allowColorControl": true, // "removeUnreachableDeviceMinutes": false // }] @@ -89,6 +102,9 @@ class TwinklyPlatform { this.isBrightnessControlEnabled = config["allowBrightnessControl"]; if (this.isBrightnessControlEnabled === undefined) { this.isBrightnessControlEnabled = true; } + this.isColorControlEnabled = config["allowColorControl"]; + if (this.isColorControlEnabled === undefined) { this.isColorControlEnabled = true; } + this.verbose = config.verbose; this.timeout = config.timeout || 1000; this.scanInterval = config.scanInterval || 60_000; @@ -212,6 +228,18 @@ class TwinklyPlatform { device => device.getBrightness(), (device, value) => device.setBrightness(value)); } + + if (this.isColorControlEnabled) { + this.registerCharacteristic(uuid, lightService, Characteristic.Hue, + device => device.getHue(), + (device, value) => device.setHue(value)); + } + + if (this.isColorControlEnabled) { + this.registerCharacteristic(uuid, lightService, Characteristic.Saturation, + device => device.getSaturation(), + (device, value) => device.setSaturation(value)); + } this.accessories.set(uuid, accessory); } diff --git a/lib/Color.js b/lib/Color.js new file mode 100644 index 0000000..0c67dec --- /dev/null +++ b/lib/Color.js @@ -0,0 +1,36 @@ +function rgbColors(obj) { + rgbInt = HSB2RGB({h: obj.hue, s: obj.saturation, b: obj.brightness}); + return [rgbInt.r, rgbInt.g, rgbInt.b]; +}; + +// https://stackoverflow.com/questions/17242144/javascript-convert-hsb-hsv-color-to-rgb-accurately +function HSB2RGB(hsb) { + + var rgb = { }; + var h = Math.round(hsb.h % 360); + var s = Math.round(hsb.s * 255 / 100); + var v = Math.round(hsb.b * 255 / 100); + + if (s == 0) { + + rgb.r = rgb.g = rgb.b = v; + } else { + var t1 = v; + var t2 = (255 - s) * v / 255; + var t3 = (t1 - t2) * (h % 60) / 60; + + if (h == 360) h = 0; + + if (h < 60) { rgb.r = t1; rgb.b = t2; rgb.g = t2 + t3 } + else if (h < 120) { rgb.g = t1; rgb.b = t2; rgb.r = t1 - t3 } + else if (h < 180) { rgb.g = t1; rgb.r = t2; rgb.b = t2 + t3 } + else if (h < 240) { rgb.b = t1; rgb.r = t2; rgb.g = t1 - t3 } + else if (h < 300) { rgb.b = t1; rgb.g = t2; rgb.r = t2 + t3 } + else if (h < 360) { rgb.r = t1; rgb.g = t2; rgb.b = t1 - t3 } + else { rgb.r = 0; rgb.g = 0; rgb.b = 0 } + } + + return { r: Math.round(rgb.r), g: Math.round(rgb.g), b: Math.round(rgb.b) }; +}; + +exports.rgbColors = rgbColors; \ No newline at end of file diff --git a/lib/Twinkly.js b/lib/Twinkly.js index 78d3b95..aca8df5 100644 --- a/lib/Twinkly.js +++ b/lib/Twinkly.js @@ -1,5 +1,6 @@ const {RequestQueue} = require("./RequestQueue"); const {Movie} = require("./Movie"); +const Color = require("./Color"); class Twinkly { constructor(log, address, timeout, isVerbose) { @@ -20,6 +21,10 @@ class Twinkly { this.ledProfile = null; this.generation = null; this.initPromise = null; + + this.hue = 180; + this.saturation = 70; + this.brightness = 100; } logVerbose(msg) { @@ -81,9 +86,45 @@ class Twinkly { } setBrightness(brightness) { + this.brightness = brightness; + this.log("Set Brightness: " + this.brightness); return this.requestService.postJson("led/out/brightness", {type: "A", value: brightness}); } + getHue() { + return this.requestService.get("led/out/brightness") + .then(json => this.hue); + } + + setHue(hue) { + this.hue = hue; + this.log("Set Hue: " + this.hue); + return this.setColorEffect(); + } + + getSaturation() { + return this.requestService.get("led/out/brightness") + .then(json => this.saturation); + } + + setSaturation(saturation) { + this.saturation = saturation || 70; // not sure why this is being called with an undefined value + this.log("Set Saturation: " + this.saturation); + return this.setColorEffect(); + } + + setColorEffect() { + var color = Color.rgbColors( + { + hue: this.hue, + saturation: this.saturation, + brightness: 100 + } + ); + this.log("Color: " + color); + return this.setColor(color) + } + setColor(color) { return this.ensureDeviceInfo() .then(() => this.setMovie(Movie.repeatedColors(this.ledCount, [color])));