homebridge-tuya-plus 4.1.0-dev.68 → 4.1.0-dev.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/config.schema.json
CHANGED
|
@@ -531,7 +531,7 @@
|
|
|
531
531
|
"functionBody": "return model.devices && model.devices[arrayIndices] && ['Convector', 'Fan', 'FanLight'].includes(model.devices[arrayIndices].type);"
|
|
532
532
|
}
|
|
533
533
|
},
|
|
534
|
-
"
|
|
534
|
+
"dpFanDirection": {
|
|
535
535
|
"type": "integer",
|
|
536
536
|
"placeholder": "63",
|
|
537
537
|
"condition": {
|
|
@@ -585,6 +585,51 @@
|
|
|
585
585
|
"functionBody": "return model.devices && model.devices[arrayIndices] && ['FanLight'].includes(model.devices[arrayIndices].type);"
|
|
586
586
|
}
|
|
587
587
|
},
|
|
588
|
+
"lightUseEnum": {
|
|
589
|
+
"title": "Light uses On/Off text values",
|
|
590
|
+
"description": "Enable if the light appears in HomeKit but toggling it does nothing. Some fans type the light data point as an enum of text values (e.g. \"On\"/\"Off\") rather than a boolean.",
|
|
591
|
+
"type": "boolean",
|
|
592
|
+
"placeholder": false,
|
|
593
|
+
"condition": {
|
|
594
|
+
"functionBody": "return model.devices && model.devices[arrayIndices] && ['FanLight'].includes(model.devices[arrayIndices].type);"
|
|
595
|
+
}
|
|
596
|
+
},
|
|
597
|
+
"lightOnCommand": {
|
|
598
|
+
"title": "Light \"on\" value",
|
|
599
|
+
"description": "The text value sent to turn the light on when \"Light uses On/Off text values\" is enabled.",
|
|
600
|
+
"type": "string",
|
|
601
|
+
"placeholder": "On",
|
|
602
|
+
"condition": {
|
|
603
|
+
"functionBody": "return model.devices && model.devices[arrayIndices] && ['FanLight'].includes(model.devices[arrayIndices].type) && model.devices[arrayIndices].lightUseEnum;"
|
|
604
|
+
}
|
|
605
|
+
},
|
|
606
|
+
"lightOffCommand": {
|
|
607
|
+
"title": "Light \"off\" value",
|
|
608
|
+
"description": "The text value sent to turn the light off when \"Light uses On/Off text values\" is enabled.",
|
|
609
|
+
"type": "string",
|
|
610
|
+
"placeholder": "Off",
|
|
611
|
+
"condition": {
|
|
612
|
+
"functionBody": "return model.devices && model.devices[arrayIndices] && ['FanLight'].includes(model.devices[arrayIndices].type) && model.devices[arrayIndices].lightUseEnum;"
|
|
613
|
+
}
|
|
614
|
+
},
|
|
615
|
+
"directionForwardCommand": {
|
|
616
|
+
"title": "Forward direction value",
|
|
617
|
+
"description": "The value sent for the normal/forward direction. Set this when direction lives on a shared \"mode\" data point (e.g. Windmode \"Normal\").",
|
|
618
|
+
"type": "string",
|
|
619
|
+
"placeholder": "forward",
|
|
620
|
+
"condition": {
|
|
621
|
+
"functionBody": "return model.devices && model.devices[arrayIndices] && ['FanLight'].includes(model.devices[arrayIndices].type);"
|
|
622
|
+
}
|
|
623
|
+
},
|
|
624
|
+
"directionReverseCommand": {
|
|
625
|
+
"title": "Reverse direction value",
|
|
626
|
+
"description": "The value sent for the reverse direction. Set this when direction lives on a shared \"mode\" data point (e.g. Windmode \"Reverse\").",
|
|
627
|
+
"type": "string",
|
|
628
|
+
"placeholder": "reverse",
|
|
629
|
+
"condition": {
|
|
630
|
+
"functionBody": "return model.devices && model.devices[arrayIndices] && ['FanLight'].includes(model.devices[arrayIndices].type);"
|
|
631
|
+
}
|
|
632
|
+
},
|
|
588
633
|
"dpChildLock": {
|
|
589
634
|
"type": "integer",
|
|
590
635
|
"placeholder": "6",
|
|
@@ -46,6 +46,19 @@ class SimpleFanLightAccessory extends BaseAccessory {
|
|
|
46
46
|
this.useBrightness = this._coerceBoolean(this.device.context.useBrightness, true);
|
|
47
47
|
this.useColorTemp = this._coerceBoolean(this.device.context.useColorTemp, true);
|
|
48
48
|
|
|
49
|
+
// Some fans (e.g. the Orient AEON V.C.) type the light DP as an enum of
|
|
50
|
+
// 'On'/'Off' strings rather than a boolean. When lightUseEnum is set we
|
|
51
|
+
// read/write those command strings; otherwise the DP stays a plain bool.
|
|
52
|
+
this.lightUseEnum = this._coerceBoolean(this.device.context.lightUseEnum, false);
|
|
53
|
+
this.lightOnCommand = this.device.context.lightOnCommand || 'On';
|
|
54
|
+
this.lightOffCommand = this.device.context.lightOffCommand || 'Off';
|
|
55
|
+
|
|
56
|
+
// The direction DP's forward/reverse command strings are configurable so
|
|
57
|
+
// fans that carry direction on a shared "mode" DP (e.g. Windmode
|
|
58
|
+
// 'Normal'/'Reverse') can be mapped onto HomeKit's RotationDirection.
|
|
59
|
+
this.directionForwardCommand = this.device.context.directionForwardCommand || 'forward';
|
|
60
|
+
this.directionReverseCommand = this.device.context.directionReverseCommand || 'reverse';
|
|
61
|
+
|
|
49
62
|
let minWhiteColor = parseInt(this.device.context.minWhiteColor);
|
|
50
63
|
let maxWhiteColor = parseInt(this.device.context.maxWhiteColor);
|
|
51
64
|
if (isNaN(minWhiteColor)) minWhiteColor = DEFAULT_MIN_WHITE_COLOR;
|
|
@@ -126,7 +139,8 @@ class SimpleFanLightAccessory extends BaseAccessory {
|
|
|
126
139
|
}
|
|
127
140
|
|
|
128
141
|
if (changes.hasOwnProperty(this.dpLightOn) && characteristicLightOn) {
|
|
129
|
-
|
|
142
|
+
const lightOn = this._getLightOn(changes[this.dpLightOn]);
|
|
143
|
+
if (characteristicLightOn.value !== lightOn) characteristicLightOn.updateValue(lightOn);
|
|
130
144
|
}
|
|
131
145
|
|
|
132
146
|
if (changes.hasOwnProperty(this.dpBrightness) && characteristicBrightness) {
|
|
@@ -204,13 +218,13 @@ class SimpleFanLightAccessory extends BaseAccessory {
|
|
|
204
218
|
|
|
205
219
|
_getFanDirection(dp) {
|
|
206
220
|
const {Characteristic} = this.hap;
|
|
207
|
-
return dp ===
|
|
221
|
+
return dp === this.directionReverseCommand
|
|
208
222
|
? Characteristic.RotationDirection.COUNTER_CLOCKWISE
|
|
209
223
|
: Characteristic.RotationDirection.CLOCKWISE;
|
|
210
224
|
}
|
|
211
225
|
|
|
212
226
|
setFanDirection(value) {
|
|
213
|
-
const tuyaVal = (value === 1) ?
|
|
227
|
+
const tuyaVal = (value === 1) ? this.directionReverseCommand : this.directionForwardCommand;
|
|
214
228
|
return this.setStateAsync(this.dpFanDirection, tuyaVal);
|
|
215
229
|
}
|
|
216
230
|
|
|
@@ -219,10 +233,14 @@ class SimpleFanLightAccessory extends BaseAccessory {
|
|
|
219
233
|
}
|
|
220
234
|
|
|
221
235
|
_getLightOn(dp) {
|
|
236
|
+
if (this.lightUseEnum) return dp === this.lightOnCommand;
|
|
222
237
|
return !!dp;
|
|
223
238
|
}
|
|
224
239
|
|
|
225
240
|
setLightOn(value) {
|
|
241
|
+
if (this.lightUseEnum) {
|
|
242
|
+
return this.setStateAsync(this.dpLightOn, value ? this.lightOnCommand : this.lightOffCommand);
|
|
243
|
+
}
|
|
226
244
|
return this.setStateAsync(this.dpLightOn, value);
|
|
227
245
|
}
|
|
228
246
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-tuya-plus",
|
|
3
|
-
"version": "4.1.0-dev.
|
|
3
|
+
"version": "4.1.0-dev.69",
|
|
4
4
|
"description": "A community-maintained Homebridge plugin for controlling Tuya devices locally over LAN, with an optional Tuya Cloud fallback for whatever the LAN can't reach. Includes new features, fixes, and updated device support. PRs welcome: if it runs, it ships (almost).",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -14,13 +14,19 @@ function makeFanLight(state = {}, context = {}) {
|
|
|
14
14
|
|
|
15
15
|
instance.dpFanOn = '60';
|
|
16
16
|
instance.dpRotationSpeed = '62';
|
|
17
|
-
instance.dpFanDirection = '63';
|
|
17
|
+
instance.dpFanDirection = device.context.dpFanDirection || '63';
|
|
18
|
+
instance.dpLightOn = device.context.dpLightOn || '20';
|
|
18
19
|
instance.dpColorTemp = '23';
|
|
19
20
|
instance.maxSpeed = parseInt(device.context.maxSpeed) || 6;
|
|
20
21
|
instance.fanDefaultSpeed = parseInt(device.context.fanDefaultSpeed) || 1;
|
|
21
22
|
instance.fanCurrentSpeed = 0;
|
|
22
23
|
instance.useStrings = instance._coerceBoolean(device.context.useStrings, true);
|
|
23
24
|
instance.singleDpWrites = instance._coerceBoolean(device.context.singleDpWrites, false);
|
|
25
|
+
instance.lightUseEnum = instance._coerceBoolean(device.context.lightUseEnum, false);
|
|
26
|
+
instance.lightOnCommand = device.context.lightOnCommand || 'On';
|
|
27
|
+
instance.lightOffCommand = device.context.lightOffCommand || 'Off';
|
|
28
|
+
instance.directionForwardCommand = device.context.directionForwardCommand || 'forward';
|
|
29
|
+
instance.directionReverseCommand = device.context.directionReverseCommand || 'reverse';
|
|
24
30
|
if (device.context.minWhiteColor !== undefined) instance.minWhiteColor = parseInt(device.context.minWhiteColor);
|
|
25
31
|
if (device.context.maxWhiteColor !== undefined) instance.maxWhiteColor = parseInt(device.context.maxWhiteColor);
|
|
26
32
|
|
|
@@ -164,6 +170,119 @@ describe('SimpleFanLightAccessory — setFanOn(false)', () => {
|
|
|
164
170
|
});
|
|
165
171
|
});
|
|
166
172
|
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// Enum light on/off: fans whose Light DP is an 'On'/'Off' enum rather than a
|
|
175
|
+
// boolean (e.g. Orient AEON V.C.). Default stays boolean for compatibility.
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
describe('SimpleFanLightAccessory — enum light (lightUseEnum)', () => {
|
|
178
|
+
test('reads the enum command strings, not truthiness', () => {
|
|
179
|
+
const { instance } = makeFanLight(
|
|
180
|
+
{ '20': 'Off' },
|
|
181
|
+
{ lightUseEnum: true }
|
|
182
|
+
);
|
|
183
|
+
// The boolean path would read the non-empty string 'Off' as ON (the bug).
|
|
184
|
+
expect(instance.getLightOn()).toBe(false);
|
|
185
|
+
instance.device.state['20'] = 'On';
|
|
186
|
+
expect(instance.getLightOn()).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('setLightOn writes the On/Off command strings', () => {
|
|
190
|
+
const { instance, device } = makeFanLight(
|
|
191
|
+
{ '20': 'Off' },
|
|
192
|
+
{ lightUseEnum: true }
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
instance.setLightOn(true);
|
|
196
|
+
expect(device.update).toHaveBeenCalledWith({ '20': 'On' });
|
|
197
|
+
|
|
198
|
+
device.state['20'] = 'On';
|
|
199
|
+
instance.setLightOn(false);
|
|
200
|
+
expect(device.update).toHaveBeenCalledWith({ '20': 'Off' });
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('honours custom on/off command strings', () => {
|
|
204
|
+
const { instance, device } = makeFanLight(
|
|
205
|
+
{ '101': 'off' },
|
|
206
|
+
{ dpLightOn: '101', lightUseEnum: true, lightOnCommand: 'on', lightOffCommand: 'off' }
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
expect(instance.getLightOn()).toBe(false);
|
|
210
|
+
instance.setLightOn(true);
|
|
211
|
+
expect(device.update).toHaveBeenCalledWith({ '101': 'on' });
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('default (flag off) keeps the boolean behaviour', () => {
|
|
215
|
+
const { instance, device } = makeFanLight({ '20': false });
|
|
216
|
+
|
|
217
|
+
expect(instance._getLightOn(true)).toBe(true);
|
|
218
|
+
expect(instance._getLightOn(false)).toBe(false);
|
|
219
|
+
|
|
220
|
+
instance.setLightOn(true);
|
|
221
|
+
expect(device.update).toHaveBeenCalledWith({ '20': true });
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// Configurable direction commands: map a shared "mode" DP (Windmode
|
|
227
|
+
// Normal/Reverse) onto HomeKit's RotationDirection toggle.
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
describe('SimpleFanLightAccessory — configurable direction commands', () => {
|
|
230
|
+
const { RotationDirection } = HAP.Characteristic;
|
|
231
|
+
|
|
232
|
+
test('reads the reverse command as COUNTER_CLOCKWISE', () => {
|
|
233
|
+
const { instance } = makeFanLight(
|
|
234
|
+
{ '3': 'Reverse' },
|
|
235
|
+
{ dpFanDirection: '3', directionForwardCommand: 'Normal', directionReverseCommand: 'Reverse' }
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
expect(instance.getFanDirection()).toBe(RotationDirection.COUNTER_CLOCKWISE);
|
|
239
|
+
instance.device.state['3'] = 'Normal';
|
|
240
|
+
expect(instance.getFanDirection()).toBe(RotationDirection.CLOCKWISE);
|
|
241
|
+
// Unrelated Windmode values (e.g. Sleep set from the Tuya app) read as forward.
|
|
242
|
+
instance.device.state['3'] = 'Sleep';
|
|
243
|
+
expect(instance.getFanDirection()).toBe(RotationDirection.CLOCKWISE);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('setFanDirection writes the configured command strings', () => {
|
|
247
|
+
const { instance, device } = makeFanLight(
|
|
248
|
+
{ '3': 'Normal' },
|
|
249
|
+
{ dpFanDirection: '3', directionForwardCommand: 'Normal', directionReverseCommand: 'Reverse' }
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
instance.setFanDirection(RotationDirection.COUNTER_CLOCKWISE);
|
|
253
|
+
expect(device.update).toHaveBeenCalledWith({ '3': 'Reverse' });
|
|
254
|
+
|
|
255
|
+
device.state['3'] = 'Reverse';
|
|
256
|
+
instance.setFanDirection(RotationDirection.CLOCKWISE);
|
|
257
|
+
expect(device.update).toHaveBeenCalledWith({ '3': 'Normal' });
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('defaults remain forward/reverse when unconfigured', () => {
|
|
261
|
+
const { instance, device } = makeFanLight({ '63': 'forward' });
|
|
262
|
+
|
|
263
|
+
expect(instance._getFanDirection('reverse')).toBe(RotationDirection.COUNTER_CLOCKWISE);
|
|
264
|
+
instance.setFanDirection(RotationDirection.COUNTER_CLOCKWISE);
|
|
265
|
+
expect(device.update).toHaveBeenCalledWith({ '63': 'reverse' });
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
// Direction DP resolution: dpFanDirection overrides the default DP.
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
describe('SimpleFanLightAccessory — direction DP resolution', () => {
|
|
273
|
+
test('honours the dpFanDirection config key', () => {
|
|
274
|
+
const { instance } = makeInstance(SimpleFanLightAccessory, {}, { type: 'FanLight', dpFanDirection: 3 });
|
|
275
|
+
instance._registerCharacteristics({});
|
|
276
|
+
expect(instance.dpFanDirection).toBe('3');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test('defaults to DP 63 when unset', () => {
|
|
280
|
+
const { instance } = makeInstance(SimpleFanLightAccessory, {}, { type: 'FanLight' });
|
|
281
|
+
instance._registerCharacteristics({});
|
|
282
|
+
expect(instance.dpFanDirection).toBe('63');
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
167
286
|
// ---------------------------------------------------------------------------
|
|
168
287
|
// convertColorTempFromTuyaToHomeKit — default range, inverted (issue #44)
|
|
169
288
|
// ---------------------------------------------------------------------------
|
|
@@ -820,6 +820,9 @@ Every option below is optional — the defaults match a common 3-speed fan. The
|
|
|
820
820
|
Default: 3 */
|
|
821
821
|
"maxSpeed": 3,
|
|
822
822
|
|
|
823
|
+
/* Override the default datapoint identifier of direction control (forward/reverse) */
|
|
824
|
+
"dpFanDirection": 63,
|
|
825
|
+
|
|
823
826
|
/* Speed to use when the fan is switched on from off. Default: 1 */
|
|
824
827
|
"fanDefaultSpeed": 1,
|
|
825
828
|
|
|
@@ -926,6 +929,48 @@ If the light, brightness and turning the fan **off** all work, but turning the f
|
|
|
926
929
|
}
|
|
927
930
|
```
|
|
928
931
|
|
|
932
|
+
#### Lights that use `On`/`Off` text values instead of a boolean
|
|
933
|
+
|
|
934
|
+
Some fans (e.g. the **Orient AEON V.C.**) type their light data point as an *enum of text values* — it sends `"On"` / `"Off"` strings rather than a boolean. With such a fan the light appears in HomeKit but toggling it does nothing, because the plugin's default light handling expects a boolean. Set `"lightUseEnum": true` to make the plugin read and write the text values instead. The command strings default to `"On"` / `"Off"` and can be overridden with `lightOnCommand` / `lightOffCommand`.
|
|
935
|
+
|
|
936
|
+
#### Direction carried on a shared "mode" data point
|
|
937
|
+
|
|
938
|
+
On some fans, forward/reverse is not its own data point — it is one of several values of a shared *mode* data point (e.g. a `Windmode` enum with values like `Normal`, `Reverse`, `Sleep`, `Breeze`). Point `dpFanDirection` at that data point and set `directionForwardCommand` / `directionReverseCommand` to the values it uses (they default to `"forward"` / `"reverse"`). HomeKit's clockwise/counter-clockwise direction toggle then maps onto `Normal` / `Reverse`. Any other mode value (`Sleep`, `Breeze`, …) set from the Tuya app simply reads back as the forward direction.
|
|
939
|
+
|
|
940
|
+
> Note: because direction shares the mode data point, selecting a normal speed writes `Normal` to it, so the fan can only be in one mode at a time — this mirrors how the physical fan works. Modes that HomeKit has no control for (Turbo, Sleep, Breeze) and the countdown timer are not exposed; set them from the Tuya / Smart Life app.
|
|
941
|
+
|
|
942
|
+
A full example for the Orient AEON V.C. (Power = DP 1, WindSpeed enum `"1"`–`"5"` = DP 2, Windmode = DP 3, Light enum `"On"`/`"Off"` = DP 101):
|
|
943
|
+
|
|
944
|
+
```json5
|
|
945
|
+
{
|
|
946
|
+
"type": "FanLight",
|
|
947
|
+
"name": "Orient AEON Fan",
|
|
948
|
+
"id": "032000123456789abcde",
|
|
949
|
+
"key": "0123456789abcdef",
|
|
950
|
+
|
|
951
|
+
/* Power (boolean) */
|
|
952
|
+
"dpFanOn": 1,
|
|
953
|
+
|
|
954
|
+
/* WindSpeed enum "1".."5" — parsed as levels 1..5 and sent as strings */
|
|
955
|
+
"dpRotationSpeed": 2,
|
|
956
|
+
"maxSpeed": 5,
|
|
957
|
+
"useStrings": true,
|
|
958
|
+
|
|
959
|
+
/* Light is an "On"/"Off" enum, not a boolean; no brightness/colour temp */
|
|
960
|
+
"dpLightOn": 101,
|
|
961
|
+
"lightUseEnum": true,
|
|
962
|
+
"useBrightness": false,
|
|
963
|
+
"useColorTemp": false,
|
|
964
|
+
|
|
965
|
+
/* Direction lives on the Windmode data point as Normal/Reverse */
|
|
966
|
+
"dpFanDirection": 3,
|
|
967
|
+
"directionForwardCommand": "Normal",
|
|
968
|
+
"directionReverseCommand": "Reverse"
|
|
969
|
+
}
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
> On this fan, reverse airflow only runs at the lower speeds — that limit is enforced by the fan's own firmware, not the plugin, so the HomeKit speed slider still shows the full range.
|
|
973
|
+
|
|
929
974
|
### Irrigation Systems / Sprinklers
|
|
930
975
|
|
|
931
976
|
Multi-valve Tuya irrigation/sprinkler controllers (the battery-powered Wi-Fi "faucet timers" that expose several `switch_*` valves and a `battery_percentage`) are exposed as a single, fully-fledged HomeKit **Irrigation System** accessory:
|