homebridge-tuya-plus 3.14.0-dev.52 → 3.14.0-dev.54
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/config.schema.json
CHANGED
|
@@ -862,6 +862,15 @@
|
|
|
862
862
|
"condition": {
|
|
863
863
|
"functionBody": "return model.devices && model.devices[arrayIndices] && ['IrrigationSystem'].includes(model.devices[arrayIndices].type) && !model.devices[arrayIndices].noBattery;"
|
|
864
864
|
}
|
|
865
|
+
},
|
|
866
|
+
"dpRainState": {
|
|
867
|
+
"type": ["integer", "string"],
|
|
868
|
+
"placeholder": "49 or rain_sensor_state",
|
|
869
|
+
"title": "Rain-sensor data-point (numeric id, or cloud code)",
|
|
870
|
+
"description": "Enum data-point reporting rainfall ('rain' / 'no_rain'), surfaced as a Leak Detected characteristic on the irrigation system so HomeKit can notify and automate on rain. Defaults to DP 49; if your controller doesn't report it, Leak Detected simply stays clear.",
|
|
871
|
+
"condition": {
|
|
872
|
+
"functionBody": "return model.devices && model.devices[arrayIndices] && ['IrrigationSystem'].includes(model.devices[arrayIndices].type);"
|
|
873
|
+
}
|
|
865
874
|
}
|
|
866
875
|
}
|
|
867
876
|
}
|
|
@@ -212,15 +212,17 @@ class IrrigationSystemAccessory extends BaseAccessory {
|
|
|
212
212
|
if (battery) this.accessory.removeService(battery);
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
// Rain
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
215
|
+
// Rain state is surfaced as a LeakDetected characteristic ON the
|
|
216
|
+
// IrrigationSystem service itself (see _registerCharacteristics), never as
|
|
217
|
+
// a separate LeakSensor/ContactSensor service: a second sensor service
|
|
218
|
+
// makes the Home app fragment the sprinkler into "sub-accessories" and
|
|
219
|
+
// blocks control from the main tile. Drop any standalone rain-sensor
|
|
220
|
+
// service left over from an older version so the accessory stays a clean,
|
|
221
|
+
// single-category sprinkler tile.
|
|
220
222
|
[Service.ContactSensor, Service.LeakSensor].forEach(S => {
|
|
221
223
|
const svc = this.accessory.getService(S);
|
|
222
224
|
if (svc) {
|
|
223
|
-
this.log.debug('Removing rain sensor service %s (
|
|
225
|
+
this.log.debug('Removing standalone rain sensor service %s (rain state is now a LeakDetected characteristic)', svc.displayName);
|
|
224
226
|
this.accessory.removeService(svc);
|
|
225
227
|
}
|
|
226
228
|
});
|
|
@@ -265,6 +267,7 @@ class IrrigationSystemAccessory extends BaseAccessory {
|
|
|
265
267
|
// configured instead and works over either transport.
|
|
266
268
|
this.dpBattery = this._resolveDP(this.device.context.dpBattery, '46');
|
|
267
269
|
this.dpCharging = this._resolveDP(this.device.context.dpCharging, '101');
|
|
270
|
+
this.dpRainState = this._resolveDP(this.device.context.dpRainState, '49');
|
|
268
271
|
|
|
269
272
|
// Per-zone runtime state
|
|
270
273
|
this._valves = this._getValveConfigs();
|
|
@@ -295,6 +298,15 @@ class IrrigationSystemAccessory extends BaseAccessory {
|
|
|
295
298
|
.updateValue(0)
|
|
296
299
|
.onGet(() => this._systemRemaining());
|
|
297
300
|
|
|
301
|
+
// Rain sensor -> LeakDetected, carried on the IrrigationSystem service
|
|
302
|
+
// itself (a separate LeakSensor service would fragment the tile — see
|
|
303
|
+
// _verifyCachedPlatformAccessory). "rain" reads as a leak so HomeKit can
|
|
304
|
+
// notify/automate on it; everything else — "no_rain", or a device that
|
|
305
|
+
// doesn't report it — stays "no leak".
|
|
306
|
+
irrigation.getCharacteristic(Characteristic.LeakDetected)
|
|
307
|
+
.updateValue(this._leakDetected(dps[this.dpRainState]))
|
|
308
|
+
.onGet(() => this._leakDetected(this.getStateAsync(this.dpRainState)));
|
|
309
|
+
|
|
298
310
|
// --- Valve characteristics ---
|
|
299
311
|
this._valves.forEach(cfg => {
|
|
300
312
|
const valve = this.accessory.getServiceById(Service.Valve, 'valve-' + cfg.dp);
|
|
@@ -425,6 +437,12 @@ class IrrigationSystemAccessory extends BaseAccessory {
|
|
|
425
437
|
.updateValue(this._chargingState(changes[this.dpCharging]));
|
|
426
438
|
}
|
|
427
439
|
|
|
440
|
+
if (changes.hasOwnProperty(this.dpRainState)) {
|
|
441
|
+
const irrigation = this.accessory.getService(Service.IrrigationSystem);
|
|
442
|
+
if (irrigation) irrigation.getCharacteristic(Characteristic.LeakDetected)
|
|
443
|
+
.updateValue(this._leakDetected(changes[this.dpRainState]));
|
|
444
|
+
}
|
|
445
|
+
|
|
428
446
|
if (valveChanged) this._syncAggregate();
|
|
429
447
|
}
|
|
430
448
|
|
|
@@ -729,6 +747,21 @@ class IrrigationSystemAccessory extends BaseAccessory {
|
|
|
729
747
|
: Characteristic.ChargingState.NOT_CHARGING;
|
|
730
748
|
}
|
|
731
749
|
|
|
750
|
+
/* ------------------------------------------------------------------ *
|
|
751
|
+
* Rain sensor mapping
|
|
752
|
+
* ------------------------------------------------------------------ */
|
|
753
|
+
|
|
754
|
+
// rain_sensor_state ("rain"/"no_rain") -> LeakDetected. Only an explicit
|
|
755
|
+
// "rain" reads as a leak; anything else — "no_rain", an unknown value, or a
|
|
756
|
+
// device that never reports the sensor — is "no leak", so the characteristic
|
|
757
|
+
// sits steady at LEAK_NOT_DETECTED on devices without a rain sensor.
|
|
758
|
+
_leakDetected(value) {
|
|
759
|
+
const {Characteristic} = this.hap;
|
|
760
|
+
return ('' + value).trim().toLowerCase() === 'rain'
|
|
761
|
+
? Characteristic.LeakDetected.LEAK_DETECTED
|
|
762
|
+
: Characteristic.LeakDetected.LEAK_NOT_DETECTED;
|
|
763
|
+
}
|
|
764
|
+
|
|
732
765
|
/* ------------------------------------------------------------------ *
|
|
733
766
|
* Batched writes — coalesce DP updates into a single Tuya command
|
|
734
767
|
* ------------------------------------------------------------------ */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-tuya-plus",
|
|
3
|
-
"version": "3.14.0-dev.
|
|
4
|
-
"description": "A community-maintained Homebridge plugin for controlling Tuya devices
|
|
3
|
+
"version": "3.14.0-dev.54",
|
|
4
|
+
"description": "A community-maintained Homebridge plugin for controlling Tuya devices locally over LAN, with an optional Tuya Cloud fallback for whatever the LAN can't reach. Includes new features, fixes, and updated device support. PRs welcome: if it runs, it ships (almost).",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "jest",
|
|
@@ -645,6 +645,76 @@ describe('IrrigationSystemAccessory — charging state', () => {
|
|
|
645
645
|
});
|
|
646
646
|
});
|
|
647
647
|
|
|
648
|
+
describe('IrrigationSystemAccessory — rain sensor (LeakDetected)', () => {
|
|
649
|
+
test('exposes LeakDetected on the IrrigationSystem service itself (no separate sensor service)', () => {
|
|
650
|
+
const { accessory } = makeHarness({ '1': false, '49': 'no_rain' });
|
|
651
|
+
const irr = irrigation(accessory);
|
|
652
|
+
expect(irr.getCharacteristic(Characteristic.LeakDetected).value)
|
|
653
|
+
.toBe(Characteristic.LeakDetected.LEAK_NOT_DETECTED);
|
|
654
|
+
// A standalone sensor service would fragment the sprinkler tile — it must
|
|
655
|
+
// never be created; the state rides on the system service instead.
|
|
656
|
+
expect(accessory.getService(Service.LeakSensor)).toBeUndefined();
|
|
657
|
+
expect(accessory.getService(Service.ContactSensor)).toBeUndefined();
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
test('reports a leak while the device reports "rain"', () => {
|
|
661
|
+
const { accessory } = makeHarness({ '1': false, '49': 'rain' });
|
|
662
|
+
expect(irrigation(accessory).getCharacteristic(Characteristic.LeakDetected).value)
|
|
663
|
+
.toBe(Characteristic.LeakDetected.LEAK_DETECTED);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
test('defaults to "no leak" when the device never reports the rain sensor', () => {
|
|
667
|
+
const { accessory } = makeHarness({ '1': false });
|
|
668
|
+
expect(irrigation(accessory).getCharacteristic(Characteristic.LeakDetected).value)
|
|
669
|
+
.toBe(Characteristic.LeakDetected.LEAK_NOT_DETECTED);
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
test('a device-side rain change is mirrored onto LeakDetected', () => {
|
|
673
|
+
const { accessory, device } = makeHarness({ '1': false, '49': 'no_rain' });
|
|
674
|
+
const leak = irrigation(accessory).getCharacteristic(Characteristic.LeakDetected);
|
|
675
|
+
|
|
676
|
+
device.emitChange({ '49': 'rain' });
|
|
677
|
+
expect(leak.value).toBe(Characteristic.LeakDetected.LEAK_DETECTED);
|
|
678
|
+
|
|
679
|
+
device.emitChange({ '49': 'no_rain' });
|
|
680
|
+
expect(leak.value).toBe(Characteristic.LeakDetected.LEAK_NOT_DETECTED);
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
test('onGet reflects the live device state', () => {
|
|
684
|
+
const { accessory, device } = makeHarness({ '1': false, '49': 'no_rain' });
|
|
685
|
+
const leak = irrigation(accessory).getCharacteristic(Characteristic.LeakDetected);
|
|
686
|
+
device.state['49'] = 'rain';
|
|
687
|
+
expect(leak.triggerGet()).toBe(Characteristic.LeakDetected.LEAK_DETECTED);
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
test('onGet throws while disconnected (HomeKit shows "No Response")', () => {
|
|
691
|
+
const { accessory, device } = makeHarness({ '1': false, '49': 'no_rain' });
|
|
692
|
+
device.connected = false;
|
|
693
|
+
expect(() => irrigation(accessory).getCharacteristic(Characteristic.LeakDetected).triggerGet()).toThrow();
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
test('_leakDetected maps only "rain" (case/space-insensitive) to a leak', () => {
|
|
697
|
+
const { instance } = makeHarness({ '1': false });
|
|
698
|
+
expect(instance._leakDetected('rain')).toBe(Characteristic.LeakDetected.LEAK_DETECTED);
|
|
699
|
+
expect(instance._leakDetected(' RAIN ')).toBe(Characteristic.LeakDetected.LEAK_DETECTED);
|
|
700
|
+
expect(instance._leakDetected('no_rain')).toBe(Characteristic.LeakDetected.LEAK_NOT_DETECTED);
|
|
701
|
+
expect(instance._leakDetected(undefined)).toBe(Characteristic.LeakDetected.LEAK_NOT_DETECTED);
|
|
702
|
+
expect(instance._leakDetected(null)).toBe(Characteristic.LeakDetected.LEAK_NOT_DETECTED);
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
test('a custom dpRainState data-point (e.g. a Tuya code) is honoured', () => {
|
|
706
|
+
const { accessory, device } = makeHarness(
|
|
707
|
+
{ '1': false, rain_sensor_state: 'rain' },
|
|
708
|
+
{ dpRainState: 'rain_sensor_state' }
|
|
709
|
+
);
|
|
710
|
+
const leak = irrigation(accessory).getCharacteristic(Characteristic.LeakDetected);
|
|
711
|
+
expect(leak.value).toBe(Characteristic.LeakDetected.LEAK_DETECTED);
|
|
712
|
+
|
|
713
|
+
device.emitChange({ rain_sensor_state: 'no_rain' });
|
|
714
|
+
expect(leak.value).toBe(Characteristic.LeakDetected.LEAK_NOT_DETECTED);
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
|
|
648
718
|
describe('IrrigationSystemAccessory — data-points addressed by code', () => {
|
|
649
719
|
// The accessory is transport-agnostic: a data-point may be a numeric id or a
|
|
650
720
|
// Tuya "code". The LAN+cloud TuyaDevice keeps state dual-keyed and translates
|
|
@@ -758,7 +758,8 @@ Multi-valve Tuya irrigation/sprinkler controllers (the battery-powered Wi-Fi "fa
|
|
|
758
758
|
|
|
759
759
|
* one **Irrigation System** tile that contains every zone,
|
|
760
760
|
* one **Valve** per zone (`ValveType = Irrigation`) — each with its own on/off, its own **Duration** picker and a live countdown,
|
|
761
|
-
* an optional **Battery** service (level, low-battery warning, and — for solar/USB-C rechargeable units that report it — live charging status)
|
|
761
|
+
* an optional **Battery** service (level, low-battery warning, and — for solar/USB-C rechargeable units that report it — live charging status),
|
|
762
|
+
* a **Leak Detected** characteristic on the system tile for controllers with a rain sensor (`rain_sensor_state`, DP `49`) — HomeKit reports a leak while it's raining, so you can be notified or automate on it.
|
|
762
763
|
|
|
763
764
|
Because these devices are slow to respond, all zone changes that happen close together — turning the whole system on/off, or running a scene that toggles several zones — are merged into a **single** Tuya command instead of a burst of them.
|
|
764
765
|
|
|
@@ -807,6 +808,12 @@ Each zone has its own **Duration**. When a zone is switched on it runs for that
|
|
|
807
808
|
|
|
808
809
|
Switching the whole Irrigation System tile **off** closes every open zone, and switching it **on** opens every zone — each as one combined command (mirroring the physical "all" button many of these controllers have). Either direction can be disabled with `masterTurnsOffAllZones` / `masterTurnsOnAllZones`.
|
|
809
810
|
|
|
811
|
+
#### Rain sensor
|
|
812
|
+
|
|
813
|
+
Controllers with a rain sensor report it on `rain_sensor_state` (DP `49`, an enum of `rain` / `no_rain`). The plugin surfaces this as a **Leak Detected** characteristic **on the Irrigation System tile itself** — `rain` reads as "leak detected", anything else as "no leak". HomeKit can then show the rainfall state and drive notifications or automations from it (e.g. skip a watering scene while it's raining).
|
|
814
|
+
|
|
815
|
+
It's deliberately carried on the system tile rather than as a separate sensor accessory: a standalone sensor makes the Home app split the sprinkler into sub-accessories and blocks control from the main tile. There's nothing to turn on — it's present by default, and stays "no leak" on controllers that don't report a rain sensor. The data-point defaults to DP `49` and can be remapped with `dpRainState` (e.g. to the `rain_sensor_state` code on a cloud device).
|
|
816
|
+
|
|
810
817
|
#### Full Configuration
|
|
811
818
|
|
|
812
819
|
```json5
|
|
@@ -845,6 +852,9 @@ Switching the whole Irrigation System tile **off** closes every open zone, and s
|
|
|
845
852
|
"masterTurnsOffAllZones": true,
|
|
846
853
|
"commandDebounce": 500, /* ms window for merging zone changes into one command */
|
|
847
854
|
|
|
855
|
+
/* --- Rain sensor (shown as Leak Detected on the system tile) --- */
|
|
856
|
+
"dpRainState": 49, /* enum rain/no_rain DP; omit to use the default, or if not reported */
|
|
857
|
+
|
|
848
858
|
/* --- Battery (omit / set noBattery:true if mains-powered) --- */
|
|
849
859
|
"dpBattery": 46,
|
|
850
860
|
"lowBatteryThreshold": 20,
|