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 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:
@@ -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', 'SimpleDimmer'].includes(model.devices[arrayIndices].type);"
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: WledDimmerAccessory,
64
+ simpledimmer: SimpleDimmerAccessory,
64
65
  wleddimmer: WledDimmerAccessory,
65
66
  simpledimmer2: SimpleDimmer2Accessory,
66
67
  simpleblinds: SimpleBlindsAccessory,
@@ -76,7 +76,7 @@ class BaseAccessory {
76
76
  }
77
77
 
78
78
  getState(dp, callback) {
79
- if (!this.device.connected) return callback(true);
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(!ret);
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
- this.__ret = this.device.update({[dp.toString()] : dps[dp]});
118
+ if (this.device.update({[dp.toString()] : dps[dp]}) === false) failed = true;
118
119
  }
119
120
  }
120
- callback && callback(!this.__ret);
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(true);
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
- setMultiStateAsync(dps) {
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
- return;
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
- return;
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.setMultiStateAsync(props);
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.setMultiStateAsync(props);
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.device.state[this.dpMode] === this.cmdWhite) return this.convertBrightnessFromTuyaToHomeKit(this.device.state[this.dpColor]).b;
207
- return this.convertColorFromTuyaToHomeKit(this.device.state[this.dpColor]).b;
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.device.state[this.dpMode] !== this.cmdWhite) return 0;
222
- return this.convertColorTemperatureFromTuyaToHomeKit(this.device.state[this.dpColorTemperature]);
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.device.state[this.dpMode] === this.cmdWhite) return 0;
238
- return this.convertColorFromTuyaToHomeKit(this.device.state[this.dpColor]).h;
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.device.state[this.dpColor]).s;
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.setMultiStateAsync({[this.dpMode]: this.cmdColor, [this.dpColor]: newValue});
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.device.state[this.dpMode] === this.cmdWhite) return this.convertBrightnessFromTuyaToHomeKit(this.device.state[this.dpBrightness]);
138
- return this.convertColorFromTuyaToHomeKit(this.device.state[this.dpColor]).b;
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.device.state[this.dpMode] !== this.cmdWhite) return this.minWhiteColor;
149
- return this.convertColorTemperatureFromTuyaToHomeKit(this.device.state[this.dpColorTemperature]);
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.device.state[this.dpMode] === this.cmdWhite) {
169
- return this.convertHomeKitColorTemperatureToHomeKitColor(this.convertColorTemperatureFromTuyaToHomeKit(this.device.state[this.dpColorTemperature])).h;
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.device.state[this.dpColor]).h;
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.device.state[this.dpMode] === this.cmdWhite) {
180
- return this.convertHomeKitColorTemperatureToHomeKitColor(this.convertColorTemperatureFromTuyaToHomeKit(this.device.state[this.dpColorTemperature])).s;
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.device.state[this.dpColor]).s;
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.setMultiStateAsync({[this.dpMode]: this.cmdColor, [this.dpColor]: newValue});
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.device.state[this.dpMode] === this.cmdWhite) return this.convertBrightnessFromTuyaToHomeKit(this.device.state[this.dpBrightness]);
200
- return this.convertColorFromTuyaToHomeKit(this.device.state[this.dpColor]).b;
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.device.state[this.dpMode] !== this.cmdWhite) return 0;
210
- return this.convertColorTemperatureFromTuyaToHomeKit(this.device.state[this.dpColorTemperature]);
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.device.state[this.dpMode] === this.cmdWhite) return 0;
225
- return this.convertColorFromTuyaToHomeKit(this.device.state[this.dpColor]).h;
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.device.state[this.dpMode] === this.cmdWhite) return 0;
234
- return this.convertColorFromTuyaToHomeKit(this.device.state[this.dpColor]).s;
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.setMultiStateAsync({[this.dpMode]: this.cmdColor, [this.dpColor]: newValue});
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.device.state[this.dpAction]);
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.device.state[this.dpAction]);
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.setStateAsync(this.dpAction, this.cmdStop);
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.setStateAsync(this.dpAction, this.cmdStop);
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.device.state[this.dpBrightness]);
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
- const characteristicOn = service.getCharacteristic(Characteristic.On)
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
- const characteristicBrightness = service.getCharacteristic(Characteristic.Brightness)
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) && characteristicOn.value !== changes[this.dpPower]) characteristicOn.updateValue(changes[this.dpPower]);
40
- if (changes.hasOwnProperty(this.dpBrightness) && this.convertBrightnessFromHomeKitToTuya(characteristicBrightness.value) !== changes[this.dpBrightness])
41
- characteristicBrightness.updateValue(this.convertBrightnessFromTuyaToHomeKit(changes[this.dpBrightness]));
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.device.state[this.dpBrightness]);
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.info(`[SimpleGarageDoor] ${this.device.context.name}: ignoring unknown state DP value ${JSON.stringify(changes[this.dpState])}`);
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.info(`[SimpleGarageDoor] ${this.device.context.name}: open (dp${this.dpOpen})`);
258
- this.setMultiStateLegacyAsync({[this.dpOpen]: true});
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.info(`[SimpleGarageDoor] ${name}: close ignored — a stop-before-close is already running`);
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.info(`[SimpleGarageDoor] ${name}: close (dp${this.dpClose}) — gate already stopped`);
274
- this.setMultiStateLegacyAsync({[this.dpClose]: true});
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.info(`[SimpleGarageDoor] ${name}: stop-before-close — stop (dp${this.dpStop}) now, close (dp${this.dpClose}) in ${this.stopBeforeCloseMs}ms`);
278
- this.setMultiStateLegacyAsync({[this.dpStop]: true});
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.info(`[SimpleGarageDoor] ${name}: stop-before-close — firing close (dp${this.dpClose})`);
282
- this.setMultiStateLegacyAsync({[this.dpClose]: true});
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.info(`[SimpleGarageDoor] ${name}: partial press ignored — a partial open is already running`);
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.info(`[SimpleGarageDoor] ${name}: partial open — opening, will stop in ${this.partialOpenMs}ms`);
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.info(`[SimpleGarageDoor] ${name}: partial open — firing stop (dp${this.dpStop})`);
322
- this.setMultiStateLegacyAsync({[this.dpStop]: true});
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)
@@ -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.setMultiStateAsync(props);
86
+ this.setMultiStateInBackground(props);
86
87
  resolvers.forEach(r => r());
87
88
  }, 500);
88
89
  });