homebridge-tuya-plus 3.14.0-dev.16 → 3.14.0-dev.18
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/Changelog.md +5 -0
- package/lib/BaseAccessory.js +40 -13
- package/lib/ConvectorAccessory.js +1 -1
- package/lib/CustomMultiOutletAccessory.js +2 -1
- package/lib/MultiOutletAccessory.js +2 -1
- package/lib/OilDiffuserAccessory.js +10 -8
- package/lib/RGBTWLightAccessory.js +13 -11
- package/lib/RGBTWOutletAccessory.js +11 -9
- package/lib/SimpleBlindsAccessory.js +5 -5
- package/lib/SimpleDimmer2Accessory.js +1 -1
- package/lib/SimpleDimmerAccessory.js +1 -1
- package/lib/SimpleGarageDoorAccessory.js +22 -9
- package/lib/SimpleHeaterAccessory.js +2 -2
- package/lib/SwitchAccessory.js +2 -1
- package/lib/TWLightAccessory.js +2 -2
- package/lib/TuyaCloudDevice.js +15 -5
- package/lib/ValveAccessory.js +9 -5
- package/lib/VerticalBlindsWithTilt.js +5 -3
- package/lib/WledDimmerAccessory.js +3 -1
- package/package.json +1 -1
- package/test/BaseAccessory.test.js +83 -10
- package/test/MultiOutletAccessory.test.js +12 -0
- package/test/RGBTWLightAccessory.test.js +3 -3
- package/test/SimpleFanLightAccessory.test.js +3 -3
- package/test/SimpleGarageDoorAccessory.test.js +4 -2
- package/test/TuyaCloudDevice.test.js +22 -1
package/Changelog.md
CHANGED
|
@@ -16,6 +16,11 @@ All notable changes to this project will be documented in this file. This projec
|
|
|
16
16
|
* [*] **IrrigationSystem: stop the valve toggle flickering after a press** — tapping a zone briefly snapped back to the old state before settling on the new one. The `Active` getters returned the raw `device.state`, which (for cloud devices) only advances once the realtime stream echoes the write back, so a read in that window reported the pre-press value. The getters now report the value HomeKit already shows (optimistic on press, then confirmed/corrected by device-side change events); they still surface "No Response" while disconnected.
|
|
17
17
|
* [*] **Tuya Cloud: report real device online/offline status.** Cloud devices previously always showed as reachable. They now mirror Tuya's `online` flag (read from the device record on connect and re-checked when the realtime stream reconnects), so HomeKit shows **"No Response"** when the device is genuinely offline. If the lookup isn't permitted (the project lacks the device-management API), the device is assumed reachable so control is never blocked.
|
|
18
18
|
* [*] **Signal unreachable devices with `HapStatusError`.** The shared getters (`getStateAsync` / `getDividedStateAsync`, used by nearly every accessory's read handlers, plus the irrigation `Active` getter) threw a plain `Error` to make HomeKit show "No Response". Newer Homebridge logs a characteristic warning for that before falling back to a generic status; they now throw `HapStatusError(SERVICE_COMMUNICATION_FAILURE)` directly — same "No Response", no warning spam. No behaviour change for users.
|
|
19
|
+
* [*] **Surface write and connection failures to HomeKit, universally across device types.** Previously a command sent to an unreachable device was silently dropped while HomeKit still reported the tap as successful, and several accessories kept showing their last-known state instead of going "No Response" — the clearest example was a LAN gate that logged `skipping write, device not connected` yet still appeared online and "accepted" open/close taps. Now:
|
|
20
|
+
* **Writes** to a disconnected device (or a write the device/cloud rejects) fail the HomeKit operation instead of pretending success. The shared write helpers (`setState` / `setMultiState` / `setMultiStateLegacy` and their `*Async` variants) now signal a communication failure, so accessories whose set handlers swallowed it (garage door, switches/outlets, RGB hue + saturation, blinds, vertical-tilt blinds) report it on a tap.
|
|
21
|
+
* **Reads** that returned a cached/optimistic or constant value (brightness, colour, hue/saturation, blind position/state, garage-door state, valve in-use/duration, heater state, …) now report **"No Response"** when the device is unreachable — matching the read handlers that already did.
|
|
22
|
+
* **Cloud** commands that fail over HTTP (a network error, or a command Tuya rejects) are surfaced too: `TuyaCloudDevice.update()` now resolves to the real command result so the write helper can await it, rather than firing and forgetting.
|
|
23
|
+
* Internal/background writes (auto-shutoff timers, multi-step open/close sequences, debounced batches) stay non-fatal via dedicated non-throwing helpers (`setStateInBackground` / `setMultiStateInBackground` / `setMultiStateLegacyInBackground`), so an unreachable device can never crash Homebridge with an unhandled error.
|
|
19
24
|
|
|
20
25
|
## 2.0.1 (2021-03-25)
|
|
21
26
|
This update includes the following changes:
|
package/lib/BaseAccessory.js
CHANGED
|
@@ -76,7 +76,7 @@ class BaseAccessory {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
getState(dp, callback) {
|
|
79
|
-
if (!this.device.connected) return callback(
|
|
79
|
+
if (!this.device.connected) return callback(this._commError());
|
|
80
80
|
const _callback = () => {
|
|
81
81
|
if (Array.isArray(dp)) {
|
|
82
82
|
const ret = {};
|
|
@@ -101,29 +101,30 @@ class BaseAccessory {
|
|
|
101
101
|
//For devices like the DETA Smart Fan Controller Switch that by default set the speed as 3 the new code in the setMultiState function causes issues.
|
|
102
102
|
if (!this.device.connected) {
|
|
103
103
|
this.log.debug(`${this.device.context.name}: skipping write, device not connected`);
|
|
104
|
-
return callback && callback();
|
|
104
|
+
return callback && callback(this._commError());
|
|
105
105
|
}
|
|
106
106
|
const ret = this.device.update(dps);
|
|
107
|
-
callback && callback(
|
|
107
|
+
callback && callback(ret === false ? this._commError() : null);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
setMultiState(dps, callback) {
|
|
111
111
|
if (!this.device.connected) {
|
|
112
112
|
this.log.debug(`${this.device.context.name}: skipping write, device not connected`);
|
|
113
|
-
return callback && callback();
|
|
113
|
+
return callback && callback(this._commError());
|
|
114
114
|
}
|
|
115
|
+
let failed = false;
|
|
115
116
|
for (const dp in dps) {
|
|
116
117
|
if (dps.hasOwnProperty(dp) && dps[dp] !== this.device.state[dp]){
|
|
117
|
-
|
|
118
|
+
if (this.device.update({[dp.toString()] : dps[dp]}) === false) failed = true;
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
|
-
callback && callback(
|
|
121
|
+
callback && callback(failed ? this._commError() : null);
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
getDividedState(dp, divisor, callback) {
|
|
124
125
|
this.getState(dp, (err, data) => {
|
|
125
126
|
if (err) return callback(err);
|
|
126
|
-
if (!isFinite(data)) return callback(
|
|
127
|
+
if (!isFinite(data)) return callback(this._commError());
|
|
127
128
|
|
|
128
129
|
callback(null, this._getDividedState(data, divisor));
|
|
129
130
|
});
|
|
@@ -143,24 +144,50 @@ class BaseAccessory {
|
|
|
143
144
|
return this.setMultiStateAsync({[dp.toString()]: value});
|
|
144
145
|
}
|
|
145
146
|
|
|
146
|
-
|
|
147
|
+
// Throws a "No Response" HapStatusError when the device is unreachable or the
|
|
148
|
+
// write is rejected, so a get/set handler that returns this promise surfaces
|
|
149
|
+
// the failure to HomeKit instead of silently pretending the write succeeded.
|
|
150
|
+
// `device.update` returns a boolean for LAN devices and an awaitable boolean
|
|
151
|
+
// for cloud devices (false == the command did not go through).
|
|
152
|
+
async setMultiStateAsync(dps) {
|
|
147
153
|
if (!this.device.connected) {
|
|
148
154
|
this.log.debug(`${this.device.context.name}: skipping write, device not connected`);
|
|
149
|
-
|
|
155
|
+
throw this._commError();
|
|
150
156
|
}
|
|
157
|
+
const writes = [];
|
|
151
158
|
for (const dp in dps) {
|
|
152
159
|
if (dps.hasOwnProperty(dp) && dps[dp] !== this.device.state[dp]) {
|
|
153
|
-
this.device.update({[dp.toString()]: dps[dp]});
|
|
160
|
+
writes.push(this.device.update({[dp.toString()]: dps[dp]}));
|
|
154
161
|
}
|
|
155
162
|
}
|
|
163
|
+
const results = await Promise.all(writes);
|
|
164
|
+
if (results.some(ok => ok === false)) throw this._commError();
|
|
156
165
|
}
|
|
157
166
|
|
|
158
|
-
setMultiStateLegacyAsync(dps) {
|
|
167
|
+
async setMultiStateLegacyAsync(dps) {
|
|
159
168
|
if (!this.device.connected) {
|
|
160
169
|
this.log.debug(`${this.device.context.name}: skipping write, device not connected`);
|
|
161
|
-
|
|
170
|
+
throw this._commError();
|
|
162
171
|
}
|
|
163
|
-
this.device.update(dps);
|
|
172
|
+
if (await this.device.update(dps) === false) throw this._commError();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Non-throwing write helpers for callers that are NOT inside a HomeKit
|
|
176
|
+
// get/set handler — setTimeout callbacks, multi-step command sequences, etc.
|
|
177
|
+
// The async helpers above reject on a disconnected/failed write so HomeKit
|
|
178
|
+
// can show "No Response"; in a detached/background context there is no
|
|
179
|
+
// HomeKit request to fail and an unhandled rejection would crash Homebridge,
|
|
180
|
+
// so these swallow the error (the helper has already logged it).
|
|
181
|
+
setStateInBackground(dp, value) {
|
|
182
|
+
this.setStateAsync(dp, value).catch(() => {});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
setMultiStateInBackground(dps) {
|
|
186
|
+
this.setMultiStateAsync(dps).catch(() => {});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
setMultiStateLegacyInBackground(dps) {
|
|
190
|
+
this.setMultiStateLegacyAsync(dps).catch(() => {});
|
|
164
191
|
}
|
|
165
192
|
|
|
166
193
|
getDividedStateAsync(dp, divisor) {
|
|
@@ -55,7 +55,7 @@ class ConvectorAccessory extends BaseAccessory {
|
|
|
55
55
|
service.getCharacteristic(Characteristic.TargetHeaterCoolerState)
|
|
56
56
|
.setProps({ minValue: 1, maxValue: 1, validValues: [Characteristic.TargetHeaterCoolerState.HEAT] })
|
|
57
57
|
.updateValue(this._getTargetHeaterCoolerState())
|
|
58
|
-
.onGet(() => this._getTargetHeaterCoolerState())
|
|
58
|
+
.onGet(() => { if (!this.device.connected) throw this._commError(); return this._getTargetHeaterCoolerState(); })
|
|
59
59
|
.onSet(() => this.setStateAsync(this.dpActive, true));
|
|
60
60
|
|
|
61
61
|
const characteristicCurrentTemperature = service.getCharacteristic(Characteristic.CurrentTemperature)
|
|
@@ -74,6 +74,7 @@ class CustomMultiOutletAccessory extends BaseAccessory {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
setPower(dp, value) {
|
|
77
|
+
if (!this.device.connected) throw this._commError();
|
|
77
78
|
if (!this._pendingPower) {
|
|
78
79
|
this._pendingPower = {props: {}, resolvers: []};
|
|
79
80
|
}
|
|
@@ -87,7 +88,7 @@ class CustomMultiOutletAccessory extends BaseAccessory {
|
|
|
87
88
|
this._pendingPower.timer = setTimeout(() => {
|
|
88
89
|
const {props, resolvers} = this._pendingPower;
|
|
89
90
|
this._pendingPower = null;
|
|
90
|
-
this.
|
|
91
|
+
this.setMultiStateInBackground(props);
|
|
91
92
|
resolvers.forEach(r => r());
|
|
92
93
|
}, 500);
|
|
93
94
|
});
|
|
@@ -69,6 +69,7 @@ class MultiOutletAccessory extends BaseAccessory {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
setPower(dp, value) {
|
|
72
|
+
if (!this.device.connected) throw this._commError();
|
|
72
73
|
if (!this._pendingPower) {
|
|
73
74
|
this._pendingPower = {props: {}, resolvers: []};
|
|
74
75
|
}
|
|
@@ -82,7 +83,7 @@ class MultiOutletAccessory extends BaseAccessory {
|
|
|
82
83
|
this._pendingPower.timer = setTimeout(() => {
|
|
83
84
|
const {props, resolvers} = this._pendingPower;
|
|
84
85
|
this._pendingPower = null;
|
|
85
|
-
this.
|
|
86
|
+
this.setMultiStateInBackground(props);
|
|
86
87
|
resolvers.forEach(r => r());
|
|
87
88
|
}, 500);
|
|
88
89
|
});
|
|
@@ -203,8 +203,8 @@ class OilDiffuserAccessory extends BaseAccessory {
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
getBrightness() {
|
|
206
|
-
if (this.
|
|
207
|
-
return this.convertColorFromTuyaToHomeKit(this.
|
|
206
|
+
if (this.getStateAsync(this.dpMode) === this.cmdWhite) return this.convertBrightnessFromTuyaToHomeKit(this.getStateAsync(this.dpColor)).b;
|
|
207
|
+
return this.convertColorFromTuyaToHomeKit(this.getStateAsync(this.dpColor)).b;
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
setBrightness(value) {
|
|
@@ -218,8 +218,8 @@ class OilDiffuserAccessory extends BaseAccessory {
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
getColorTemperature() {
|
|
221
|
-
if (this.
|
|
222
|
-
return this.convertColorTemperatureFromTuyaToHomeKit(this.
|
|
221
|
+
if (this.getStateAsync(this.dpMode) !== this.cmdWhite) return 0;
|
|
222
|
+
return this.convertColorTemperatureFromTuyaToHomeKit(this.getStateAsync(this.dpColorTemperature));
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
setColorTemperature(value) {
|
|
@@ -234,8 +234,8 @@ class OilDiffuserAccessory extends BaseAccessory {
|
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
getHue() {
|
|
237
|
-
if (this.
|
|
238
|
-
return this.convertColorFromTuyaToHomeKit(this.
|
|
237
|
+
if (this.getStateAsync(this.dpMode) === this.cmdWhite) return 0;
|
|
238
|
+
return this.convertColorFromTuyaToHomeKit(this.getStateAsync(this.dpColor)).h;
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
setHue(value) {
|
|
@@ -243,7 +243,7 @@ class OilDiffuserAccessory extends BaseAccessory {
|
|
|
243
243
|
}
|
|
244
244
|
|
|
245
245
|
getSaturation() {
|
|
246
|
-
return this.convertColorFromTuyaToHomeKit(this.
|
|
246
|
+
return this.convertColorFromTuyaToHomeKit(this.getStateAsync(this.dpColor)).s;
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
setSaturation(value) {
|
|
@@ -251,6 +251,8 @@ class OilDiffuserAccessory extends BaseAccessory {
|
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
_setHueSaturation(prop) {
|
|
254
|
+
if (!this.device.connected) throw this._commError();
|
|
255
|
+
|
|
254
256
|
if (!this._pendingHueSaturation) {
|
|
255
257
|
this._pendingHueSaturation = {props: {}, resolvers: []};
|
|
256
258
|
}
|
|
@@ -266,7 +268,7 @@ class OilDiffuserAccessory extends BaseAccessory {
|
|
|
266
268
|
this._pendingHueSaturation = null;
|
|
267
269
|
|
|
268
270
|
const newValue = this.convertColorFromHomeKitToTuya(props);
|
|
269
|
-
this.
|
|
271
|
+
this.setMultiStateInBackground({[this.dpMode]: this.cmdColor, [this.dpColor]: newValue});
|
|
270
272
|
|
|
271
273
|
resolvers.forEach(r => r());
|
|
272
274
|
}, 500);
|
|
@@ -134,8 +134,8 @@ class RGBTWLightAccessory extends BaseAccessory {
|
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
getBrightness() {
|
|
137
|
-
if (this.
|
|
138
|
-
return this.convertColorFromTuyaToHomeKit(this.
|
|
137
|
+
if (this.getStateAsync(this.dpMode) === this.cmdWhite) return this.convertBrightnessFromTuyaToHomeKit(this.getStateAsync(this.dpBrightness));
|
|
138
|
+
return this.convertColorFromTuyaToHomeKit(this.getStateAsync(this.dpColor)).b;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
setBrightness(value) {
|
|
@@ -145,8 +145,8 @@ class RGBTWLightAccessory extends BaseAccessory {
|
|
|
145
145
|
|
|
146
146
|
getColorTemperature() {
|
|
147
147
|
this.log.debug(`getColorTemperature`);
|
|
148
|
-
if (this.
|
|
149
|
-
return this.convertColorTemperatureFromTuyaToHomeKit(this.
|
|
148
|
+
if (this.getStateAsync(this.dpMode) !== this.cmdWhite) return this.minWhiteColor;
|
|
149
|
+
return this.convertColorTemperatureFromTuyaToHomeKit(this.getStateAsync(this.dpColorTemperature));
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
setColorTemperature(value) {
|
|
@@ -165,10 +165,10 @@ class RGBTWLightAccessory extends BaseAccessory {
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
getHue() {
|
|
168
|
-
if (this.
|
|
169
|
-
return this.convertHomeKitColorTemperatureToHomeKitColor(this.convertColorTemperatureFromTuyaToHomeKit(this.
|
|
168
|
+
if (this.getStateAsync(this.dpMode) === this.cmdWhite) {
|
|
169
|
+
return this.convertHomeKitColorTemperatureToHomeKitColor(this.convertColorTemperatureFromTuyaToHomeKit(this.getStateAsync(this.dpColorTemperature))).h;
|
|
170
170
|
}
|
|
171
|
-
return this.convertColorFromTuyaToHomeKit(this.
|
|
171
|
+
return this.convertColorFromTuyaToHomeKit(this.getStateAsync(this.dpColor)).h;
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
setHue(value) {
|
|
@@ -176,10 +176,10 @@ class RGBTWLightAccessory extends BaseAccessory {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
getSaturation() {
|
|
179
|
-
if (this.
|
|
180
|
-
return this.convertHomeKitColorTemperatureToHomeKitColor(this.convertColorTemperatureFromTuyaToHomeKit(this.
|
|
179
|
+
if (this.getStateAsync(this.dpMode) === this.cmdWhite) {
|
|
180
|
+
return this.convertHomeKitColorTemperatureToHomeKitColor(this.convertColorTemperatureFromTuyaToHomeKit(this.getStateAsync(this.dpColorTemperature))).s;
|
|
181
181
|
}
|
|
182
|
-
return this.convertColorFromTuyaToHomeKit(this.
|
|
182
|
+
return this.convertColorFromTuyaToHomeKit(this.getStateAsync(this.dpColor)).s;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
setSaturation(value) {
|
|
@@ -187,6 +187,8 @@ class RGBTWLightAccessory extends BaseAccessory {
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
_setHueSaturation(prop) {
|
|
190
|
+
if (!this.device.connected) throw this._commError();
|
|
191
|
+
|
|
190
192
|
if (!this._pendingHueSaturation) {
|
|
191
193
|
this._pendingHueSaturation = {props: {}, resolvers: []};
|
|
192
194
|
}
|
|
@@ -208,7 +210,7 @@ class RGBTWLightAccessory extends BaseAccessory {
|
|
|
208
210
|
const newValue = this.convertColorFromHomeKitToTuya(props);
|
|
209
211
|
|
|
210
212
|
if (!(this.device.state[this.dpMode] === this.cmdWhite && isSham)) {
|
|
211
|
-
this.
|
|
213
|
+
this.setMultiStateInBackground({[this.dpMode]: this.cmdColor, [this.dpColor]: newValue});
|
|
212
214
|
}
|
|
213
215
|
this.characteristicColorTemperature.updateValue(this.minWhiteColor);
|
|
214
216
|
|
|
@@ -196,8 +196,8 @@ class RGBTWOutletAccessory extends BaseAccessory {
|
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
getBrightness() {
|
|
199
|
-
if (this.
|
|
200
|
-
return this.convertColorFromTuyaToHomeKit(this.
|
|
199
|
+
if (this.getStateAsync(this.dpMode) === this.cmdWhite) return this.convertBrightnessFromTuyaToHomeKit(this.getStateAsync(this.dpBrightness));
|
|
200
|
+
return this.convertColorFromTuyaToHomeKit(this.getStateAsync(this.dpColor)).b;
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
setBrightness(value) {
|
|
@@ -206,8 +206,8 @@ class RGBTWOutletAccessory extends BaseAccessory {
|
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
getColorTemperature() {
|
|
209
|
-
if (this.
|
|
210
|
-
return this.convertColorTemperatureFromTuyaToHomeKit(this.
|
|
209
|
+
if (this.getStateAsync(this.dpMode) !== this.cmdWhite) return 0;
|
|
210
|
+
return this.convertColorTemperatureFromTuyaToHomeKit(this.getStateAsync(this.dpColorTemperature));
|
|
211
211
|
}
|
|
212
212
|
|
|
213
213
|
setColorTemperature(value) {
|
|
@@ -221,8 +221,8 @@ class RGBTWOutletAccessory extends BaseAccessory {
|
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
getHue() {
|
|
224
|
-
if (this.
|
|
225
|
-
return this.convertColorFromTuyaToHomeKit(this.
|
|
224
|
+
if (this.getStateAsync(this.dpMode) === this.cmdWhite) return 0;
|
|
225
|
+
return this.convertColorFromTuyaToHomeKit(this.getStateAsync(this.dpColor)).h;
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
setHue(value) {
|
|
@@ -230,8 +230,8 @@ class RGBTWOutletAccessory extends BaseAccessory {
|
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
getSaturation() {
|
|
233
|
-
if (this.
|
|
234
|
-
return this.convertColorFromTuyaToHomeKit(this.
|
|
233
|
+
if (this.getStateAsync(this.dpMode) === this.cmdWhite) return 0;
|
|
234
|
+
return this.convertColorFromTuyaToHomeKit(this.getStateAsync(this.dpColor)).s;
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
setSaturation(value) {
|
|
@@ -239,6 +239,8 @@ class RGBTWOutletAccessory extends BaseAccessory {
|
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
_setHueSaturation(prop) {
|
|
242
|
+
if (!this.device.connected) throw this._commError();
|
|
243
|
+
|
|
242
244
|
if (!this._pendingHueSaturation) {
|
|
243
245
|
this._pendingHueSaturation = {props: {}, resolvers: []};
|
|
244
246
|
}
|
|
@@ -257,7 +259,7 @@ class RGBTWOutletAccessory extends BaseAccessory {
|
|
|
257
259
|
const newValue = this.convertColorFromHomeKitToTuya(props);
|
|
258
260
|
|
|
259
261
|
if (!(this.device.state[this.dpMode] === this.cmdWhite && isSham)) {
|
|
260
|
-
this.
|
|
262
|
+
this.setMultiStateInBackground({[this.dpMode]: this.cmdColor, [this.dpColor]: newValue});
|
|
261
263
|
}
|
|
262
264
|
this.characteristicColorTemperature.updateValue(0);
|
|
263
265
|
|
|
@@ -92,7 +92,7 @@ class SimpleBlindsAccessory extends BaseAccessory {
|
|
|
92
92
|
|
|
93
93
|
const characteristicPositionState = service.getCharacteristic(Characteristic.PositionState)
|
|
94
94
|
.updateValue(this._getPositionState())
|
|
95
|
-
.onGet(() => this._getPositionState());
|
|
95
|
+
.onGet(() => { if (!this.device.connected) throw this._commError(); return this._getPositionState(); });
|
|
96
96
|
|
|
97
97
|
this.device.on('change', changes => {
|
|
98
98
|
this.log.debug('[TuyaAccessory] Blinds saw change to ' + changes[this.dpAction]);
|
|
@@ -176,7 +176,7 @@ class SimpleBlindsAccessory extends BaseAccessory {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
getCurrentPosition() {
|
|
179
|
-
return this._getCurrentPosition(this.
|
|
179
|
+
return this._getCurrentPosition(this.getStateAsync(this.dpAction));
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
_getCurrentPosition(dp) {
|
|
@@ -191,7 +191,7 @@ class SimpleBlindsAccessory extends BaseAccessory {
|
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
getTargetPosition() {
|
|
194
|
-
return this._getTargetPosition(this.
|
|
194
|
+
return this._getTargetPosition(this.getStateAsync(this.dpAction));
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
_getTargetPosition(dp) {
|
|
@@ -234,7 +234,7 @@ class SimpleBlindsAccessory extends BaseAccessory {
|
|
|
234
234
|
this.log.debug('[TuyaAccessory] Blinds will stop in ' + duration + 'ms');
|
|
235
235
|
this.changeTimeout = setTimeout(() => {
|
|
236
236
|
this.log.debug('[TuyaAccessory] Blinds asked to stop');
|
|
237
|
-
this.
|
|
237
|
+
this.setStateInBackground(this.dpAction, this.cmdStop);
|
|
238
238
|
}, duration);
|
|
239
239
|
}
|
|
240
240
|
return result;
|
|
@@ -246,7 +246,7 @@ class SimpleBlindsAccessory extends BaseAccessory {
|
|
|
246
246
|
this.log.debug('[TuyaAccessory] Blinds will stop in ' + duration + 'ms');
|
|
247
247
|
this.changeTimeout = setTimeout(() => {
|
|
248
248
|
this.log.debug('[TuyaAccessory] Blinds asked to stop');
|
|
249
|
-
this.
|
|
249
|
+
this.setStateInBackground(this.dpAction, this.cmdStop);
|
|
250
250
|
}, duration);
|
|
251
251
|
}
|
|
252
252
|
return result;
|
|
@@ -43,7 +43,7 @@ class SimpleDimmer2Accessory extends BaseAccessory {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
getBrightness() {
|
|
46
|
-
return this.convertBrightnessFromTuyaToHomeKit(this.
|
|
46
|
+
return this.convertBrightnessFromTuyaToHomeKit(this.getStateAsync(this.dpBrightness));
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
setBrightness(value) {
|
|
@@ -43,7 +43,7 @@ class SimpleDimmerAccessory extends BaseAccessory {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
getBrightness() {
|
|
46
|
-
return this.convertBrightnessFromTuyaToHomeKit(this.
|
|
46
|
+
return this.convertBrightnessFromTuyaToHomeKit(this.getStateAsync(this.dpBrightness));
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
setBrightness(value) {
|
|
@@ -117,25 +117,25 @@ class SimpleGarageDoorAccessory extends BaseAccessory {
|
|
|
117
117
|
|
|
118
118
|
this.characteristicTargetDoorState = service.getCharacteristic(Characteristic.TargetDoorState)
|
|
119
119
|
.updateValue(initialTarget)
|
|
120
|
-
.onGet(() => this.accessory.context.cachedTargetDoorState)
|
|
120
|
+
.onGet(() => this._reportDoorState(this.accessory.context.cachedTargetDoorState))
|
|
121
121
|
.onSet(value => this.setTargetDoorState(value));
|
|
122
122
|
|
|
123
123
|
this.characteristicCurrentDoorState = service.getCharacteristic(Characteristic.CurrentDoorState)
|
|
124
124
|
.updateValue(this.currentDoorState)
|
|
125
|
-
.onGet(() => this.currentDoorState);
|
|
125
|
+
.onGet(() => this._reportDoorState(this.currentDoorState));
|
|
126
126
|
|
|
127
127
|
// The controller exposes limit switches (l_open/l_close) but they don't
|
|
128
128
|
// work in practice, and there's no obstruction sensor wired up, so this
|
|
129
129
|
// is always reported clear.
|
|
130
130
|
service.getCharacteristic(Characteristic.ObstructionDetected)
|
|
131
131
|
.updateValue(false)
|
|
132
|
-
.onGet(() => false);
|
|
132
|
+
.onGet(() => this._reportDoorState(false));
|
|
133
133
|
|
|
134
134
|
const partialSwitch = this.accessory.getServiceById(Service.Switch, 'partialOpen');
|
|
135
135
|
if (partialSwitch) {
|
|
136
136
|
const onChar = partialSwitch.getCharacteristic(Characteristic.On)
|
|
137
137
|
.updateValue(isOpen)
|
|
138
|
-
.onGet(() => this.currentDoorState === Characteristic.CurrentDoorState.OPEN)
|
|
138
|
+
.onGet(() => this._reportDoorState(this.currentDoorState === Characteristic.CurrentDoorState.OPEN))
|
|
139
139
|
.onSet(value => {
|
|
140
140
|
// Stateful: the switch mirrors CurrentDoorState (ON while
|
|
141
141
|
// the gate is open in HomeKit's view — see
|
|
@@ -229,7 +229,19 @@ class SimpleGarageDoorAccessory extends BaseAccessory {
|
|
|
229
229
|
}
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
// onGet helper: a cached/optimistic door value is fine to report while the
|
|
233
|
+
// gate is reachable, but when it's offline HomeKit must show "No Response"
|
|
234
|
+
// instead of a stale state that makes the gate look online and accept taps.
|
|
235
|
+
_reportDoorState(value) {
|
|
236
|
+
if (!this.device.connected) throw this._commError();
|
|
237
|
+
return value;
|
|
238
|
+
}
|
|
239
|
+
|
|
232
240
|
setTargetDoorState(value) {
|
|
241
|
+
// Surface "No Response" for a tap on an unreachable gate instead of
|
|
242
|
+
// silently dropping the command (the write helpers would otherwise just
|
|
243
|
+
// log and skip, leaving HomeKit to believe the open/close succeeded).
|
|
244
|
+
if (!this.device.connected) throw this._commError();
|
|
233
245
|
// A direct open/close (the GarageDoorOpener target, a Force switch, or
|
|
234
246
|
// the partial switch tapped OFF) is manual control: cancel any pending
|
|
235
247
|
// partial-open auto-stop so it can't halt this movement part-way.
|
|
@@ -255,7 +267,7 @@ class SimpleGarageDoorAccessory extends BaseAccessory {
|
|
|
255
267
|
_sendOpen() {
|
|
256
268
|
this._cancelPendingClose();
|
|
257
269
|
this.log.info(`[SimpleGarageDoor] ${this.device.context.name}: open (dp${this.dpOpen})`);
|
|
258
|
-
this.
|
|
270
|
+
this.setMultiStateLegacyInBackground({[this.dpOpen]: true});
|
|
259
271
|
}
|
|
260
272
|
|
|
261
273
|
// Close is ignored while the gate is actively moving, so fire it directly
|
|
@@ -271,15 +283,15 @@ class SimpleGarageDoorAccessory extends BaseAccessory {
|
|
|
271
283
|
}
|
|
272
284
|
if (this._isStopped()) {
|
|
273
285
|
this.log.info(`[SimpleGarageDoor] ${name}: close (dp${this.dpClose}) — gate already stopped`);
|
|
274
|
-
this.
|
|
286
|
+
this.setMultiStateLegacyInBackground({[this.dpClose]: true});
|
|
275
287
|
return;
|
|
276
288
|
}
|
|
277
289
|
this.log.info(`[SimpleGarageDoor] ${name}: stop-before-close — stop (dp${this.dpStop}) now, close (dp${this.dpClose}) in ${this.stopBeforeCloseMs}ms`);
|
|
278
|
-
this.
|
|
290
|
+
this.setMultiStateLegacyInBackground({[this.dpStop]: true});
|
|
279
291
|
this.pendingCloseTimer = setTimeout(() => {
|
|
280
292
|
this.pendingCloseTimer = null;
|
|
281
293
|
this.log.info(`[SimpleGarageDoor] ${name}: stop-before-close — firing close (dp${this.dpClose})`);
|
|
282
|
-
this.
|
|
294
|
+
this.setMultiStateLegacyInBackground({[this.dpClose]: true});
|
|
283
295
|
}, this.stopBeforeCloseMs);
|
|
284
296
|
}
|
|
285
297
|
|
|
@@ -305,6 +317,7 @@ class SimpleGarageDoorAccessory extends BaseAccessory {
|
|
|
305
317
|
_handlePartialOpen() {
|
|
306
318
|
const {Characteristic} = this.hap;
|
|
307
319
|
const name = this.device.context.name;
|
|
320
|
+
if (!this.device.connected) throw this._commError();
|
|
308
321
|
if (!this.partialOpenMs) return;
|
|
309
322
|
if (this.partialStopTimer) {
|
|
310
323
|
// Re-entrant press while a partial is already armed (e.g. a
|
|
@@ -319,7 +332,7 @@ class SimpleGarageDoorAccessory extends BaseAccessory {
|
|
|
319
332
|
this.partialStopTimer = setTimeout(() => {
|
|
320
333
|
this.partialStopTimer = null;
|
|
321
334
|
this.log.info(`[SimpleGarageDoor] ${name}: partial open — firing stop (dp${this.dpStop})`);
|
|
322
|
-
this.
|
|
335
|
+
this.setMultiStateLegacyInBackground({[this.dpStop]: true});
|
|
323
336
|
}, this.partialOpenMs);
|
|
324
337
|
}
|
|
325
338
|
|
|
@@ -45,7 +45,7 @@ class SimpleHeaterAccessory extends BaseAccessory {
|
|
|
45
45
|
|
|
46
46
|
const characteristicCurrentHeaterCoolerState = service.getCharacteristic(Characteristic.CurrentHeaterCoolerState)
|
|
47
47
|
.updateValue(this._getCurrentHeaterCoolerState(dps))
|
|
48
|
-
.onGet(() => this.currentHeaterCoolerState);
|
|
48
|
+
.onGet(() => { if (!this.device.connected) throw this._commError(); return this.currentHeaterCoolerState; });
|
|
49
49
|
|
|
50
50
|
service.getCharacteristic(Characteristic.TargetHeaterCoolerState)
|
|
51
51
|
.setProps({
|
|
@@ -54,7 +54,7 @@ class SimpleHeaterAccessory extends BaseAccessory {
|
|
|
54
54
|
validValues: [Characteristic.TargetHeaterCoolerState.HEAT]
|
|
55
55
|
})
|
|
56
56
|
.updateValue(this._getTargetHeaterCoolerState())
|
|
57
|
-
.onGet(() => this._getTargetHeaterCoolerState())
|
|
57
|
+
.onGet(() => { if (!this.device.connected) throw this._commError(); return this._getTargetHeaterCoolerState(); })
|
|
58
58
|
.onSet(() => this.setStateAsync(this.dpActive, true));
|
|
59
59
|
|
|
60
60
|
const characteristicCurrentTemperature = service.getCharacteristic(Characteristic.CurrentTemperature)
|
package/lib/SwitchAccessory.js
CHANGED
|
@@ -69,6 +69,7 @@ class SwitchAccessory extends BaseAccessory {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
setPower(dp, value) {
|
|
72
|
+
if (!this.device.connected) throw this._commError();
|
|
72
73
|
if (!this._pendingPower) {
|
|
73
74
|
this._pendingPower = {props: {}, resolvers: []};
|
|
74
75
|
}
|
|
@@ -82,7 +83,7 @@ class SwitchAccessory extends BaseAccessory {
|
|
|
82
83
|
this._pendingPower.timer = setTimeout(() => {
|
|
83
84
|
const {props, resolvers} = this._pendingPower;
|
|
84
85
|
this._pendingPower = null;
|
|
85
|
-
this.
|
|
86
|
+
this.setMultiStateInBackground(props);
|
|
86
87
|
resolvers.forEach(r => r());
|
|
87
88
|
}, 500);
|
|
88
89
|
});
|
package/lib/TWLightAccessory.js
CHANGED
|
@@ -72,7 +72,7 @@ class TWLightAccessory extends BaseAccessory {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
getBrightness() {
|
|
75
|
-
return this.convertBrightnessFromTuyaToHomeKit(this.
|
|
75
|
+
return this.convertBrightnessFromTuyaToHomeKit(this.getStateAsync(this.dpBrightness));
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
setBrightness(value) {
|
|
@@ -80,7 +80,7 @@ class TWLightAccessory extends BaseAccessory {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
getColorTemperature() {
|
|
83
|
-
return this.convertColorTemperatureFromTuyaToHomeKit(this.
|
|
83
|
+
return this.convertColorTemperatureFromTuyaToHomeKit(this.getStateAsync(this.dpColorTemperature));
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
setColorTemperature(value) {
|
package/lib/TuyaCloudDevice.js
CHANGED
|
@@ -173,7 +173,15 @@ class TuyaCloudDevice extends EventEmitter {
|
|
|
173
173
|
// `this.state`: the accessory already reflects the user's intent in HomeKit
|
|
174
174
|
// immediately, and leaving `state` untouched lets the realtime stream
|
|
175
175
|
// confirm the real device state without a spurious "revert" while a sleepy
|
|
176
|
-
// device catches up.
|
|
176
|
+
// device catches up.
|
|
177
|
+
//
|
|
178
|
+
// Return contract mirrors TuyaAccessory.update() as far as a synchronous
|
|
179
|
+
// caller is concerned (truthy on a no-op, `false` when not connected) but,
|
|
180
|
+
// unlike the LAN class, the actual command travels over HTTP and only its
|
|
181
|
+
// outcome reveals a failure. So when a command IS dispatched we return the
|
|
182
|
+
// promise that resolves to the boolean result (and never rejects), letting
|
|
183
|
+
// `BaseAccessory.setMultiStateAsync` await it and surface a rejected cloud
|
|
184
|
+
// command to HomeKit instead of silently dropping it.
|
|
177
185
|
update(dps) {
|
|
178
186
|
if (!dps || typeof dps !== 'object') return true;
|
|
179
187
|
|
|
@@ -185,13 +193,15 @@ class TuyaCloudDevice extends EventEmitter {
|
|
|
185
193
|
return false;
|
|
186
194
|
}
|
|
187
195
|
|
|
188
|
-
this.api.sendCommands(this.context.id, commands)
|
|
196
|
+
return this.api.sendCommands(this.context.id, commands)
|
|
189
197
|
.then(ok => {
|
|
190
198
|
if (!ok) this.log.warn(`${this.context.name}: Tuya Cloud rejected command ${JSON.stringify(commands)}`);
|
|
199
|
+
return ok;
|
|
191
200
|
})
|
|
192
|
-
.catch(ex =>
|
|
193
|
-
|
|
194
|
-
|
|
201
|
+
.catch(ex => {
|
|
202
|
+
this.log.error(`${this.context.name}: cloud command failed: ${ex.message}`);
|
|
203
|
+
return false;
|
|
204
|
+
});
|
|
195
205
|
}
|
|
196
206
|
|
|
197
207
|
/* ------------------------------------------------------------------ *
|
package/lib/ValveAccessory.js
CHANGED
|
@@ -49,11 +49,11 @@ class ValveAccessory extends BaseAccessory {
|
|
|
49
49
|
.onSet(value => this.setStateAsync(this.dpPower, value));
|
|
50
50
|
|
|
51
51
|
const characteristicInUse = service.getCharacteristic(Characteristic.InUse)
|
|
52
|
-
.onGet(() => characteristicActive.value);
|
|
52
|
+
.onGet(() => { if (!this.device.connected) throw this._commError(); return characteristicActive.value; });
|
|
53
53
|
|
|
54
54
|
if (!this.noTimer) {
|
|
55
55
|
service.getCharacteristic(Characteristic.SetDuration)
|
|
56
|
-
.onGet(() => this.setDuration)
|
|
56
|
+
.onGet(() => { if (!this.device.connected) throw this._commError(); return this.setDuration; })
|
|
57
57
|
.on('change', (data) => {
|
|
58
58
|
this.log.info('Water Valve Time Duration Set to: ' + data.newValue / 60 + ' Minutes');
|
|
59
59
|
this.setDuration = data.newValue;
|
|
@@ -66,7 +66,8 @@ class ValveAccessory extends BaseAccessory {
|
|
|
66
66
|
clearTimeout(this.timer);
|
|
67
67
|
this.timer = setTimeout(() => {
|
|
68
68
|
this.log.info('Water Valve Timer Expired. Shutting OFF Valve');
|
|
69
|
-
|
|
69
|
+
this.setStateInBackground(this.dpPower, 0);
|
|
70
|
+
service.getCharacteristic(Characteristic.Active).updateValue(0);
|
|
70
71
|
service.getCharacteristic(Characteristic.InUse).updateValue(0);
|
|
71
72
|
this.lastActivationTime = null;
|
|
72
73
|
}, (data.newValue * 1000));
|
|
@@ -75,6 +76,7 @@ class ValveAccessory extends BaseAccessory {
|
|
|
75
76
|
|
|
76
77
|
service.getCharacteristic(Characteristic.RemainingDuration)
|
|
77
78
|
.onGet(() => {
|
|
79
|
+
if (!this.device.connected) throw this._commError();
|
|
78
80
|
let remainingTime = this.setDuration - Math.floor(((new Date()).getTime() - this.lastActivationTime) / 1000);
|
|
79
81
|
return (remainingTime && remainingTime > 0) ? remainingTime : 0;
|
|
80
82
|
});
|
|
@@ -97,7 +99,8 @@ class ValveAccessory extends BaseAccessory {
|
|
|
97
99
|
clearTimeout(this.timer);
|
|
98
100
|
this.timer = setTimeout(() => {
|
|
99
101
|
this.log.info('Water Valve Timer Expired. Shutting OFF Valve');
|
|
100
|
-
|
|
102
|
+
this.setStateInBackground(this.dpPower, 0);
|
|
103
|
+
service.getCharacteristic(Characteristic.Active).updateValue(0);
|
|
101
104
|
service.getCharacteristic(Characteristic.InUse).updateValue(0);
|
|
102
105
|
this.lastActivationTime = null;
|
|
103
106
|
}, (this.setDuration * 1000));
|
|
@@ -114,7 +117,8 @@ class ValveAccessory extends BaseAccessory {
|
|
|
114
117
|
clearTimeout(this.timer);
|
|
115
118
|
this.timer = setTimeout(() => {
|
|
116
119
|
this.log.info('Water Valve Timer Expired. Shutting OFF Valve');
|
|
117
|
-
|
|
120
|
+
this.setStateInBackground(this.dpPower, 0);
|
|
121
|
+
service.getCharacteristic(Characteristic.Active).updateValue(0);
|
|
118
122
|
this.lastActivationTime = null;
|
|
119
123
|
}, (this.setDuration * 1000));
|
|
120
124
|
}
|
|
@@ -38,16 +38,16 @@ class VerticalBlindsWithTilt extends BaseAccessory {
|
|
|
38
38
|
|
|
39
39
|
const characteristicCurrentPosition = service.getCharacteristic(Characteristic.CurrentPosition)
|
|
40
40
|
.updateValue(this.currentPosition)
|
|
41
|
-
.onGet(() => this.currentPosition);
|
|
41
|
+
.onGet(() => { if (!this.device.connected) throw this._commError(); return this.currentPosition; });
|
|
42
42
|
|
|
43
43
|
const characteristicTargetPosition = service.getCharacteristic(Characteristic.TargetPosition)
|
|
44
44
|
.updateValue(this.currentPosition)
|
|
45
|
-
.onGet(() => this.currentPosition)
|
|
45
|
+
.onGet(() => { if (!this.device.connected) throw this._commError(); return this.currentPosition; })
|
|
46
46
|
.onSet(value => this.setPosition(value));
|
|
47
47
|
|
|
48
48
|
const characteristicPositionState = service.getCharacteristic(Characteristic.PositionState)
|
|
49
49
|
.updateValue(Characteristic.PositionState.STOPPED)
|
|
50
|
-
.onGet(() => Characteristic.PositionState.STOPPED);
|
|
50
|
+
.onGet(() => { if (!this.device.connected) throw this._commError(); return Characteristic.PositionState.STOPPED; });
|
|
51
51
|
|
|
52
52
|
const characteristicCurrentHorizontalTilt = service.getCharacteristic(Characteristic.CurrentHorizontalTiltAngle)
|
|
53
53
|
.updateValue(this._getTiltAngle(dps[this.dpTiltState] || dps[this.dpTilt]))
|
|
@@ -76,6 +76,7 @@ class VerticalBlindsWithTilt extends BaseAccessory {
|
|
|
76
76
|
|
|
77
77
|
setPosition(value) {
|
|
78
78
|
const {Characteristic} = this.hap;
|
|
79
|
+
if (!this.device.connected) throw this._commError();
|
|
79
80
|
|
|
80
81
|
if (value === 0) {
|
|
81
82
|
if (this.currentPosition === 0) {
|
|
@@ -205,6 +206,7 @@ class VerticalBlindsWithTilt extends BaseAccessory {
|
|
|
205
206
|
}
|
|
206
207
|
|
|
207
208
|
setTiltAngle(value) {
|
|
209
|
+
if (!this.device.connected) throw this._commError();
|
|
208
210
|
const tuyaValue = Math.round((value / 1.8) + 50);
|
|
209
211
|
const clampedValue = Math.max(0, Math.min(100, tuyaValue));
|
|
210
212
|
|
|
@@ -196,6 +196,7 @@ class WledDimmerAccessory extends BaseAccessory {
|
|
|
196
196
|
});
|
|
197
197
|
})
|
|
198
198
|
.on('get', cb => {
|
|
199
|
+
if (!this.device.connected) return cb(this._commError());
|
|
199
200
|
cb(null, this.device.state[this.dpPower] && this._wledCurrentEffectIndex === (index + 1));
|
|
200
201
|
});
|
|
201
202
|
|
|
@@ -326,6 +327,7 @@ class WledDimmerAccessory extends BaseAccessory {
|
|
|
326
327
|
}
|
|
327
328
|
|
|
328
329
|
getBrightness() {
|
|
330
|
+
if (!this.device.connected) throw this._commError();
|
|
329
331
|
if (this.syncBrightnessToWled) {
|
|
330
332
|
// If we already have a cached WLED brightness, return it immediately.
|
|
331
333
|
if (this._lastWledPercent != null) {
|
|
@@ -363,7 +365,7 @@ class WledDimmerAccessory extends BaseAccessory {
|
|
|
363
365
|
maxTuyaBrightness
|
|
364
366
|
);
|
|
365
367
|
// Fire-and-forget; don't tie HomeKit callback to Tuya I/O.
|
|
366
|
-
this.
|
|
368
|
+
this.setStateInBackground(this.dpBrightness, maxTuyaBrightness);
|
|
367
369
|
}
|
|
368
370
|
|
|
369
371
|
// Compute desired WLED brightness once.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-tuya-plus",
|
|
3
|
-
"version": "3.14.0-dev.
|
|
3
|
+
"version": "3.14.0-dev.18",
|
|
4
4
|
"description": "A community-maintained Homebridge plugin for controlling Tuya devices locally over LAN. Includes new features, fixes, and updated device support.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -181,10 +181,10 @@ describe('setStateAsync', () => {
|
|
|
181
181
|
expect(device.update).not.toHaveBeenCalled();
|
|
182
182
|
});
|
|
183
183
|
|
|
184
|
-
test('
|
|
184
|
+
test('rejects with a comm-failure HapStatusError when device is not connected', async () => {
|
|
185
185
|
const { instance, device } = make({ '1': false });
|
|
186
186
|
device.connected = false;
|
|
187
|
-
expect(
|
|
187
|
+
await expect(instance.setStateAsync('1', true)).rejects.toBeInstanceOf(HAP.HapStatusError);
|
|
188
188
|
expect(device.update).not.toHaveBeenCalled();
|
|
189
189
|
});
|
|
190
190
|
});
|
|
@@ -204,12 +204,29 @@ describe('setMultiStateAsync', () => {
|
|
|
204
204
|
expect(device.update).not.toHaveBeenCalled();
|
|
205
205
|
});
|
|
206
206
|
|
|
207
|
-
test('
|
|
207
|
+
test('resolves when the device accepts the writes', async () => {
|
|
208
|
+
const { instance } = make({ '1': false });
|
|
209
|
+
await expect(instance.setMultiStateAsync({ '1': true })).resolves.toBeUndefined();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('rejects with a comm-failure HapStatusError when device is not connected', async () => {
|
|
208
213
|
const { instance, device } = make({ '1': false });
|
|
209
214
|
device.connected = false;
|
|
210
|
-
expect(
|
|
215
|
+
await expect(instance.setMultiStateAsync({ '1': true, '2': 50 })).rejects.toBeInstanceOf(HAP.HapStatusError);
|
|
211
216
|
expect(device.update).not.toHaveBeenCalled();
|
|
212
217
|
});
|
|
218
|
+
|
|
219
|
+
test('rejects when the device write is not accepted (returns false)', async () => {
|
|
220
|
+
const { instance, device } = make({ '1': false });
|
|
221
|
+
device.update.mockReturnValue(false);
|
|
222
|
+
await expect(instance.setMultiStateAsync({ '1': true })).rejects.toBeInstanceOf(HAP.HapStatusError);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('awaits an async device write result (cloud) and rejects on failure', async () => {
|
|
226
|
+
const { instance, device } = make({ '1': false });
|
|
227
|
+
device.update.mockResolvedValue(false);
|
|
228
|
+
await expect(instance.setMultiStateAsync({ '1': true })).rejects.toBeInstanceOf(HAP.HapStatusError);
|
|
229
|
+
});
|
|
213
230
|
});
|
|
214
231
|
|
|
215
232
|
describe('setMultiStateLegacyAsync', () => {
|
|
@@ -220,12 +237,34 @@ describe('setMultiStateLegacyAsync', () => {
|
|
|
220
237
|
expect(device.update).toHaveBeenCalledWith({ '1': true, '3': '2' });
|
|
221
238
|
});
|
|
222
239
|
|
|
223
|
-
test('
|
|
240
|
+
test('rejects with a comm-failure HapStatusError when device is not connected', async () => {
|
|
224
241
|
const { instance, device } = make();
|
|
225
242
|
device.connected = false;
|
|
226
|
-
expect(
|
|
243
|
+
await expect(instance.setMultiStateLegacyAsync({ '1': true })).rejects.toBeInstanceOf(HAP.HapStatusError);
|
|
227
244
|
expect(device.update).not.toHaveBeenCalled();
|
|
228
245
|
});
|
|
246
|
+
|
|
247
|
+
test('rejects when the device write is not accepted (returns false)', async () => {
|
|
248
|
+
const { instance, device } = make();
|
|
249
|
+
device.update.mockReturnValue(false);
|
|
250
|
+
await expect(instance.setMultiStateLegacyAsync({ '1': true })).rejects.toBeInstanceOf(HAP.HapStatusError);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe('background write helpers (never throw/reject)', () => {
|
|
255
|
+
test('setStateInBackground swallows a disconnected-device failure', async () => {
|
|
256
|
+
const { instance, device } = make({ '1': false });
|
|
257
|
+
device.connected = false;
|
|
258
|
+
expect(() => instance.setStateInBackground('1', true)).not.toThrow();
|
|
259
|
+
// Give the rejected inner promise a tick to settle; it must be caught.
|
|
260
|
+
await Promise.resolve();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('setMultiStateLegacyInBackground still dispatches the write when connected', () => {
|
|
264
|
+
const { instance, device } = make();
|
|
265
|
+
instance.setMultiStateLegacyInBackground({ '1': true });
|
|
266
|
+
expect(device.update).toHaveBeenCalledWith({ '1': true });
|
|
267
|
+
});
|
|
229
268
|
});
|
|
230
269
|
|
|
231
270
|
describe('getDividedStateAsync', () => {
|
|
@@ -243,14 +282,48 @@ describe('getDividedStateAsync', () => {
|
|
|
243
282
|
});
|
|
244
283
|
});
|
|
245
284
|
|
|
285
|
+
describe('getState (callback)', () => {
|
|
286
|
+
test('returns the DP value via the callback when connected', done => {
|
|
287
|
+
const { instance } = make({ '1': true });
|
|
288
|
+
instance.getState('1', (err, value) => {
|
|
289
|
+
expect(err).toBeNull();
|
|
290
|
+
expect(value).toBe(true);
|
|
291
|
+
done();
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('invokes the callback with a comm-failure error when not connected', () => {
|
|
296
|
+
const { instance, device } = make({ '1': true });
|
|
297
|
+
device.connected = false;
|
|
298
|
+
const cb = jest.fn();
|
|
299
|
+
instance.getState('1', cb);
|
|
300
|
+
expect(cb).toHaveBeenCalledWith(expect.any(HAP.HapStatusError));
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
246
304
|
describe('setMultiState (legacy callback)', () => {
|
|
247
|
-
test('
|
|
305
|
+
test('invokes the callback without error on a successful write', () => {
|
|
306
|
+
const { instance } = make({ '1': false });
|
|
307
|
+
const cb = jest.fn();
|
|
308
|
+
instance.setMultiState({ '1': true }, cb);
|
|
309
|
+
expect(cb).toHaveBeenCalledWith(null);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test('invokes the callback with a comm-failure error when not connected', () => {
|
|
248
313
|
const { instance, device } = make({ '1': false });
|
|
249
314
|
device.connected = false;
|
|
250
315
|
const cb = jest.fn();
|
|
251
316
|
instance.setMultiState({ '1': true }, cb);
|
|
252
317
|
expect(device.update).not.toHaveBeenCalled();
|
|
253
|
-
expect(cb).toHaveBeenCalledWith();
|
|
318
|
+
expect(cb).toHaveBeenCalledWith(expect.any(HAP.HapStatusError));
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test('invokes the callback with a comm-failure error when the write is not accepted', () => {
|
|
322
|
+
const { instance, device } = make({ '1': false });
|
|
323
|
+
device.update.mockReturnValue(false);
|
|
324
|
+
const cb = jest.fn();
|
|
325
|
+
instance.setMultiState({ '1': true }, cb);
|
|
326
|
+
expect(cb).toHaveBeenCalledWith(expect.any(HAP.HapStatusError));
|
|
254
327
|
});
|
|
255
328
|
|
|
256
329
|
test('tolerates a missing callback when not connected', () => {
|
|
@@ -261,13 +334,13 @@ describe('setMultiState (legacy callback)', () => {
|
|
|
261
334
|
});
|
|
262
335
|
|
|
263
336
|
describe('setMultiStateLegacy (legacy callback)', () => {
|
|
264
|
-
test('
|
|
337
|
+
test('invokes the callback with a comm-failure error when not connected', () => {
|
|
265
338
|
const { instance, device } = make();
|
|
266
339
|
device.connected = false;
|
|
267
340
|
const cb = jest.fn();
|
|
268
341
|
instance.setMultiStateLegacy({ '1': true }, cb);
|
|
269
342
|
expect(device.update).not.toHaveBeenCalled();
|
|
270
|
-
expect(cb).toHaveBeenCalledWith();
|
|
343
|
+
expect(cb).toHaveBeenCalledWith(expect.any(HAP.HapStatusError));
|
|
271
344
|
});
|
|
272
345
|
});
|
|
273
346
|
|
|
@@ -86,4 +86,16 @@ describe('MultiOutletAccessory.setPower — debounce batching', () => {
|
|
|
86
86
|
await p;
|
|
87
87
|
expect(device.update).not.toHaveBeenCalled();
|
|
88
88
|
});
|
|
89
|
+
|
|
90
|
+
test('surfaces a comm-failure error (No Response) when disconnected', () => {
|
|
91
|
+
// A tap on an unreachable outlet must fail in HomeKit instead of being
|
|
92
|
+
// silently dropped after the debounce.
|
|
93
|
+
const { instance, device } = makeOutlet({ '1': false });
|
|
94
|
+
device.connected = false;
|
|
95
|
+
let err;
|
|
96
|
+
try { instance.setPower('1', true); } catch (e) { err = e; }
|
|
97
|
+
expect(err).toBeInstanceOf(HAP.HapStatusError);
|
|
98
|
+
expect(err.hapStatus).toBe(HAP.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
|
|
99
|
+
expect(device.update).not.toHaveBeenCalled();
|
|
100
|
+
});
|
|
89
101
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const RGBTWLightAccessory = require('../lib/RGBTWLightAccessory');
|
|
4
|
-
const { makeInstance, makeMockCharacteristic } = require('./support/mocks');
|
|
4
|
+
const { makeInstance, makeMockCharacteristic, HAP } = require('./support/mocks');
|
|
5
5
|
|
|
6
6
|
function makeLight(state = {}, context = {}) {
|
|
7
7
|
const result = makeInstance(RGBTWLightAccessory, state, { colorFunction: 'HEXHSB', ...context });
|
|
@@ -88,12 +88,12 @@ describe('RGBTWLightAccessory.getColorTemperature', () => {
|
|
|
88
88
|
// setColorTemperature
|
|
89
89
|
// ---------------------------------------------------------------------------
|
|
90
90
|
describe('RGBTWLightAccessory.setColorTemperature', () => {
|
|
91
|
-
test('
|
|
91
|
+
test('rejects (No Response) and writes nothing when device is not connected', async () => {
|
|
92
92
|
const { instance, device } = makeLight({ '2': 'white', '4': 255 });
|
|
93
93
|
instance.characteristicHue = makeMockCharacteristic(0);
|
|
94
94
|
instance.characteristicSaturation = makeMockCharacteristic(0);
|
|
95
95
|
device.connected = false;
|
|
96
|
-
expect(
|
|
96
|
+
await expect(instance.setColorTemperature(200)).rejects.toBeInstanceOf(HAP.HapStatusError);
|
|
97
97
|
expect(device.update).not.toHaveBeenCalled();
|
|
98
98
|
});
|
|
99
99
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const SimpleFanLightAccessory = require('../lib/SimpleFanLightAccessory');
|
|
4
|
-
const { makeInstance } = require('./support/mocks');
|
|
4
|
+
const { makeInstance, HAP } = require('./support/mocks');
|
|
5
5
|
|
|
6
6
|
// Mirror the state that _registerCharacteristics would establish (the mock
|
|
7
7
|
// service shares a single characteristic, so wiring the fields manually keeps
|
|
@@ -290,10 +290,10 @@ describe('SimpleFanLightAccessory.getColorTemp / setColorTemp', () => {
|
|
|
290
290
|
expect(device.update).toHaveBeenCalledWith({ '23': 1000 });
|
|
291
291
|
});
|
|
292
292
|
|
|
293
|
-
test('setColorTemp
|
|
293
|
+
test('setColorTemp rejects (No Response) and writes nothing when disconnected', async () => {
|
|
294
294
|
const { instance, device } = makeFanLight({ '23': 500 });
|
|
295
295
|
device.connected = false;
|
|
296
|
-
expect(
|
|
296
|
+
await expect(instance.setColorTemp(370)).rejects.toBeInstanceOf(HAP.HapStatusError);
|
|
297
297
|
expect(device.update).not.toHaveBeenCalled();
|
|
298
298
|
});
|
|
299
299
|
});
|
|
@@ -373,11 +373,13 @@ describe('SimpleGarageDoorAccessory.setTargetDoorState — close (stop-before-cl
|
|
|
373
373
|
// Disconnect handling
|
|
374
374
|
// ---------------------------------------------------------------------------
|
|
375
375
|
describe('SimpleGarageDoorAccessory — disconnected', () => {
|
|
376
|
-
test('
|
|
376
|
+
test('Surfaces a No Response error and writes nothing when disconnected', () => {
|
|
377
377
|
const { instance, device } = makeSimpleGarage();
|
|
378
378
|
device.connected = false;
|
|
379
379
|
|
|
380
|
-
|
|
380
|
+
// A tap on an unreachable gate must fail in HomeKit instead of being
|
|
381
|
+
// silently dropped while HomeKit believes the open/close succeeded.
|
|
382
|
+
expect(() => instance.setTargetDoorState(TDS.OPEN)).toThrow(HAP.HapStatusError);
|
|
381
383
|
|
|
382
384
|
expect(device.update).not.toHaveBeenCalled();
|
|
383
385
|
});
|
|
@@ -56,10 +56,31 @@ describe('TuyaCloudDevice', () => {
|
|
|
56
56
|
const api = makeApi();
|
|
57
57
|
const dev = makeDevice(api);
|
|
58
58
|
expect(dev.connected).toBe(false);
|
|
59
|
-
expect(
|
|
59
|
+
expect(dev.update({switch_1: true})).toBe(false);
|
|
60
60
|
expect(api.sendCommands).not.toHaveBeenCalled();
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
test('update resolves to the cloud command result so failures are awaitable', async () => {
|
|
64
|
+
const api = makeApi();
|
|
65
|
+
const dev = makeDevice(api);
|
|
66
|
+
await dev._connect();
|
|
67
|
+
|
|
68
|
+
api.sendCommands.mockResolvedValueOnce(true);
|
|
69
|
+
await expect(dev.update({switch_1: true})).resolves.toBe(true);
|
|
70
|
+
|
|
71
|
+
api.sendCommands.mockResolvedValueOnce(false);
|
|
72
|
+
await expect(dev.update({switch_1: true})).resolves.toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('update resolves to false (never rejects) when the cloud request throws', async () => {
|
|
76
|
+
const api = makeApi();
|
|
77
|
+
const dev = makeDevice(api);
|
|
78
|
+
await dev._connect();
|
|
79
|
+
|
|
80
|
+
api.sendCommands.mockRejectedValueOnce(new Error('network down'));
|
|
81
|
+
await expect(dev.update({switch_1: true})).resolves.toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
63
84
|
test('applyStatus emits only the changed data-points', async () => {
|
|
64
85
|
const api = makeApi();
|
|
65
86
|
const dev = makeDevice(api);
|