homebridge-tuya-plus 3.14.0-dev.12 → 3.14.0-dev.14
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
|
@@ -12,6 +12,8 @@ All notable changes to this project will be documented in this file. This projec
|
|
|
12
12
|
* [*] **Fix realtime (MQTT) cloud updates being silently dropped** — external changes (physical buttons, the Tuya app, the device's own timers) now show up in HomeKit within a second or two. The decryptor was verifying the AES-GCM auth tag (`decipher.final()`), but Tuya's real status frames don't carry a tag that verifies against the documented AAD, so every realtime message was being thrown away. Now decrypts with `update()` only, matching the official `tuya/tuya-homebridge` and `0x5e/homebridge-tuya-platform` implementations.
|
|
13
13
|
* [*] **Fix cloud irrigation valves that could be turned on but not off** — the per-zone write coalescer was dropping any command that matched the last-known `device.state`. Cloud devices never optimistically advance `state` (it only moves once the realtime stream confirms the device), so an "off" issued before the "on" was echoed matched the stale "off" and was discarded — HomeKit showed the zone closed while it kept running. Queued commands are now sent as-is (callers already queue only genuine changes).
|
|
14
14
|
* [*] **IrrigationSystem: remove the rain sensor.** It never reported reliably on these devices, and bundling a sensor (a different HomeKit category) in the same accessory forced the Home app to fragment the sprinkler into "sub-accessories" — blocking control from the main tile and hiding the system master on/off. The accessory is now a single, clean sprinkler tile (IrrigationSystem + valves + optional battery); any leftover Contact/Leak sensor service from a previous build is removed automatically on restart. The `noRainSensor`, `rainSensorType`, `rainInverted`, `dpRain` and `rainOnValue` options are gone.
|
|
15
|
+
* [*] **IrrigationSystem: add the HAP Service Label service for multi-valve controllers** — an accessory that exposes a collection of same-type services (more than one `Valve`) must include a `ServiceLabel` service to anchor each valve's `ServiceLabelIndex`. It was missing, so stricter Home app clients (notably iOS) scattered the zones as separate tiles instead of nesting them under the single irrigation tile. The service is added automatically (with the Arabic-numerals namespace) whenever there is more than one valve; user-set zone names still take precedence.
|
|
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.
|
|
15
17
|
|
|
16
18
|
## 2.0.1 (2021-03-25)
|
|
17
19
|
This update includes the following changes:
|
|
@@ -13,6 +13,8 @@ const BaseAccessory = require('./BaseAccessory');
|
|
|
13
13
|
* ├─ Valve "Zone A" (linked, ValveType=IRRIGATION, ServiceLabelIndex 1)
|
|
14
14
|
* ├─ Valve "Zone B" (linked, ServiceLabelIndex 2)
|
|
15
15
|
* ├─ ...
|
|
16
|
+
* ServiceLabel (labels the multi-valve collection so the Home app nests
|
|
17
|
+
* the zones under the system tile; added when >1 valve)
|
|
16
18
|
* Battery (BatteryLevel + StatusLowBattery)
|
|
17
19
|
*
|
|
18
20
|
* Each Valve carries its own SetDuration/RemainingDuration so the Home app shows
|
|
@@ -140,6 +142,25 @@ class IrrigationSystemAccessory extends BaseAccessory {
|
|
|
140
142
|
irrigation.addLinkedService(valve);
|
|
141
143
|
});
|
|
142
144
|
|
|
145
|
+
// --- Service Label (required when the accessory exposes a *collection*
|
|
146
|
+
// of same-type services, i.e. more than one Valve) ---
|
|
147
|
+
// HAP's Service Label profile says an accessory holding several services
|
|
148
|
+
// of the same type must carry a Service Label service, with each member
|
|
149
|
+
// service tagged by a ServiceLabelIndex. We already set the indices; the
|
|
150
|
+
// anchoring Service Label service was missing. Without it the stricter
|
|
151
|
+
// Home app clients (notably iOS) scatter the zones as separate tiles
|
|
152
|
+
// rather than nesting them under the irrigation system — macOS happens to
|
|
153
|
+
// render leniently either way. The namespace is set in
|
|
154
|
+
// _registerCharacteristics. A single-valve system is not a collection, so
|
|
155
|
+
// it needs no label.
|
|
156
|
+
const multiValve = valveConfigs.length > 1;
|
|
157
|
+
const existingLabel = this.accessory.getService(Service.ServiceLabel);
|
|
158
|
+
if (multiValve && !existingLabel) {
|
|
159
|
+
this.accessory.addService(Service.ServiceLabel, this.device.context.name);
|
|
160
|
+
} else if (!multiValve && existingLabel) {
|
|
161
|
+
this.accessory.removeService(existingLabel);
|
|
162
|
+
}
|
|
163
|
+
|
|
143
164
|
// --- Battery (optional) ---
|
|
144
165
|
if (this._hasBattery()) {
|
|
145
166
|
if (!this.accessory.getService(Service.Battery)) {
|
|
@@ -217,7 +238,7 @@ class IrrigationSystemAccessory extends BaseAccessory {
|
|
|
217
238
|
|
|
218
239
|
this._systemActiveChar = irrigation.getCharacteristic(Characteristic.Active)
|
|
219
240
|
.updateValue(this._anyValveOn() ? Characteristic.Active.ACTIVE : Characteristic.Active.INACTIVE)
|
|
220
|
-
.onGet(() => this.
|
|
241
|
+
.onGet(() => this._reportCached(this._systemActiveChar))
|
|
221
242
|
.onSet(value => this._setSystemActive(value));
|
|
222
243
|
|
|
223
244
|
this._systemInUseChar = irrigation.getCharacteristic(Characteristic.InUse)
|
|
@@ -260,10 +281,10 @@ class IrrigationSystemAccessory extends BaseAccessory {
|
|
|
260
281
|
.updateValue(0)
|
|
261
282
|
.onGet(() => this._valveRemaining(cfg));
|
|
262
283
|
|
|
263
|
-
valve.getCharacteristic(Characteristic.Active)
|
|
284
|
+
const activeChar = valve.getCharacteristic(Characteristic.Active)
|
|
264
285
|
.updateValue(on ? Characteristic.Active.ACTIVE : Characteristic.Active.INACTIVE)
|
|
265
|
-
.onGet(() => (this.getStateAsync(cfg.dp) ? 1 : 0))
|
|
266
286
|
.onSet(value => this._setValveActive(cfg, value));
|
|
287
|
+
activeChar.onGet(() => this._reportCached(activeChar));
|
|
267
288
|
|
|
268
289
|
valve.getCharacteristic(Characteristic.InUse)
|
|
269
290
|
.updateValue(on ? Characteristic.InUse.IN_USE : Characteristic.InUse.NOT_IN_USE);
|
|
@@ -273,6 +294,19 @@ class IrrigationSystemAccessory extends BaseAccessory {
|
|
|
273
294
|
if (on) this._reflectValve(cfg, true);
|
|
274
295
|
});
|
|
275
296
|
|
|
297
|
+
// --- Service Label namespace ---
|
|
298
|
+
// Anchors the per-valve ServiceLabelIndex so the Home app groups the
|
|
299
|
+
// zones under the irrigation tile (see _verifyCachedPlatformAccessory).
|
|
300
|
+
// Arabic numerals: zones are labelled 1, 2, 3… — the user-set zone names
|
|
301
|
+
// (ConfiguredName) still take precedence in the Home app.
|
|
302
|
+
if (this._valves.length > 1) {
|
|
303
|
+
const serviceLabel = this.accessory.getService(Service.ServiceLabel);
|
|
304
|
+
if (serviceLabel) {
|
|
305
|
+
serviceLabel.getCharacteristic(Characteristic.ServiceLabelNamespace)
|
|
306
|
+
.updateValue(Characteristic.ServiceLabelNamespace.ARABIC_NUMERALS);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
276
310
|
// --- Battery ---
|
|
277
311
|
if (this._hasBattery()) {
|
|
278
312
|
const battery = this.accessory.getService(Service.Battery);
|
|
@@ -330,6 +364,19 @@ class IrrigationSystemAccessory extends BaseAccessory {
|
|
|
330
364
|
if (valveChanged) this._syncAggregate();
|
|
331
365
|
}
|
|
332
366
|
|
|
367
|
+
// onGet handler for the system / valve Active characteristics. Returns the
|
|
368
|
+
// value HomeKit already shows — the optimistic value written on press and
|
|
369
|
+
// then confirmed (or corrected) by device-side change events — rather than
|
|
370
|
+
// the raw `device.state`. Cloud devices don't advance `device.state` until
|
|
371
|
+
// the realtime stream echoes the write back, so reading it here returned the
|
|
372
|
+
// pre-press value for a moment and made the Home app toggle visibly flicker
|
|
373
|
+
// back to the old state before settling. Throwing while disconnected keeps
|
|
374
|
+
// the HomeKit "No Response" behaviour.
|
|
375
|
+
_reportCached(char) {
|
|
376
|
+
if (!this.device.connected) throw new Error('Not connected');
|
|
377
|
+
return char.value;
|
|
378
|
+
}
|
|
379
|
+
|
|
333
380
|
/* ------------------------------------------------------------------ *
|
|
334
381
|
* Valve activation + timer logic
|
|
335
382
|
* ------------------------------------------------------------------ */
|
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.14",
|
|
4
4
|
"description": "A community-maintained Homebridge plugin for controlling Tuya devices locally over LAN. Includes new features, fixes, and updated device support.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -154,6 +154,19 @@ describe('IrrigationSystemAccessory — service topology', () => {
|
|
|
154
154
|
});
|
|
155
155
|
});
|
|
156
156
|
|
|
157
|
+
test('adds a ServiceLabel (ARABIC_NUMERALS) so the multi-valve zones group under the system', () => {
|
|
158
|
+
const { accessory } = makeHarness({ '1': false, '2': false, '3': false, '4': false });
|
|
159
|
+
const label = accessory.getService(Service.ServiceLabel);
|
|
160
|
+
expect(label).toBeDefined();
|
|
161
|
+
expect(label.getCharacteristic(Characteristic.ServiceLabelNamespace).value)
|
|
162
|
+
.toBe(Characteristic.ServiceLabelNamespace.ARABIC_NUMERALS);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('a single-valve system omits the ServiceLabel (no collection to label)', () => {
|
|
166
|
+
const { accessory } = makeHarness({ '1': false }, { valveCount: 1 });
|
|
167
|
+
expect(accessory.getService(Service.ServiceLabel)).toBeUndefined();
|
|
168
|
+
});
|
|
169
|
+
|
|
157
170
|
test('IrrigationSystem advertises the required characteristics', () => {
|
|
158
171
|
const { accessory } = makeHarness({ '1': false });
|
|
159
172
|
const irr = irrigation(accessory);
|
|
@@ -285,6 +298,38 @@ describe('IrrigationSystemAccessory — valve activation & timer', () => {
|
|
|
285
298
|
jest.advanceTimersByTime(600 * 1000);
|
|
286
299
|
expect(device.update.mock.calls.length).toBe(calls);
|
|
287
300
|
});
|
|
301
|
+
|
|
302
|
+
test('valve Active onGet reports the optimistic value, not the lagging device state (no flicker)', () => {
|
|
303
|
+
// Cloud devices don't advance device.state until the realtime stream
|
|
304
|
+
// echoes the write back. Emulate that (update is a no-op on state) and
|
|
305
|
+
// confirm a freshly-pressed valve keeps reading ON instead of briefly
|
|
306
|
+
// reverting to the pre-press value.
|
|
307
|
+
const { accessory, device } = makeHarness({ '1': false }, { defaultDuration: 0 });
|
|
308
|
+
device.update.mockImplementation(() => true);
|
|
309
|
+
const v = valve(accessory, 1);
|
|
310
|
+
|
|
311
|
+
v.getCharacteristic(Characteristic.Active).triggerSet(1);
|
|
312
|
+
jest.advanceTimersByTime(500);
|
|
313
|
+
|
|
314
|
+
expect(device.state['1']).toBe(false); // not echoed yet
|
|
315
|
+
expect(v.getCharacteristic(Characteristic.Active).triggerGet()).toBe(Characteristic.Active.ACTIVE);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test('system Active onGet reports the cached aggregate, not the lagging device state', () => {
|
|
319
|
+
const { accessory, device } = makeHarness({ '1': false, '2': false }, { defaultDuration: 0 });
|
|
320
|
+
device.update.mockImplementation(() => true);
|
|
321
|
+
valve(accessory, 1).getCharacteristic(Characteristic.Active).triggerSet(1);
|
|
322
|
+
jest.advanceTimersByTime(500);
|
|
323
|
+
|
|
324
|
+
expect(irrigation(accessory).getCharacteristic(Characteristic.Active).triggerGet())
|
|
325
|
+
.toBe(Characteristic.Active.ACTIVE);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test('Active onGet still throws while disconnected (HomeKit shows "No Response")', () => {
|
|
329
|
+
const { accessory, device } = makeHarness({ '1': false });
|
|
330
|
+
device.connected = false;
|
|
331
|
+
expect(() => valve(accessory, 1).getCharacteristic(Characteristic.Active).triggerGet()).toThrow();
|
|
332
|
+
});
|
|
288
333
|
});
|
|
289
334
|
|
|
290
335
|
describe('IrrigationSystemAccessory — write batching (one Tuya command)', () => {
|
package/test/support/mocks.js
CHANGED
|
@@ -34,6 +34,7 @@ const HAP = {
|
|
|
34
34
|
ProgramMode: { UUID: 'D1', NO_PROGRAM_SCHEDULED: 0, PROGRAM_SCHEDULED: 1, PROGRAM_SCHEDULED_MANUAL_MODE: 2 },
|
|
35
35
|
IsConfigured: { UUID: 'D6', NOT_CONFIGURED: 0, CONFIGURED: 1 },
|
|
36
36
|
ServiceLabelIndex: { UUID: 'CB' },
|
|
37
|
+
ServiceLabelNamespace: { UUID: 'CD', DOTS: 0, ARABIC_NUMERALS: 1 },
|
|
37
38
|
ConfiguredName: { UUID: 'E3' },
|
|
38
39
|
BatteryLevel: { UUID: '68' },
|
|
39
40
|
StatusLowBattery: { UUID: '79', BATTERY_LEVEL_NORMAL: 0, BATTERY_LEVEL_LOW: 1 },
|
|
@@ -65,6 +66,7 @@ const HAP = {
|
|
|
65
66
|
LockMechanism: { UUID: '45' },
|
|
66
67
|
Valve: { UUID: 'D0' },
|
|
67
68
|
IrrigationSystem: { UUID: 'CF' },
|
|
69
|
+
ServiceLabel: { UUID: 'CC' },
|
|
68
70
|
Battery: { UUID: '96' },
|
|
69
71
|
ContactSensor: { UUID: '80' },
|
|
70
72
|
LeakSensor: { UUID: '83' },
|