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,532 @@
1
+ const BaseAccessory = require('./BaseAccessory');
2
+ const DP_SWITCH = '1';
3
+ const DP_PM25 = '2';
4
+ const DP_MODE = '3'
5
+ const DP_FAN_SPEED = '4';
6
+ const DP_LOCK_PHYSICAL_CONTROLS = '7';
7
+ const DP_AIR_QUALITY = '22';
8
+ const STATE_OTHER = 9;
9
+ /**
10
+ * Accessory for Air Purifiers, with an optional setting to also include Air Quality sensor details.
11
+ *
12
+ *
13
+ * Extra settings:
14
+ * - noRotationSpeed - boolean which, if set to true, will disable the fan speed control
15
+ * - fanSpeedSteps - The number of fan speed stops that the device supports. The default is 100.
16
+ * - showAirQuality - boolean for enabling the air quality service. The default is false, which
17
+ * will not include air quality values.
18
+ * - nameAirQuality - Allows customisation of the air quality sensor name. Default is 'Air Quality'
19
+ * - noChildLock - boolean for disabling the child lock feature. The default is false, which
20
+ * will enable the child lock feature
21
+ *
22
+ * Standard Air Purifier Data Points (dp):
23
+ * 1. switch
24
+ * 2. pm25
25
+ * 3. mode
26
+ * 4. fan_speed_enum
27
+ * 5. filter (Filter Usage)
28
+ * 6. anion
29
+ * 7. child_lock
30
+ * 8. light
31
+ * 9. uv (UV Disinfection)
32
+ * 10. wet (Humidify)
33
+ * 11. filter_reset (Reset Filter)
34
+ * 12. temp_indoor (Indoor Temp)
35
+ * 13. humidity (Indoor Humidity)
36
+ * 14. tvoc
37
+ * 15. eco2 (eCO2)
38
+ * 16. filter_days (Filter Days Left)
39
+ * 17. total_runtime
40
+ * 18. countdown_sete
41
+ * 19. countdown_left
42
+ * 20. total_pm
43
+ * 21. ???
44
+ * 22. air_quality (verified on Breville Smart Air Connect)
45
+ * ???. fault (Fault Alarm)
46
+ *
47
+ * This accessory maps the DP ids to the following Characteristics:
48
+ * - 1 - Characteristic.ACTIVE / Characteristic.CurrentAirPurifierState
49
+ * - 2 - Characteristic.PM2_5Density / Characteristic.AirQuality
50
+ * - 3 - Characteristic.TargetAirPurifierState
51
+ * - 4 - Characteristic.RotationSpeed
52
+ * - 7 - Characteristic.LockPhysicalControls
53
+ *
54
+ * Device compatability:
55
+ * - Some devices, like the Breville Smart Air Connect, return and expect text rather than numeric Characteristics. To handle these properly, ensure that you set the 'manufacturer' configuration to 'Breville'.
56
+ *
57
+ * Future Enhancements:
58
+ * - Some of this implementation is very similar to AirConditionerAccessory. Likely some scope
59
+ * for refactoring this.
60
+ * - Some air purifiers support more of the standard data points than the original test device,
61
+ * so additional services like Filter Maintenance could be added
62
+ * https://developers.homebridge.io/#/service/FilterMaintenance
63
+ * - _getAirQuality currenly calculates a value based on the pm25 value. I do not have a device
64
+ * that supports the air_quality data point to see what the return type would be
65
+ *
66
+ * Notes:
67
+ * Initial testing was performed on a Elechomes KJ200G-A3B-UK. This required in the configuration for the API version to
68
+ * be specified. This device returned data point ids: 1, 2, 3, 4, 6, 7, 17, 20
69
+ *
70
+ * Sample configuration for KJ200G-A3B-UK:
71
+ * {
72
+ * "name": "Living Room Air Purifier",
73
+ * "type": "AirPurifier",
74
+ * "manufacturer": "Elechomes",
75
+ * "model": "KJ200G-A3B-UK",
76
+ * "id": "REDACTED",
77
+ * "key": "REDACTED",
78
+ * "ip": "192.168.99.50",
79
+ * "version": "3.3",
80
+ * "fanSpeedSteps": 3,
81
+ * "showAirQuality": true
82
+ * }
83
+ *
84
+ *
85
+ */
86
+ class AirPurifierAccessory extends BaseAccessory {
87
+ static getCategory(Categories) {
88
+ return Categories.AIR_PURIFIER;
89
+ }
90
+ constructor(...props) {
91
+ super(...props);
92
+ const {Characteristic} = this.hap;
93
+
94
+ if (this.device.context.noRotationSpeed) {
95
+
96
+ let fanSpeedSteps = (
97
+ this.device.context.fanSpeedSteps &&
98
+ isFinite(this.device.context.fanSpeedSteps) &&
99
+ this.device.context.fanSpeedSteps > 0 &&
100
+ this.device.context.fanSpeedSteps < 100) ? this.device.context.fanSpeedSteps : 100;
101
+ let _fanSpeedLabels = {};
102
+ // Special handling for particular devices //
103
+ switch (this.device.context.manufacturer) {
104
+ case 'Breville':
105
+ _fanSpeedLabels = {0: 'off', 1: 'low', 2: 'mid', 3: 'high', 4: 'turbo'};
106
+ this._rotationSteps = [...Array(5).keys()];
107
+ fanSpeedSteps = 5;
108
+ break;
109
+ case 'Proscenic':
110
+ _fanSpeedLabels = {0: 'sleep', 1: 'mid', 2: 'high', 3: 'auto'};
111
+ fanSpeedSteps = 3;
112
+ this._rotationSteps = [...Array(4).keys()];
113
+ break;
114
+ case 'siguro':
115
+ _fanSpeedLabels = {0: 'sleep', 1: 'auto'};
116
+ fanSpeedSteps = 2;
117
+ this._rotationSteps = [...Array(2).keys()];
118
+ break;
119
+ default: // Just use numeric values
120
+ this._rotationSteps = [...Array(fanSpeedSteps).keys()];
121
+ for (let i = 0; i <= fanSpeedSteps; i++) {
122
+ _fanSpeedLabels[i] = i;
123
+ }
124
+ }
125
+ this._rotationStops = {0: _fanSpeedLabels[0]};
126
+ for (let i = 0; i < 100; i++) {
127
+ const _rotationStep = Math.floor(fanSpeedSteps * i / 100);
128
+ this._rotationStops[i+1] = _fanSpeedLabels[_rotationStep];
129
+ }
130
+ }
131
+ this.airQualityLevels = [
132
+ [200, Characteristic.AirQuality.POOR],
133
+ [150, Characteristic.AirQuality.INFERIOR],
134
+ [100, Characteristic.AirQuality.FAIR],
135
+ [50, Characteristic.AirQuality.GOOD],
136
+ [0, Characteristic.AirQuality.EXCELLENT],
137
+ ];
138
+ this.cmdAuto = 'AUTO';
139
+ if (this.device.context.cmdAuto) {
140
+ if (/^a[a-z]+$/i.test(this.device.context.cmdAuto)) this.cmdAuto = ('' + this.device.context.cmdAuto).trim();
141
+ else throw new Error('The cmdAuto doesn\'t appear to be valid: ' + this.device.context.cmdAuto);
142
+ }
143
+ }
144
+ /**
145
+ * Register the services that this accessory supports.
146
+ */
147
+ _registerPlatformAccessory() {
148
+ const {Service} = this.hap;
149
+ /* Add the main air purifier */
150
+ this.accessory.addService(Service.AirPurifier, this.device.context.name);
151
+ /* If configured to include air quality data, include that service too */
152
+ if (this.device.context.showAirQuality) {
153
+ this._addAirQualityService();
154
+ }
155
+ super._registerPlatformAccessory();
156
+ }
157
+ /**
158
+ * Method to add the AirQualitySensor service to the accessory.
159
+ *
160
+ * This is seperate as it may be called after the initial _registerPlatformAccessory call,
161
+ * if the configuration is updated after the device is first added.
162
+ */
163
+ _addAirQualityService() {
164
+ const {Service} = this.hap;
165
+ const nameAirQuality = this.device.context.nameAirQuality || 'Air Quality';
166
+ this.log.info('Adding air quality sensor: %s', nameAirQuality);
167
+ this.accessory.addService(Service.AirQualitySensor, nameAirQuality);
168
+ }
169
+ /**
170
+ * Register the Characteristics that this accessory supports.
171
+ * @param {*} dps
172
+ */
173
+ _registerCharacteristics(dps) {
174
+ const {Service, Characteristic} = this.hap;
175
+ /* Air purifier service characteristics */
176
+ const airPurifierService = this.accessory.getService(Service.AirPurifier);
177
+ this._checkServiceName(airPurifierService, this.device.context.name);
178
+ this.log.debug('_registerCharacteristics dps: %o', dps);
179
+ const characteristicActive = airPurifierService.getCharacteristic(Characteristic.Active)
180
+ .updateValue(this._getActive(dps[DP_SWITCH]))
181
+ .on('get', this.getActive.bind(this))
182
+ .on('set', this.setActive.bind(this));
183
+ const characteristicCurrentAirPurifierState = airPurifierService.getCharacteristic(Characteristic.CurrentAirPurifierState)
184
+ .updateValue(this._getCurrentAirPurifierState(dps[DP_SWITCH]))
185
+ .on('get', this.getCurrentAirPurifierState.bind(this));
186
+
187
+
188
+ const characteristicTargetAirPurifierState = airPurifierService.getCharacteristic(Characteristic.TargetAirPurifierState)
189
+ .updateValue(this._getTargetAirPurifierState(this._getMode(dps)))
190
+ .on('get', this.getTargetAirPurifierState.bind(this))
191
+ .on('set', this.setTargetAirPurifierState.bind(this));
192
+
193
+ let characteristicLockPhysicalControls;
194
+ if (!this.device.context.noChildLock) {
195
+ characteristicLockPhysicalControls = airPurifierService.getCharacteristic(Characteristic.LockPhysicalControls)
196
+ .updateValue(this._getLockPhysicalControls(dps[DP_LOCK_PHYSICAL_CONTROLS]))
197
+ .on('get', this.getLockPhysicalControls.bind(this))
198
+ .on('set', this.setLockPhysicalControls.bind(this));
199
+ } else {
200
+ this._removeCharacteristic(airPurifierService, Characteristic.LockPhysicalControls);
201
+ }
202
+ const characteristicRotationSpeed = airPurifierService.getCharacteristic(Characteristic.RotationSpeed)
203
+ .updateValue(this._getRotationSpeed(dps))
204
+ .on('get', this.getRotationSpeed.bind(this))
205
+ .on('set', this.setRotationSpeed.bind(this));
206
+ /* Air quality sensor characteristics */
207
+ let airQualitySensorService = this.accessory.getService(Service.AirQualitySensor);
208
+ let characteristicAirQuality;
209
+ let characteristicPM25Density;
210
+ /* Ensure the air quality sensor service existance aligns with the configuration.
211
+ * If configured to include air quality data, and the service was not already registered, register it.
212
+ * If configured to not include it, but the service this there, remove it
213
+ */
214
+ if (!airQualitySensorService && this.device.context.showAirQuality) {
215
+ this._addAirQualityService();
216
+ airQualitySensorService = this.accessory.getService(Service.AirQualitySensor);
217
+ } else if (airQualitySensorService && !this.device.context.showAirQuality) {
218
+ this.accessory.removeService(airQualitySensorService);
219
+ }
220
+ if (airQualitySensorService) {
221
+ const nameAirQuality = this.device.context.nameAirQuality || 'Air Quality';
222
+ this._checkServiceName(airQualitySensorService, nameAirQuality);
223
+ characteristicAirQuality = airQualitySensorService.getCharacteristic(Characteristic.AirQuality)
224
+ .updateValue(this._getAirQuality(dps))
225
+ .on('get', this.getAirQuality.bind(this));
226
+ characteristicPM25Density = airQualitySensorService.getCharacteristic(Characteristic.PM2_5Density)
227
+ .updateValue(dps[DP_PM25])
228
+ .on('get', this.getPM25.bind(this));
229
+ }
230
+ /* Listen for changes */
231
+ this.device.on('change', (changes, state) => {
232
+ this.log.debug('Changes: %o, State: %o', changes, state);
233
+ if (changes.hasOwnProperty(DP_SWITCH)) {
234
+ /* On/Off state change */
235
+ const newActive = this._getActive(changes[DP_SWITCH]);
236
+
237
+ // switch power on before other updates to avoid "Turning on..." state
238
+ if (changes[DP_SWITCH]) {
239
+ this.log.debug("Switching state first");
240
+ characteristicActive.updateValue(newActive);
241
+
242
+ characteristicCurrentAirPurifierState.updateValue(
243
+ this._getCurrentAirPurifierState(changes[DP_SWITCH]));
244
+ }
245
+
246
+ if (!changes.hasOwnProperty(DP_FAN_SPEED)) {
247
+ characteristicRotationSpeed.updateValue(this._getRotationSpeed(state));
248
+ }
249
+ if (!changes.hasOwnProperty(DP_MODE)) {
250
+ characteristicTargetAirPurifierState.updateValue(
251
+ this._getTargetAirPurifierState(this._getMode(state)));
252
+ }
253
+
254
+ // switch power off after other updates to avoid "Turning off..." state
255
+ if (!changes[DP_SWITCH]) {
256
+ this.log.debug("Switching state last");
257
+ characteristicCurrentAirPurifierState.updateValue(
258
+ this._getCurrentAirPurifierState(changes[DP_SWITCH]));
259
+
260
+ characteristicActive.updateValue(newActive);
261
+ }
262
+ }
263
+
264
+ if (changes.hasOwnProperty(DP_FAN_SPEED)) {
265
+ /* Fan speed change */
266
+ const newRotationSpeed = this._getRotationSpeed(state);
267
+ // Proscenic "auto" fan speed is not mapped and should not trigger a rotation speed update
268
+ if (newRotationSpeed) {
269
+ if (characteristicRotationSpeed.value !== newRotationSpeed) {
270
+ characteristicRotationSpeed.updateValue(newRotationSpeed);
271
+ }
272
+ }
273
+
274
+ if (!changes.hasOwnProperty(DP_MODE)) {
275
+ characteristicTargetAirPurifierState.updateValue(
276
+ this._getTargetAirPurifierState(this._getMode(state)));
277
+ }
278
+ }
279
+
280
+ if (characteristicLockPhysicalControls && changes.hasOwnProperty(DP_LOCK_PHYSICAL_CONTROLS)) {
281
+ /* Child Lock change */
282
+ const newLockPhysicalControls = this._getLockPhysicalControls(changes[DP_LOCK_PHYSICAL_CONTROLS]);
283
+ if (characteristicLockPhysicalControls.value !== newLockPhysicalControls) {
284
+ characteristicLockPhysicalControls.updateValue(newLockPhysicalControls);
285
+ }
286
+ }
287
+ if (changes.hasOwnProperty(DP_MODE)) {
288
+ /* Change to the running mode */
289
+ const newTargetAirPurifierState = this._getTargetAirPurifierState(changes[DP_MODE]);
290
+ if (characteristicTargetAirPurifierState.value !== newTargetAirPurifierState) {
291
+ characteristicTargetAirPurifierState.updateValue(newTargetAirPurifierState);
292
+ }
293
+ }
294
+ if (airQualitySensorService && changes.hasOwnProperty(DP_PM25)) {
295
+ /* Change to the air quality */
296
+ const newPM25 = changes[DP_PM25];
297
+ if (characteristicPM25Density.value !== newPM25) {
298
+ characteristicPM25Density.updateValue(newPM25);
299
+ }
300
+ if (!changes.hasOwnProperty(DP_AIR_QUALITY)) {
301
+ characteristicAirQuality.updateValue(this._getAirQuality(state));
302
+ }
303
+ }
304
+ });
305
+ }
306
+
307
+ /* Proscenic air purifier does not support DP_MODE but has fan speed 'auto' */
308
+ _getMode(state) {
309
+ if (state[DP_MODE]) {
310
+ return state[DP_MODE];
311
+ } else {
312
+ return state[DP_FAN_SPEED] == 'auto' ? 'auto' : 'manual';
313
+ }
314
+ }
315
+
316
+ getActive(callback) {
317
+ this.getState(DP_SWITCH, (err, dp) => {
318
+ if (err) {
319
+ return callback(err);
320
+ }
321
+ callback(null, this._getActive(dp));
322
+ });
323
+ }
324
+ _getActive(dp) {
325
+ const {Characteristic} = this.hap;
326
+ return dp ? Characteristic.Active.ACTIVE : Characteristic.Active.INACTIVE;
327
+ }
328
+ setActive(value, callback) {
329
+ const {Characteristic} = this.hap;
330
+ switch (value) {
331
+ case Characteristic.Active.ACTIVE:
332
+ return this.setState(DP_SWITCH, true, callback);
333
+ case Characteristic.Active.INACTIVE:
334
+ return this.setState(DP_SWITCH, false, callback);
335
+ }
336
+ callback();
337
+ }
338
+ getAirQuality(callback) {
339
+ this.getState([DP_PM25], (err, dps) => {
340
+ if (err) {
341
+ return callback(err);
342
+ }
343
+ callback(null, this._getAirQuality(dps));
344
+ });
345
+ }
346
+ _getAirQuality(dps) {
347
+ const {Characteristic} = this.hap;
348
+ /* TODO: Other DP values can be used for Air Quality */
349
+ switch (this.device.context.manufacturer) {
350
+ case 'Breville':
351
+ if (dps[DP_AIR_QUALITY]) {
352
+ switch (dps[DP_AIR_QUALITY]) {
353
+ case 'poor':
354
+ return Characteristic.AirQuality.POOR
355
+ case 'good':
356
+ return Characteristic.AirQuality.GOOD
357
+ case 'great':
358
+ return Characteristic.AirQuality.EXCELLENT
359
+ default:
360
+ this.log.warn('Unhandled _getAirQuality value: %s', dps[DP_AIR_QUALITY]);
361
+ return Characteristic.AirQuality.UNKNOWN
362
+ }
363
+ }
364
+ break;
365
+ default:
366
+ if (dps[DP_PM25]) {
367
+ /* Loop through the air quality levels until a match is found */
368
+ for (var item of this.airQualityLevels) {
369
+ if (dps[DP_PM25] >= item[0]) {
370
+ return item[1];
371
+ }
372
+ }
373
+ }
374
+ }
375
+ /* Default return value if nothing has already returned */
376
+ return 0;
377
+ }
378
+ getCurrentAirPurifierState(callback) {
379
+ this.getState(DP_SWITCH, (err, dp) => {
380
+ if (err) return callback(err);
381
+ callback(null, this._getCurrentAirPurifierState(dp));
382
+ });
383
+ }
384
+ _getCurrentAirPurifierState(dp) {
385
+ const {Characteristic} = this.hap;
386
+ /* There isn't really a direct mapping to this from the purifier,
387
+ * so just using as inactive or purifying.
388
+ */
389
+ return dp ? Characteristic.CurrentAirPurifierState.PURIFYING_AIR : Characteristic.CurrentAirPurifierState.INACTIVE;
390
+ }
391
+ getLockPhysicalControls(callback) {
392
+ this.getState(DP_LOCK_PHYSICAL_CONTROLS, (err, dp) => {
393
+ if (err) {
394
+ return callback(err);
395
+ }
396
+ callback(null, this._getLockPhysicalControls(dp));
397
+ });
398
+ }
399
+ _getLockPhysicalControls(dp) {
400
+ const {Characteristic} = this.hap;
401
+ return dp ? Characteristic.LockPhysicalControls.CONTROL_LOCK_ENABLED : Characteristic.LockPhysicalControls.CONTROL_LOCK_DISABLED;
402
+ }
403
+ setLockPhysicalControls(value, callback) {
404
+ const {Characteristic} = this.hap;
405
+ switch (value) {
406
+ case Characteristic.LockPhysicalControls.CONTROL_LOCK_ENABLED:
407
+ return this.setState(DP_LOCK_PHYSICAL_CONTROLS, true, callback);
408
+ case Characteristic.LockPhysicalControls.CONTROL_LOCK_DISABLED:
409
+ return this.setState(DP_LOCK_PHYSICAL_CONTROLS, false, callback);
410
+ }
411
+ callback();
412
+ }
413
+
414
+ getPM25(callback) {
415
+ this.getState(DP_PM25, (err, dp) => {
416
+ if (err) {
417
+ return callback(err);
418
+ }
419
+ callback(null, dp);
420
+ });
421
+ }
422
+
423
+ getRotationSpeed(callback) {
424
+ this.getState([DP_SWITCH, DP_FAN_SPEED], (err, dps) => {
425
+ if (err) {
426
+ return callback(err);
427
+ }
428
+ callback(null, this._getRotationSpeed(dps));
429
+ });
430
+ }
431
+ _getRotationSpeed(dps) {
432
+ if (!dps[DP_SWITCH]) {
433
+ return 0;
434
+ } else if (this._hkRotationSpeed) {
435
+ const currntRotationSpeed = this.convertRotationSpeedFromHomeKitToTuya(this._hkRotationSpeed);
436
+ return currntRotationSpeed === dps[DP_FAN_SPEED] ? this._hkRotationSpeed : this._hkRotationSpeed = this.convertRotationSpeedFromTuyaToHomeKit(dps[DP_FAN_SPEED]);
437
+ }
438
+ return this._hkRotationSpeed = this.convertRotationSpeedFromTuyaToHomeKit(dps[DP_FAN_SPEED]);
439
+ }
440
+ setRotationSpeed(value, callback) {
441
+ const {Characteristic} = this.hap;
442
+ if (value === 0) {
443
+ this.setActive(Characteristic.Active.INACTIVE, callback);
444
+ } else {
445
+ this._hkRotationSpeed = value;
446
+ // This code was only sending the first code, not the second...
447
+ //const newState = {DP_SWITCH: true, DP_FAN_SPEED: this.convertRotationSpeedFromHomeKitToTuya(value)};
448
+ //this.log.debug('setRotationSpeed value: %s. State: %s', value, newState);
449
+ //return this.setMultiState(newState, callback);
450
+ return this.setState(DP_FAN_SPEED, this.convertRotationSpeedFromHomeKitToTuya(value), callback);
451
+ }
452
+ }
453
+
454
+ getTargetAirPurifierState(callback) {
455
+ this.getState([DP_MODE, DP_FAN_SPEED], (err, dps) => {
456
+ if (err) {
457
+ return callback(err);
458
+ }
459
+
460
+ callback(null, this._getTargetAirPurifierState(this._getMode(dps)));
461
+ });
462
+ }
463
+
464
+ _getTargetAirPurifierState(dp) {
465
+ const {Characteristic} = this.hap;
466
+ switch (dp) {
467
+ case 'manual':
468
+ case 'Manual':
469
+ return Characteristic.TargetAirPurifierState.MANUAL;
470
+ case 'Sleep':
471
+ //TODO: Handle differently than passing through?
472
+ // eslint-disable-next-line no-fallthrough
473
+ case 'auto':
474
+ case 'Auto':
475
+ return Characteristic.TargetAirPurifierState.AUTO;
476
+ default:
477
+ this.log.warn('Unhandled getTargetAirPurifierState value: %s', dp);
478
+ return STATE_OTHER;
479
+ }
480
+ }
481
+ setTargetAirPurifierState(value, callback) {
482
+ const {Characteristic} = this.hap;
483
+ switch (value) {
484
+ case Characteristic.TargetAirPurifierState.MANUAL:
485
+ if (this.device.context.manufacturer == 'Breville') {
486
+ return this.setState(DP_MODE, 'manual', callback);
487
+ } else if (this.device.context.manufacturer == 'Proscenic') {
488
+ // When going from auto to manual, set to the lowest speed
489
+ return this.setState(DP_FAN_SPEED, 'sleep', callback);
490
+
491
+ } else if (this.device.context.manufacturer == 'siguro') {
492
+ // When going from auto to manual, set to the lowest speed
493
+ return this.setState(DP_FAN_SPEED, 'sleep', callback);
494
+ } else {
495
+ return this.setState(DP_MODE, 'Manual', callback);
496
+ }
497
+
498
+ case Characteristic.TargetAirPurifierState.AUTO:
499
+ if (this.device.context.manufacturer == 'Breville') {
500
+ return this.setState(DP_MODE, 'auto', callback);
501
+ } else if (this.device.context.manufacturer == 'Proscenic') {
502
+ return this.setState(DP_FAN_SPEED, 'auto', callback);
503
+ } else if (this.device.context.manufacturer == 'siguro') {
504
+ return this.setState(DP_FAN_SPEED, 'auto', callback);
505
+ } else {
506
+ return this.setState(DP_MODE, 'Auto', callback);
507
+ }
508
+ default:
509
+ //TODO: Can we do anything about Sleep?
510
+ this.log.warn('Unhandled setTargetAirPurifierState value: %s', value);
511
+ }
512
+ callback();
513
+ }
514
+ getKeyByValue(object, value) {
515
+ return Object.keys(object).find(key => object[key] === value);
516
+ }
517
+ convertRotationSpeedFromHomeKitToTuya(value) {
518
+ this.log.debug('convertRotationSpeedFromHomeKitToTuya: %s: %s', value, this._rotationStops[parseInt(value)]);
519
+ return this._rotationStops[parseInt(value)];
520
+ }
521
+
522
+ convertRotationSpeedFromTuyaToHomeKit(value) {
523
+ this.log.debug('convertRotationSpeedFromTuyaToHomeKit: %s: %s', value, this.getKeyByValue(this._rotationStops, value));
524
+ let speed = this.device.context.fanSpeedSteps ? '' + this.getKeyByValue(this._rotationStops, value) : this.getKeyByValue(this._rotationStops, value);
525
+ if (speed === undefined) {
526
+ return 0;
527
+ }
528
+ return speed;
529
+ }
530
+
531
+ }
532
+ module.exports = AirPurifierAccessory;