Skip to content

Commit

Permalink
feat: Improves handling of brightness.
Browse files Browse the repository at this point in the history
This features allows more granular control over how this plugin perceives the brightness that it gets reported from Tuya.
  • Loading branch information
milo526 committed May 18, 2021
1 parent c41cdbc commit 2abebd3
Show file tree
Hide file tree
Showing 9 changed files with 146 additions and 45 deletions.
26 changes: 26 additions & 0 deletions config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,20 @@
"placeholder": "35.0",
"required": false
},
"min_brightness": {
"title": "Minimal Brightness",
"type": "string",
"pattern": "^-?\\d+$",
"description": "The brightness value that Tuya returns when your light is off.",
"required": false
},
"max_brightness": {
"title": "Maximal Brightness",
"type": "string",
"pattern": "^-?\\d+$",
"description": "The brightness value that Tuya returns when your light is at its brightest.",
"required": false
},
"current_temperature_factor": {
"title": "Temperature Factor",
"type": "string",
Expand Down Expand Up @@ -299,6 +313,18 @@
"functionBody": "return (model.defaults[arrayIndices].device_type == 'climate')"
}
},
{
"key": "defaults[].min_brightness",
"condition": {
"functionBody": "return (model.defaults[arrayIndices].device_type == 'light')"
}
},
{
"key": "defaults[].max_brightness",
"condition": {
"functionBody": "return (model.defaults[arrayIndices].device_type == 'light')"
}
},
{
"key": "defaults[].current_temperature_factor",
"condition": {
Expand Down
6 changes: 6 additions & 0 deletions src/accessories/BaseAccessory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,12 @@ export abstract class BaseAccessory {
return this.debouncedDeviceStateRequestPromise.promise;
}

/**
* Caches the remote state
* @param method
* @param payload
* @param cache tuya value to store in the cache
*/
public async setDeviceState<Method extends TuyaApiMethod, T>(
method: Method,
payload: TuyaApiPayload<Method>,
Expand Down
18 changes: 18 additions & 0 deletions src/accessories/characteristics/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,31 @@ export abstract class TuyaWebCharacteristic<
this.log(LogLevel.ERROR, message, ...args);
}

/**
* Getter tuya HomeKit;
* Should provide HomeKit compatible data homeKit callback
* @param callback
*/
public getRemoteValue?(callback: CharacteristicGetCallback): void;

/**
* Setter homeKit HomeKit
* Called when value is changed in HomeKit.
* Must update remote value
* Must call callback after completion
* @param homekitValue
* @param callback
*/
public setRemoteValue?(
homekitValue: CharacteristicValue,
callback: CharacteristicSetCallback
): void;

/**
* Updates the cached value for the device.
* @param data
* @param callback
*/
public updateValue?(
data?: Accessory["deviceConfig"]["data"],
callback?: CharacteristicGetCallback
Expand Down
80 changes: 61 additions & 19 deletions src/accessories/characteristics/brightness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { inspect } from "util";
import { TuyaWebCharacteristic } from "./base";
import { BaseAccessory } from "../BaseAccessory";
import { DeviceState } from "../../api/response";
import { MapRange } from "../../helpers/MapRange";

export class BrightnessCharacteristic extends TuyaWebCharacteristic {
public static Title = "Characteristic.Brightness";
Expand All @@ -16,8 +17,6 @@ export class BrightnessCharacteristic extends TuyaWebCharacteristic {
return accessory.platform.Characteristic.Brightness;
}

public static DEFAULT_VALUE = 100;

public static isSupportedByAccessory(accessory): boolean {
const configData = accessory.deviceConfig.data;
return (
Expand All @@ -26,6 +25,34 @@ export class BrightnessCharacteristic extends TuyaWebCharacteristic {
);
}

public static DEFAULT_VALUE = 100;

public get usesColorBrightness(): boolean {
const deviceData = this.accessory.deviceConfig.data;
return (
deviceData?.color_mode !== undefined &&
deviceData?.color_mode in COLOR_MODES &&
deviceData?.color?.brightness !== undefined
);
}

public get rangeMapper(): MapRange {
let minTuya = 10;
let maxTuya = 100;
if (
this.accessory.deviceConfig.config?.min_brightness !== undefined &&
this.accessory.deviceConfig.config?.max_brightness !== undefined
) {
minTuya = Number(this.accessory.deviceConfig.config?.min_brightness);
maxTuya = Number(this.accessory.deviceConfig.config?.max_brightness);
} else if (this.usesColorBrightness) {
minTuya = 1;
maxTuya = 255;
}

return MapRange.tuya(minTuya, maxTuya).homeKit(0, 100);
}

public getRemoteValue(callback: CharacteristicGetCallback): void {
this.accessory
.getDeviceState()
Expand All @@ -40,11 +67,16 @@ export class BrightnessCharacteristic extends TuyaWebCharacteristic {
homekitValue: CharacteristicValue,
callback: CharacteristicSetCallback
): void {
// Set device state in Tuya Web API
const value = ((homekitValue as number) / 10) * 9 + 10;
const value = this.rangeMapper.homekitToTuya(Number(homekitValue));

this.accessory
.setDeviceState("brightnessSet", { value }, { brightness: homekitValue })
.setDeviceState(
"brightnessSet",
{ value },
this.usesColorBrightness
? { color: { brightness: value } }
: { brightness: value }
)
.then(() => {
this.debug("[SET] %s", value);
callback();
Expand All @@ -53,26 +85,36 @@ export class BrightnessCharacteristic extends TuyaWebCharacteristic {
}

updateValue(data: DeviceState, callback?: CharacteristicGetCallback): void {
// data.brightness only valid for color_mode != color > https://github.com/PaulAnnekov/tuyaha/blob/master/tuyaha/devices/light.py
// however, according to local tuya app, calculation for color_mode=color is still incorrect (even more so in lower range)
let stateValue: number | undefined;
if (
data?.color_mode !== undefined &&
data?.color_mode in COLOR_MODES &&
data?.color?.brightness !== undefined
) {
stateValue = Number(data.color.brightness);
} else if (data?.brightness) {
stateValue = Math.round((Number(data.brightness) / 255) * 100);
const tuyaValue = Number(
this.usesColorBrightness ? data.color?.brightness : data.brightness
);
const homekitValue = this.rangeMapper.tuyaToHomekit(tuyaValue);

if (homekitValue > 100) {
this.warn(
"Characteristic 'Brightness' will receive value higher than allowed (%s) since provided Tuya value (%s) " +
"exceeds configured maximum Tuya value (%s). Please update your configuration!",
homekitValue,
tuyaValue,
this.rangeMapper.tuyaEnd
);
} else if (homekitValue < 0) {
this.warn(
"Characteristic 'Brightness' will receive value lower than allowed (%s) since provided Tuya value (%s) " +
"is lower than configured minimum Tuya value (%s). Please update your configuration!",
homekitValue,
tuyaValue,
this.rangeMapper.tuyaStart
);
}

if (stateValue) {
if (homekitValue) {
this.accessory.setCharacteristic(
this.homekitCharacteristic,
stateValue,
homekitValue,
!callback
);
callback && callback(null, stateValue);
callback && callback(null, homekitValue);
return;
}

Expand Down
6 changes: 3 additions & 3 deletions src/accessories/characteristics/colorTemperature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export class ColorTemperatureCharacteristic extends TuyaWebCharacteristic {
return accessory.deviceConfig.data.color_temp !== undefined;
}

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

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

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

this.accessory
.setDeviceState("colorTemperatureSet", { value }, { color_temp: value })
Expand All @@ -60,7 +60,7 @@ export class ColorTemperatureCharacteristic extends TuyaWebCharacteristic {
updateValue(data: DeviceState, callback?: CharacteristicGetCallback): void {
if (data?.color_temp !== undefined) {
const homekitColorTemp = Math.round(
this.rangeMapper.inverseMap(Number(data.color_temp))
this.rangeMapper.homekitToTuya(Number(data.color_temp))
);
this.accessory.setCharacteristic(
this.homekitCharacteristic,
Expand Down
8 changes: 4 additions & 4 deletions src/accessories/characteristics/rotationSpeed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ export class RotationSpeedCharacteristic extends TuyaWebCharacteristic {
return accessory.platform.Characteristic.RotationSpeed;
}

public range = MapRange.from(
public range = MapRange.tuya(1, this.maxSpeedLevel).homeKit(
this.minStep,
this.maxSpeedLevel * this.minStep
).to(1, this.maxSpeedLevel);
);

public setProps(char?: Characteristic): Characteristic | undefined {
return char?.setProps({
Expand Down Expand Up @@ -64,7 +64,7 @@ export class RotationSpeedCharacteristic extends TuyaWebCharacteristic {
callback: CharacteristicSetCallback
): void {
// Set device state in Tuya Web API
let value = this.range.map(Number(homekitValue));
let value = this.range.homekitToTuya(Number(homekitValue));
// Set value to 1 if value is too small
value = value < 1 ? 1 : value;
// Set value to minSpeedLevel if value is too large
Expand All @@ -81,7 +81,7 @@ export class RotationSpeedCharacteristic extends TuyaWebCharacteristic {

updateValue(data: DeviceState, callback?: CharacteristicGetCallback): void {
if (data?.speed !== undefined) {
const speed = this.range.inverseMap(Number(data.speed));
const speed = this.range.tuyaToHomekit(Number(data.speed));
this.accessory.setCharacteristic(
this.homekitCharacteristic,
speed,
Expand Down
2 changes: 2 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export type TuyaDeviceDefaults = {
fan_characteristics: "Speed"[];
light_characteristics: ("Brightness" | "Color" | "Color Temperature")[];
cover_characteristics: "Stop"[];
min_brightness: string | number;
max_brightness: string | number;
};

type Config = {
Expand Down
31 changes: 17 additions & 14 deletions src/helpers/MapRange.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,35 @@
export class MapRange {
private constructor(
private fromStart,
private fromEnd,
private toStart,
private toEnd
public readonly tuyaStart: number,
public readonly tuyaEnd: number,
public readonly homekitStart: number,
public readonly homekitEnd: number
) {}

static from(start, end): { to: (start: number, end: number) => MapRange } {
static tuya(
start,
end
): { homeKit: (start: number, end: number) => MapRange } {
return {
to: (toStart, toEnd) => {
homeKit: (toStart, toEnd) => {
return new MapRange(start, end, toStart, toEnd);
},
};
}

public map(input: number): number {
public tuyaToHomekit(tuyaValue: number): number {
return (
((input - this.fromStart) * (this.toEnd - this.toStart)) /
(this.fromEnd - this.fromStart) +
this.toStart
((tuyaValue - this.tuyaStart) * (this.homekitEnd - this.homekitStart)) /
(this.tuyaEnd - this.tuyaStart) +
this.homekitStart
);
}

public inverseMap(input: number): number {
public homekitToTuya(homeKitValue: number): number {
return (
((input - this.toStart) * (this.fromEnd - this.fromStart)) /
(this.toEnd - this.toStart) +
this.fromStart
((homeKitValue - this.homekitStart) * (this.tuyaEnd - this.tuyaStart)) /
(this.homekitEnd - this.homekitStart) +
this.tuyaStart
);
}
}
14 changes: 9 additions & 5 deletions test/tuyawebapi.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TuyaWebApi } from "../src/TuyaWebApi";
import { TuyaWebApi } from "../src/api/service";

import { config } from "./environment";
import assert from "assert";
Expand All @@ -24,7 +24,11 @@ describe("TuyaWebApi", () => {
.getOrRefreshToken()
.then((session) => {
api.session = session || null;
assert.notEqual(session.accessToken, null, "No valid access token.");
assert.notStrictEqual(
session.accessToken,
null,
"No valid access token."
);
done();
})
.catch((error) => {
Expand All @@ -33,7 +37,7 @@ describe("TuyaWebApi", () => {
});

it("should have the area base url set to EU server", (done) => {
assert.equal(
assert.strictEqual(
api.session.areaBaseUrl,
"https://px1.tuyaeu.com",
"Area Base URL is not set."
Expand All @@ -47,7 +51,7 @@ describe("TuyaWebApi", () => {
api
.discoverDevices()
.then((devices) => {
assert.notEqual(devices.length, 0, "No devices found");
assert.notStrictEqual(devices.length, 0, "No devices found");
done();
})
.catch((error) => {
Expand All @@ -62,7 +66,7 @@ describe("TuyaWebApi", () => {
api
.getDeviceState(deviceId)
.then((data) => {
assert.notEqual(data.state, null, "No device state received");
assert.notStrictEqual(data.state, null, "No device state received");
done();
})
.catch((error) => {
Expand Down

0 comments on commit 2abebd3

Please sign in to comment.