homebridge-tuya-plus 3.14.0-dev.17 → 3.14.0-dev.19
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/config.schema.json +1 -1
- package/index.js +2 -1
- 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 +10 -6
- package/lib/SimpleGarageDoorAccessory.js +31 -18
- 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 +16 -12
- package/lib/VerticalBlindsWithTilt.js +5 -3
- package/lib/WledDimmerAccessory.js +46 -81
- 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/test/getCategory.test.js +1 -0
- package/wiki/Supported-Device-Types.md +8 -6
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/config.schema.json
CHANGED
|
@@ -318,7 +318,7 @@
|
|
|
318
318
|
"title": "Sync brightness to WLED (IP[:port])",
|
|
319
319
|
"placeholder": "192.168.1.100",
|
|
320
320
|
"condition": {
|
|
321
|
-
"functionBody": "return model.devices && model.devices[arrayIndices] && ['WledDimmer'
|
|
321
|
+
"functionBody": "return model.devices && model.devices[arrayIndices] && ['WledDimmer'].includes(model.devices[arrayIndices].type);"
|
|
322
322
|
}
|
|
323
323
|
},
|
|
324
324
|
"dpColorTemperature": {
|
package/index.js
CHANGED
|
@@ -18,6 +18,7 @@ const ConvectorAccessory = require('./lib/ConvectorAccessory');
|
|
|
18
18
|
const GarageDoorAccessory = require('./lib/GarageDoorAccessory');
|
|
19
19
|
const SimpleGarageDoorAccessory = require('./lib/SimpleGarageDoorAccessory');
|
|
20
20
|
const WledDimmerAccessory = require('./lib/WledDimmerAccessory');
|
|
21
|
+
const SimpleDimmerAccessory = require('./lib/SimpleDimmerAccessory');
|
|
21
22
|
const SimpleDimmer2Accessory = require('./lib/SimpleDimmer2Accessory');
|
|
22
23
|
const SimpleBlindsAccessory = require('./lib/SimpleBlindsAccessory');
|
|
23
24
|
const SimpleHeaterAccessory = require('./lib/SimpleHeaterAccessory');
|
|
@@ -60,7 +61,7 @@ const CLASS_DEF = {
|
|
|
60
61
|
convector: ConvectorAccessory,
|
|
61
62
|
garagedoor: GarageDoorAccessory,
|
|
62
63
|
simplegaragedoor: SimpleGarageDoorAccessory,
|
|
63
|
-
simpledimmer:
|
|
64
|
+
simpledimmer: SimpleDimmerAccessory,
|
|
64
65
|
wleddimmer: WledDimmerAccessory,
|
|
65
66
|
simpledimmer2: SimpleDimmer2Accessory,
|
|
66
67
|
simpleblinds: SimpleBlindsAccessory,
|
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) {
|
|
@@ -25,25 +25,29 @@ class SimpleDimmerAccessory extends BaseAccessory {
|
|
|
25
25
|
this.dpPower = this._getCustomDP(this.device.context.dpPower) || '1';
|
|
26
26
|
this.dpBrightness = this._getCustomDP(this.device.context.dpBrightness) || this._getCustomDP(this.device.context.dp) || '2';
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
this._characteristicOn = service.getCharacteristic(Characteristic.On)
|
|
29
29
|
.updateValue(dps[this.dpPower])
|
|
30
30
|
.onGet(() => this.getStateAsync(this.dpPower))
|
|
31
31
|
.onSet(value => this.setStateAsync(this.dpPower, value));
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
this._characteristicBrightness = service.getCharacteristic(Characteristic.Brightness)
|
|
34
34
|
.updateValue(this.convertBrightnessFromTuyaToHomeKit(dps[this.dpBrightness]))
|
|
35
35
|
.onGet(() => this.getBrightness())
|
|
36
36
|
.onSet(value => this.setBrightness(value));
|
|
37
37
|
|
|
38
|
+
this._registerChangeListener();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_registerChangeListener() {
|
|
38
42
|
this.device.on('change', changes => {
|
|
39
|
-
if (changes.hasOwnProperty(this.dpPower) &&
|
|
40
|
-
if (changes.hasOwnProperty(this.dpBrightness) && this.convertBrightnessFromHomeKitToTuya(
|
|
41
|
-
|
|
43
|
+
if (changes.hasOwnProperty(this.dpPower) && this._characteristicOn.value !== changes[this.dpPower]) this._characteristicOn.updateValue(changes[this.dpPower]);
|
|
44
|
+
if (changes.hasOwnProperty(this.dpBrightness) && this.convertBrightnessFromHomeKitToTuya(this._characteristicBrightness.value) !== changes[this.dpBrightness])
|
|
45
|
+
this._characteristicBrightness.updateValue(this.convertBrightnessFromTuyaToHomeKit(changes[this.dpBrightness]));
|
|
42
46
|
});
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
getBrightness() {
|
|
46
|
-
return this.convertBrightnessFromTuyaToHomeKit(this.
|
|
50
|
+
return this.convertBrightnessFromTuyaToHomeKit(this.getStateAsync(this.dpBrightness));
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
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
|
|
@@ -197,7 +197,7 @@ class SimpleGarageDoorAccessory extends BaseAccessory {
|
|
|
197
197
|
if (this.pendingCloseTimer) return;
|
|
198
198
|
const isOpen = this._mapDpState(changes[this.dpState]);
|
|
199
199
|
if (isOpen === null) {
|
|
200
|
-
this.log.
|
|
200
|
+
this.log.debug(`[SimpleGarageDoor] ${this.device.context.name}: ignoring unknown state DP value ${JSON.stringify(changes[this.dpState])}`);
|
|
201
201
|
return;
|
|
202
202
|
}
|
|
203
203
|
this._applyReportedState(isOpen);
|
|
@@ -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.
|
|
@@ -254,8 +266,8 @@ class SimpleGarageDoorAccessory extends BaseAccessory {
|
|
|
254
266
|
// own, even mid-close. Abandon any pending stop-before-close.
|
|
255
267
|
_sendOpen() {
|
|
256
268
|
this._cancelPendingClose();
|
|
257
|
-
this.log.
|
|
258
|
-
this.
|
|
269
|
+
this.log.debug(`[SimpleGarageDoor] ${this.device.context.name}: open (dp${this.dpOpen})`);
|
|
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
|
|
@@ -266,20 +278,20 @@ class SimpleGarageDoorAccessory extends BaseAccessory {
|
|
|
266
278
|
if (this.pendingCloseTimer) {
|
|
267
279
|
// A stop-before-close is already running — don't restart it (and
|
|
268
280
|
// push the close out) on a duplicate or retransmitted request.
|
|
269
|
-
this.log.
|
|
281
|
+
this.log.debug(`[SimpleGarageDoor] ${name}: close ignored — a stop-before-close is already running`);
|
|
270
282
|
return;
|
|
271
283
|
}
|
|
272
284
|
if (this._isStopped()) {
|
|
273
|
-
this.log.
|
|
274
|
-
this.
|
|
285
|
+
this.log.debug(`[SimpleGarageDoor] ${name}: close (dp${this.dpClose}) — gate already stopped`);
|
|
286
|
+
this.setMultiStateLegacyInBackground({[this.dpClose]: true});
|
|
275
287
|
return;
|
|
276
288
|
}
|
|
277
|
-
this.log.
|
|
278
|
-
this.
|
|
289
|
+
this.log.debug(`[SimpleGarageDoor] ${name}: stop-before-close — stop (dp${this.dpStop}) now, close (dp${this.dpClose}) in ${this.stopBeforeCloseMs}ms`);
|
|
290
|
+
this.setMultiStateLegacyInBackground({[this.dpStop]: true});
|
|
279
291
|
this.pendingCloseTimer = setTimeout(() => {
|
|
280
292
|
this.pendingCloseTimer = null;
|
|
281
|
-
this.log.
|
|
282
|
-
this.
|
|
293
|
+
this.log.debug(`[SimpleGarageDoor] ${name}: stop-before-close — firing close (dp${this.dpClose})`);
|
|
294
|
+
this.setMultiStateLegacyInBackground({[this.dpClose]: true});
|
|
283
295
|
}, this.stopBeforeCloseMs);
|
|
284
296
|
}
|
|
285
297
|
|
|
@@ -305,21 +317,22 @@ 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
|
|
311
324
|
// HomeKit/iOS WRITE retransmit) — ignore so the stop isn't pushed
|
|
312
325
|
// out, which would let the gate run further than intended.
|
|
313
|
-
this.log.
|
|
326
|
+
this.log.debug(`[SimpleGarageDoor] ${name}: partial press ignored — a partial open is already running`);
|
|
314
327
|
return;
|
|
315
328
|
}
|
|
316
329
|
|
|
317
|
-
this.log.
|
|
330
|
+
this.log.debug(`[SimpleGarageDoor] ${name}: partial open — opening, will stop in ${this.partialOpenMs}ms`);
|
|
318
331
|
this._applyTarget(Characteristic.TargetDoorState.OPEN);
|
|
319
332
|
this.partialStopTimer = setTimeout(() => {
|
|
320
333
|
this.partialStopTimer = null;
|
|
321
|
-
this.log.
|
|
322
|
-
this.
|
|
334
|
+
this.log.debug(`[SimpleGarageDoor] ${name}: partial open — firing stop (dp${this.dpStop})`);
|
|
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
|
});
|