homebridge-tuya-plus 3.14.0-dev.32 → 3.14.0-dev.33

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/AGENTS.md CHANGED
@@ -91,6 +91,29 @@ When adding or changing a device type, follow the existing pattern:
91
91
  `no-prototype-builtins`) as tech debt; don't rely on or expand that. Keep new
92
92
  code clean.
93
93
 
94
+ ## Logging
95
+
96
+ Users run this in Homebridge and read these logs; a chatty plugin is a real
97
+ complaint. The bar for a non-debug line is high.
98
+
99
+ - **`debug` is the default.** Anything routine, per-message, per-state-change,
100
+ per-connect, or protocol-level (odd/raw/malformed frames, reconnects, socket
101
+ recycling, DP dumps, "X changed: …") goes to `this.log.debug`. If it can fire
102
+ more than a handful of times in normal operation, it's `debug`.
103
+ - **`info`/`warn`/`error` are for what the user must see or act on**, and the
104
+ level must match real severity — a harmless condition (e.g. a cloud-fallback
105
+ failure while the device is reachable over the LAN) is not a `warn`/`error`.
106
+ Prefer one actionable line over a vague one; name the device.
107
+ - **Never spam.** A condition that recurs on a timer or hot path must be
108
+ deduplicated (surface once, then drop repeats to `debug` until it changes) and
109
+ its retry backed off — see `TuyaCloudDevice._onConnectFailure` and the
110
+ discovery port-in-use guard for the pattern.
111
+ - **Use the Homebridge logger** (`this.log` / `this.log.debug|info|warn|error`),
112
+ never `console.*`, in plugin/runtime code. (The standalone `bin/` CLIs are the
113
+ exception: their `console.*` is intentional user-facing output.)
114
+ - **Logging must never throw and must never leak secrets** — don't dump a whole
115
+ config/props object (it carries the device's local `key`); log the id/name.
116
+
94
117
  ## Backwards compatibility (read before changing behavior)
95
118
 
96
119
  This is the most important rule in this repo. Users have working setups and
package/index.js CHANGED
@@ -413,7 +413,10 @@ class TuyaLan {
413
413
  )
414
414
  return;
415
415
 
416
- this.log.info(
416
+ // Each cached accessory starts faulted ("No Response") until its
417
+ // device connects — expected, and once per accessory at startup, so
418
+ // it belongs at debug rather than as a wall of cryptic info lines.
419
+ this.log.debug(
417
420
  'Marked %s unreachable by faulting Service.%s.%s',
418
421
  accessory.displayName,
419
422
  service.displayName,
@@ -80,7 +80,7 @@ class AirPurifierAccessory extends BaseAccessory {
80
80
  _addAirQualityService() {
81
81
  const {Service} = this.hap;
82
82
  const nameAirQuality = this.device.context.nameAirQuality || 'Air Quality';
83
- this.log.info('Adding air quality sensor: %s', nameAirQuality);
83
+ this.log.debug('Adding air quality sensor: %s', nameAirQuality);
84
84
  this.accessory.addService(Service.AirQualitySensor, nameAirQuality);
85
85
  }
86
86
 
@@ -204,7 +204,7 @@ class BaseAccessory {
204
204
  this.colorFunction = this.device.context.colorFunction && {HSB: 'HSB', HEXHSB: 'HEXHSB'}[this.device.context.colorFunction.toUpperCase()];
205
205
  if (!this.colorFunction && value) {
206
206
  this.colorFunction = {12: 'HSB', 14: 'HEXHSB'}[value.length] || 'Unknown';
207
- if (this.colorFunction) this.log.info(`Color format for ${this.device.context.name} (${this.device.context.version}) identified as ${this.colorFunction} (length: ${value.length}).`);
207
+ if (this.colorFunction) this.log.debug(`Color format for ${this.device.context.name} (${this.device.context.version}) identified as ${this.colorFunction} (length: ${value.length}).`);
208
208
  }
209
209
  if (!this.colorFunction) {
210
210
  this.colorFunction = 'Unknown';
@@ -11,7 +11,8 @@ const LOCK_TIMEOUT = 5000;
11
11
 
12
12
  class DoorbellDelegate /*extends CameraStreamingDelegate*/ {
13
13
 
14
- constructor() {
14
+ constructor(log) {
15
+ this.log = log || console;
15
16
  this.cameraImage = undefined;
16
17
  }
17
18
 
@@ -27,7 +28,7 @@ class DoorbellDelegate /*extends CameraStreamingDelegate*/ {
27
28
  var body=Buffer.alloc(0);
28
29
 
29
30
  if (res.statusCode!==200) {
30
- return console.error('HTTP '+res.statusCode);
31
+ return me.log.error('Doorbell image fetch failed: HTTP %s', res.statusCode);
31
32
  }
32
33
 
33
34
  res.on('data', function(chunk) {
@@ -36,13 +37,12 @@ class DoorbellDelegate /*extends CameraStreamingDelegate*/ {
36
37
 
37
38
  res.on('end', function() {
38
39
  me.setCameraImage(body);
39
- console.log("Image stored");
40
+ me.log.debug('Doorbell image stored.');
40
41
  callback();
41
- console.log("Callback after image store complete");
42
42
  });
43
43
 
44
44
  res.on('error', function(err) {
45
- console.error(err);
45
+ me.log.error('Doorbell image fetch error:', err);
46
46
  });
47
47
  }
48
48
  );
@@ -96,12 +96,9 @@ class DoorbellAccessory extends BaseAccessory {
96
96
 
97
97
 
98
98
  this.device.on('change', (changes, state) => {
99
- //console.log(`Changes: ${JSON.stringify(changes)}, State: ${JSON.stringify(state)}`);
100
-
101
99
  if (changes.hasOwnProperty(DP_DOORBELL)) {
102
100
  const urlBase64 = changes[DP_DOORBELL];
103
101
  const strUrl = Buffer.from(urlBase64, 'base64');
104
- //console.log(`Image URL: ${strUrl}`);
105
102
 
106
103
  this.streamingDelegate.storeImage(strUrl.toString(), () => {
107
104
  service.updateCharacteristic(Characteristic.ProgrammableSwitchEvent, Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS);
@@ -109,18 +106,18 @@ class DoorbellAccessory extends BaseAccessory {
109
106
  }
110
107
 
111
108
  if (changes.hasOwnProperty(DP_DOOR)) {
112
- console.log("Door button pressed");
109
+ this.log.debug('Door button pressed');
113
110
  }
114
111
 
115
112
  if (changes.hasOwnProperty(DP_GATE)) {
116
- console.log("Gate button pressed");
113
+ this.log.debug('Gate button pressed');
117
114
  }
118
115
  });
119
116
  }
120
117
 
121
118
  configureCamera() {
122
- this.streamingDelegate = new DoorbellDelegate();
123
- console.log("created streaming delegate");
119
+ this.streamingDelegate = new DoorbellDelegate(this.log);
120
+ this.log.debug('Created camera streaming delegate.');
124
121
  const options = {
125
122
  cameraStreamCount: 1,
126
123
  delegate: this.streamingDelegate,
@@ -157,12 +154,8 @@ class DoorbellAccessory extends BaseAccessory {
157
154
  },
158
155
  };
159
156
 
160
- //console.log("created options");
161
157
  const cameraController = new this.hap.CameraController(options);
162
- //console.log("created controller");
163
-
164
158
  this.accessory.configureController(cameraController);
165
- //console.log("configured controller with accessory");
166
159
  }
167
160
 
168
161
  pushDoor(callback) {
@@ -105,7 +105,7 @@ class GarageDoorAccessory extends BaseAccessory {
105
105
  .onGet(() => this.getCurrentDoorState());
106
106
 
107
107
  this.device.on('change', changes => {
108
- this._alwaysLog('changed:' + JSON.stringify(changes));
108
+ this._debugLog('changed:' + JSON.stringify(changes));
109
109
 
110
110
  if (changes.hasOwnProperty(this.dpStatus)) {
111
111
  const newCurrentDoorState = this._getCurrentDoorState(changes[this.dpStatus]);
@@ -161,7 +161,7 @@ class IrrigationSystemAccessory extends BaseAccessory {
161
161
  this.accessory.services
162
162
  .filter(service => service.UUID === Service.Valve.UUID && !validValveSubtypes.includes(service.subtype))
163
163
  .forEach(service => {
164
- this.log.info('Removing stale valve service %s', service.displayName);
164
+ this.log.debug('Removing stale valve service %s', service.displayName);
165
165
  this.accessory.removeService(service);
166
166
  });
167
167
 
@@ -178,7 +178,7 @@ class IrrigationSystemAccessory extends BaseAccessory {
178
178
  [Service.ContactSensor, Service.LeakSensor].forEach(S => {
179
179
  const svc = this.accessory.getService(S);
180
180
  if (svc) {
181
- this.log.info('Removing rain sensor service %s (no longer supported)', svc.displayName);
181
+ this.log.debug('Removing rain sensor service %s (no longer supported)', svc.displayName);
182
182
  this.accessory.removeService(svc);
183
183
  }
184
184
  });
@@ -34,7 +34,7 @@ class MultiOutletAccessory extends BaseAccessory {
34
34
  this.accessory.services
35
35
  .filter(service => service.UUID === Service.Outlet.UUID && !_validServices.includes(service))
36
36
  .forEach(service => {
37
- this.log.info('Removing', service.displayName);
37
+ this.log.debug('Removing', service.displayName);
38
38
  this.accessory.removeService(service);
39
39
  });
40
40
  }
@@ -90,7 +90,7 @@ class SimpleHeaterAccessory extends BaseAccessory {
90
90
  characteristicCurrentTemperature.updateValue(this._getDividedState(changes[this.dpCurrentTemperature], this.temperatureDivisor, this.currentTemperatureOffset));
91
91
 
92
92
  characteristicCurrentHeaterCoolerState.updateValue(this._getCurrentHeaterCoolerState(state));
93
- this.log.info('SimpleHeater changed: ' + JSON.stringify(state));
93
+ this.log.debug('SimpleHeater changed: ' + JSON.stringify(state));
94
94
  });
95
95
  }
96
96
 
@@ -31,7 +31,7 @@ class SimpleLightAccessory extends BaseAccessory {
31
31
 
32
32
  this.device.on('change', (changes, state) => {
33
33
  if (changes.hasOwnProperty(this.dpPower) && characteristicOn.value !== changes[this.dpPower]) characteristicOn.updateValue(changes[this.dpPower]);
34
- this.log.info('SimpleLight changed: ' + JSON.stringify(state));
34
+ this.log.debug('SimpleLight changed: ' + JSON.stringify(state));
35
35
  });
36
36
  }
37
37
  }
@@ -34,7 +34,7 @@ class SwitchAccessory extends BaseAccessory {
34
34
  this.accessory.services
35
35
  .filter(service => service.UUID === Service.Switch.UUID && !_validServices.includes(service))
36
36
  .forEach(service => {
37
- this.log.info('Removing', service.displayName);
37
+ this.log.debug('Removing', service.displayName);
38
38
  this.accessory.removeService(service);
39
39
  });
40
40
  }
@@ -13,7 +13,13 @@ class TuyaAccessory extends EventEmitter {
13
13
  constructor(props) {
14
14
  super();
15
15
 
16
- if (!(props.id && props.key && props.ip) && !props.fake) return this.log.info('Insufficient details to initialize:', props);
16
+ if (!(props.id && props.key && props.ip) && !props.fake) {
17
+ // `this.log` isn't assigned yet, and `props` carries the device's
18
+ // local key — log via props.log and name only the id (never dump the
19
+ // whole config, which leaks the key into logs/bug reports).
20
+ if (props.log) props.log.warn('Insufficient details to initialize device %s (need id, key and ip).', props.id || '(unknown id)');
21
+ return;
22
+ }
17
23
 
18
24
  this.log = props.log;
19
25
 
@@ -53,7 +59,6 @@ class TuyaAccessory extends EventEmitter {
53
59
 
54
60
  if (this._protocolVersion >= 3.2) {
55
61
  this.context.pingGap = Math.min(this.context.pingGap || 9, 9);
56
- //this.log.info(`Changing ping gap for ${this.context.name} to ${this.context.pingGap}s`);
57
62
  }
58
63
 
59
64
  this.connected = false;
@@ -80,7 +85,6 @@ class TuyaAccessory extends EventEmitter {
80
85
  this._incrementAttemptCounter();
81
86
 
82
87
  (this._socket.reconnect = () => {
83
- //this.log.debug(`reconnect called for ${this.context.name}`);
84
88
  if (this._socket._pinger) {
85
89
  clearTimeout(this._socket._pinger);
86
90
  this._socket._pinger = null;
@@ -196,7 +200,11 @@ class TuyaAccessory extends EventEmitter {
196
200
 
197
201
  this._socket.on('error', err => {
198
202
  this.connected = false;
199
- this.log.info(`Socket had a problem and will reconnect to ${this.context.name} (${err && err.code || err})`);
203
+ // Routine: many Tuya devices recycle their LAN socket every few
204
+ // minutes, and a transient ECONNRESET/EPIPE reconnects silently.
205
+ // Keep the churn at debug; a genuine outage surfaces as HomeKit
206
+ // "No Response" (and via the cloud-fallback warnings).
207
+ this.log.debug(`Socket had a problem and will reconnect to ${this.context.name} (${err && err.code || err})`);
200
208
 
201
209
  if (err && (err.code === 'ECONNRESET' || err.code === 'EPIPE') && this._connectionAttempts < 10) {
202
210
  this.log.debug(`Reconnecting with connection attempts = ${this._connectionAttempts}`);
@@ -209,10 +217,10 @@ class TuyaAccessory extends EventEmitter {
209
217
  if (err) {
210
218
  if (err.code === 'ENOBUFS') {
211
219
  this.log.warn('Operating system complained of resource exhaustion; did I open too many sockets?');
212
- this.log.info('Slowing down retry attempts; if you see this happening often, it could mean some sort of incompatibility.');
220
+ this.log.debug('Slowing down retry attempts; if you see this happening often, it could mean some sort of incompatibility.');
213
221
  delay = 60000;
214
222
  } else if (this._connectionAttempts > 10) {
215
- this.log.info('Slowing down retry attempts; if you see this happening often, it could mean some sort of incompatibility.');
223
+ this.log.debug('Slowing down retry attempts; if you see this happening often, it could mean some sort of incompatibility.');
216
224
  delay = 60000;
217
225
  }
218
226
  }
@@ -240,14 +248,15 @@ class TuyaAccessory extends EventEmitter {
240
248
  clearTimeout(this._socket._pinger);
241
249
  this._socket._pinger = null;
242
250
  }
243
-
244
- //this.log.info('Closed connection with', this.context.name);
245
251
  });
246
252
 
247
253
  this._socket.on('end', () => {
248
254
  this.connected = false;
249
255
  this.session_key = null;
250
- this.log.info('Disconnected from', this.context.name);
256
+ // Routine for devices that recycle their socket; we reconnect
257
+ // promptly below, so keep it at debug rather than announcing a
258
+ // disconnect every few minutes.
259
+ this.log.debug('Disconnected from', this.context.name);
251
260
 
252
261
  // The device closed the connection on its own. This is routine for
253
262
  // many Tuya devices (e.g. LED ceiling lights) that recycle their
@@ -300,7 +309,7 @@ class TuyaAccessory extends EventEmitter {
300
309
  let data = task.msg.slice(len - size, len - 8).toString('utf8').trim().replace(/\0/g, '');
301
310
 
302
311
  if (this.context.intro === false && cmd !== 9)
303
- this.log.info('Message from', this.context.name + ':', data);
312
+ this.log.debug('Message from', this.context.name + ':', data);
304
313
 
305
314
  switch (cmd) {
306
315
  case 7:
@@ -334,7 +343,6 @@ class TuyaAccessory extends EventEmitter {
334
343
  }
335
344
 
336
345
  if (data && data.dps) {
337
- //this.log.info('Update from', this.context.name, 'with command', cmd + ':', data.dps);
338
346
  this._change(data.dps);
339
347
  }
340
348
  break;
@@ -343,7 +351,7 @@ class TuyaAccessory extends EventEmitter {
343
351
  case 10:
344
352
  if (data) {
345
353
  if (data === 'json obj data unvalid') {
346
- this.log.info(`${this.context.name} (${this.context.version}) didn't respond with its current state.`);
354
+ this.log.debug(`${this.context.name} (${this.context.version}) didn't respond with its current state.`);
347
355
  this.emit('change', {}, this.state);
348
356
  break;
349
357
  }
@@ -407,7 +415,7 @@ class TuyaAccessory extends EventEmitter {
407
415
  }
408
416
 
409
417
  if (cmd === 10 && decryptedMsg === 'json obj data unvalid') {
410
- this.log.info(`${this.context.name} (${this.context.version}) didn't respond with its current state.`);
418
+ this.log.debug(`${this.context.name} (${this.context.version}) didn't respond with its current state.`);
411
419
  this.emit('change', {}, this.state);
412
420
  return callback();
413
421
  }
@@ -426,7 +434,6 @@ class TuyaAccessory extends EventEmitter {
426
434
  case 10:
427
435
  if (data) {
428
436
  if (data.dps) {
429
- //this.log.info(`Heard back from ${this.context.name} with command ${cmd}`);
430
437
  this._change(data.dps);
431
438
  } else {
432
439
  this.log.debug(`Malformed message from ${this.context.name} with command ${cmd}:`, decryptedMsg);
@@ -536,7 +543,7 @@ class TuyaAccessory extends EventEmitter {
536
543
  }
537
544
 
538
545
  if (cmd === 10 && parsedPayload === 'json obj data unvalid') {
539
- this.log.info(`${this.context.name} (${this.context.version}) didn't respond with its current state.`);
546
+ this.log.debug(`${this.context.name} (${this.context.version}) didn't respond with its current state.`);
540
547
  this.emit('change', {}, this.state);
541
548
  return callback();
542
549
  }
@@ -547,7 +554,6 @@ class TuyaAccessory extends EventEmitter {
547
554
  case 16:
548
555
  if (parsedPayload) {
549
556
  if (parsedPayload.dps) {
550
- //this.log.info(`Heard back from ${this.context.name} with command ${cmd}`);
551
557
  this._change(parsedPayload.dps);
552
558
  } else {
553
559
  this.log.debug(`Malformed message from ${this.context.name} with command ${cmd}:`, decryptedMsg);
@@ -602,7 +608,7 @@ class TuyaAccessory extends EventEmitter {
602
608
  decipher.setAuthTag(tag);
603
609
  decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
604
610
  } catch(ex) {
605
- this.log.info(`Failed to decrypt message from ${this.context.name} (${this.context.version}) with command ${cmd}:`, ex);
611
+ this.log.debug(`Failed to decrypt message from ${this.context.name} (${this.context.version}) with command ${cmd}:`, ex);
606
612
  return callback();
607
613
  }
608
614
 
@@ -662,7 +668,7 @@ class TuyaAccessory extends EventEmitter {
662
668
  }
663
669
 
664
670
  if (cmd === 10 && parsedPayload === 'json obj data unvalid') {
665
- this.log.info(`${this.context.name} (${this.context.version}) didn't respond with its current state.`);
671
+ this.log.debug(`${this.context.name} (${this.context.version}) didn't respond with its current state.`);
666
672
  this.emit('change', {}, this.state);
667
673
  return callback();
668
674
  }
@@ -701,7 +707,6 @@ class TuyaAccessory extends EventEmitter {
701
707
 
702
708
  let result = false;
703
709
  if (hasDataPoint) {
704
- //this.log.info(" Sending", this.context.name, JSON.stringify(dps));
705
710
  const t = (Date.now() / 1000).toFixed(0);
706
711
  const payload = {
707
712
  devId: this.context.id,
@@ -724,13 +729,14 @@ class TuyaAccessory extends EventEmitter {
724
729
  data,
725
730
  cmd: this._protocolVersion >= 3.4 ? 13 : 7
726
731
  });
727
- if (result !== true) this.log.info(" Result", result);
732
+ // A non-true result here is the socket reporting backpressure (or a
733
+ // closed connection), not necessarily a dropped command; callers that
734
+ // care already check the boolean. Keep it as a debug breadcrumb.
735
+ if (result !== true) this.log.debug(`${this.context.name}: socket write returned ${JSON.stringify(result)}.`);
728
736
  if (this.context.sendEmptyUpdate) {
729
- //this.log.info(" Sending", this.context.name, 'empty signature');
730
737
  this._send({cmd: this._protocolVersion >= 3.4 ? 13 : 7});
731
738
  }
732
739
  } else {
733
- //this.log.info(`Sending first query to ${this.context.name} (${this.context.version})`);
734
740
  result = this._send({
735
741
  data: {
736
742
  gwId: this.context.id,
@@ -962,7 +968,7 @@ class TuyaAccessory extends EventEmitter {
962
968
  }
963
969
 
964
970
  _fakeUpdate(dps) {
965
- this.log.info('Fake update:', JSON.stringify(dps));
971
+ this.log.debug('Fake update:', JSON.stringify(dps));
966
972
  Object.keys(dps).forEach(dp => {
967
973
  this.state[dp] = dps[dp];
968
974
  });
@@ -19,6 +19,7 @@ class TuyaDiscovery extends EventEmitter {
19
19
  this.limitedIds = [];
20
20
  this._servers = {};
21
21
  this._running = false;
22
+ this._portInUseWarned = {}; // ports already warned as in-use, so repeats stay at debug
22
23
  }
23
24
 
24
25
  start(props) {
@@ -85,7 +86,8 @@ class TuyaDiscovery extends EventEmitter {
85
86
  server.on('message', this._onDgramMessage.bind(this, port));
86
87
 
87
88
  server.bind(port, () => {
88
- this.log.info(`Discovery - Discovery started on port ${port}.`);
89
+ this._portInUseWarned[port] = false; // a successful bind re-arms the in-use warning
90
+ this.log.debug(`Discovery started on port ${port}.`);
89
91
  });
90
92
  }
91
93
 
@@ -101,20 +103,30 @@ class TuyaDiscovery extends EventEmitter {
101
103
  this._stop(port);
102
104
 
103
105
  if (err && err.code === 'EADDRINUSE') {
104
- this.log.warn(`Discovery - Port ${port} is in use. Will retry in 15 seconds.`);
106
+ // The retry runs every 15s for as long as the port stays taken (usually
107
+ // another Tuya plugin or a second instance). Warn once so it's
108
+ // actionable, then drop to debug so a persistent conflict doesn't flood
109
+ // the log; a later successful bind re-arms the warning.
110
+ const message = `Port ${port} is in use (is another Tuya plugin or instance running?). Will retry in 15 seconds.`;
111
+ if (this._portInUseWarned[port]) {
112
+ this.log.debug(`Discovery - ${message}`);
113
+ } else {
114
+ this.log.warn(`Discovery - ${message}`);
115
+ this._portInUseWarned[port] = true;
116
+ }
105
117
 
106
118
  setTimeout(() => {
107
119
  this._start(port);
108
120
  }, 15000);
109
121
  } else {
110
- this.log.error(`Discovery - Port ${port} failed:\n${err.stack}`);
122
+ this.log.error(`Discovery - Port ${port} failed:\n${err && err.stack || err}`);
111
123
  }
112
124
  }
113
125
 
114
126
  _onDgramClose(port) {
115
127
  this._stop(port);
116
128
 
117
- this.log.info(`Discovery - Port ${port} closed.${this._running ? ' Restarting...' : ''}`);
129
+ this.log.debug(`Discovery - Port ${port} closed.${this._running ? ' Restarting...' : ''}`);
118
130
  if (this._running)
119
131
  setTimeout(() => {
120
132
  this._start(port);
@@ -152,7 +164,7 @@ class TuyaDiscovery extends EventEmitter {
152
164
  const size = pkt.readUInt32BE(12);
153
165
 
154
166
  if (len - size < 8) {
155
- this.log.error(`Discovery - UDP from ${info.address}:${port} size ${len - size}`);
167
+ this.log.debug(`Discovery - undersized UDP packet from ${info.address}:${port} (ignored).`);
156
168
  return;
157
169
  }
158
170
 
@@ -174,11 +186,14 @@ class TuyaDiscovery extends EventEmitter {
174
186
  if (result && result.gwId && result.ip) {
175
187
  this._onDiscover(result);
176
188
  } else {
177
- this.log.error(`Discovery - UDP from ${info.address}:${port} decrypted`, cleanMsg.toString('hex'));
189
+ this.log.debug(`Discovery - unrecognised UDP payload from ${info.address}:${port} (ignored):`, cleanMsg.toString('hex'));
178
190
  }
179
191
  } catch (ex) {
180
- this.log.error(`Discovery - Failed to parse discovery response on port ${port}: ${decryptedMsg}`);
181
- this.log.error(`Discovery - Failed to parse discovery raw message on port ${port}: ${pkt.toString('hex')}`);
192
+ // Not every Tuya-framed packet on these ports is a discovery reply we
193
+ // can parse (other broadcast types, foreign accounts, partial packets);
194
+ // none of it is an error worth surfacing.
195
+ this.log.debug(`Discovery - could not parse discovery response on port ${port}: ${decryptedMsg}`);
196
+ this.log.debug(`Discovery - raw unparsed message on port ${port}: ${pkt.toString('hex')}`);
182
197
  }
183
198
  }
184
199
 
@@ -230,7 +245,8 @@ class TuyaDiscovery extends EventEmitter {
230
245
  this._onDiscover(payload);
231
246
  }
232
247
  } catch (ex) {
233
- this.log.error(
248
+ // Foreign or partial packets on the v3.5 reply port are expected noise.
249
+ this.log.debug(
234
250
  `Discovery v3.5 – failed to decrypt packet from ${info.address}:${srcPort}:`,
235
251
  ex.message
236
252
  );
@@ -67,10 +67,10 @@ class VerticalBlindsWithTilt extends BaseAccessory {
67
67
 
68
68
  _getInitialPosition(dps) {
69
69
  if (this.accessory.context.cachedPosition !== undefined) {
70
- this.log('[TuyaAccessory] Restored position from cache:', this.accessory.context.cachedPosition + '%');
70
+ this.log.debug('[TuyaAccessory] Restored position from cache:', this.accessory.context.cachedPosition + '%');
71
71
  return this.accessory.context.cachedPosition;
72
72
  }
73
- this.log('[TuyaAccessory] Initial position unknown (first time setup), defaulting to open (100%)');
73
+ this.log.debug('[TuyaAccessory] Initial position unknown (first time setup), defaulting to open (100%)');
74
74
  return 100;
75
75
  }
76
76
 
@@ -80,21 +80,21 @@ class VerticalBlindsWithTilt extends BaseAccessory {
80
80
 
81
81
  if (value === 0) {
82
82
  if (this.currentPosition === 0) {
83
- this.log('[TuyaAccessory] Blinds already closed, skipping close command');
83
+ this.log.debug('[TuyaAccessory] Blinds already closed, skipping close command');
84
84
  return;
85
85
  }
86
86
 
87
- this.log('[TuyaAccessory] Closing blinds');
87
+ this.log.debug('[TuyaAccessory] Closing blinds');
88
88
  this.lastCloseTime = Date.now();
89
89
  this.isMoving = true;
90
90
 
91
91
  if (this.lastTiltCommand && (Date.now() - this.lastTiltCommand.time < this.timeToClose * 1000)) {
92
- this.log('[TuyaAccessory] Close command received while tilt was pending - rescheduling tilt delay');
92
+ this.log.debug('[TuyaAccessory] Close command received while tilt was pending - rescheduling tilt delay');
93
93
  if (this.tiltBufferTimeout) { clearTimeout(this.tiltBufferTimeout); this.tiltBufferTimeout = null; }
94
94
  if (this.tiltDelayTimeout) clearTimeout(this.tiltDelayTimeout);
95
95
  const delayMs = this.timeToClose * 1000;
96
96
  this.tiltDelayTimeout = setTimeout(() => {
97
- this.log('[TuyaAccessory] Executing delayed tilt angle:', this.lastTiltCommand.angle, '-> Tuya percent_control:', this.lastTiltCommand.value);
97
+ this.log.debug('[TuyaAccessory] Executing delayed tilt angle:', this.lastTiltCommand.angle, '-> Tuya percent_control:', this.lastTiltCommand.value);
98
98
  this._executeTilt(this.lastTiltCommand.value);
99
99
  this.tiltDelayTimeout = null;
100
100
  this.lastTiltCommand = null;
@@ -106,7 +106,7 @@ class VerticalBlindsWithTilt extends BaseAccessory {
106
106
 
107
107
  if (this.positionStateTimeout) clearTimeout(this.positionStateTimeout);
108
108
  this.positionStateTimeout = setTimeout(() => {
109
- this.log('[TuyaAccessory] Blinds finished closing');
109
+ this.log.debug('[TuyaAccessory] Blinds finished closing');
110
110
  this.currentPosition = 0;
111
111
  this.accessory.context.cachedPosition = 0;
112
112
  this.characteristicCurrentPosition.updateValue(0);
@@ -120,19 +120,19 @@ class VerticalBlindsWithTilt extends BaseAccessory {
120
120
 
121
121
  } else if (value === 100) {
122
122
  if (this.currentPosition === 100) {
123
- this.log('[TuyaAccessory] Blinds already open, skipping open command');
123
+ this.log.debug('[TuyaAccessory] Blinds already open, skipping open command');
124
124
  return;
125
125
  }
126
126
 
127
- this.log('[TuyaAccessory] Opening blinds');
127
+ this.log.debug('[TuyaAccessory] Opening blinds');
128
128
 
129
129
  if (this.lastTiltCommand && (Date.now() - this.lastTiltCommand.time < this.timeToClose * 1000)) {
130
- this.log('[TuyaAccessory] Open command received while tilt was pending - rescheduling tilt delay');
130
+ this.log.debug('[TuyaAccessory] Open command received while tilt was pending - rescheduling tilt delay');
131
131
  if (this.tiltBufferTimeout) { clearTimeout(this.tiltBufferTimeout); this.tiltBufferTimeout = null; }
132
132
  if (this.tiltDelayTimeout) clearTimeout(this.tiltDelayTimeout);
133
133
  const delayMs = this.timeToClose * 1000;
134
134
  this.tiltDelayTimeout = setTimeout(() => {
135
- this.log('[TuyaAccessory] Executing delayed tilt angle:', this.lastTiltCommand.angle, '-> Tuya percent_control:', this.lastTiltCommand.value);
135
+ this.log.debug('[TuyaAccessory] Executing delayed tilt angle:', this.lastTiltCommand.angle, '-> Tuya percent_control:', this.lastTiltCommand.value);
136
136
  this._executeTilt(this.lastTiltCommand.value);
137
137
  this.tiltDelayTimeout = null;
138
138
  this.lastTiltCommand = null;
@@ -149,7 +149,7 @@ class VerticalBlindsWithTilt extends BaseAccessory {
149
149
 
150
150
  if (this.positionStateTimeout) clearTimeout(this.positionStateTimeout);
151
151
  this.positionStateTimeout = setTimeout(() => {
152
- this.log('[TuyaAccessory] Blinds finished opening');
152
+ this.log.debug('[TuyaAccessory] Blinds finished opening');
153
153
  this.currentPosition = 100;
154
154
  this.accessory.context.cachedPosition = 100;
155
155
  this.characteristicCurrentPosition.updateValue(100);
@@ -161,15 +161,15 @@ class VerticalBlindsWithTilt extends BaseAccessory {
161
161
  return this.setStateAsync(this.dpAction, 'open');
162
162
 
163
163
  } else {
164
- this.log('[TuyaAccessory] Partial position requested, opening fully');
164
+ this.log.debug('[TuyaAccessory] Partial position requested, opening fully');
165
165
 
166
166
  if (this.lastTiltCommand && (Date.now() - this.lastTiltCommand.time < this.timeToClose * 1000)) {
167
- this.log('[TuyaAccessory] Partial open command received while tilt was pending - rescheduling tilt delay');
167
+ this.log.debug('[TuyaAccessory] Partial open command received while tilt was pending - rescheduling tilt delay');
168
168
  if (this.tiltBufferTimeout) { clearTimeout(this.tiltBufferTimeout); this.tiltBufferTimeout = null; }
169
169
  if (this.tiltDelayTimeout) clearTimeout(this.tiltDelayTimeout);
170
170
  const delayMs = this.timeToClose * 1000;
171
171
  this.tiltDelayTimeout = setTimeout(() => {
172
- this.log('[TuyaAccessory] Executing delayed tilt angle:', this.lastTiltCommand.angle, '-> Tuya percent_control:', this.lastTiltCommand.value);
172
+ this.log.debug('[TuyaAccessory] Executing delayed tilt angle:', this.lastTiltCommand.angle, '-> Tuya percent_control:', this.lastTiltCommand.value);
173
173
  this._executeTilt(this.lastTiltCommand.value);
174
174
  this.tiltDelayTimeout = null;
175
175
  this.lastTiltCommand = null;
@@ -186,7 +186,7 @@ class VerticalBlindsWithTilt extends BaseAccessory {
186
186
 
187
187
  if (this.positionStateTimeout) clearTimeout(this.positionStateTimeout);
188
188
  this.positionStateTimeout = setTimeout(() => {
189
- this.log('[TuyaAccessory] Blinds finished opening');
189
+ this.log.debug('[TuyaAccessory] Blinds finished opening');
190
190
  this.currentPosition = 100;
191
191
  this.accessory.context.cachedPosition = 100;
192
192
  this.characteristicCurrentPosition.updateValue(100);
@@ -223,12 +223,12 @@ class VerticalBlindsWithTilt extends BaseAccessory {
223
223
  ? (this.timeToClose * 1000) - timeSinceClose
224
224
  : (this.timeToClose * 1000) - timeSinceOpen;
225
225
  const action = shouldDelayForClose ? 'close' : 'open';
226
- this.log('[TuyaAccessory] Tilt command received during', action, '- delaying by', Math.round(delayMs / 1000), 'seconds');
226
+ this.log.debug('[TuyaAccessory] Tilt command received during', action, '- delaying by', Math.round(delayMs / 1000), 'seconds');
227
227
 
228
228
  this._cancelPendingTilts();
229
229
 
230
230
  this.tiltDelayTimeout = setTimeout(() => {
231
- this.log('[TuyaAccessory] Executing delayed tilt angle:', value, '-> Tuya percent_control:', clampedValue);
231
+ this.log.debug('[TuyaAccessory] Executing delayed tilt angle:', value, '-> Tuya percent_control:', clampedValue);
232
232
  this._executeTilt(clampedValue);
233
233
  this.tiltDelayTimeout = null;
234
234
  this.lastCloseTime = 0;
@@ -237,7 +237,7 @@ class VerticalBlindsWithTilt extends BaseAccessory {
237
237
  }, delayMs);
238
238
 
239
239
  } else {
240
- this.log('[TuyaAccessory] Setting tilt angle:', value, '-> Tuya percent_control:', clampedValue);
240
+ this.log.debug('[TuyaAccessory] Setting tilt angle:', value, '-> Tuya percent_control:', clampedValue);
241
241
 
242
242
  if (this.tiltBufferTimeout) clearTimeout(this.tiltBufferTimeout);
243
243
 
@@ -258,7 +258,7 @@ class VerticalBlindsWithTilt extends BaseAccessory {
258
258
 
259
259
  _executeTilt(value) {
260
260
  if (!this.device.connected) {
261
- this.log('[TuyaAccessory] Cannot execute tilt - device not connected');
261
+ this.log.debug('[TuyaAccessory] Cannot execute tilt - device not connected');
262
262
  return;
263
263
  }
264
264
  this.device.update({[this.dpTilt]: value});
@@ -268,12 +268,12 @@ class VerticalBlindsWithTilt extends BaseAccessory {
268
268
  if (this.tiltBufferTimeout) {
269
269
  clearTimeout(this.tiltBufferTimeout);
270
270
  this.tiltBufferTimeout = null;
271
- this.log('[TuyaAccessory] Cleared pending tilt buffer');
271
+ this.log.debug('[TuyaAccessory] Cleared pending tilt buffer');
272
272
  }
273
273
  if (this.tiltDelayTimeout) {
274
274
  clearTimeout(this.tiltDelayTimeout);
275
275
  this.tiltDelayTimeout = null;
276
- this.log('[TuyaAccessory] Cleared pending tilt delay');
276
+ this.log.debug('[TuyaAccessory] Cleared pending tilt delay');
277
277
  }
278
278
  }
279
279
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-tuya-plus",
3
- "version": "3.14.0-dev.32",
3
+ "version": "3.14.0-dev.33",
4
4
  "description": "A community-maintained Homebridge plugin for controlling Tuya devices in HomeKit. LAN-first, with an optional Tuya Cloud fallback for any device the LAN can't reach.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -607,9 +607,10 @@ describe('graceful disconnect handling', () => {
607
607
  jest.advanceTimersByTime(1000);
608
608
  expect(pingErrors).toHaveLength(0);
609
609
 
610
- // The log reflects a clean disconnect, never a ping failure.
611
- expect(device.log.info).toHaveBeenCalledWith('Disconnected from', device.context.name);
612
- const logged = device.log.info.mock.calls.flat().join(' ');
610
+ // The log reflects a clean disconnect, never a ping failure. (A device
611
+ // recycling its socket is routine, so the disconnect is logged at debug.)
612
+ expect(device.log.debug).toHaveBeenCalledWith('Disconnected from', device.context.name);
613
+ const logged = device.log.debug.mock.calls.flat().join(' ');
613
614
  expect(logged).not.toMatch(/ERR_PING_TIMED_OUT/);
614
615
  });
615
616