homebridge-tuya-plus 4.1.0-dev.63 → 4.1.0-dev.64
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 +17 -0
- package/lib/SimpleFanAccessory.js +41 -4
- package/package.json +1 -1
- package/test/SimpleFanAccessory.test.js +101 -0
- package/wiki/Supported-Device-Types.md +10 -1
package/config.schema.json
CHANGED
|
@@ -538,6 +538,23 @@
|
|
|
538
538
|
"functionBody": "return model.devices && model.devices[arrayIndices] && ['Fan', 'FanLight'].includes(model.devices[arrayIndices].type);"
|
|
539
539
|
}
|
|
540
540
|
},
|
|
541
|
+
"dpSwing": {
|
|
542
|
+
"title": "Swing/oscillation DP",
|
|
543
|
+
"description": "Data point of the fan's oscillation switch (a boolean DP). When set, a Swing (oscillation) control is shown in the Home app. Leave empty if the fan has no oscillation.",
|
|
544
|
+
"type": "integer",
|
|
545
|
+
"condition": {
|
|
546
|
+
"functionBody": "return model.devices && model.devices[arrayIndices] && ['Fan'].includes(model.devices[arrayIndices].type);"
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
"noDirection": {
|
|
550
|
+
"title": "Hide the rotation direction control",
|
|
551
|
+
"description": "Enable if the fan has no forward/reverse direction, or if the direction data point is actually a mode enum. Prevents writing forward/reverse into that data point.",
|
|
552
|
+
"type": "boolean",
|
|
553
|
+
"placeholder": false,
|
|
554
|
+
"condition": {
|
|
555
|
+
"functionBody": "return model.devices && model.devices[arrayIndices] && ['Fan'].includes(model.devices[arrayIndices].type);"
|
|
556
|
+
}
|
|
557
|
+
},
|
|
541
558
|
"maxSpeed": {
|
|
542
559
|
"type": "integer",
|
|
543
560
|
"placeholder": "3",
|
|
@@ -22,6 +22,13 @@ class SimpleFanAccessory extends BaseAccessory {
|
|
|
22
22
|
this.dpFanOn = this._getCustomDP(this.device.context.dpFanOn) || '1';
|
|
23
23
|
this.dpRotationSpeed = this._getCustomDP(this.device.context.dpRotationSpeed) || '3';
|
|
24
24
|
this.dpFanDirection = this._getCustomDP(this.device.context.dpFanDirection) || '2';
|
|
25
|
+
// Oscillation is opt-in: SwingMode is only exposed when the fan's boolean
|
|
26
|
+
// swing DP is configured, so existing setups are unchanged (issue #96).
|
|
27
|
+
this.dpSwing = this._getCustomDP(this.device.context.dpSwing);
|
|
28
|
+
// Some fans use DP 2 for a mode enum (nature/sleep/…) rather than a
|
|
29
|
+
// rotation direction; RotationDirection would then write forward/reverse
|
|
30
|
+
// into that enum. noDirection drops the characteristic for those fans.
|
|
31
|
+
this.noDirection = this._coerceBoolean(this.device.context.noDirection, false);
|
|
25
32
|
|
|
26
33
|
this.maxSpeed = parseInt(this.device.context.maxSpeed) || 3;
|
|
27
34
|
this.fanDefaultSpeed = parseInt(this.device.context.fanDefaultSpeed) || 1;
|
|
@@ -44,10 +51,21 @@ class SimpleFanAccessory extends BaseAccessory {
|
|
|
44
51
|
.onGet(() => this.getSpeed())
|
|
45
52
|
.onSet(value => this.setSpeed(value));
|
|
46
53
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
let characteristicFanDirection;
|
|
55
|
+
if (!this.noDirection) {
|
|
56
|
+
characteristicFanDirection = serviceFan.getCharacteristic(Characteristic.RotationDirection)
|
|
57
|
+
.updateValue(this._getFanDirection(dps[this.dpFanDirection]))
|
|
58
|
+
.onGet(() => this.getFanDirection())
|
|
59
|
+
.onSet(value => this.setFanDirection(value));
|
|
60
|
+
} else this._removeCharacteristic(serviceFan, Characteristic.RotationDirection);
|
|
61
|
+
|
|
62
|
+
let characteristicSwingMode;
|
|
63
|
+
if (this.dpSwing) {
|
|
64
|
+
characteristicSwingMode = serviceFan.getCharacteristic(Characteristic.SwingMode)
|
|
65
|
+
.updateValue(this._getSwingMode(dps[this.dpSwing]))
|
|
66
|
+
.onGet(() => this.getSwingMode())
|
|
67
|
+
.onSet(value => this.setSwingMode(value));
|
|
68
|
+
} else this._removeCharacteristic(serviceFan, Characteristic.SwingMode);
|
|
51
69
|
|
|
52
70
|
this.device.on('change', (changes, state) => {
|
|
53
71
|
if (changes.hasOwnProperty(this.dpFanOn) && characteristicFanOn.value !== changes[this.dpFanOn])
|
|
@@ -61,6 +79,11 @@ class SimpleFanAccessory extends BaseAccessory {
|
|
|
61
79
|
if (characteristicFanDirection.value !== dir) characteristicFanDirection.updateValue(dir);
|
|
62
80
|
}
|
|
63
81
|
|
|
82
|
+
if (characteristicSwingMode && changes.hasOwnProperty(this.dpSwing)) {
|
|
83
|
+
const swing = this._getSwingMode(changes[this.dpSwing]);
|
|
84
|
+
if (characteristicSwingMode.value !== swing) characteristicSwingMode.updateValue(swing);
|
|
85
|
+
}
|
|
86
|
+
|
|
64
87
|
this.log.debug('SimpleFan changed: ' + JSON.stringify(state));
|
|
65
88
|
});
|
|
66
89
|
}
|
|
@@ -134,6 +157,20 @@ class SimpleFanAccessory extends BaseAccessory {
|
|
|
134
157
|
return this.setStateAsync(this.dpFanDirection, tuyaVal);
|
|
135
158
|
}
|
|
136
159
|
|
|
160
|
+
getSwingMode() {
|
|
161
|
+
return this._getSwingMode(this.getStateAsync(this.dpSwing));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
_getSwingMode(dp) {
|
|
165
|
+
const {Characteristic} = this.hap;
|
|
166
|
+
return dp ? Characteristic.SwingMode.SWING_ENABLED : Characteristic.SwingMode.SWING_DISABLED;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
setSwingMode(value) {
|
|
170
|
+
const {Characteristic} = this.hap;
|
|
171
|
+
return this.setStateAsync(this.dpSwing, value === Characteristic.SwingMode.SWING_ENABLED);
|
|
172
|
+
}
|
|
173
|
+
|
|
137
174
|
convertRotationSpeedFromTuyaToHomeKit(value) {
|
|
138
175
|
const v = parseInt(value) || 0;
|
|
139
176
|
if (v <= 0) return 0;
|
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.64",
|
|
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": {
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const SimpleFanAccessory = require('../lib/SimpleFanAccessory');
|
|
4
|
+
const { makeInstance, HAP } = require('./support/mocks');
|
|
5
|
+
|
|
6
|
+
const { SwingMode, RotationDirection } = HAP.Characteristic;
|
|
7
|
+
|
|
8
|
+
// The mock Fan service shares a single characteristic, so the write-path tests
|
|
9
|
+
// wire the DP fields manually (mirroring what _registerCharacteristics would do)
|
|
10
|
+
// and assert on the conversion helpers and the DP packets that go out. The
|
|
11
|
+
// registration tests below drive _registerCharacteristics itself.
|
|
12
|
+
function makeFan(state = {}, context = {}) {
|
|
13
|
+
const result = makeInstance(SimpleFanAccessory, state, { type: 'Fan', ...context });
|
|
14
|
+
const { instance } = result;
|
|
15
|
+
|
|
16
|
+
instance.dpFanOn = '1';
|
|
17
|
+
instance.dpRotationSpeed = '3';
|
|
18
|
+
instance.dpFanDirection = '2';
|
|
19
|
+
instance.dpSwing = instance._getCustomDP(context.dpSwing);
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// _getSwingMode — boolean DP maps to HomeKit SwingMode
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
describe('SimpleFanAccessory._getSwingMode', () => {
|
|
27
|
+
test('truthy DP enables swing', () => {
|
|
28
|
+
const { instance } = makeFan();
|
|
29
|
+
expect(instance._getSwingMode(true)).toBe(SwingMode.SWING_ENABLED);
|
|
30
|
+
expect(instance._getSwingMode(1)).toBe(SwingMode.SWING_ENABLED);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('falsy DP disables swing', () => {
|
|
34
|
+
const { instance } = makeFan();
|
|
35
|
+
expect(instance._getSwingMode(false)).toBe(SwingMode.SWING_DISABLED);
|
|
36
|
+
expect(instance._getSwingMode(0)).toBe(SwingMode.SWING_DISABLED);
|
|
37
|
+
expect(instance._getSwingMode(undefined)).toBe(SwingMode.SWING_DISABLED);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// getSwingMode / setSwingMode — read and write the configured swing DP
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
describe('SimpleFanAccessory.getSwingMode / setSwingMode', () => {
|
|
45
|
+
test('getSwingMode reads and converts the swing DP', () => {
|
|
46
|
+
const { instance } = makeFan({ '4': true }, { dpSwing: 4 });
|
|
47
|
+
expect(instance.getSwingMode()).toBe(SwingMode.SWING_ENABLED);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('setSwingMode writes a boolean true to the swing DP', () => {
|
|
51
|
+
const { instance, device } = makeFan({ '4': false }, { dpSwing: 4 });
|
|
52
|
+
instance.setSwingMode(SwingMode.SWING_ENABLED);
|
|
53
|
+
expect(device.update).toHaveBeenCalledWith({ '4': true });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('setSwingMode writes a boolean false to the swing DP', () => {
|
|
57
|
+
const { instance, device } = makeFan({ '4': true }, { dpSwing: 4 });
|
|
58
|
+
instance.setSwingMode(SwingMode.SWING_DISABLED);
|
|
59
|
+
expect(device.update).toHaveBeenCalledWith({ '4': false });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('setSwingMode rejects (No Response) and writes nothing when disconnected', async () => {
|
|
63
|
+
const { instance, device } = makeFan({ '4': false }, { dpSwing: 4 });
|
|
64
|
+
device.connected = false;
|
|
65
|
+
await expect(instance.setSwingMode(SwingMode.SWING_ENABLED)).rejects.toBeInstanceOf(HAP.HapStatusError);
|
|
66
|
+
expect(device.update).not.toHaveBeenCalled();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Registration — SwingMode is opt-in and RotationDirection is opt-out
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
describe('SimpleFanAccessory._registerCharacteristics', () => {
|
|
74
|
+
const register = context => {
|
|
75
|
+
const { instance, accessory } = makeInstance(SimpleFanAccessory, {}, { type: 'Fan', ...context });
|
|
76
|
+
const service = accessory._mockService;
|
|
77
|
+
service.getCharacteristic.mockClear();
|
|
78
|
+
instance._registerCharacteristics(instance.device.state);
|
|
79
|
+
return service;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
test('registers SwingMode when dpSwing is configured', () => {
|
|
83
|
+
const service = register({ dpSwing: 4 });
|
|
84
|
+
expect(service.getCharacteristic).toHaveBeenCalledWith(SwingMode);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('does not register SwingMode when dpSwing is absent (backwards compatible)', () => {
|
|
88
|
+
const service = register({});
|
|
89
|
+
expect(service.getCharacteristic).not.toHaveBeenCalledWith(SwingMode);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('registers RotationDirection by default', () => {
|
|
93
|
+
const service = register({});
|
|
94
|
+
expect(service.getCharacteristic).toHaveBeenCalledWith(RotationDirection);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('drops RotationDirection when noDirection is set', () => {
|
|
98
|
+
const service = register({ noDirection: true });
|
|
99
|
+
expect(service.getCharacteristic).not.toHaveBeenCalledWith(RotationDirection);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -732,7 +732,16 @@ These are accessories that may act as a regulator switch or an inbuilt regulator
|
|
|
732
732
|
"dpRotationSpeed": "2",
|
|
733
733
|
|
|
734
734
|
/* Override the default datapoint identifier of direction control (forward/reverse) */
|
|
735
|
-
"dpRotationDirection": 63
|
|
735
|
+
"dpRotationDirection": 63,
|
|
736
|
+
|
|
737
|
+
/* Datapoint of the oscillation switch (a boolean DP). When set, a Swing
|
|
738
|
+
(oscillation) control is shown in the Home app. Omit if the fan has none. */
|
|
739
|
+
"dpSwing": 4,
|
|
740
|
+
|
|
741
|
+
/* Hide the rotation direction control. Enable for fans that have no
|
|
742
|
+
forward/reverse, or whose direction datapoint is actually a mode enum,
|
|
743
|
+
so forward/reverse is never written into it. */
|
|
744
|
+
"noDirection": true
|
|
736
745
|
}
|
|
737
746
|
```
|
|
738
747
|
|