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.
@@ -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/leak sensors are no longer supported: they never reported
216
- // reliably on these devices, and bundling a sensor in the same accessory
217
- // forced the Home app to fragment the sprinkler into "sub-accessories"
218
- // (blocking control from the main tile). Drop any left over from an older
219
- // version so the accessory stays a clean, single-category sprinkler tile.
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 (no longer supported)', svc.displayName);
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.52",
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.",
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,