homebridge-tuya-plus 3.14.0-dev.31 → 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 +23 -0
- package/index.js +4 -1
- package/lib/AirPurifierAccessory.js +1 -1
- package/lib/BaseAccessory.js +1 -1
- package/lib/DoorbellAccessory.js +9 -16
- package/lib/GarageDoorAccessory.js +1 -1
- package/lib/IrrigationSystemAccessory.js +2 -2
- package/lib/MultiOutletAccessory.js +1 -1
- package/lib/SimpleHeaterAccessory.js +1 -1
- package/lib/SimpleLightAccessory.js +1 -1
- package/lib/SwitchAccessory.js +1 -1
- package/lib/TuyaAccessory.js +47 -41
- package/lib/TuyaDiscovery.js +25 -9
- package/lib/VerticalBlindsWithTilt.js +22 -22
- package/package.json +1 -1
- package/test/TuyaAccessory.protocol.test.js +4 -3
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
|
-
|
|
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.
|
|
83
|
+
this.log.debug('Adding air quality sensor: %s', nameAirQuality);
|
|
84
84
|
this.accessory.addService(Service.AirQualitySensor, nameAirQuality);
|
|
85
85
|
}
|
|
86
86
|
|
package/lib/BaseAccessory.js
CHANGED
|
@@ -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.
|
|
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';
|
package/lib/DoorbellAccessory.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
+
this.log.debug('Door button pressed');
|
|
113
110
|
}
|
|
114
111
|
|
|
115
112
|
if (changes.hasOwnProperty(DP_GATE)) {
|
|
116
|
-
|
|
113
|
+
this.log.debug('Gate button pressed');
|
|
117
114
|
}
|
|
118
115
|
});
|
|
119
116
|
}
|
|
120
117
|
|
|
121
118
|
configureCamera() {
|
|
122
|
-
this.streamingDelegate = new DoorbellDelegate();
|
|
123
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
34
|
+
this.log.debug('SimpleLight changed: ' + JSON.stringify(state));
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
37
|
}
|
package/lib/SwitchAccessory.js
CHANGED
|
@@ -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.
|
|
37
|
+
this.log.debug('Removing', service.displayName);
|
|
38
38
|
this.accessory.removeService(service);
|
|
39
39
|
});
|
|
40
40
|
}
|
package/lib/TuyaAccessory.js
CHANGED
|
@@ -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)
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
312
|
+
this.log.debug('Message from', this.context.name + ':', data);
|
|
304
313
|
|
|
305
314
|
switch (cmd) {
|
|
306
315
|
case 7:
|
|
@@ -328,13 +337,12 @@ class TuyaAccessory extends EventEmitter {
|
|
|
328
337
|
data = JSON.parse(decryptedMsg);
|
|
329
338
|
} catch (ex) {
|
|
330
339
|
data = decryptedMsg;
|
|
331
|
-
this.log.
|
|
332
|
-
this.log.
|
|
340
|
+
this.log.debug(`Odd message from ${this.context.name} with command ${cmd}:`, data);
|
|
341
|
+
this.log.debug(`Raw message from ${this.context.name} (${this.context.version}) with command ${cmd}:`, task.msg.toString('hex'));
|
|
333
342
|
break;
|
|
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.
|
|
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
|
}
|
|
@@ -351,8 +359,8 @@ class TuyaAccessory extends EventEmitter {
|
|
|
351
359
|
try {
|
|
352
360
|
data = JSON.parse(data);
|
|
353
361
|
} catch (ex) {
|
|
354
|
-
this.log.
|
|
355
|
-
this.log.
|
|
362
|
+
this.log.debug(`Malformed update from ${this.context.name} with command ${cmd}:`, data);
|
|
363
|
+
this.log.debug(`Raw update from ${this.context.name} (${this.context.version}) with command ${cmd}:`, task.msg.toString('hex'));
|
|
356
364
|
break;
|
|
357
365
|
}
|
|
358
366
|
|
|
@@ -361,8 +369,8 @@ class TuyaAccessory extends EventEmitter {
|
|
|
361
369
|
break;
|
|
362
370
|
|
|
363
371
|
default:
|
|
364
|
-
this.log.
|
|
365
|
-
this.log.
|
|
372
|
+
this.log.debug(`Odd message from ${this.context.name} with command ${cmd}:`, data);
|
|
373
|
+
this.log.debug(`Raw message from ${this.context.name} (${this.context.version}) with command ${cmd}:`, task.msg.toString('hex'));
|
|
366
374
|
}
|
|
367
375
|
|
|
368
376
|
callback();
|
|
@@ -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.
|
|
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
|
}
|
|
@@ -416,8 +424,8 @@ class TuyaAccessory extends EventEmitter {
|
|
|
416
424
|
try {
|
|
417
425
|
data = JSON.parse(decryptedMsg);
|
|
418
426
|
} catch(ex) {
|
|
419
|
-
this.log.
|
|
420
|
-
this.log.
|
|
427
|
+
this.log.debug(`Odd message from ${this.context.name} with command ${cmd}:`, decryptedMsg);
|
|
428
|
+
this.log.debug(`Raw message from ${this.context.name} (${this.context.version}) with command ${cmd}:`, task.msg.toString('hex'));
|
|
421
429
|
return callback();
|
|
422
430
|
}
|
|
423
431
|
|
|
@@ -426,18 +434,17 @@ 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
|
-
this.log.
|
|
433
|
-
this.log.
|
|
439
|
+
this.log.debug(`Malformed message from ${this.context.name} with command ${cmd}:`, decryptedMsg);
|
|
440
|
+
this.log.debug(`Raw message from ${this.context.name} (${this.context.version}) with command ${cmd}:`, task.msg.toString('hex'));
|
|
434
441
|
}
|
|
435
442
|
}
|
|
436
443
|
break;
|
|
437
444
|
|
|
438
445
|
default:
|
|
439
|
-
this.log.
|
|
440
|
-
this.log.
|
|
446
|
+
this.log.debug(`Odd message from ${this.context.name} with command ${cmd}:`, decryptedMsg);
|
|
447
|
+
this.log.debug(`Raw message from ${this.context.name} (${this.context.version}) with command ${cmd}:`, task.msg.toString('hex'));
|
|
441
448
|
}
|
|
442
449
|
|
|
443
450
|
callback();
|
|
@@ -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.
|
|
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,18 +554,17 @@ 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
|
-
this.log.
|
|
554
|
-
this.log.
|
|
559
|
+
this.log.debug(`Malformed message from ${this.context.name} with command ${cmd}:`, decryptedMsg);
|
|
560
|
+
this.log.debug(`Raw message from ${this.context.name} (${this.context.version}) with command ${cmd}:`, task.msg.toString('hex'));
|
|
555
561
|
}
|
|
556
562
|
}
|
|
557
563
|
break;
|
|
558
564
|
|
|
559
565
|
default:
|
|
560
|
-
this.log.
|
|
561
|
-
this.log.
|
|
566
|
+
this.log.debug(`Odd message from ${this.context.name} with command ${cmd}:`, decryptedMsg);
|
|
567
|
+
this.log.debug(`Raw message from ${this.context.name} (${this.context.version}) with command ${cmd}:`, task.msg.toString('hex'));
|
|
562
568
|
}
|
|
563
569
|
|
|
564
570
|
callback();
|
|
@@ -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.
|
|
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.
|
|
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
|
}
|
|
@@ -674,11 +680,11 @@ class TuyaAccessory extends EventEmitter {
|
|
|
674
680
|
if (parsedPayload && parsedPayload.dps) {
|
|
675
681
|
this._change(parsedPayload.dps);
|
|
676
682
|
} else {
|
|
677
|
-
this.log.
|
|
683
|
+
this.log.debug(`Malformed message from ${this.context.name} with command ${cmd}:`, payload.toString('utf8'));
|
|
678
684
|
}
|
|
679
685
|
break;
|
|
680
686
|
default:
|
|
681
|
-
this.log.
|
|
687
|
+
this.log.debug(`Odd message from ${this.context.name} with command ${cmd}:`, payload.toString('utf8'));
|
|
682
688
|
}
|
|
683
689
|
|
|
684
690
|
callback();
|
|
@@ -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
|
-
|
|
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.
|
|
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
|
});
|
package/lib/TuyaDiscovery.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
189
|
+
this.log.debug(`Discovery - unrecognised UDP payload from ${info.address}:${port} (ignored):`, cleanMsg.toString('hex'));
|
|
178
190
|
}
|
|
179
191
|
} catch (ex) {
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
612
|
-
|
|
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
|
|