From bad377bfc85b8f6196dd40b41e1ec36f0435ee79 Mon Sep 17 00:00:00 2001 From: scooterpsu <3433982+scooterpsu@users.noreply.github.com> Date: Fri, 17 May 2024 13:02:03 -0400 Subject: [PATCH 1/2] New Device Type: Star Projector Added new device type: Star Projector This adds support for the BlissLights Sky Lite Evolve Star Projector https://www.amazon.com/dp/B0B4VPM54L?th=1 Essentially an RGB light with a switch for the rotation motor. The rotation could be broken out into a brightness slider, but at max speed the movement is too subtle for me to justify the effort. Other models come with laser stars, which would require additional switches. DO NOT UPDATE FIRMWARE!! I'm unsure if the update moved to a Tuya 3.4+ scheme, or just changed something in the packet format, but it became unusable after updating. --- config.schema.json | 25 +++- index.js | 4 +- lib/StarProjectorAccessory.js | 251 ++++++++++++++++++++++++++++++++++ 3 files changed, 273 insertions(+), 7 deletions(-) create mode 100644 lib/StarProjectorAccessory.js diff --git a/config.schema.json b/config.schema.json index bfea9b9..106aca3 100644 --- a/config.schema.json +++ b/config.schema.json @@ -119,6 +119,12 @@ "enum": [ "AirPurifier" ] + }, + { + "title": "Star Projector", + "enum": [ + "StarProjector" + ] } ] }, @@ -222,7 +228,7 @@ "type": "integer", "placeholder": "2", "condition": { - "functionBody": "return model.devices && model.devices[arrayIndices] && ['TWLight', 'RGBTWLight', 'SimpleDimmer','RGBTWOutlet'].includes(model.devices[arrayIndices].type);" + "functionBody": "return model.devices && model.devices[arrayIndices] && ['TWLight', 'RGBTWLight', 'SimpleDimmer','RGBTWOutlet', 'StarProjector'].includes(model.devices[arrayIndices].type);" } }, "dpColorTemperature": { @@ -249,28 +255,28 @@ "type": "integer", "placeholder": "2", "condition": { - "functionBody": "return model.devices && model.devices[arrayIndices] && ['RGBTWLight', 'RGBTWOutlet'].includes(model.devices[arrayIndices].type);" + "functionBody": "return model.devices && model.devices[arrayIndices] && ['RGBTWLight', 'RGBTWOutlet', 'StarProjector'].includes(model.devices[arrayIndices].type);" } }, "dpColor": { "type": "integer", "placeholder": "5", "condition": { - "functionBody": "return model.devices && model.devices[arrayIndices] && ['RGBTWLight','RGBTWOutlet'].includes(model.devices[arrayIndices].type);" + "functionBody": "return model.devices && model.devices[arrayIndices] && ['RGBTWLight','RGBTWOutlet', 'StarProjector'].includes(model.devices[arrayIndices].type);" } }, "colorFunction": { "type": "string", "placeholder": "HEXHSB", "condition": { - "functionBody": "return model.devices && model.devices[arrayIndices] && ['RGBTWLight','RGBTWOutlet'].includes(model.devices[arrayIndices].type);" + "functionBody": "return model.devices && model.devices[arrayIndices] && ['RGBTWLight','RGBTWOutlet', 'StarProjector'].includes(model.devices[arrayIndices].type);" } }, "scaleBrightness": { "type": "integer", "placeholder": "255", "condition": { - "functionBody": "return model.devices && model.devices[arrayIndices] && ['RGBTWLight','RGBTWOutlet'].includes(model.devices[arrayIndices].type);" + "functionBody": "return model.devices && model.devices[arrayIndices] && ['RGBTWLight','RGBTWOutlet', 'StarProjector'].includes(model.devices[arrayIndices].type);" } }, "scaleWhiteColor": { @@ -520,7 +526,7 @@ "type": "integer", "placeholder": 1, "condition": { - "functionBody": "return model.devices && model.devices[arrayIndices] && ['RGBTWOutlet'].includes(model.devices[arrayIndices].type);" + "functionBody": "return model.devices && model.devices[arrayIndices] && ['RGBTWOutlet', 'StarProjector'].includes(model.devices[arrayIndices].type);" } }, "dpBlindType": { @@ -529,6 +535,13 @@ "condition": { "functionBody": "return model.devices && model.devices[arrayIndices] && ['SimpleBlinds'].includes(model.devices[arrayIndices].type);" } + }, + "dpMotor": { + "type": "integer", + "placeholder": 1, + "condition": { + "functionBody": "return model.devices && model.devices[arrayIndices] && ['StarProjector'].includes(model.devices[arrayIndices].type);" + } } } } diff --git a/index.js b/index.js index e1d1aa9..159960f 100644 --- a/index.js +++ b/index.js @@ -23,6 +23,7 @@ const SwitchAccessory = require('./lib/SwitchAccessory'); const ValveAccessory = require('./lib/ValveAccessory'); const OilDiffuserAccessory = require('./lib/OilDiffuserAccessory'); const DoorbellAccessory = require('./lib/DoorbellAccessory'); +const StarProjectorAccessory = require ('./lib/StarProjectorAccessory'); const PLUGIN_NAME = 'homebridge-tuya'; const PLATFORM_NAME = 'TuyaLan'; @@ -49,7 +50,8 @@ const CLASS_DEF = { fanlight: SimpleFanLightAccessory, watervalve: ValveAccessory, oildiffuser: OilDiffuserAccessory, - doorbell: DoorbellAccessory + doorbell: DoorbellAccessory, + starprojector: StarProjectorAccessory }; let Characteristic, PlatformAccessory, Service, Categories, AdaptiveLightingController, UUID; diff --git a/lib/StarProjectorAccessory.js b/lib/StarProjectorAccessory.js new file mode 100644 index 0000000..67a8b09 --- /dev/null +++ b/lib/StarProjectorAccessory.js @@ -0,0 +1,251 @@ +const BaseAccessory = require('./BaseAccessory'); +const async = require('async'); + +class StarProjectorAccessory extends BaseAccessory { + static getCategory(Categories) { + return Categories.LIGHT; + } + + constructor(...props) { + super(...props); + } + + _registerPlatformAccessory() { + this._verifyCachedPlatformAccessory(); + this._justRegistered = true; + + super._registerPlatformAccessory(); + } + + _verifyCachedPlatformAccessory() { + if (this._justRegistered) return; + + const {Service} = this.hap; + + const lightName = 'RGBTWLight - ' + this.device.context.name; + let lightService = this.accessory.getServiceByUUIDAndSubType(Service.Lightbulb, 'lightbulb'); + if (lightService) this._checkServiceName(lightService, lightName); + else lightService = this.accessory.addService(Service.Lightbulb, lightName, 'lightbulb'); + + const switchName = this.device.context.name + ' Rotation'; + let switchService = this.accessory.getServiceByUUIDAndSubType(Service.Switch, 'switch'); + if (switchService) this._checkServiceName(outletService, switchName); + else switchService = this.accessory.addService(Service.Switch, switchName, 'switch'); + + this.accessory.services + .forEach(service => { + if ((service.UUID === Service.Outlet.UUID && service !== outletService) || (service.UUID === Service.Lightbulb.UUID && service !== lightService)) + this.accessory.removeService(service); + }); + } + + _registerCharacteristics(dps) { + this._verifyCachedPlatformAccessory(); + + const {Service, Characteristic, EnergyCharacteristics} = this.hap; + + const lightService = this.accessory.getServiceByUUIDAndSubType(Service.Lightbulb, 'lightbulb'); + const switchService = this.accessory.getServiceByUUIDAndSubType(Service.Switch, 'switch'); + + this.dpLight = this._getCustomDP(this.device.context.dpLight) || '1'; + this.dpMode = this._getCustomDP(this.device.context.dpMode) || '2'; + this.dpBrightness = this._getCustomDP(this.device.context.dpBrightness) || '3'; + this.dpColorTemperature = this._getCustomDP(this.device.context.dpColorTemperature) || '4'; + this.dpColor = this._getCustomDP(this.device.context.dpColor) || '5'; + + this.dpMotor = this._getCustomDP(this.device.context.dpMotor) || '101'; + + this._detectColorFunction(dps[this.dpColor]); + + this.cmdWhite = 'white'; + if (this.device.context.cmdWhite) { + if (/^w[a-z]+$/i.test(this.device.context.cmdWhite)) this.cmdWhite = ('' + this.device.context.cmdWhite).trim(); + else throw new Error(`The cmdWhite doesn't appear to be valid: ${this.device.context.cmdWhite}`); + } + + this.cmdColor = 'colour'; + if (this.device.context.cmdColor) { + if (/^c[a-z]+$/i.test(this.device.context.cmdColor)) this.cmdColor = ('' + this.device.context.cmdColor).trim(); + else throw new Error(`The cmdColor doesn't appear to be valid: ${this.device.context.cmdColor}`); + } else if (this.device.context.cmdColour) { + if (/^c[a-z]+$/i.test(this.device.context.cmdColour)) this.cmdColor = ('' + this.device.context.cmdColour).trim(); + else throw new Error(`The cmdColour doesn't appear to be valid: ${this.device.context.cmdColour}`); + } + + const characteristicLightOn = lightService.getCharacteristic(Characteristic.On) + .updateValue(dps[this.dpLight]) + .on('get', this.getState.bind(this, this.dpLight)) + .on('set', this.setState.bind(this, this.dpLight)); + + const characteristicBrightness = lightService.getCharacteristic(Characteristic.Brightness) + .updateValue(dps[this.dpMode] === this.cmdWhite ? this.convertBrightnessFromTuyaToHomeKit(dps[this.dpBrightness]) : this.convertColorFromTuyaToHomeKit(dps[this.dpColor]).b) + .on('get', this.getBrightness.bind(this)) + .on('set', this.setBrightness.bind(this)); + + const characteristicColorTemperature = lightService.getCharacteristic(Characteristic.ColorTemperature) + .setProps({ + minValue: 0, + maxValue: 600 + }) + .updateValue(dps[this.dpMode] === this.cmdWhite ? this.convertColorTemperatureFromTuyaToHomeKit(dps[this.dpColorTemperature]) : 0) + .on('get', this.getColorTemperature.bind(this)) + .on('set', this.setColorTemperature.bind(this)); + + const characteristicHue = lightService.getCharacteristic(Characteristic.Hue) + .updateValue(dps[this.dpMode] === this.cmdWhite ? 0 : this.convertColorFromTuyaToHomeKit(dps[this.dpColor]).h) + .on('get', this.getHue.bind(this)) + .on('set', this.setHue.bind(this)); + + const characteristicSaturation = lightService.getCharacteristic(Characteristic.Saturation) + .updateValue(dps[this.dpMode] === this.cmdWhite ? 0 : this.convertColorFromTuyaToHomeKit(dps[this.dpColor]).s) + .on('get', this.getSaturation.bind(this)) + .on('set', this.setSaturation.bind(this)); + + this.characteristicHue = characteristicHue; + this.characteristicSaturation = characteristicSaturation; + this.characteristicColorTemperature = characteristicColorTemperature; + + const characteristicSwitchOn = switchService.getCharacteristic(Characteristic.On) + .updateValue(dps[this.dpMotor]) + .on('get', this.getState.bind(this, this.dpMotor)) + .on('set', this.setState.bind(this, this.dpMotor)); + + this.device.on('change', (changes, state) => { + if (changes.hasOwnProperty(this.dpLight) && characteristicLightOn.value !== changes[this.dpLight]) characteristicLightOn.updateValue(changes[this.dpLight]); + + switch (state[this.dpMode]) { + case this.cmdWhite: + if (changes.hasOwnProperty(this.dpBrightness) && this.convertBrightnessFromHomeKitToTuya(characteristicBrightness.value) !== changes[this.dpBrightness]) + characteristicBrightness.updateValue(this.convertBrightnessFromTuyaToHomeKit(changes[this.dpBrightness])); + + if (changes.hasOwnProperty(this.dpColorTemperature) && this.convertColorTemperatureFromHomeKitToTuya(characteristicColorTemperature.value) !== changes[this.dpColorTemperature]) { + + const newColorTemperature = this.convertColorTemperatureFromTuyaToHomeKit(changes[this.dpColorTemperature]); + const newColor = this.convertHomeKitColorTemperatureToHomeKitColor(newColorTemperature); + + characteristicHue.updateValue(newColor.h); + characteristicSaturation.updateValue(newColor.s); + characteristicColorTemperature.updateValue(newColorTemperature); + + } else if (changes[this.dpMode] && !changes.hasOwnProperty(this.dpColorTemperature)) { + + const newColorTemperature = this.convertColorTemperatureFromTuyaToHomeKit(state[this.dpColorTemperature]); + const newColor = this.convertHomeKitColorTemperatureToHomeKitColor(newColorTemperature); + + characteristicHue.updateValue(newColor.h); + characteristicSaturation.updateValue(newColor.s); + characteristicColorTemperature.updateValue(newColorTemperature); + } + + break; + + default: + if (changes.hasOwnProperty(this.dpColor)) { + const oldColor = this.convertColorFromTuyaToHomeKit(this.convertColorFromHomeKitToTuya({ + h: characteristicHue.value, + s: characteristicSaturation.value, + b: characteristicBrightness.value + })); + const newColor = this.convertColorFromTuyaToHomeKit(changes[this.dpColor]); + + if (oldColor.h !== newColor.h) characteristicHue.updateValue(newColor.h); + if (oldColor.s !== newColor.s) characteristicSaturation.updateValue(newColor.s); + if (oldColor.b !== newColor.b) characteristicBrightness.updateValue(newColor.b); + + if (characteristicColorTemperature.value !== 0) characteristicColorTemperature.updateValue(0); + + } else if (changes[this.dpMode]) { + if (characteristicColorTemperature.value !== 0) characteristicColorTemperature.updateValue(0); + } + } + + if (changes.hasOwnProperty(this.dpPower) && characteristicSwitchOn.value !== changes[this.dpPower]) characteristicSwitchOn.updateValue(changes[this.dpPower]); + + }); + } + + getBrightness(callback) { + if (this.device.state[this.dpMode] === this.cmdWhite) return callback(null, this.convertBrightnessFromTuyaToHomeKit(this.device.state[this.dpBrightness])); + callback(null, this.convertColorFromTuyaToHomeKit(this.device.state[this.dpColor]).b); + } + + setBrightness(value, callback) { + if (this.device.state[this.dpMode] === this.cmdWhite) return this.setState(this.dpBrightness, this.convertBrightnessFromHomeKitToTuya(value), callback); + this.setState(this.dpColor, this.convertColorFromHomeKitToTuya({b: value}), callback); + } + + getColorTemperature(callback) { + if (this.device.state[this.dpMode] !== this.cmdWhite) return callback(null, 0); + callback(null, this.convertColorTemperatureFromTuyaToHomeKit(this.device.state[this.dpColorTemperature])); + } + + setColorTemperature(value, callback) { + if (value === 0) return callback(null, true); + + const newColor = this.convertHomeKitColorTemperatureToHomeKitColor(value); + this.characteristicHue.updateValue(newColor.h); + this.characteristicSaturation.updateValue(newColor.s); + + this.setMultiState({[this.dpMode]: this.cmdWhite, [this.dpColorTemperature]: this.convertColorTemperatureFromHomeKitToTuya(value)}, callback); + } + + getHue(callback) { + if (this.device.state[this.dpMode] === this.cmdWhite) return callback(null, 0); + callback(null, this.convertColorFromTuyaToHomeKit(this.device.state[this.dpColor]).h); + } + + setHue(value, callback) { + this._setHueSaturation({h: value}, callback); + } + + getSaturation(callback) { + if (this.device.state[this.dpMode] === this.cmdWhite) return callback(null, 0); + callback(null, this.convertColorFromTuyaToHomeKit(this.device.state[this.dpColor]).s); + } + + setSaturation(value, callback) { + this._setHueSaturation({s: value}, callback); + } + + _setHueSaturation(prop, callback) { + if (!this._pendingHueSaturation) { + this._pendingHueSaturation = {props: {}, callbacks: []}; + } + + if (prop) { + if (this._pendingHueSaturation.timer) clearTimeout(this._pendingHueSaturation.timer); + + this._pendingHueSaturation.props = {...this._pendingHueSaturation.props, ...prop}; + this._pendingHueSaturation.callbacks.push(callback); + + this._pendingHueSaturation.timer = setTimeout(() => { + this._setHueSaturation(); + }, 500); + return; + } + + //this.characteristicColorTemperature.updateValue(0); + + const callbacks = this._pendingHueSaturation.callbacks; + const callEachBack = err => { + async.eachSeries(callbacks, (callback, next) => { + try { + callback(err); + } catch (ex) {} + next(); + }, () => { + this.characteristicColorTemperature.updateValue(0); + }); + }; + + const isSham = this._pendingHueSaturation.props.h === 0 && this._pendingHueSaturation.props.s === 0; + const newValue = this.convertColorFromHomeKitToTuya(this._pendingHueSaturation.props); + this._pendingHueSaturation = null; + + if (this.device.state[this.dpMode] === this.cmdWhite && isSham) return callEachBack(); + + this.setMultiState({[this.dpMode]: this.cmdColor, [this.dpColor]: newValue}, callEachBack); + } +} + +module.exports = StarProjectorAccessory; From e12c8be6548fc2460877ab98ea0cdd841b760531 Mon Sep 17 00:00:00 2001 From: scooterpsu <3433982+scooterpsu@users.noreply.github.com> Date: Fri, 17 May 2024 13:05:00 -0400 Subject: [PATCH 2/2] Fix for RGB light saturation being set to hue value incorrectly --- lib/RGBTWLightAccessory.js | 2 +- lib/RGBTWOutletAccessory.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/RGBTWLightAccessory.js b/lib/RGBTWLightAccessory.js index 7ccd21b..e7f4089 100644 --- a/lib/RGBTWLightAccessory.js +++ b/lib/RGBTWLightAccessory.js @@ -126,7 +126,7 @@ class RGBTWLightAccessory extends BaseAccessory { if (oldColor.b !== newColor.b) characteristicBrightness.updateValue(newColor.b); if (oldColor.h !== newColor.h) characteristicHue.updateValue(newColor.h); - if (oldColor.s !== newColor.s) characteristicSaturation.updateValue(newColor.h); + if (oldColor.s !== newColor.s) characteristicSaturation.updateValue(newColor.s); if (characteristicColorTemperature.value !== 0) characteristicColorTemperature.updateValue(0); diff --git a/lib/RGBTWOutletAccessory.js b/lib/RGBTWOutletAccessory.js index 72eb6c6..1605891 100644 --- a/lib/RGBTWOutletAccessory.js +++ b/lib/RGBTWOutletAccessory.js @@ -181,7 +181,7 @@ class RGBTWOutletAccessory extends BaseAccessory { if (oldColor.b !== newColor.b) characteristicBrightness.updateValue(newColor.b); if (oldColor.h !== newColor.h) characteristicHue.updateValue(newColor.h); - if (oldColor.s !== newColor.s) characteristicSaturation.updateValue(newColor.h); + if (oldColor.s !== newColor.s) characteristicSaturation.updateValue(newColor.s); if (characteristicColorTemperature.value !== 0) characteristicColorTemperature.updateValue(0);