homebridge-tuya-plus 3.14.0-dev.3 → 3.14.0-dev.5
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 +8 -0
- package/Readme.MD +8 -0
- package/config.schema.json +82 -13
- package/index.js +92 -3
- package/lib/IrrigationSystemAccessory.js +31 -7
- package/lib/TuyaCloudApi.js +290 -0
- package/lib/TuyaCloudDevice.js +188 -0
- package/lib/TuyaCloudMessaging.js +232 -0
- package/package.json +4 -1
- package/test/IrrigationSystemAccessory.test.js +49 -0
- package/test/TuyaCloudApi.test.js +196 -0
- package/test/TuyaCloudDevice.test.js +105 -0
- package/test/TuyaCloudMessaging.test.js +94 -0
- package/wiki/Supported-Device-Types.md +14 -0
- package/wiki/Tuya-Cloud-Setup.md +160 -0
package/Changelog.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. This project uses [semantic versioning](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## Unreleased
|
|
6
|
+
|
|
7
|
+
* [+] **Tuya Cloud support for devices that can't be reached over the LAN** — most notably battery-powered "sleepy" irrigation/sprinkler timers, which sleep almost all the time and only ever talk to Tuya's cloud, so the local protocol can never reach them. The plugin stays LAN-first: cloud is strictly opt-in. Add a top-level `cloud` credentials block (or a per-device `cloud` object) and set `"cloud": true` on the device.
|
|
8
|
+
* Realtime updates arrive over Tuya's **MQTT** message service (via the optional `mqtt` dependency, installed automatically); initial state and control use the Tuya OpenAPI. There is no polling.
|
|
9
|
+
* Works with both **Custom** and **Smart Home** Cloud projects (the latter via app-account login).
|
|
10
|
+
* The existing **IrrigationSystem** accessory works unchanged over the cloud — its data-points are simply addressed by Tuya "code" (e.g. `switch_1`, `battery_percentage`) instead of a numeric id; the device logs its codes on startup. Battery-only controllers with no rain sensor: set `"noRainSensor": true`.
|
|
11
|
+
* See the wiki: **[Tuya Cloud Setup](https://github.com/adrianjagielak/homebridge-tuya-plus/blob/main/wiki/Tuya-Cloud-Setup.md)**.
|
|
12
|
+
|
|
5
13
|
## 2.0.1 (2021-03-25)
|
|
6
14
|
This update includes the following changes:
|
|
7
15
|
|
package/Readme.MD
CHANGED
|
@@ -69,6 +69,14 @@ Protocol **3.5** is currently the newest Tuya LAN protocol in existence: it is t
|
|
|
69
69
|
|
|
70
70
|
This plugin is nevertheless **3.6-ready**: if a device reports a newer protocol version (e.g. `3.6`) in its broadcast, the plugin automatically talks to it using the newest (3.5/GCM) protocol stack while tagging payloads with the device's reported version — the same forward-compatibility strategy used by tinytuya. Should such a device misbehave, you can pin it to a specific protocol with the `forceVersion` device option (e.g. `"forceVersion": "3.5"`), or use `version` to set a protocol for devices that can't be discovered (e.g. on another subnet).
|
|
71
71
|
|
|
72
|
+
## Cloud devices (for hardware that can't be controlled locally)
|
|
73
|
+
|
|
74
|
+
This is a **LAN-first** plugin — virtually every Tuya device is controlled locally, which is faster, more private, and keeps working without internet. A few devices, though, simply **can't** be reached over the LAN: battery-powered "sleepy" devices — for example the **irrigation / faucet timers** — sleep almost all the time and only ever connect out to Tuya's cloud, so they never answer on the local network.
|
|
75
|
+
|
|
76
|
+
For exactly these cases the plugin can talk to a device through the **Tuya Cloud** instead — strictly **opt-in, per device**. You add your Tuya Cloud project credentials once and set `"cloud": true` on the device; everything else works the same.
|
|
77
|
+
|
|
78
|
+
👉 **[Tuya Cloud Setup guide](https://github.com/adrianjagielak/homebridge-tuya-plus/blob/main/wiki/Tuya-Cloud-Setup.md)** — how to get credentials and configure a cloud device.
|
|
79
|
+
|
|
72
80
|
## Installation Instructions
|
|
73
81
|
|
|
74
82
|
#### Option 1: Install via Homebridge Config UI X:
|
package/config.schema.json
CHANGED
|
@@ -19,12 +19,71 @@
|
|
|
19
19
|
"placeholder": "Timeout (millisecond) for device IP address discovery",
|
|
20
20
|
"default": 60000
|
|
21
21
|
},
|
|
22
|
+
"cloud": {
|
|
23
|
+
"type": "object",
|
|
24
|
+
"title": "Tuya Cloud (optional — only for devices that can't be reached over the LAN)",
|
|
25
|
+
"description": "This plugin is LAN-first. A few devices — notably battery-powered 'sleepy' irrigation timers — sleep most of the time and only ever talk to Tuya's cloud, so they cannot be controlled locally. Create a free Cloud project at iot.tuya.com, enter its credentials here, then tick 'Control via Tuya Cloud' on each such device. See the wiki for step-by-step setup.",
|
|
26
|
+
"properties": {
|
|
27
|
+
"accessId": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"title": "Access ID / Client ID"
|
|
30
|
+
},
|
|
31
|
+
"accessKey": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"title": "Access Secret / Client Secret"
|
|
34
|
+
},
|
|
35
|
+
"region": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"title": "Data center / region",
|
|
38
|
+
"default": "us",
|
|
39
|
+
"description": "Must match the region your Tuya / Smart Life app account is registered in.",
|
|
40
|
+
"oneOf": [
|
|
41
|
+
{ "title": "Central Europe", "enum": ["eu"] },
|
|
42
|
+
{ "title": "Western Europe (Azure)", "enum": ["eu-w"] },
|
|
43
|
+
{ "title": "Western America", "enum": ["us"] },
|
|
44
|
+
{ "title": "Eastern America (Azure)", "enum": ["us-e"] },
|
|
45
|
+
{ "title": "China", "enum": ["cn"] },
|
|
46
|
+
{ "title": "India", "enum": ["in"] },
|
|
47
|
+
{ "title": "Singapore", "enum": ["sg"] }
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
"username": {
|
|
51
|
+
"type": "string",
|
|
52
|
+
"title": "App account email/phone (Smart Home projects only)",
|
|
53
|
+
"description": "For 'Smart Home' Cloud projects: the Tuya / Smart Life app account that owns the devices. Leave blank for 'Custom' projects (which link devices by QR scan)."
|
|
54
|
+
},
|
|
55
|
+
"password": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"title": "App account password (Smart Home projects only)"
|
|
58
|
+
},
|
|
59
|
+
"countryCode": {
|
|
60
|
+
"type": "string",
|
|
61
|
+
"title": "Country calling code (Smart Home projects only)",
|
|
62
|
+
"placeholder": "e.g. 1, 44, 48"
|
|
63
|
+
},
|
|
64
|
+
"schema": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"title": "App (Smart Home projects only)",
|
|
67
|
+
"default": "tuyaSmart",
|
|
68
|
+
"oneOf": [
|
|
69
|
+
{ "title": "Tuya Smart", "enum": ["tuyaSmart"] },
|
|
70
|
+
{ "title": "Smart Life", "enum": ["smartlife"] }
|
|
71
|
+
]
|
|
72
|
+
},
|
|
73
|
+
"realtime": {
|
|
74
|
+
"type": "boolean",
|
|
75
|
+
"title": "Realtime updates (MQTT)",
|
|
76
|
+
"default": true,
|
|
77
|
+
"description": "Receive instant updates over Tuya's MQTT message service (uses the 'mqtt' package, installed automatically). If disabled, devices stay controllable and show their state at startup, but external changes (physical buttons, the device's own timers) won't be reflected until restart."
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
22
81
|
"devices": {
|
|
23
82
|
"type": "array",
|
|
24
83
|
"orderable": false,
|
|
25
84
|
"items": {
|
|
26
85
|
"type": "object",
|
|
27
|
-
"required": ["type", "name", "id"
|
|
86
|
+
"required": ["type", "name", "id"],
|
|
28
87
|
"properties": {
|
|
29
88
|
"type": {
|
|
30
89
|
"type": "string",
|
|
@@ -136,8 +195,18 @@
|
|
|
136
195
|
}
|
|
137
196
|
},
|
|
138
197
|
"key": {
|
|
139
|
-
"title": "Tuya Key",
|
|
198
|
+
"title": "Tuya Key (local key — required for LAN devices)",
|
|
140
199
|
"type": "string",
|
|
200
|
+
"description": "The device's local key. Required for normal (LAN) devices. Not needed for cloud devices (tick 'Control via Tuya Cloud' below).",
|
|
201
|
+
"condition": {
|
|
202
|
+
"functionBody": "return model.devices && model.devices[arrayIndices].type !== 'null';"
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
"cloud": {
|
|
206
|
+
"type": ["boolean", "object"],
|
|
207
|
+
"title": "Control via Tuya Cloud (instead of the LAN)",
|
|
208
|
+
"default": false,
|
|
209
|
+
"description": "Enable for devices that can't be reached locally (e.g. battery-powered irrigation timers). Uses the platform-level 'cloud' credentials above. Advanced: instead of a checkbox you can set this to an object with per-device { accessId, accessKey, region, … } to use different credentials for this one device.",
|
|
141
210
|
"condition": {
|
|
142
211
|
"functionBody": "return model.devices && model.devices[arrayIndices].type !== 'null';"
|
|
143
212
|
}
|
|
@@ -648,7 +717,7 @@
|
|
|
648
717
|
"type": "integer",
|
|
649
718
|
"title": "Number of Valves / Zones",
|
|
650
719
|
"placeholder": 4,
|
|
651
|
-
"description": "How many valves/zones the controller has. They are assumed to be on data-points 1, 2, 3, … For non-sequential data-points use the 'valves' list instead.",
|
|
720
|
+
"description": "How many valves/zones the controller has. They are assumed to be on data-points 1, 2, 3, … (or, for cloud devices, on the codes switch_1, switch_2, …). For non-sequential data-points use the 'valves' list instead.",
|
|
652
721
|
"condition": {
|
|
653
722
|
"functionBody": "return model.devices && model.devices[arrayIndices] && ['IrrigationSystem'].includes(model.devices[arrayIndices].type);"
|
|
654
723
|
}
|
|
@@ -662,7 +731,7 @@
|
|
|
662
731
|
"type": "object",
|
|
663
732
|
"properties": {
|
|
664
733
|
"name": { "type": "string", "title": "Zone name", "placeholder": "Lawn" },
|
|
665
|
-
"dp": { "type": "integer", "title": "Data-point", "placeholder": 1 },
|
|
734
|
+
"dp": { "type": ["integer", "string"], "title": "Data-point (numeric id, or cloud code)", "placeholder": "1 or switch_1" },
|
|
666
735
|
"defaultDuration": { "type": "integer", "title": "Default run time (seconds, 0 = run indefinitely)", "placeholder": 600 }
|
|
667
736
|
}
|
|
668
737
|
},
|
|
@@ -724,9 +793,9 @@
|
|
|
724
793
|
}
|
|
725
794
|
},
|
|
726
795
|
"dpBattery": {
|
|
727
|
-
"type": "integer",
|
|
728
|
-
"placeholder": 46,
|
|
729
|
-
"title": "Battery data-point",
|
|
796
|
+
"type": ["integer", "string"],
|
|
797
|
+
"placeholder": "46 or battery_percentage",
|
|
798
|
+
"title": "Battery data-point (numeric id, or cloud code)",
|
|
730
799
|
"condition": {
|
|
731
800
|
"functionBody": "return model.devices && model.devices[arrayIndices] && ['IrrigationSystem'].includes(model.devices[arrayIndices].type) && !model.devices[arrayIndices].noBattery;"
|
|
732
801
|
}
|
|
@@ -741,9 +810,9 @@
|
|
|
741
810
|
}
|
|
742
811
|
},
|
|
743
812
|
"dpCharging": {
|
|
744
|
-
"type": "integer",
|
|
745
|
-
"placeholder": 101,
|
|
746
|
-
"title": "Charging-status data-point",
|
|
813
|
+
"type": ["integer", "string"],
|
|
814
|
+
"placeholder": "101 or charge_state",
|
|
815
|
+
"title": "Charging-status data-point (numeric id, or cloud code)",
|
|
747
816
|
"description": "Boolean data-point reporting whether the battery is charging (e.g. solar / USB-C units). HomeKit shows Charging / Not Charging; if your controller doesn't report this, it shows Not Chargeable.",
|
|
748
817
|
"condition": {
|
|
749
818
|
"functionBody": "return model.devices && model.devices[arrayIndices] && ['IrrigationSystem'].includes(model.devices[arrayIndices].type) && !model.devices[arrayIndices].noBattery;"
|
|
@@ -778,9 +847,9 @@
|
|
|
778
847
|
}
|
|
779
848
|
},
|
|
780
849
|
"dpRain": {
|
|
781
|
-
"type": "integer",
|
|
782
|
-
"placeholder": 49,
|
|
783
|
-
"title": "Rain sensor data-point",
|
|
850
|
+
"type": ["integer", "string"],
|
|
851
|
+
"placeholder": "49 or rain_sensor_state",
|
|
852
|
+
"title": "Rain sensor data-point (numeric id, or cloud code)",
|
|
784
853
|
"condition": {
|
|
785
854
|
"functionBody": "return model.devices && model.devices[arrayIndices] && ['IrrigationSystem'].includes(model.devices[arrayIndices].type) && !model.devices[arrayIndices].noRainSensor;"
|
|
786
855
|
}
|
package/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
const TuyaAccessory = require('./lib/TuyaAccessory');
|
|
2
2
|
const TuyaDiscovery = require('./lib/TuyaDiscovery');
|
|
3
|
+
const TuyaCloudApi = require('./lib/TuyaCloudApi');
|
|
4
|
+
const TuyaCloudDevice = require('./lib/TuyaCloudDevice');
|
|
5
|
+
const TuyaCloudMessaging = require('./lib/TuyaCloudMessaging');
|
|
3
6
|
|
|
4
7
|
const OutletAccessory = require('./lib/OutletAccessory');
|
|
5
8
|
const SimpleLightAccessory = require('./lib/SimpleLightAccessory');
|
|
@@ -36,6 +39,13 @@ const PLATFORM_NAME = 'TuyaLan';
|
|
|
36
39
|
const UUID_SEED = 'homebridge-tuya';
|
|
37
40
|
const DEFAULT_DISCOVER_TIMEOUT = 60000;
|
|
38
41
|
|
|
42
|
+
// Lenient boolean coercion (matches BaseAccessory._coerceBoolean) so config
|
|
43
|
+
// values like true / "true" / 1 all read as true.
|
|
44
|
+
const coerceBoolean = (b, df = false) =>
|
|
45
|
+
typeof b === 'boolean' ? b :
|
|
46
|
+
typeof b === 'string' ? b.toLowerCase().trim() === 'true' :
|
|
47
|
+
typeof b === 'number' ? b !== 0 : df;
|
|
48
|
+
|
|
39
49
|
const CLASS_DEF = {
|
|
40
50
|
outlet: OutletAccessory,
|
|
41
51
|
simplelight: SimpleLightAccessory,
|
|
@@ -82,6 +92,11 @@ class TuyaLan {
|
|
|
82
92
|
[this.log, this.config, this.api] = [...props];
|
|
83
93
|
|
|
84
94
|
this.cachedAccessories = new Map();
|
|
95
|
+
// Shared Tuya Cloud clients, keyed by credential set, so several
|
|
96
|
+
// cloud devices on the same project share one token + one realtime
|
|
97
|
+
// (MQTT) connection. Empty unless cloud devices are configured.
|
|
98
|
+
this.cloudApis = new Map();
|
|
99
|
+
this.cloudMessagers = new Map();
|
|
85
100
|
this.api.hap.EnergyCharacteristics = require('./lib/EnergyCharacteristics')(this.api.hap);
|
|
86
101
|
|
|
87
102
|
if(!this.config || !this.config.devices) {
|
|
@@ -100,10 +115,12 @@ class TuyaLan {
|
|
|
100
115
|
const devices = {};
|
|
101
116
|
const connectedDevices = [];
|
|
102
117
|
const fakeDevices = [];
|
|
118
|
+
const cloudDevices = [];
|
|
103
119
|
this.config.devices.forEach(device => {
|
|
104
120
|
try {
|
|
105
121
|
device.id = ('' + device.id).trim();
|
|
106
|
-
|
|
122
|
+
// Cloud devices don't need a local key; only trim when present.
|
|
123
|
+
if (device.key != null) device.key = ('' + device.key).trim();
|
|
107
124
|
device.type = ('' + device.type).trim();
|
|
108
125
|
|
|
109
126
|
device.ip = ('' + (device.ip || '')).trim();
|
|
@@ -112,12 +129,20 @@ class TuyaLan {
|
|
|
112
129
|
if (!device.type) return this.log.error('%s (%s) doesn\'t have a type defined.', device.name || 'Unnamed device', device.id);
|
|
113
130
|
if (!CLASS_DEF[device.type.toLowerCase()]) return this.log.error('%s (%s) doesn\'t have a valid type defined.', device.name || 'Unnamed device', device.id);
|
|
114
131
|
|
|
115
|
-
if (device
|
|
132
|
+
if (this._isCloudDevice(device)) cloudDevices.push({name: device.id.slice(8), ...device});
|
|
133
|
+
else if (device.fake) fakeDevices.push({name: device.id.slice(8), ...device});
|
|
116
134
|
else devices[device.id] = {name: device.id.slice(8), ...device};
|
|
117
135
|
});
|
|
118
136
|
|
|
137
|
+
// Cloud devices are reached over the internet, not the LAN, so they need
|
|
138
|
+
// no discovery — wire them up right away.
|
|
139
|
+
cloudDevices.forEach(config => this._addCloudAccessory(config));
|
|
140
|
+
|
|
119
141
|
const deviceIds = Object.keys(devices);
|
|
120
|
-
if (deviceIds.length === 0)
|
|
142
|
+
if (deviceIds.length === 0) {
|
|
143
|
+
if (cloudDevices.length === 0) this.log.error('No valid configured devices found.');
|
|
144
|
+
return; // cloud-only (or empty) configuration: nothing to discover over LAN
|
|
145
|
+
}
|
|
121
146
|
|
|
122
147
|
this.log.info('Starting discovery...');
|
|
123
148
|
|
|
@@ -176,6 +201,70 @@ class TuyaLan {
|
|
|
176
201
|
}, this.config.discoverTimeout ?? DEFAULT_DISCOVER_TIMEOUT);
|
|
177
202
|
}
|
|
178
203
|
|
|
204
|
+
/* ------------------------------------------------------------------ *
|
|
205
|
+
* Tuya Cloud helpers (for devices that can't be reached over the LAN,
|
|
206
|
+
* e.g. battery-powered "sleepy" irrigation timers). This plugin stays
|
|
207
|
+
* LAN-first; these paths are only exercised by devices opting in with
|
|
208
|
+
* `cloud: true` (or a per-device `cloud` credentials object).
|
|
209
|
+
* ------------------------------------------------------------------ */
|
|
210
|
+
|
|
211
|
+
_isCloudDevice(device) {
|
|
212
|
+
return !!(device && (device.cloud === true || (typeof device.cloud === 'object' && device.cloud)));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Effective cloud credentials/options for a device: the platform-level
|
|
216
|
+
// `cloud` block, overlaid with any per-device `cloud` object.
|
|
217
|
+
_resolveCloudConfig(device) {
|
|
218
|
+
const platform = (this.config.cloud && typeof this.config.cloud === 'object') ? this.config.cloud : {};
|
|
219
|
+
const perDevice = (typeof device.cloud === 'object' && device.cloud) ? device.cloud : {};
|
|
220
|
+
return {...platform, ...perDevice};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// One TuyaCloudApi per credential set, so multiple cloud devices on the
|
|
224
|
+
// same Tuya project share a single token.
|
|
225
|
+
_getCloudApi(cloudCfg) {
|
|
226
|
+
const key = TuyaCloudApi.keyFor(cloudCfg);
|
|
227
|
+
if (!this.cloudApis.has(key)) {
|
|
228
|
+
this.cloudApis.set(key, new TuyaCloudApi({...cloudCfg, log: this.log}));
|
|
229
|
+
}
|
|
230
|
+
return this.cloudApis.get(key);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// One shared realtime (MQTT) stream per credential set, unless realtime is
|
|
234
|
+
// disabled. Returns null when realtime is off — the device then shows its
|
|
235
|
+
// initial state and stays controllable, but won't receive live updates.
|
|
236
|
+
_getCloudMessaging(api, cloudCfg) {
|
|
237
|
+
const realtime = cloudCfg.realtime === undefined ? true : coerceBoolean(cloudCfg.realtime, true);
|
|
238
|
+
if (!realtime) return null;
|
|
239
|
+
const key = TuyaCloudApi.keyFor(cloudCfg);
|
|
240
|
+
if (!this.cloudMessagers.has(key)) {
|
|
241
|
+
this.cloudMessagers.set(key, new TuyaCloudMessaging({api, log: this.log}));
|
|
242
|
+
}
|
|
243
|
+
return this.cloudMessagers.get(key);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
_addCloudAccessory(config) {
|
|
247
|
+
const cloudCfg = this._resolveCloudConfig(config);
|
|
248
|
+
if (!cloudCfg.accessId || !cloudCfg.accessKey) {
|
|
249
|
+
return this.log.error('%s (%s) is configured for the Tuya Cloud, but no credentials were found. Add a top-level "cloud" block (accessId, accessKey, region) or a per-device "cloud" object.', config.name, config.id);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const api = this._getCloudApi(cloudCfg);
|
|
253
|
+
const messaging = this._getCloudMessaging(api, cloudCfg);
|
|
254
|
+
|
|
255
|
+
this.log.info('Adding cloud device: %s (%s) via %s', config.name, config.id, api.endpoint);
|
|
256
|
+
|
|
257
|
+
this.addAccessory(new TuyaCloudDevice({
|
|
258
|
+
...config,
|
|
259
|
+
cloud: true, // normalise so accessories can detect cloud mode
|
|
260
|
+
cloudApi: api,
|
|
261
|
+
messaging,
|
|
262
|
+
log: this.log,
|
|
263
|
+
UUID: UUID.generate(UUID_SEED + ':' + config.id),
|
|
264
|
+
connect: false
|
|
265
|
+
}));
|
|
266
|
+
}
|
|
267
|
+
|
|
179
268
|
registerPlatformAccessories(platformAccessories) {
|
|
180
269
|
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, Array.isArray(platformAccessories) ? platformAccessories : [platformAccessories]);
|
|
181
270
|
}
|
|
@@ -45,15 +45,20 @@ class IrrigationSystemAccessory extends BaseAccessory {
|
|
|
45
45
|
// Self-contained (no reliance on instance state) so it is safe to call
|
|
46
46
|
// both during early service reconciliation and later when wiring
|
|
47
47
|
// characteristics.
|
|
48
|
+
const cloud = this._isCloud();
|
|
48
49
|
const defaultDuration = isFinite(this.device.context.defaultDuration) ? parseInt(this.device.context.defaultDuration) : 600;
|
|
49
50
|
|
|
50
51
|
if (Array.isArray(this.device.context.valves) && this.device.context.valves.length) {
|
|
51
52
|
return this.device.context.valves.map((valve, i) => {
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
// A data-point may be a numeric LAN id (1, 2, …) or a Tuya Cloud
|
|
54
|
+
// code (e.g. "switch_1"). Accept either; only an empty value is
|
|
55
|
+
// invalid.
|
|
56
|
+
const dp = (valve && valve.dp !== undefined && valve.dp !== null) ? ('' + valve.dp).trim() : '';
|
|
57
|
+
if (!dp) {
|
|
58
|
+
throw new Error(`The valve definition #${i + 1} is missing a 'dp': ${JSON.stringify(valve)}`);
|
|
54
59
|
}
|
|
55
60
|
return {
|
|
56
|
-
dp
|
|
61
|
+
dp,
|
|
57
62
|
name: (('' + (valve.name || '')).trim()) || ('Zone ' + (i + 1)),
|
|
58
63
|
index: i + 1,
|
|
59
64
|
duration: isFinite(valve.defaultDuration) ? parseInt(valve.defaultDuration) : defaultDuration
|
|
@@ -66,7 +71,9 @@ class IrrigationSystemAccessory extends BaseAccessory {
|
|
|
66
71
|
const configs = [];
|
|
67
72
|
for (let i = 0; i < count; i++) {
|
|
68
73
|
configs.push({
|
|
69
|
-
|
|
74
|
+
// Cloud devices address valves by code (switch_1, switch_2, …);
|
|
75
|
+
// LAN devices by numeric data-point id (1, 2, …).
|
|
76
|
+
dp: cloud ? ('switch_' + (i + 1)) : String(i + 1),
|
|
70
77
|
name: 'Valve ' + (letters[i] || (i + 1)),
|
|
71
78
|
index: i + 1,
|
|
72
79
|
duration: defaultDuration
|
|
@@ -75,6 +82,21 @@ class IrrigationSystemAccessory extends BaseAccessory {
|
|
|
75
82
|
return configs;
|
|
76
83
|
}
|
|
77
84
|
|
|
85
|
+
// True when this device is reached over the Tuya Cloud (data-points keyed by
|
|
86
|
+
// string code) rather than the LAN (numeric data-point ids).
|
|
87
|
+
_isCloud() {
|
|
88
|
+
return this._coerceBoolean(this.device.context.cloud, false);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Resolve a configurable data-point that may be given as a numeric LAN id or
|
|
92
|
+
// a Tuya Cloud code, falling back to a sensible default for the active
|
|
93
|
+
// transport.
|
|
94
|
+
_resolveDP(value, cloudDefault, lanDefault) {
|
|
95
|
+
const v = (value === undefined || value === null) ? '' : ('' + value).trim();
|
|
96
|
+
if (v !== '') return v;
|
|
97
|
+
return this._isCloud() ? cloudDefault : lanDefault;
|
|
98
|
+
}
|
|
99
|
+
|
|
78
100
|
_hasBattery() {
|
|
79
101
|
return !this._coerceBoolean(this.device.context.noBattery, false);
|
|
80
102
|
}
|
|
@@ -186,9 +208,11 @@ class IrrigationSystemAccessory extends BaseAccessory {
|
|
|
186
208
|
this._cascadeOn = this._coerceBoolean(this.device.context.masterTurnsOnAllZones, true);
|
|
187
209
|
this._cascadeOff = this._coerceBoolean(this.device.context.masterTurnsOffAllZones, true);
|
|
188
210
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
this.
|
|
211
|
+
// Data-points accept a numeric LAN id or a Tuya Cloud code; defaults
|
|
212
|
+
// differ per transport (cloud uses the standard Tuya codes).
|
|
213
|
+
this.dpBattery = this._resolveDP(this.device.context.dpBattery, 'battery_percentage', '46');
|
|
214
|
+
this.dpCharging = this._resolveDP(this.device.context.dpCharging, 'charge_state', '101');
|
|
215
|
+
this.dpRain = this._resolveDP(this.device.context.dpRain, 'rain_sensor_state', '49');
|
|
192
216
|
this._rainOnValue = ('' + (this.device.context.rainOnValue || 'rain')).trim();
|
|
193
217
|
this._rainInverted = this._coerceBoolean(this.device.context.rainInverted, false);
|
|
194
218
|
|