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.
- package/.github/ISSUE_TEMPLATE/bug_report.md +40 -0
- package/.github/ISSUE_TEMPLATE/new-device.md +24 -0
- package/.github/workflows/codeql-analysis.yml +67 -0
- package/.github/workflows/lint.yml +23 -0
- package/Changelog.md +63 -0
- package/LICENSE +21 -0
- package/Readme.MD +106 -0
- package/assets/Tuya-Plugin-Branding.png +0 -0
- package/bin/cli-decode.js +197 -0
- package/bin/cli-find.js +207 -0
- package/bin/cli.js +13 -0
- package/config-example.MD +43 -0
- package/config.schema.json +538 -0
- package/eslint.config.mjs +15 -0
- package/index.js +242 -0
- package/lib/AirConditionerAccessory.js +445 -0
- package/lib/AirPurifierAccessory.js +532 -0
- package/lib/BaseAccessory.js +290 -0
- package/lib/ConvectorAccessory.js +313 -0
- package/lib/CustomMultiOutletAccessory.js +111 -0
- package/lib/DehumidifierAccessory.js +301 -0
- package/lib/DoorbellAccessory.js +208 -0
- package/lib/EnergyCharacteristics.js +86 -0
- package/lib/GarageDoorAccessory.js +307 -0
- package/lib/MultiOutletAccessory.js +106 -0
- package/lib/OilDiffuserAccessory.js +480 -0
- package/lib/OutletAccessory.js +83 -0
- package/lib/RGBTWLightAccessory.js +234 -0
- package/lib/RGBTWOutletAccessory.js +296 -0
- package/lib/SimpleBlindsAccessory.js +299 -0
- package/lib/SimpleDimmer2Accessory.js +54 -0
- package/lib/SimpleDimmerAccessory.js +54 -0
- package/lib/SimpleFanAccessory.js +137 -0
- package/lib/SimpleFanLightAccessory.js +201 -0
- package/lib/SimpleHeaterAccessory.js +154 -0
- package/lib/SimpleLightAccessory.js +39 -0
- package/lib/SwitchAccessory.js +106 -0
- package/lib/TWLightAccessory.js +91 -0
- package/lib/TuyaAccessory.js +746 -0
- package/lib/TuyaDiscovery.js +165 -0
- package/lib/ValveAccessory.js +150 -0
- 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;
|