From 99a6498ed0bcd24f49dea68924f13ebca340f57e Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 29 Dec 2022 21:03:39 +0800 Subject: [PATCH 1/6] Reuse light characteristics. --- src/accessory/CameraAccessory.ts | 49 +--- src/accessory/DimmerAccessory.ts | 3 +- src/accessory/LightAccessory.ts | 338 ++------------------------ src/accessory/characteristic/Light.ts | 321 ++++++++++++++++++++++++ 4 files changed, 353 insertions(+), 358 deletions(-) create mode 100644 src/accessory/characteristic/Light.ts diff --git a/src/accessory/CameraAccessory.ts b/src/accessory/CameraAccessory.ts index 139987d7..dd7b876e 100644 --- a/src/accessory/CameraAccessory.ts +++ b/src/accessory/CameraAccessory.ts @@ -1,7 +1,7 @@ -import { TuyaDeviceSchemaIntegerProperty, TuyaDeviceStatus } from '../device/TuyaDevice'; +import { TuyaDeviceStatus } from '../device/TuyaDevice'; import { TuyaStreamingDelegate } from '../util/TuyaStreamDelegate'; -import { limit, remap } from '../util/util'; import BaseAccessory from './BaseAccessory'; +import { configureLight } from './characteristic/Light'; import { configureOn } from './characteristic/On'; import { configureProgrammableSwitchEvent, onProgrammableSwitchEvent } from './characteristic/ProgrammableSwitchEvent'; @@ -15,7 +15,7 @@ const SCHEMA_CODE = { // Notifies when a doorbell ring occurs. ALARM_MESSAGE: ['alarm_message'], LIGHT_ON: ['floodlight_switch'], - LIGHT_BRIGHTNESS: ['floodlight_lightness'], + LIGHT_BRIGHT: ['floodlight_lightness'], }; export default class CameraAccessory extends BaseAccessory { @@ -29,41 +29,18 @@ export default class CameraAccessory extends BaseAccessory { configureServices() { this.configureDoorbell(); this.configureCamera(); - this.configureFloodLight(); this.configureMotion(); - } - configureFloodLight() { - const onSchema = this.getSchema(...SCHEMA_CODE.LIGHT_ON); - if (!onSchema) { - return; - } - - const service = this.getLightService(); - - configureOn(this, service, onSchema); - - const brightnessSchema = this.getSchema(...SCHEMA_CODE.LIGHT_BRIGHTNESS); - if (brightnessSchema) { - const { min, max } = brightnessSchema.property as TuyaDeviceSchemaIntegerProperty; - service.getCharacteristic(this.Characteristic.Brightness) - .onGet(() => { - const status = this.getStatus(brightnessSchema.code)!; - let value = status.value as number; - value = remap(value, 0, max, 0, 100); - value = Math.round(value); - value = limit(value, min, max); - return value; - }) - .onSet(value => { - this.log.debug(`Characteristic.Brightness set to: ${value}`); - let brightValue = value as number; - brightValue = remap(brightValue, 0, 100, 0, max); - brightValue = Math.round(brightValue); - brightValue = limit(brightValue, min, max); - this.sendCommands([{ code: brightnessSchema.code, value: brightValue }], true); - }); - } + // FloodLight + configureLight( + this, + this.getLightService(), + this.getSchema(...SCHEMA_CODE.LIGHT_ON), + this.getSchema(...SCHEMA_CODE.LIGHT_BRIGHT), + undefined, + undefined, + undefined, + ); } configureMotion() { diff --git a/src/accessory/DimmerAccessory.ts b/src/accessory/DimmerAccessory.ts index b797224e..08edf763 100644 --- a/src/accessory/DimmerAccessory.ts +++ b/src/accessory/DimmerAccessory.ts @@ -83,7 +83,8 @@ export default class DimmerAccessory extends BaseAccessory { brightValue = Math.round(brightValue); brightValue = limit(brightValue, min, max); this.sendCommands([{ code: schema.code, value: brightValue }], true); - }).setProps(props); + }) + .setProps(props); } diff --git a/src/accessory/LightAccessory.ts b/src/accessory/LightAccessory.ts index 0cf0be5c..f46d3b2d 100644 --- a/src/accessory/LightAccessory.ts +++ b/src/accessory/LightAccessory.ts @@ -1,9 +1,7 @@ -import { TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty, TuyaDeviceStatus } from '../device/TuyaDevice'; -import { kelvinToHSV, kelvinToMired, miredToKelvin } from '../util/color'; -import { limit, remap } from '../util/util'; import BaseAccessory from './BaseAccessory'; import { configureOn } from './characteristic/On'; import { configureMotionDetected } from './characteristic/MotionDetected'; +import { configureLight } from './characteristic/Light'; const SCHEMA_CODE = { ON: ['switch_led'], @@ -16,335 +14,33 @@ const SCHEMA_CODE = { POWER_SWITCH: ['switch'], }; -const DEFAULT_COLOR_TEMPERATURE_KELVIN = 6500; - -enum LightAccessoryType { - Unknown = 'Unknown', - Normal = 'Normal', // Normal Accessory, similar to SwitchAccessory, OutletAccessory. - C = 'C', // Accessory with brightness. - CW = 'CW', // Accessory with brightness and color temperature (Cold and Warm). - RGB = 'RGB', // Accessory with color (RGB <--> HSB). - RGBC = 'RGBC', // Accessory with color and brightness. - RGBCW = 'RGBCW', // Accessory with color, brightness and color temperature (two work mode). -} - -type TuyaDeviceSchemaColorProperty = { - h: TuyaDeviceSchemaIntegerProperty; - s: TuyaDeviceSchemaIntegerProperty; - v: TuyaDeviceSchemaIntegerProperty; -}; - export default class LightAccessory extends BaseAccessory { - static readonly LightAccessoryType = LightAccessoryType; requiredSchema() { return [SCHEMA_CODE.ON]; } configureServices() { - const type = this.getAccessoryType(); - this.log.info('Light Accessory type:', type); - - switch (type) { - case LightAccessoryType.Normal: - configureOn(this, this.getLightService(), this.getSchema(...SCHEMA_CODE.ON)); - break; - case LightAccessoryType.C: - configureOn(this, this.getLightService(), this.getSchema(...SCHEMA_CODE.ON)); - this.configureBrightness(); - break; - case LightAccessoryType.CW: - configureOn(this, this.getLightService(), this.getSchema(...SCHEMA_CODE.ON)); - this.configureBrightness(); - this.configureColourTemperature(); - break; - case LightAccessoryType.RGB: - configureOn(this, this.getLightService(), this.getSchema(...SCHEMA_CODE.ON)); - this.configureBrightness(); - this.configureHue(); - this.configureSaturation(); - break; - case LightAccessoryType.RGBC: - case LightAccessoryType.RGBCW: - configureOn(this, this.getLightService(), this.getSchema(...SCHEMA_CODE.ON)); - this.configureBrightness(); - this.configureColourTemperature(); - this.configureHue(); - this.configureSaturation(); - break; - } - - this.configurePIR(); - - // RGB Power Switch - this.configurePowerSwitch(); - } - - - getLightService() { - return this.accessory.getService(this.Service.Lightbulb) - || this.accessory.addService(this.Service.Lightbulb); - } - - getAccessoryType() { - const on = this.getSchema(...SCHEMA_CODE.ON); - const bright = this.getSchema(...SCHEMA_CODE.BRIGHTNESS); - const temp = this.getSchema(...SCHEMA_CODE.COLOR_TEMP); - const color = this.getSchema(...SCHEMA_CODE.COLOR); - const mode = this.getSchema(...SCHEMA_CODE.WORK_MODE)?.property as TuyaDeviceSchemaEnumProperty; - const { h, s, v } = (color?.property || {}) as never; - - let accessoryType: LightAccessoryType; - if (on && bright && temp && h && s && v && mode && mode.range.includes('colour') && mode.range.includes('white')) { - accessoryType = LightAccessoryType.RGBCW; - } else if (on && bright && !temp && h && s && v && mode && mode.range.includes('colour') && mode.range.includes('white')) { - accessoryType = LightAccessoryType.RGBC; - } else if (on && !temp && h && s && v) { - accessoryType = LightAccessoryType.RGB; - } else if (on && bright && temp) { - accessoryType = LightAccessoryType.CW; - } else if (on && bright && !temp) { - accessoryType = LightAccessoryType.C; - } else if (on && !bright && !temp) { - accessoryType = LightAccessoryType.Normal; - } else { - accessoryType = LightAccessoryType.Unknown; - } - - return accessoryType; - } - - getColorValue() { - const schema = this.getSchema(...SCHEMA_CODE.COLOR); - const status = this.getStatus(schema!.code); - if (!status || !status.value || status.value === '' || status.value === '{}') { - return { h: 0, s: 0, v: 0 }; - } - - const { h, s, v } = JSON.parse(status.value as string); - return { - h: h as number, - s: s as number, - v: v as number, - }; - } - - inWhiteMode() { - if (this.getAccessoryType() === LightAccessoryType.C - || this.getAccessoryType() === LightAccessoryType.CW) { - return true; - } - - const mode = this.getSchema(...SCHEMA_CODE.WORK_MODE); - if (!mode) { - return false; - } - const status = this.getStatus(mode.code); - if (!status) { - return false; - } - return (status.value === 'white'); - } - - inColorMode() { - if (this.getAccessoryType() === LightAccessoryType.RGB) { - return true; - } - - const mode = this.getSchema(...SCHEMA_CODE.WORK_MODE); - if (!mode) { - return false; - } - const status = this.getStatus(mode.code); - if (!status) { - return false; - } - return (status.value === 'colour'); - } - - configureBrightness() { - const service = this.getLightService(); - service.getCharacteristic(this.Characteristic.Brightness) - .onGet(() => { - if (this.inColorMode()) { - // Color mode, get brightness from `color_data.v` - const schema = this.getSchema(...SCHEMA_CODE.COLOR)!; - const { max } = (schema.property as TuyaDeviceSchemaColorProperty).v; - const status = this.getColorValue().v; - const value = Math.round(100 * status / max); - return limit(value, 0, 100); - } else if (this.inWhiteMode()) { - // White mode, get brightness from `brightness_value` - const schema = this.getSchema(...SCHEMA_CODE.BRIGHTNESS)!; - const { max } = schema.property as TuyaDeviceSchemaIntegerProperty; - const status = this.getStatus(schema.code)!; - const value = Math.round(100 * (status.value as number) / max); - return limit(value, 0, 100); - } else { - // Unsupported mode - return 100; - } + const service = this.accessory.getService(this.Service.Lightbulb) + || this.accessory.addService(this.Service.Lightbulb); - }) - .onSet((value) => { - this.log.debug(`Characteristic.Brightness set to: ${value}`); - if (this.inColorMode()) { - // Color mode, set brightness to `color_data.v` - const colorSchema = this.getSchema(...SCHEMA_CODE.COLOR)!; - const { min, max } = (colorSchema.property as TuyaDeviceSchemaColorProperty).v; - const colorValue = this.getColorValue(); - colorValue.v = Math.round(value as number * max / 100); - colorValue.v = limit(colorValue.v, min, max); - this.sendCommands([{ code: colorSchema.code, value: JSON.stringify(colorValue) }], true); - } else if (this.inWhiteMode()) { - // White mode, set brightness to `brightness_value` - const brightSchema = this.getSchema(...SCHEMA_CODE.BRIGHTNESS)!; - const { min, max } = brightSchema.property as TuyaDeviceSchemaIntegerProperty; - let brightValue = Math.round(value as number * max / 100); - brightValue = limit(brightValue, min, max); - this.sendCommands([{ code: brightSchema.code, value: brightValue }], true); - } else { - // Unsupported mode - this.log.warn('Neither color mode nor white mode.'); - } - }); + configureLight( + this, + service, + this.getSchema(...SCHEMA_CODE.ON), + this.getSchema(...SCHEMA_CODE.BRIGHTNESS), + this.getSchema(...SCHEMA_CODE.COLOR_TEMP), + this.getSchema(...SCHEMA_CODE.COLOR), + this.getSchema(...SCHEMA_CODE.WORK_MODE), + ); - } - - configureColourTemperature() { - const type = this.getAccessoryType(); - const props = { minValue: 140, maxValue: 500, minStep: 1 }; - - if (type === LightAccessoryType.RGBC) { - props.minValue = props.maxValue = Math.round(kelvinToMired(DEFAULT_COLOR_TEMPERATURE_KELVIN)); - } - this.log.debug('Set props for ColorTemperature:', props); - - const service = this.getLightService(); - service.getCharacteristic(this.Characteristic.ColorTemperature) - .onGet(() => { - if (type === LightAccessoryType.RGBC) { - return props.minValue; - } - - const schema = this.getSchema(...SCHEMA_CODE.COLOR_TEMP)!; - const { min, max } = schema.property as TuyaDeviceSchemaIntegerProperty; - const status = this.getStatus(schema.code)!; - const kelvin = remap(status.value as number, min, max, miredToKelvin(props.maxValue), miredToKelvin(props.minValue)); - const mired = Math.round(kelvinToMired(kelvin)); - return limit(mired, props.minValue, props.maxValue); - }) - .onSet((value) => { - this.log.debug(`Characteristic.ColorTemperature set to: ${value}`); - - const commands: TuyaDeviceStatus[] = []; - const mode = this.getSchema(...SCHEMA_CODE.WORK_MODE); - if (mode) { - commands.push({ code: mode.code, value: 'white' }); - } - - if (type !== LightAccessoryType.RGBC) { - const schema = this.getSchema(...SCHEMA_CODE.COLOR_TEMP)!; - const { min, max } = schema.property as TuyaDeviceSchemaIntegerProperty; - const kelvin = miredToKelvin(value as number); - const temp = Math.round(remap(kelvin, miredToKelvin(props.maxValue), miredToKelvin(props.minValue), min, max)); - commands.push({ code: schema.code, value: temp }); - } + // PIR + configureOn(this, undefined, this.getSchema(...SCHEMA_CODE.PIR_ON)); + configureMotionDetected(this, undefined, this.getSchema(...SCHEMA_CODE.PIR)); - this.sendCommands(commands, true); - }) - .setProps(props); - - } - - configureHue() { - const service = this.getLightService(); - const colorSchema = this.getSchema(...SCHEMA_CODE.COLOR)!; - const { min, max } = (colorSchema.property as TuyaDeviceSchemaColorProperty).h; - service.getCharacteristic(this.Characteristic.Hue) - .onGet(() => { - if (this.inWhiteMode()) { - return kelvinToHSV(DEFAULT_COLOR_TEMPERATURE_KELVIN)!.h; - } - - const hue = Math.round(360 * this.getColorValue().h / max); - return limit(hue, 0, 360); - }) - .onSet((value) => { - this.log.debug(`Characteristic.Hue set to: ${value}`); - const colorValue = this.getColorValue(); - colorValue.h = Math.round(value as number * max / 360); - colorValue.h = limit(colorValue.h, min, max); - const commands: TuyaDeviceStatus[] = [{ - code: colorSchema.code, - value: JSON.stringify(colorValue), - }]; - - const mode = this.getSchema(...SCHEMA_CODE.WORK_MODE); - if (mode) { - commands.push({ code: mode.code, value: 'colour' }); - } - - this.sendCommands(commands, true); - }); - } - - configureSaturation() { - const service = this.getLightService(); - const colorSchema = this.getSchema(...SCHEMA_CODE.COLOR)!; - const { min, max } = (colorSchema.property as TuyaDeviceSchemaColorProperty).s; - service.getCharacteristic(this.Characteristic.Saturation) - .onGet(() => { - if (this.inWhiteMode()) { - return kelvinToHSV(DEFAULT_COLOR_TEMPERATURE_KELVIN)!.s; - } - - const saturation = Math.round(100 * this.getColorValue().s / max); - return limit(saturation, 0, 100); - }) - .onSet((value) => { - this.log.debug(`Characteristic.Saturation set to: ${value}`); - const colorValue = this.getColorValue(); - colorValue.s = Math.round(value as number * max / 100); - colorValue.s = limit(colorValue.s, min, max); - const commands: TuyaDeviceStatus[] = [{ - code: colorSchema.code, - value: JSON.stringify(colorValue), - }]; - - const mode = this.getSchema(...SCHEMA_CODE.WORK_MODE); - if (mode) { - commands.push({ code: mode.code, value: 'colour' }); - } - - this.sendCommands(commands, true); - }); - } - - configurePIR() { - const onSchema = this.getSchema(...SCHEMA_CODE.PIR_ON); - if (onSchema) { - const service = this.accessory.getService(onSchema.code) - || this.accessory.addService(this.Service.Switch, onSchema.code, onSchema.code); - configureOn(this, service, onSchema); - } - - const motionSchema = this.getSchema(...SCHEMA_CODE.PIR); - configureMotionDetected(this, undefined, motionSchema); - - } - - configurePowerSwitch() { - const schema = this.getSchema(...SCHEMA_CODE.POWER_SWITCH); - if (!schema) { - return; - } - - const service = this.accessory.getService(schema.code) - || this.accessory.addService(this.Service.Switch, schema.code, schema.code); - - configureOn(this, service, schema); + // RGB Power Switch + configureOn(this, undefined, this.getSchema(...SCHEMA_CODE.POWER_SWITCH)); } } diff --git a/src/accessory/characteristic/Light.ts b/src/accessory/characteristic/Light.ts new file mode 100644 index 00000000..88aba040 --- /dev/null +++ b/src/accessory/characteristic/Light.ts @@ -0,0 +1,321 @@ +import { Service } from 'homebridge'; +import { TuyaDeviceSchema, TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty, TuyaDeviceStatus } from '../../device/TuyaDevice'; +import { kelvinToHSV, kelvinToMired, miredToKelvin } from '../../util/color'; +import { limit, remap } from '../../util/util'; +import BaseAccessory from '../BaseAccessory'; +import { configureOn } from './On'; + +const DEFAULT_COLOR_TEMPERATURE_KELVIN = 6500; + +enum LightType { + Unknown = 'Unknown', + Normal = 'Normal', // Normal Accessory, similar to SwitchAccessory, OutletAccessory. + C = 'C', // Accessory with brightness. + CW = 'CW', // Accessory with brightness and color temperature (Cold and Warm). + RGB = 'RGB', // Accessory with color (RGB <--> HSB). + RGBC = 'RGBC', // Accessory with color and brightness. + RGBCW = 'RGBCW', // Accessory with color, brightness and color temperature (two work mode). +} + +type TuyaDeviceSchemaColorProperty = { + h: TuyaDeviceSchemaIntegerProperty; + s: TuyaDeviceSchemaIntegerProperty; + v: TuyaDeviceSchemaIntegerProperty; +}; + +function getLightType( + accessory: BaseAccessory, + on?: TuyaDeviceSchema, + bright?: TuyaDeviceSchema, + temp?: TuyaDeviceSchema, + color?: TuyaDeviceSchema, + mode?: TuyaDeviceSchema, +) { + const modeRange = (mode?.property as TuyaDeviceSchemaEnumProperty).range; + const { h, s, v } = (color?.property || {}) as never; + + let lightType: LightType; + if (on && bright && temp && h && s && v && mode && modeRange.includes('colour') && modeRange.includes('white')) { + lightType = LightType.RGBCW; + } else if (on && bright && !temp && h && s && v && mode && modeRange.includes('colour') && modeRange.includes('white')) { + lightType = LightType.RGBC; + } else if (on && !temp && h && s && v) { + lightType = LightType.RGB; + } else if (on && bright && temp) { + lightType = LightType.CW; + } else if (on && bright && !temp) { + lightType = LightType.C; + } else if (on && !bright && !temp) { + lightType = LightType.Normal; + } else { + lightType = LightType.Unknown; + } + + return lightType; +} + +function getColorValue(accessory: BaseAccessory, schema: TuyaDeviceSchema) { + const status = accessory.getStatus(schema!.code); + if (!status || !status.value || status.value === '' || status.value === '{}') { + return { h: 0, s: 0, v: 0 }; + } + + const { h, s, v } = JSON.parse(status.value as string); + return { + h: h as number, + s: s as number, + v: v as number, + }; +} + +function inWhiteMode( + accessory: BaseAccessory, + lightType: LightType, + modeSchema?: TuyaDeviceSchema, +) { + if (lightType === LightType.C || lightType === LightType.CW) { + return true; + } + + if (!modeSchema) { + return false; + } + const status = accessory.getStatus(modeSchema.code)!; + return (status.value === 'white'); +} + +function inColorMode( + accessory: BaseAccessory, + lightType: LightType, + modeSchema?: TuyaDeviceSchema, +) { + if (lightType === LightType.RGB) { + return true; + } + + if (!modeSchema) { + return false; + } + const status = accessory.getStatus(modeSchema.code)!; + return (status.value === 'colour'); +} + +function configureBrightness( + accessory: BaseAccessory, + service: Service, + lightType: LightType, + brightSchema?: TuyaDeviceSchema, + colorSchema?: TuyaDeviceSchema, + modeSchema?: TuyaDeviceSchema, +) { + + service.getCharacteristic(accessory.Characteristic.Brightness) + .onGet(() => { + if (inColorMode(accessory, lightType, modeSchema)) { + // Color mode, get brightness from `color_data.v` + const { max } = (colorSchema!.property as TuyaDeviceSchemaColorProperty).v; + const colorValue = getColorValue(accessory, colorSchema!); + const value = Math.round(100 * colorValue.v / max); + return limit(value, 0, 100); + } else if (inWhiteMode(accessory, lightType, modeSchema)) { + // White mode, get brightness from `brightness_value` + const { max } = brightSchema!.property as TuyaDeviceSchemaIntegerProperty; + const status = accessory.getStatus(brightSchema!.code)!; + const value = Math.round(100 * (status.value as number) / max); + return limit(value, 0, 100); + } else { + // Unsupported mode + return 100; + } + }) + .onSet((value) => { + accessory.log.debug(`Characteristic.Brightness set to: ${value}`); + if (inColorMode(accessory, lightType, modeSchema)) { + // Color mode, set brightness to `color_data.v` + const { min, max } = (colorSchema!.property as TuyaDeviceSchemaColorProperty).v; + const colorValue = getColorValue(accessory, colorSchema!); + colorValue.v = Math.round(value as number * max / 100); + colorValue.v = limit(colorValue.v, min, max); + accessory.sendCommands([{ code: colorSchema!.code, value: JSON.stringify(colorValue) }], true); + } else if (inWhiteMode(accessory, lightType, modeSchema)) { + // White mode, set brightness to `brightness_value` + const { min, max } = brightSchema!.property as TuyaDeviceSchemaIntegerProperty; + let brightValue = Math.round(value as number * max / 100); + brightValue = limit(brightValue, min, max); + accessory.sendCommands([{ code: brightSchema!.code, value: brightValue }], true); + } else { + // Unsupported mode + accessory.log.warn('Neither color mode nor white mode.'); + } + }); + +} + +function configureColourTemperature( + accessory: BaseAccessory, + service: Service, + lightType: LightType, + tempSchema: TuyaDeviceSchema, + modeSchema?: TuyaDeviceSchema, +) { + const props = { minValue: 140, maxValue: 500, minStep: 1 }; + + if (lightType === LightType.RGBC) { + props.minValue = props.maxValue = Math.round(kelvinToMired(DEFAULT_COLOR_TEMPERATURE_KELVIN)); + } + accessory.log.debug('Set props for ColorTemperature:', props); + + service.getCharacteristic(accessory.Characteristic.ColorTemperature) + .onGet(() => { + if (lightType === LightType.RGBC) { + return props.minValue; + } + + // const schema = accessory.getSchema(...SCHEMA_CODE.COLOR_TEMP)!; + const { min, max } = tempSchema.property as TuyaDeviceSchemaIntegerProperty; + const status = accessory.getStatus(tempSchema.code)!; + const kelvin = remap(status.value as number, min, max, miredToKelvin(props.maxValue), miredToKelvin(props.minValue)); + const mired = Math.round(kelvinToMired(kelvin)); + return limit(mired, props.minValue, props.maxValue); + }) + .onSet((value) => { + accessory.log.debug(`Characteristic.ColorTemperature set to: ${value}`); + + const commands: TuyaDeviceStatus[] = []; + if (modeSchema) { + commands.push({ code: modeSchema.code, value: 'white' }); + } + + if (lightType !== LightType.RGBC) { + const { min, max } = tempSchema.property as TuyaDeviceSchemaIntegerProperty; + const kelvin = miredToKelvin(value as number); + const temp = Math.round(remap(kelvin, miredToKelvin(props.maxValue), miredToKelvin(props.minValue), min, max)); + commands.push({ code: tempSchema.code, value: temp }); + } + + accessory.sendCommands(commands, true); + }) + .setProps(props); + +} + +function configureHue( + accessory: BaseAccessory, + service: Service, + lightType: LightType, + colorSchema: TuyaDeviceSchema, + modeSchema?: TuyaDeviceSchema, +) { + const { min, max } = (colorSchema.property as TuyaDeviceSchemaColorProperty).h; + service.getCharacteristic(accessory.Characteristic.Hue) + .onGet(() => { + if (inWhiteMode(accessory, lightType, modeSchema)) { + return kelvinToHSV(DEFAULT_COLOR_TEMPERATURE_KELVIN)!.h; + } + + const hue = Math.round(360 * getColorValue(accessory, colorSchema).h / max); + return limit(hue, 0, 360); + }) + .onSet((value) => { + accessory.log.debug(`Characteristic.Hue set to: ${value}`); + const colorValue = getColorValue(accessory, colorSchema); + colorValue.h = Math.round(value as number * max / 360); + colorValue.h = limit(colorValue.h, min, max); + const commands: TuyaDeviceStatus[] = [{ + code: colorSchema.code, + value: JSON.stringify(colorValue), + }]; + + if (modeSchema) { + commands.push({ code: modeSchema.code, value: 'colour' }); + } + + accessory.sendCommands(commands, true); + }); +} + +function configureSaturation( + accessory: BaseAccessory, + service: Service, + lightType: LightType, + colorSchema: TuyaDeviceSchema, + modeSchema?: TuyaDeviceSchema, +) { + const { min, max } = (colorSchema.property as TuyaDeviceSchemaColorProperty).s; + service.getCharacteristic(accessory.Characteristic.Saturation) + .onGet(() => { + if (inWhiteMode(accessory, lightType, modeSchema)) { + return kelvinToHSV(DEFAULT_COLOR_TEMPERATURE_KELVIN)!.s; + } + + const saturation = Math.round(100 * getColorValue(accessory, colorSchema).s / max); + return limit(saturation, 0, 100); + }) + .onSet((value) => { + accessory.log.debug(`Characteristic.Saturation set to: ${value}`); + const colorValue = getColorValue(accessory, colorSchema); + colorValue.s = Math.round(value as number * max / 100); + colorValue.s = limit(colorValue.s, min, max); + const commands: TuyaDeviceStatus[] = [{ + code: colorSchema.code, + value: JSON.stringify(colorValue), + }]; + + if (modeSchema) { + commands.push({ code: modeSchema.code, value: 'colour' }); + } + + accessory.sendCommands(commands, true); + }); +} + +export function configureLight( + accessory: BaseAccessory, + service?: Service, + onSchema?: TuyaDeviceSchema, + brightSchema?: TuyaDeviceSchema, + tempSchema?: TuyaDeviceSchema, + colorSchema?: TuyaDeviceSchema, + modeSchema?: TuyaDeviceSchema, +) { + if (!onSchema) { + return; + } + + if (!service) { + service = accessory.accessory.getService(accessory.Service.Lightbulb) + || accessory.accessory.addService(accessory.Service.Lightbulb, accessory.accessory.displayName + ' Light'); + } + + const lightType = getLightType(accessory, onSchema, brightSchema, tempSchema, colorSchema, modeSchema); + accessory.log.info('Light type:', lightType); + + switch (lightType) { + case LightType.Normal: + configureOn(accessory, service, onSchema); + break; + case LightType.C: + configureOn(accessory, service, onSchema); + configureBrightness(accessory, service, lightType, brightSchema, colorSchema, modeSchema); + break; + case LightType.CW: + configureOn(accessory, service, onSchema); + configureBrightness(accessory, service, lightType, brightSchema, colorSchema, modeSchema); + configureColourTemperature(accessory, service, lightType, tempSchema!, modeSchema); + break; + case LightType.RGB: + configureOn(accessory, service, onSchema); + configureBrightness(accessory, service, lightType, brightSchema, colorSchema, modeSchema); + configureHue(accessory, service, lightType, colorSchema!, modeSchema); + configureSaturation(accessory, service, lightType, colorSchema!, modeSchema); + break; + case LightType.RGBC: + case LightType.RGBCW: + configureOn(accessory, service, onSchema); + configureBrightness(accessory, service, lightType, brightSchema, colorSchema, modeSchema); + configureColourTemperature(accessory, service, lightType, tempSchema!, modeSchema); + configureHue(accessory, service, lightType, colorSchema!, modeSchema); + configureSaturation(accessory, service, lightType, colorSchema!, modeSchema); + break; + } +} From 9866389683b75196bb229e43c9230798f06c1a30 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 29 Dec 2022 21:05:07 +0800 Subject: [PATCH 2/6] Support Diffuser RGB light. --- src/accessory/DiffuserAccessory.ts | 86 +++++++----------------------- 1 file changed, 18 insertions(+), 68 deletions(-) diff --git a/src/accessory/DiffuserAccessory.ts b/src/accessory/DiffuserAccessory.ts index 213095d6..1ec809ba 100644 --- a/src/accessory/DiffuserAccessory.ts +++ b/src/accessory/DiffuserAccessory.ts @@ -1,6 +1,5 @@ -import { TuyaDeviceSchemaIntegerProperty, TuyaDeviceStatus } from '../device/TuyaDevice'; -import { limit, remap } from '../util/util'; import BaseAccessory from './BaseAccessory'; +import { configureLight } from './characteristic/Light'; import { configureOn } from './characteristic/On'; import { configureRotationSpeedLevel } from './characteristic/RotationSpeed'; @@ -11,7 +10,7 @@ const SCHEMA_CODE = { SPRAY_LEVEL: ['level'], LIGHT_ON: ['switch_led'], LIGHT_MODE: ['work_mode'], - LIGHT_BRIGHTNESS: ['bright_value', 'bright_value_v2'], + LIGHT_BRIGHT: ['bright_value', 'bright_value_v2'], LIGHT_COLOR: ['colour_data_hsv'], SOUND_ON: ['switch_sound'], }; @@ -23,9 +22,21 @@ export default class DiffuserAccessory extends BaseAccessory { } configureServices() { - configureOn(this, undefined, this.getSchema(...SCHEMA_CODE.ON)); // Main Switch - this.configureAirPurifier(); - this.configureLight(); + // Main Switch + configureOn(this, undefined, this.getSchema(...SCHEMA_CODE.ON)); + + this.configureDiffuser(); + + configureLight( + this, + undefined, + this.getSchema(...SCHEMA_CODE.LIGHT_ON), + this.getSchema(...SCHEMA_CODE.LIGHT_BRIGHT), + undefined, + this.getSchema(...SCHEMA_CODE.LIGHT_COLOR), + this.getSchema(...SCHEMA_CODE.LIGHT_MODE), + ); + configureOn(this, undefined, this.getSchema(...SCHEMA_CODE.SOUND_ON)); // Sound Switch } @@ -34,12 +45,7 @@ export default class DiffuserAccessory extends BaseAccessory { || this.accessory.addService(this.Service.AirPurifier); } - lightService() { - return this.accessory.getService(this.Service.Lightbulb) - || this.accessory.addService(this.Service.Lightbulb, this.device.name + ' Light'); - } - - configureAirPurifier() { + configureDiffuser() { const onSchema = this.getSchema(...SCHEMA_CODE.ON); const sprayOnSchema = this.getSchema(...SCHEMA_CODE.SPRAY_ON)!; @@ -82,60 +88,4 @@ export default class DiffuserAccessory extends BaseAccessory { configureRotationSpeedLevel(this, this.mainService(), this.getSchema(...SCHEMA_CODE.SPRAY_LEVEL)); } - configureLight() { - const onSchema = this.getSchema(...SCHEMA_CODE.ON); - const lightOnSchema = this.getSchema(...SCHEMA_CODE.LIGHT_ON); - if (!lightOnSchema) { - return; - } - - this.lightService().getCharacteristic(this.Characteristic.On) - .onGet(() => { - if (onSchema && this.getStatus(onSchema.code)!.value !== true) { - return false; - } - - const status = this.getStatus(lightOnSchema.code)!; - return (status.value as boolean); - }) - .onSet(value => { - const commands = [{ code: lightOnSchema.code, value: value as boolean }]; - if (onSchema && value) { - commands.push({ code: onSchema.code, value: true }); - } - this.sendCommands(commands, true); - }); - this.configureLightBrightness(); - } - - configureLightBrightness() { - const schema = this.getSchema(...SCHEMA_CODE.LIGHT_BRIGHTNESS); - if (!schema) { - return; - } - - const lightModeSchema = this.getSchema(...SCHEMA_CODE.LIGHT_MODE); - - const { min, max } = schema.property as TuyaDeviceSchemaIntegerProperty; - this.lightService().getCharacteristic(this.Characteristic.Brightness) - .onGet(() => { - const status = this.getStatus(schema.code)!; - const value = Math.round(remap(status.value as number, 0, max, 0, 100)); - return limit(value, 0, 100); - }) - .onSet(value => { - let brightness = Math.round(remap(value as number, 0, 100, 0, max)); - brightness = limit(brightness, min, max); - - const commands: TuyaDeviceStatus[] = [{ - code: schema.code, - value: brightness, - }]; - - if (lightModeSchema) { - commands.push({ code: lightModeSchema.code, value: 'white' }); - } - this.sendCommands(commands, true); - }); - } } From 6e0fb5b55b87571433783e1d03fdfc67e61b81d5 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 29 Dec 2022 21:05:52 +0800 Subject: [PATCH 3/6] Support Fan light temperature and color. --- src/accessory/FanAccessory.ts | 42 +++++++++++++---------------------- 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/src/accessory/FanAccessory.ts b/src/accessory/FanAccessory.ts index 992d5087..85b3fff5 100644 --- a/src/accessory/FanAccessory.ts +++ b/src/accessory/FanAccessory.ts @@ -1,7 +1,7 @@ -import { TuyaDeviceSchemaIntegerProperty, TuyaDeviceSchemaType } from '../device/TuyaDevice'; -import { limit, remap } from '../util/util'; +import { TuyaDeviceSchemaType } from '../device/TuyaDevice'; import BaseAccessory from './BaseAccessory'; import { configureActive } from './characteristic/Active'; +import { configureLight } from './characteristic/Light'; import { configureLockPhysicalControls } from './characteristic/LockPhysicalControls'; import { configureOn } from './characteristic/On'; import { configureRotationSpeed, configureRotationSpeedLevel, configureRotationSpeedOn } from './characteristic/RotationSpeed'; @@ -15,7 +15,10 @@ const SCHEMA_CODE = { FAN_LOCK: ['child_lock'], FAN_SWING: ['switch_horizontal', 'switch_vertical'], LIGHT_ON: ['light', 'switch_led'], - LIGHT_BRIGHTNESS: ['bright_value'], + LIGHT_MODE: ['work_mode'], + LIGHT_BRIGHT: ['bright_value', 'bright_value_v2'], + LIGHT_TEMP: ['temp_value', 'temp_value_v2'], + LIGHT_COLOR: ['colour_data'], }; export default class FanAccessory extends BaseAccessory { @@ -54,8 +57,15 @@ export default class FanAccessory extends BaseAccessory { // Light if (this.getSchema(...SCHEMA_CODE.LIGHT_ON)) { - configureOn(this, this.lightService(), this.getSchema(...SCHEMA_CODE.LIGHT_ON)); - this.configureLightBrightness(); + configureLight( + this, + this.lightService(), + this.getSchema(...SCHEMA_CODE.LIGHT_ON), + this.getSchema(...SCHEMA_CODE.LIGHT_BRIGHT), + this.getSchema(...SCHEMA_CODE.LIGHT_TEMP), + this.getSchema(...SCHEMA_CODE.LIGHT_COLOR), + this.getSchema(...SCHEMA_CODE.LIGHT_MODE), + ); } else { this.log.warn('Remove Lightbulb Service...'); const unusedService = this.accessory.getService(this.Service.Lightbulb); @@ -116,26 +126,4 @@ export default class FanAccessory extends BaseAccessory { }); } - configureLightBrightness() { - const schema = this.getSchema(...SCHEMA_CODE.LIGHT_BRIGHTNESS); - if (!schema) { - return; - } - - const { min, max } = schema.property as TuyaDeviceSchemaIntegerProperty; - this.lightService().getCharacteristic(this.Characteristic.Brightness) - .onGet(() => { - const status = this.getStatus(schema.code)!; - const value = Math.round(remap(status.value as number, 0, max, 0, 100)); - return limit(value, 0, 100); - }) - .onSet(value => { - let brightness = Math.round(remap(value as number, 0, 100, 0, max)); - brightness = limit(brightness, min, max); - this.sendCommands([{ - code: schema.code, - value: brightness, - }], true); - }); - } } From d467b1524df03676b95120c4fcce5f5bf3c5e12e Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 29 Dec 2022 21:06:06 +0800 Subject: [PATCH 4/6] Support Humidifier light. --- src/accessory/HumidifierAccessory.ts | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/accessory/HumidifierAccessory.ts b/src/accessory/HumidifierAccessory.ts index a8921992..4b596397 100644 --- a/src/accessory/HumidifierAccessory.ts +++ b/src/accessory/HumidifierAccessory.ts @@ -4,12 +4,17 @@ import BaseAccessory from './BaseAccessory'; import { configureActive } from './characteristic/Active'; import { configureCurrentTemperature } from './characteristic/CurrentTemperature'; import { configureCurrentRelativeHumidity } from './characteristic/CurrentRelativeHumidity'; +import { configureLight } from './characteristic/Light'; const SCHEMA_CODE = { ACTIVE: ['switch'], CURRENT_HUMIDITY: ['humidity_current'], TARGET_HUMIDITY: ['humidity_set'], CURRENT_TEMP: ['temp_current'], + LIGHT_ON: ['switch_led'], + LIGHT_MODE: ['work_mode'], + LIGHT_BRIGHT: ['bright_value', 'bright_value_v2'], + LIGHT_COLOR: ['colour_data', 'colour_data_hsv'], }; export default class HumidifierAccessory extends BaseAccessory { @@ -19,13 +24,27 @@ export default class HumidifierAccessory extends BaseAccessory { } configureServices() { + // Required Characteristics configureActive(this, this.mainService(), this.getSchema(...SCHEMA_CODE.ACTIVE)); - this.configureTargetState(); this.configureCurrentState(); + this.configureTargetState(); configureCurrentRelativeHumidity(this, this.mainService(), this.getSchema(...SCHEMA_CODE.CURRENT_HUMIDITY)); + + // Optional Characteristics this.configureRelativeHumidityHumidifierThreshold(); - configureCurrentTemperature(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_TEMP)); this.configureRotationSpeed(); + + // Other + configureCurrentTemperature(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_TEMP)); + configureLight( + this, + undefined, + this.getSchema(...SCHEMA_CODE.LIGHT_ON), + this.getSchema(...SCHEMA_CODE.LIGHT_BRIGHT), + undefined, + this.getSchema(...SCHEMA_CODE.LIGHT_COLOR), + this.getSchema(...SCHEMA_CODE.LIGHT_MODE), + ); } From 612bfe115da9370e5a334d885638a0605596e3eb Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 29 Dec 2022 21:18:19 +0800 Subject: [PATCH 5/6] Remove diffuser main switch logic. --- src/accessory/DiffuserAccessory.ts | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/src/accessory/DiffuserAccessory.ts b/src/accessory/DiffuserAccessory.ts index 1ec809ba..c7257d00 100644 --- a/src/accessory/DiffuserAccessory.ts +++ b/src/accessory/DiffuserAccessory.ts @@ -1,4 +1,5 @@ import BaseAccessory from './BaseAccessory'; +import { configureActive } from './characteristic/Active'; import { configureLight } from './characteristic/Light'; import { configureOn } from './characteristic/On'; import { configureRotationSpeedLevel } from './characteristic/RotationSpeed'; @@ -46,35 +47,14 @@ export default class DiffuserAccessory extends BaseAccessory { } configureDiffuser() { - const onSchema = this.getSchema(...SCHEMA_CODE.ON); const sprayOnSchema = this.getSchema(...SCHEMA_CODE.SPRAY_ON)!; // Required Characteristics - const { INACTIVE, ACTIVE } = this.Characteristic.Active; - this.mainService().getCharacteristic(this.Characteristic.Active) - .onGet(() => { - if (onSchema && !this.getStatus(onSchema.code)!.value) { - return INACTIVE; - } + configureActive(this, this.mainService(), sprayOnSchema); - const status = this.getStatus(sprayOnSchema.code)!; - return (status.value as boolean) ? ACTIVE : INACTIVE; - }) - .onSet(value => { - const commands = [{ code: sprayOnSchema.code, value: (value === ACTIVE) }]; - if (onSchema && value === ACTIVE) { - commands.push({ code: onSchema.code, value: true }); - } - this.sendCommands(commands, true); - }); - - const { PURIFYING_AIR } = this.Characteristic.CurrentAirPurifierState; + const { INACTIVE, PURIFYING_AIR } = this.Characteristic.CurrentAirPurifierState; this.mainService().getCharacteristic(this.Characteristic.CurrentAirPurifierState) .onGet(() => { - if (onSchema && this.getStatus(onSchema.code)!.value !== true) { - return INACTIVE; - } - const status = this.getStatus(sprayOnSchema.code)!; return (status.value as boolean) ? PURIFYING_AIR : INACTIVE; }); From 64714d7ef082d90595e114c5275f034801d7b5bb Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 29 Dec 2022 21:18:45 +0800 Subject: [PATCH 6/6] Update CHANGELOG.md --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 099df631..33a9d9fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,10 @@ - Update support for RGB Power Switch (`dj`). - Support showing device online status via `StatusActive`. (#172) - Update unit and range of `RotationSpeed` with level, need clean accessory cache to take effect. (#174) -- Support Door and Window Controller motor reverse mode. +- Support Door and Window Controller motor reverse mode. (#180) +- Support Diffuser RGB light. (#184) +- Support Fan light temperature and color. (#184) +- Support Humidifier light. (#184) ## [1.6.0] - (2022.12.3)