Skip to content

Commit 2abebd3

Browse files
committed
feat: Improves handling of brightness.
This features allows more granular control over how this plugin perceives the brightness that it gets reported from Tuya.
1 parent c41cdbc commit 2abebd3

File tree

9 files changed

+146
-45
lines changed

9 files changed

+146
-45
lines changed

config.schema.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,20 @@
147147
"placeholder": "35.0",
148148
"required": false
149149
},
150+
"min_brightness": {
151+
"title": "Minimal Brightness",
152+
"type": "string",
153+
"pattern": "^-?\\d+$",
154+
"description": "The brightness value that Tuya returns when your light is off.",
155+
"required": false
156+
},
157+
"max_brightness": {
158+
"title": "Maximal Brightness",
159+
"type": "string",
160+
"pattern": "^-?\\d+$",
161+
"description": "The brightness value that Tuya returns when your light is at its brightest.",
162+
"required": false
163+
},
150164
"current_temperature_factor": {
151165
"title": "Temperature Factor",
152166
"type": "string",
@@ -299,6 +313,18 @@
299313
"functionBody": "return (model.defaults[arrayIndices].device_type == 'climate')"
300314
}
301315
},
316+
{
317+
"key": "defaults[].min_brightness",
318+
"condition": {
319+
"functionBody": "return (model.defaults[arrayIndices].device_type == 'light')"
320+
}
321+
},
322+
{
323+
"key": "defaults[].max_brightness",
324+
"condition": {
325+
"functionBody": "return (model.defaults[arrayIndices].device_type == 'light')"
326+
}
327+
},
302328
{
303329
"key": "defaults[].current_temperature_factor",
304330
"condition": {

src/accessories/BaseAccessory.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,12 @@ export abstract class BaseAccessory {
350350
return this.debouncedDeviceStateRequestPromise.promise;
351351
}
352352

353+
/**
354+
* Caches the remote state
355+
* @param method
356+
* @param payload
357+
* @param cache tuya value to store in the cache
358+
*/
353359
public async setDeviceState<Method extends TuyaApiMethod, T>(
354360
method: Method,
355361
payload: TuyaApiPayload<Method>,

src/accessories/characteristics/base.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,31 @@ export abstract class TuyaWebCharacteristic<
6161
this.log(LogLevel.ERROR, message, ...args);
6262
}
6363

64+
/**
65+
* Getter tuya HomeKit;
66+
* Should provide HomeKit compatible data homeKit callback
67+
* @param callback
68+
*/
6469
public getRemoteValue?(callback: CharacteristicGetCallback): void;
6570

71+
/**
72+
* Setter homeKit HomeKit
73+
* Called when value is changed in HomeKit.
74+
* Must update remote value
75+
* Must call callback after completion
76+
* @param homekitValue
77+
* @param callback
78+
*/
6679
public setRemoteValue?(
6780
homekitValue: CharacteristicValue,
6881
callback: CharacteristicSetCallback
6982
): void;
7083

84+
/**
85+
* Updates the cached value for the device.
86+
* @param data
87+
* @param callback
88+
*/
7189
public updateValue?(
7290
data?: Accessory["deviceConfig"]["data"],
7391
callback?: CharacteristicGetCallback

src/accessories/characteristics/brightness.ts

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { inspect } from "util";
88
import { TuyaWebCharacteristic } from "./base";
99
import { BaseAccessory } from "../BaseAccessory";
1010
import { DeviceState } from "../../api/response";
11+
import { MapRange } from "../../helpers/MapRange";
1112

1213
export class BrightnessCharacteristic extends TuyaWebCharacteristic {
1314
public static Title = "Characteristic.Brightness";
@@ -16,8 +17,6 @@ export class BrightnessCharacteristic extends TuyaWebCharacteristic {
1617
return accessory.platform.Characteristic.Brightness;
1718
}
1819

19-
public static DEFAULT_VALUE = 100;
20-
2120
public static isSupportedByAccessory(accessory): boolean {
2221
const configData = accessory.deviceConfig.data;
2322
return (
@@ -26,6 +25,34 @@ export class BrightnessCharacteristic extends TuyaWebCharacteristic {
2625
);
2726
}
2827

28+
public static DEFAULT_VALUE = 100;
29+
30+
public get usesColorBrightness(): boolean {
31+
const deviceData = this.accessory.deviceConfig.data;
32+
return (
33+
deviceData?.color_mode !== undefined &&
34+
deviceData?.color_mode in COLOR_MODES &&
35+
deviceData?.color?.brightness !== undefined
36+
);
37+
}
38+
39+
public get rangeMapper(): MapRange {
40+
let minTuya = 10;
41+
let maxTuya = 100;
42+
if (
43+
this.accessory.deviceConfig.config?.min_brightness !== undefined &&
44+
this.accessory.deviceConfig.config?.max_brightness !== undefined
45+
) {
46+
minTuya = Number(this.accessory.deviceConfig.config?.min_brightness);
47+
maxTuya = Number(this.accessory.deviceConfig.config?.max_brightness);
48+
} else if (this.usesColorBrightness) {
49+
minTuya = 1;
50+
maxTuya = 255;
51+
}
52+
53+
return MapRange.tuya(minTuya, maxTuya).homeKit(0, 100);
54+
}
55+
2956
public getRemoteValue(callback: CharacteristicGetCallback): void {
3057
this.accessory
3158
.getDeviceState()
@@ -40,11 +67,16 @@ export class BrightnessCharacteristic extends TuyaWebCharacteristic {
4067
homekitValue: CharacteristicValue,
4168
callback: CharacteristicSetCallback
4269
): void {
43-
// Set device state in Tuya Web API
44-
const value = ((homekitValue as number) / 10) * 9 + 10;
70+
const value = this.rangeMapper.homekitToTuya(Number(homekitValue));
4571

4672
this.accessory
47-
.setDeviceState("brightnessSet", { value }, { brightness: homekitValue })
73+
.setDeviceState(
74+
"brightnessSet",
75+
{ value },
76+
this.usesColorBrightness
77+
? { color: { brightness: value } }
78+
: { brightness: value }
79+
)
4880
.then(() => {
4981
this.debug("[SET] %s", value);
5082
callback();
@@ -53,26 +85,36 @@ export class BrightnessCharacteristic extends TuyaWebCharacteristic {
5385
}
5486

5587
updateValue(data: DeviceState, callback?: CharacteristicGetCallback): void {
56-
// data.brightness only valid for color_mode != color > https://github.com/PaulAnnekov/tuyaha/blob/master/tuyaha/devices/light.py
57-
// however, according to local tuya app, calculation for color_mode=color is still incorrect (even more so in lower range)
58-
let stateValue: number | undefined;
59-
if (
60-
data?.color_mode !== undefined &&
61-
data?.color_mode in COLOR_MODES &&
62-
data?.color?.brightness !== undefined
63-
) {
64-
stateValue = Number(data.color.brightness);
65-
} else if (data?.brightness) {
66-
stateValue = Math.round((Number(data.brightness) / 255) * 100);
88+
const tuyaValue = Number(
89+
this.usesColorBrightness ? data.color?.brightness : data.brightness
90+
);
91+
const homekitValue = this.rangeMapper.tuyaToHomekit(tuyaValue);
92+
93+
if (homekitValue > 100) {
94+
this.warn(
95+
"Characteristic 'Brightness' will receive value higher than allowed (%s) since provided Tuya value (%s) " +
96+
"exceeds configured maximum Tuya value (%s). Please update your configuration!",
97+
homekitValue,
98+
tuyaValue,
99+
this.rangeMapper.tuyaEnd
100+
);
101+
} else if (homekitValue < 0) {
102+
this.warn(
103+
"Characteristic 'Brightness' will receive value lower than allowed (%s) since provided Tuya value (%s) " +
104+
"is lower than configured minimum Tuya value (%s). Please update your configuration!",
105+
homekitValue,
106+
tuyaValue,
107+
this.rangeMapper.tuyaStart
108+
);
67109
}
68110

69-
if (stateValue) {
111+
if (homekitValue) {
70112
this.accessory.setCharacteristic(
71113
this.homekitCharacteristic,
72-
stateValue,
114+
homekitValue,
73115
!callback
74116
);
75-
callback && callback(null, stateValue);
117+
callback && callback(null, homekitValue);
76118
return;
77119
}
78120

src/accessories/characteristics/colorTemperature.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export class ColorTemperatureCharacteristic extends TuyaWebCharacteristic {
2222
return accessory.deviceConfig.data.color_temp !== undefined;
2323
}
2424

25-
private rangeMapper = MapRange.from(140, 500).to(10000, 1000);
25+
private rangeMapper = MapRange.tuya(140, 500).homeKit(10000, 1000);
2626

2727
public getRemoteValue(callback: CharacteristicGetCallback): void {
2828
this.accessory
@@ -46,7 +46,7 @@ export class ColorTemperatureCharacteristic extends TuyaWebCharacteristic {
4646
}
4747

4848
// Set device state in Tuya Web API
49-
const value = Math.round(this.rangeMapper.map(homekitValue));
49+
const value = Math.round(this.rangeMapper.tuyaToHomekit(homekitValue));
5050

5151
this.accessory
5252
.setDeviceState("colorTemperatureSet", { value }, { color_temp: value })
@@ -60,7 +60,7 @@ export class ColorTemperatureCharacteristic extends TuyaWebCharacteristic {
6060
updateValue(data: DeviceState, callback?: CharacteristicGetCallback): void {
6161
if (data?.color_temp !== undefined) {
6262
const homekitColorTemp = Math.round(
63-
this.rangeMapper.inverseMap(Number(data.color_temp))
63+
this.rangeMapper.homekitToTuya(Number(data.color_temp))
6464
);
6565
this.accessory.setCharacteristic(
6666
this.homekitCharacteristic,

src/accessories/characteristics/rotationSpeed.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ export class RotationSpeedCharacteristic extends TuyaWebCharacteristic {
1818
return accessory.platform.Characteristic.RotationSpeed;
1919
}
2020

21-
public range = MapRange.from(
21+
public range = MapRange.tuya(1, this.maxSpeedLevel).homeKit(
2222
this.minStep,
2323
this.maxSpeedLevel * this.minStep
24-
).to(1, this.maxSpeedLevel);
24+
);
2525

2626
public setProps(char?: Characteristic): Characteristic | undefined {
2727
return char?.setProps({
@@ -64,7 +64,7 @@ export class RotationSpeedCharacteristic extends TuyaWebCharacteristic {
6464
callback: CharacteristicSetCallback
6565
): void {
6666
// Set device state in Tuya Web API
67-
let value = this.range.map(Number(homekitValue));
67+
let value = this.range.homekitToTuya(Number(homekitValue));
6868
// Set value to 1 if value is too small
6969
value = value < 1 ? 1 : value;
7070
// Set value to minSpeedLevel if value is too large
@@ -81,7 +81,7 @@ export class RotationSpeedCharacteristic extends TuyaWebCharacteristic {
8181

8282
updateValue(data: DeviceState, callback?: CharacteristicGetCallback): void {
8383
if (data?.speed !== undefined) {
84-
const speed = this.range.inverseMap(Number(data.speed));
84+
const speed = this.range.tuyaToHomekit(Number(data.speed));
8585
this.accessory.setCharacteristic(
8686
this.homekitCharacteristic,
8787
speed,

src/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export type TuyaDeviceDefaults = {
1313
fan_characteristics: "Speed"[];
1414
light_characteristics: ("Brightness" | "Color" | "Color Temperature")[];
1515
cover_characteristics: "Stop"[];
16+
min_brightness: string | number;
17+
max_brightness: string | number;
1618
};
1719

1820
type Config = {

src/helpers/MapRange.ts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,35 @@
11
export class MapRange {
22
private constructor(
3-
private fromStart,
4-
private fromEnd,
5-
private toStart,
6-
private toEnd
3+
public readonly tuyaStart: number,
4+
public readonly tuyaEnd: number,
5+
public readonly homekitStart: number,
6+
public readonly homekitEnd: number
77
) {}
88

9-
static from(start, end): { to: (start: number, end: number) => MapRange } {
9+
static tuya(
10+
start,
11+
end
12+
): { homeKit: (start: number, end: number) => MapRange } {
1013
return {
11-
to: (toStart, toEnd) => {
14+
homeKit: (toStart, toEnd) => {
1215
return new MapRange(start, end, toStart, toEnd);
1316
},
1417
};
1518
}
1619

17-
public map(input: number): number {
20+
public tuyaToHomekit(tuyaValue: number): number {
1821
return (
19-
((input - this.fromStart) * (this.toEnd - this.toStart)) /
20-
(this.fromEnd - this.fromStart) +
21-
this.toStart
22+
((tuyaValue - this.tuyaStart) * (this.homekitEnd - this.homekitStart)) /
23+
(this.tuyaEnd - this.tuyaStart) +
24+
this.homekitStart
2225
);
2326
}
2427

25-
public inverseMap(input: number): number {
28+
public homekitToTuya(homeKitValue: number): number {
2629
return (
27-
((input - this.toStart) * (this.fromEnd - this.fromStart)) /
28-
(this.toEnd - this.toStart) +
29-
this.fromStart
30+
((homeKitValue - this.homekitStart) * (this.tuyaEnd - this.tuyaStart)) /
31+
(this.homekitEnd - this.homekitStart) +
32+
this.tuyaStart
3033
);
3134
}
3235
}

test/tuyawebapi.spec.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { TuyaWebApi } from "../src/TuyaWebApi";
1+
import { TuyaWebApi } from "../src/api/service";
22

33
import { config } from "./environment";
44
import assert from "assert";
@@ -24,7 +24,11 @@ describe("TuyaWebApi", () => {
2424
.getOrRefreshToken()
2525
.then((session) => {
2626
api.session = session || null;
27-
assert.notEqual(session.accessToken, null, "No valid access token.");
27+
assert.notStrictEqual(
28+
session.accessToken,
29+
null,
30+
"No valid access token."
31+
);
2832
done();
2933
})
3034
.catch((error) => {
@@ -33,7 +37,7 @@ describe("TuyaWebApi", () => {
3337
});
3438

3539
it("should have the area base url set to EU server", (done) => {
36-
assert.equal(
40+
assert.strictEqual(
3741
api.session.areaBaseUrl,
3842
"https://px1.tuyaeu.com",
3943
"Area Base URL is not set."
@@ -47,7 +51,7 @@ describe("TuyaWebApi", () => {
4751
api
4852
.discoverDevices()
4953
.then((devices) => {
50-
assert.notEqual(devices.length, 0, "No devices found");
54+
assert.notStrictEqual(devices.length, 0, "No devices found");
5155
done();
5256
})
5357
.catch((error) => {
@@ -62,7 +66,7 @@ describe("TuyaWebApi", () => {
6266
api
6367
.getDeviceState(deviceId)
6468
.then((data) => {
65-
assert.notEqual(data.state, null, "No device state received");
69+
assert.notStrictEqual(data.state, null, "No device state received");
6670
done();
6771
})
6872
.catch((error) => {

0 commit comments

Comments
 (0)