homebridge-tuya-plus 3.1.2

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.
Files changed (42) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +40 -0
  2. package/.github/ISSUE_TEMPLATE/new-device.md +24 -0
  3. package/.github/workflows/codeql-analysis.yml +67 -0
  4. package/.github/workflows/lint.yml +23 -0
  5. package/Changelog.md +63 -0
  6. package/LICENSE +21 -0
  7. package/Readme.MD +106 -0
  8. package/assets/Tuya-Plugin-Branding.png +0 -0
  9. package/bin/cli-decode.js +197 -0
  10. package/bin/cli-find.js +207 -0
  11. package/bin/cli.js +13 -0
  12. package/config-example.MD +43 -0
  13. package/config.schema.json +538 -0
  14. package/eslint.config.mjs +15 -0
  15. package/index.js +242 -0
  16. package/lib/AirConditionerAccessory.js +445 -0
  17. package/lib/AirPurifierAccessory.js +532 -0
  18. package/lib/BaseAccessory.js +290 -0
  19. package/lib/ConvectorAccessory.js +313 -0
  20. package/lib/CustomMultiOutletAccessory.js +111 -0
  21. package/lib/DehumidifierAccessory.js +301 -0
  22. package/lib/DoorbellAccessory.js +208 -0
  23. package/lib/EnergyCharacteristics.js +86 -0
  24. package/lib/GarageDoorAccessory.js +307 -0
  25. package/lib/MultiOutletAccessory.js +106 -0
  26. package/lib/OilDiffuserAccessory.js +480 -0
  27. package/lib/OutletAccessory.js +83 -0
  28. package/lib/RGBTWLightAccessory.js +234 -0
  29. package/lib/RGBTWOutletAccessory.js +296 -0
  30. package/lib/SimpleBlindsAccessory.js +299 -0
  31. package/lib/SimpleDimmer2Accessory.js +54 -0
  32. package/lib/SimpleDimmerAccessory.js +54 -0
  33. package/lib/SimpleFanAccessory.js +137 -0
  34. package/lib/SimpleFanLightAccessory.js +201 -0
  35. package/lib/SimpleHeaterAccessory.js +154 -0
  36. package/lib/SimpleLightAccessory.js +39 -0
  37. package/lib/SwitchAccessory.js +106 -0
  38. package/lib/TWLightAccessory.js +91 -0
  39. package/lib/TuyaAccessory.js +746 -0
  40. package/lib/TuyaDiscovery.js +165 -0
  41. package/lib/ValveAccessory.js +150 -0
  42. package/package.json +49 -0
@@ -0,0 +1,307 @@
1
+ const BaseAccessory = require('./BaseAccessory');
2
+
3
+ // define constants for Kogan garage door.
4
+ // Action
5
+ const GARAGE_DOOR_OPEN = 'open';
6
+ const GARAGE_DOOR_CLOSE = 'close';
7
+ const GARAGE_DOOR_FOPEN = 'fopen';
8
+ const GARAGE_DOOR_FCLOSE = 'fclose';
9
+
10
+ // Status or state
11
+ // yes, 'openning' is not a mistake, that's the text from the Kogan opener
12
+ const GARAGE_DOOR_OPENED = 'opened';
13
+ const GARAGE_DOOR_CLOSED = 'closed';
14
+ const GARAGE_DOOR_OPENNING = 'openning';
15
+ const GARAGE_DOOR_OPENING =
16
+ 'opening'; // 'opening' is not currently a valid value; added in case Kogan
17
+ // one day decides to correct the spelling
18
+ const GARAGE_DOOR_CLOSING = 'closing';
19
+ // Kogan garage door appears to have no stopped status
20
+
21
+ // Kogan manufacturer name
22
+ const GARAGE_DOOR_MANUFACTURER_KOGAN = 'Kogan';
23
+
24
+ // Wofea manufacturer name
25
+ const GARAGE_DOOR_MANUFACTURER_WOFEA = 'Wofea';
26
+
27
+ // main code
28
+ class GarageDoorAccessory extends BaseAccessory {
29
+ static getCategory(Categories) {
30
+ return Categories.GARAGE_DOOR_OPENER;
31
+ }
32
+
33
+ constructor(...props) {
34
+ super(...props);
35
+ }
36
+
37
+ _registerPlatformAccessory() {
38
+ const {Service} = this.hap;
39
+
40
+ this.accessory.addService(Service.GarageDoorOpener, this.device.context.name);
41
+
42
+ super._registerPlatformAccessory();
43
+ }
44
+
45
+ // function to return a ID string for log messages
46
+ _logPrefix() {
47
+ return (this.manufacturer ? this.manufacturer + ' ' : '') + 'GarageDoor';
48
+ }
49
+
50
+ // function to prefix a string ID and always log to console
51
+ _alwaysLog(...args) { this.log.info(this._logPrefix(), ...args); }
52
+
53
+ // function to log to console if debug is on
54
+ _debugLog(...args) {
55
+ this.log.debug(this._logPrefix(), ...args);
56
+ }
57
+
58
+ // function to return true if the garage door manufacturer is Kogan and false
59
+ // otherwise
60
+ _isKogan() {
61
+ if (this.manufacturer === GARAGE_DOOR_MANUFACTURER_KOGAN.trim()) {
62
+ return true;
63
+ } else {
64
+ return false;
65
+ }
66
+ }
67
+
68
+ // function to return true if the garage door manufacturer is Wofea
69
+ _isWofea() {
70
+ if (this.manufacturer === GARAGE_DOOR_MANUFACTURER_WOFEA.trim()) {
71
+ return true;
72
+ } else {
73
+ return false;
74
+ }
75
+ }
76
+
77
+ _registerCharacteristics(dps) {
78
+ const {Service, Characteristic} = this.hap;
79
+ const service = this.accessory.getService(Service.GarageDoorOpener);
80
+ this._checkServiceName(service, this.device.context.name);
81
+
82
+ // Set the manufacturer string
83
+ // If the manufacturer string matches a known manufacturer, set to that string
84
+ // Otherwise set the manufacturer to the defined value
85
+ // Otherwise set the manufacturer to a blank string
86
+ if (this.device.context.manufacturer.trim().toLowerCase() ===
87
+ GARAGE_DOOR_MANUFACTURER_KOGAN.trim().toLowerCase()) {
88
+ this.manufacturer = GARAGE_DOOR_MANUFACTURER_KOGAN.trim();
89
+ } else if (this.device.context.manufacturer.trim().toLowerCase() ===
90
+ GARAGE_DOOR_MANUFACTURER_WOFEA.trim().toLowerCase()) {
91
+ this.manufacturer = this.device.context.manufacturer.trim();
92
+ } else if (this.device.context.manufacturer) {
93
+ this.manufacturer = this.device.context.manufacturer.trim();
94
+ } else {
95
+ this.manufacturer = '';
96
+ }
97
+ // set the dpAction and dpStatus values based on the manufacturer
98
+ if (this._isKogan()) {
99
+ // Kogan SmarterHome Wireless Garage Door Opener
100
+ this._debugLog(
101
+ '_registerCharacteristics setting dpAction and dpStatus for ' +
102
+ GARAGE_DOOR_MANUFACTURER_KOGAN + ' garage door');
103
+ this.dpAction = this._getCustomDP(this.device.context.dpAction) || '101';
104
+ this.dpStatus = this._getCustomDP(this.device.context.dpStatus) || '102';
105
+ } else if (this._isWofea()) {
106
+ // Wofea Wifi Switch Smart Garage Door Opener
107
+ this._debugLog(
108
+ '_registerCharacteristics setting dpAction and dpStatus for ' +
109
+ GARAGE_DOOR_MANUFACTURER_WOFEA + ' garage door');
110
+ this.dpAction = this._getCustomDP(this.device.context.dpAction) || '1';
111
+ this.dpStatus = this._getCustomDP(this.device.context.dpStatus) || '101';
112
+ } else {
113
+ // the original garage door opener
114
+ this._debugLog(
115
+ '_registerCharacteristics setting dpAction and dpStatus for generic door');
116
+ this.dpAction = this._getCustomDP(this.device.context.dpAction) || '1';
117
+ this.dpStatus = this._getCustomDP(this.device.context.dpStatus) || '2';
118
+ }
119
+
120
+ this.currentOpen = Characteristic.CurrentDoorState.OPEN;
121
+ this.currentOpening = Characteristic.CurrentDoorState.OPENING;
122
+ this.currentClosing = Characteristic.CurrentDoorState.CLOSING;
123
+ this.currentClosed = Characteristic.CurrentDoorState.CLOSED;
124
+ this.currentStopped = Characteristic.CurrentDoorState.STOPPED;
125
+ this.targetOpen = Characteristic.TargetDoorState.OPEN;
126
+ this.targetClosed = Characteristic.TargetDoorState.CLOSED;
127
+ if (this.device.context.flipState) {
128
+ this.currentOpen = Characteristic.CurrentDoorState.CLOSED;
129
+ this.currentOpening = Characteristic.CurrentDoorState.CLOSING;
130
+ this.currentClosing = Characteristic.CurrentDoorState.OPENING;
131
+ this.currentClosed = Characteristic.CurrentDoorState.OPEN;
132
+ this.targetOpen = Characteristic.TargetDoorState.CLOSED;
133
+ this.targetClosed = Characteristic.TargetDoorState.OPEN;
134
+ }
135
+
136
+ const characteristicTargetDoorState = service.getCharacteristic(Characteristic.TargetDoorState)
137
+ .updateValue(this._getTargetDoorState(dps[this.dpStatus]))
138
+ .on('get', this.getTargetDoorState.bind(this))
139
+ .on('set', this.setTargetDoorState.bind(this));
140
+
141
+ const characteristicCurrentDoorState = service.getCharacteristic(Characteristic.CurrentDoorState)
142
+ .updateValue(this._getCurrentDoorState(dps[this.dpStatus]))
143
+ .on('get', this.getCurrentDoorState.bind(this));
144
+
145
+ this.device.on('change', changes => {
146
+ this._alwaysLog('changed:' + JSON.stringify(changes));
147
+
148
+ if (changes.hasOwnProperty(this.dpStatus)) {
149
+ const newCurrentDoorState =
150
+ this._getCurrentDoorState(changes[this.dpStatus]);
151
+ this._debugLog('on change new and old CurrentDoorState ' +
152
+ newCurrentDoorState + ' ' +
153
+ characteristicCurrentDoorState.value);
154
+ this._debugLog('on change old characteristicTargetDoorState ' +
155
+ characteristicTargetDoorState.value);
156
+
157
+ if (newCurrentDoorState == this.currentOpen &&
158
+ characteristicTargetDoorState.value !== this.targetOpen)
159
+ characteristicTargetDoorState.updateValue(this.targetOpen);
160
+
161
+ if (newCurrentDoorState == this.currentClosed &&
162
+ characteristicTargetDoorState.value !== this.targetClosed)
163
+ characteristicTargetDoorState.updateValue(this.targetClosed);
164
+
165
+ if (characteristicCurrentDoorState.value !== newCurrentDoorState) characteristicCurrentDoorState.updateValue(newCurrentDoorState);
166
+ }
167
+ });
168
+ }
169
+
170
+ getTargetDoorState(callback) {
171
+ this.getState(this.dpStatus, (err, dp) => {
172
+ if (err) return callback(err);
173
+
174
+ this._debugLog('getTargetDoorState dp ' + JSON.stringify(dp));
175
+
176
+ callback(null, this._getTargetDoorState(dp));
177
+ });
178
+ }
179
+
180
+ _getTargetDoorState(dp) {
181
+ this._debugLog('_getTargetDoorState dp ' + JSON.stringify(dp));
182
+
183
+ if (this._isKogan()) {
184
+ // translate the Kogan strings to the enumerated status values
185
+ switch (dp) {
186
+ case GARAGE_DOOR_OPENED:
187
+ case GARAGE_DOOR_OPENNING:
188
+ case GARAGE_DOOR_OPENING:
189
+ return this.targetOpen;
190
+
191
+ case GARAGE_DOOR_CLOSED:
192
+ case GARAGE_DOOR_CLOSING:
193
+ return this.targetClosed;
194
+
195
+ default:
196
+ this._alwaysLog('_getTargetDoorState UNKNOWN STATE ' +
197
+ JSON.stringify(dp));
198
+ }
199
+ } else {
200
+ // Generic garage door uses true for the opened status and false for the
201
+ // closed status
202
+ if (dp === true) {
203
+ return this.targetOpen;
204
+ } else if (dp === false) {
205
+ return this.targetClosed;
206
+ } else {
207
+ this._alwaysLog('_getTargetDoorState UNKNOWN STATE ' +
208
+ JSON.stringify(dp));
209
+ }
210
+ }
211
+ }
212
+
213
+ setTargetDoorState(value, callback) {
214
+ var newValue = GARAGE_DOOR_CLOSE;
215
+ this._debugLog('setTargetDoorState value ' + value + ' targetOpen ' +
216
+ this.targetOpen + ' targetClosed ' + this.targetClosed);
217
+
218
+ if (this._isKogan()) {
219
+ // translate the the enumerated status values to Kogan strings
220
+ switch (value) {
221
+ case this.targetOpen:
222
+ newValue = GARAGE_DOOR_OPEN;
223
+ break;
224
+
225
+ case this.targetClosed:
226
+ newValue = GARAGE_DOOR_CLOSE;
227
+ break;
228
+
229
+ default:
230
+ this._alwaysLog('setTargetDoorState UNKNOWN STATE ' +
231
+ JSON.stringify(value));
232
+ }
233
+ } else {
234
+ // Generic garage door uses true for the open action and false for the
235
+ // close action
236
+ switch (value) {
237
+ case this.targetOpen:
238
+ newValue = true;
239
+ break;
240
+
241
+ case this.targetClosed:
242
+ newValue = false;
243
+ break;
244
+
245
+ default:
246
+ this._alwaysLog('setTargetDoorState UNKNOWN STATE ' +
247
+ JSON.stringify(value));
248
+ }
249
+ }
250
+
251
+ this.setState(this.dpAction, newValue, callback);
252
+ }
253
+
254
+ getCurrentDoorState(callback) {
255
+ this.getState(this.dpStatus, (err, dpStatusValue) => {
256
+ if (err) return callback(err);
257
+
258
+ callback(null, this._getCurrentDoorState(dpStatusValue));
259
+ });
260
+ }
261
+
262
+ _getCurrentDoorState(dpStatusValue) {
263
+ this._debugLog('_getCurrentDoorState dpStatusValue ' +
264
+ JSON.stringify(dpStatusValue));
265
+
266
+ if (this._isKogan()) {
267
+ // translate the Kogan strings to the enumerated status values
268
+ switch (dpStatusValue) {
269
+ case GARAGE_DOOR_OPENED:
270
+ return this.currentOpen;
271
+
272
+ case GARAGE_DOOR_OPENNING:
273
+ case GARAGE_DOOR_OPENING:
274
+ return this.currentOpening;
275
+
276
+ case GARAGE_DOOR_CLOSING:
277
+ return this.currentClosing;
278
+
279
+ case GARAGE_DOOR_CLOSED:
280
+ return this.currentClosed;
281
+
282
+ default:
283
+ this._alwaysLog('_getCurrentDoorState UNKNOWN STATUS ' +
284
+ JSON.stringify(dpStatusValue));
285
+ }
286
+ } else {
287
+ // Generic garage door uses true for the open status and false for the
288
+ // close status. It doesn't seem to have other values for opening and
289
+ // closing. If the getState() function callback in BaseAccessory.js passed
290
+ // the dps object into this function, we may be able to infer opening and
291
+ // closing from the combined dpStatus and dpAction values. That would
292
+ // require mods to every accessory that used that callback. Not worth it.
293
+ if (dpStatusValue === true) {
294
+ // dpStatus true corresponds to an open door
295
+ return this.currentOpen;
296
+ } else if (dpStatusValue === false) {
297
+ // dpStatus false corresponds to a closed door, so assume "not open"
298
+ return this.currentClosed;
299
+ } else {
300
+ this._alwaysLog('_getCurrentDoorState UNKNOWN STATUS ' +
301
+ JSON.stringify(dpStatusValue));
302
+ }
303
+ }
304
+ }
305
+ }
306
+
307
+ module.exports = GarageDoorAccessory;
@@ -0,0 +1,106 @@
1
+ const BaseAccessory = require('./BaseAccessory');
2
+ const async = require('async');
3
+
4
+ class MultiOutletAccessory extends BaseAccessory {
5
+ static getCategory(Categories) {
6
+ return Categories.OUTLET;
7
+ }
8
+
9
+ constructor(...props) {
10
+ super(...props);
11
+ }
12
+
13
+ _registerPlatformAccessory() {
14
+ this._verifyCachedPlatformAccessory();
15
+ this._justRegistered = true;
16
+
17
+ super._registerPlatformAccessory();
18
+ }
19
+
20
+ _verifyCachedPlatformAccessory() {
21
+ if (this._justRegistered) return;
22
+
23
+ const {Service} = this.hap;
24
+
25
+ const outletCount = parseInt(this.device.context.outletCount) || 1;
26
+ const _validServices = [];
27
+ for (let i = 0; i++ < outletCount;) {
28
+ let service = this.accessory.getServiceByUUIDAndSubType(Service.Outlet, 'outlet ' + i);
29
+ if (service) this._checkServiceName(service, this.device.context.name + ' ' + i);
30
+ else service = this.accessory.addService(Service.Outlet, this.device.context.name + ' ' + i, 'outlet ' + i);
31
+
32
+ _validServices.push(service);
33
+ }
34
+
35
+ this.accessory.services
36
+ .filter(service => service.UUID === Service.Outlet.UUID && !_validServices.includes(service))
37
+ .forEach(service => {
38
+ this.log.info('Removing', service.displayName);
39
+ this.accessory.removeService(service);
40
+ });
41
+ }
42
+
43
+ _registerCharacteristics(dps) {
44
+ this._verifyCachedPlatformAccessory();
45
+
46
+ const {Service, Characteristic} = this.hap;
47
+
48
+ const characteristics = {};
49
+ this.accessory.services.forEach(service => {
50
+ if (service.UUID !== Service.Outlet.UUID || !service.subtype) return false;
51
+
52
+ let match;
53
+ if ((match = service.subtype.match(/^outlet (\d+)$/)) === null) return;
54
+
55
+ characteristics[match[1]] = service.getCharacteristic(Characteristic.On)
56
+ .updateValue(dps[match[1]])
57
+ .on('get', this.getPower.bind(this, match[1]))
58
+ .on('set', this.setPower.bind(this, match[1]));
59
+ });
60
+
61
+ this.device.on('change', (changes, state) => {
62
+ Object.keys(changes).forEach(key => {
63
+ if (characteristics[key] && characteristics[key].value !== changes[key]) characteristics[key].updateValue(changes[key]);
64
+ });
65
+ });
66
+ }
67
+
68
+ getPower(dp, callback) {
69
+ callback(null, this.device.state[dp]);
70
+ }
71
+
72
+ setPower(dp, value, callback) {
73
+ if (!this._pendingPower) {
74
+ this._pendingPower = {props: {}, callbacks: []};
75
+ }
76
+
77
+ if (dp) {
78
+ if (this._pendingPower.timer) clearTimeout(this._pendingPower.timer);
79
+
80
+ this._pendingPower.props = {...this._pendingPower.props, ...{[dp]: value}};
81
+ this._pendingPower.callbacks.push(callback);
82
+
83
+ this._pendingPower.timer = setTimeout(() => {
84
+ this.setPower();
85
+ }, 500);
86
+ return;
87
+ }
88
+
89
+ const callbacks = this._pendingPower.callbacks;
90
+ const callEachBack = err => {
91
+ async.eachSeries(callbacks, (callback, next) => {
92
+ try {
93
+ callback(err);
94
+ } catch (ex) {}
95
+ next();
96
+ });
97
+ };
98
+
99
+ const newValue = this._pendingPower.props;
100
+ this._pendingPower = null;
101
+
102
+ this.setMultiState(newValue, callEachBack);
103
+ }
104
+ }
105
+
106
+ module.exports = MultiOutletAccessory;