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
package/index.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
const TuyaAccessory = require('./lib/TuyaAccessory');
|
|
2
|
+
const TuyaDiscovery = require('./lib/TuyaDiscovery');
|
|
3
|
+
|
|
4
|
+
const OutletAccessory = require('./lib/OutletAccessory');
|
|
5
|
+
const SimpleLightAccessory = require('./lib/SimpleLightAccessory');
|
|
6
|
+
const MultiOutletAccessory = require('./lib/MultiOutletAccessory');
|
|
7
|
+
const CustomMultiOutletAccessory = require('./lib/CustomMultiOutletAccessory');
|
|
8
|
+
const RGBTWLightAccessory = require('./lib/RGBTWLightAccessory');
|
|
9
|
+
const RGBTWOutletAccessory = require('./lib/RGBTWOutletAccessory');
|
|
10
|
+
const TWLightAccessory = require('./lib/TWLightAccessory');
|
|
11
|
+
const AirConditionerAccessory = require('./lib/AirConditionerAccessory');
|
|
12
|
+
const AirPurifierAccessory = require('./lib/AirPurifierAccessory');
|
|
13
|
+
const DehumidifierAccessory = require('./lib/DehumidifierAccessory');
|
|
14
|
+
const ConvectorAccessory = require('./lib/ConvectorAccessory');
|
|
15
|
+
const GarageDoorAccessory = require('./lib/GarageDoorAccessory');
|
|
16
|
+
const SimpleDimmerAccessory = require('./lib/SimpleDimmerAccessory');
|
|
17
|
+
const SimpleDimmer2Accessory = require('./lib/SimpleDimmer2Accessory');
|
|
18
|
+
const SimpleBlindsAccessory = require('./lib/SimpleBlindsAccessory');
|
|
19
|
+
const SimpleHeaterAccessory = require('./lib/SimpleHeaterAccessory');
|
|
20
|
+
const SimpleFanAccessory = require('./lib/SimpleFanAccessory');
|
|
21
|
+
const SimpleFanLightAccessory = require('./lib/SimpleFanLightAccessory');
|
|
22
|
+
const SwitchAccessory = require('./lib/SwitchAccessory');
|
|
23
|
+
const ValveAccessory = require('./lib/ValveAccessory');
|
|
24
|
+
const OilDiffuserAccessory = require('./lib/OilDiffuserAccessory');
|
|
25
|
+
const DoorbellAccessory = require('./lib/DoorbellAccessory');
|
|
26
|
+
|
|
27
|
+
const PLUGIN_NAME = 'homebridge-tuya';
|
|
28
|
+
const PLATFORM_NAME = 'TuyaLan';
|
|
29
|
+
|
|
30
|
+
const CLASS_DEF = {
|
|
31
|
+
outlet: OutletAccessory,
|
|
32
|
+
simplelight: SimpleLightAccessory,
|
|
33
|
+
rgbtwlight: RGBTWLightAccessory,
|
|
34
|
+
rgbtwoutlet: RGBTWOutletAccessory,
|
|
35
|
+
twlight: TWLightAccessory,
|
|
36
|
+
multioutlet: MultiOutletAccessory,
|
|
37
|
+
custommultioutlet: CustomMultiOutletAccessory,
|
|
38
|
+
airconditioner: AirConditionerAccessory,
|
|
39
|
+
airpurifier: AirPurifierAccessory,
|
|
40
|
+
dehumidifier: DehumidifierAccessory,
|
|
41
|
+
convector: ConvectorAccessory,
|
|
42
|
+
garagedoor: GarageDoorAccessory,
|
|
43
|
+
simpledimmer: SimpleDimmerAccessory,
|
|
44
|
+
simpledimmer2: SimpleDimmer2Accessory,
|
|
45
|
+
simpleblinds: SimpleBlindsAccessory,
|
|
46
|
+
simpleheater: SimpleHeaterAccessory,
|
|
47
|
+
switch: SwitchAccessory,
|
|
48
|
+
fan: SimpleFanAccessory,
|
|
49
|
+
fanlight: SimpleFanLightAccessory,
|
|
50
|
+
watervalve: ValveAccessory,
|
|
51
|
+
oildiffuser: OilDiffuserAccessory,
|
|
52
|
+
doorbell: DoorbellAccessory
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
let Characteristic, PlatformAccessory, Service, Categories, AdaptiveLightingController, UUID;
|
|
56
|
+
|
|
57
|
+
module.exports = function(homebridge) {
|
|
58
|
+
({
|
|
59
|
+
platformAccessory: PlatformAccessory,
|
|
60
|
+
hap: {Characteristic, Service, AdaptiveLightingController, Accessory: {Categories}, uuid: UUID}
|
|
61
|
+
} = homebridge);
|
|
62
|
+
|
|
63
|
+
homebridge.registerPlatform(PLUGIN_NAME, PLATFORM_NAME, TuyaLan, true);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
class TuyaLan {
|
|
67
|
+
constructor(...props) {
|
|
68
|
+
[this.log, this.config, this.api] = [...props];
|
|
69
|
+
|
|
70
|
+
this.cachedAccessories = new Map();
|
|
71
|
+
this.api.hap.EnergyCharacteristics = require('./lib/EnergyCharacteristics')(this.api.hap.Characteristic);
|
|
72
|
+
|
|
73
|
+
if(!this.config || !this.config.devices) {
|
|
74
|
+
this.log("No devices found. Check that you have specified them in your config.json file.");
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this._expectedUUIDs = this.config.devices.map(device => UUID.generate(PLUGIN_NAME +(device.fake ? ':fake:' : ':') + device.id));
|
|
79
|
+
|
|
80
|
+
this.api.on('didFinishLaunching', () => {
|
|
81
|
+
this.discoverDevices();
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
discoverDevices() {
|
|
86
|
+
const devices = {};
|
|
87
|
+
const connectedDevices = [];
|
|
88
|
+
const fakeDevices = [];
|
|
89
|
+
this.config.devices.forEach(device => {
|
|
90
|
+
try {
|
|
91
|
+
device.id = ('' + device.id).trim();
|
|
92
|
+
device.key = ('' + device.key).trim();
|
|
93
|
+
device.type = ('' + device.type).trim();
|
|
94
|
+
|
|
95
|
+
device.ip = ('' + (device.ip || '')).trim();
|
|
96
|
+
} catch(ex) {}
|
|
97
|
+
|
|
98
|
+
if (!device.type) return this.log.error('%s (%s) doesn\'t have a type defined.', device.name || 'Unnamed device', device.id);
|
|
99
|
+
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);
|
|
100
|
+
|
|
101
|
+
if (device.fake) fakeDevices.push({name: device.id.slice(8), ...device});
|
|
102
|
+
else devices[device.id] = {name: device.id.slice(8), ...device};
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const deviceIds = Object.keys(devices);
|
|
106
|
+
if (deviceIds.length === 0) return this.log.error('No valid configured devices found.');
|
|
107
|
+
|
|
108
|
+
this.log.info('Starting discovery...');
|
|
109
|
+
|
|
110
|
+
TuyaDiscovery.start({ids: deviceIds, log: this.log})
|
|
111
|
+
.on('discover', config => {
|
|
112
|
+
if (!config || !config.id) return;
|
|
113
|
+
if (!devices[config.id]) return this.log.warn('Discovered a device that has not been configured yet (%s@%s).', config.id, config.ip);
|
|
114
|
+
|
|
115
|
+
connectedDevices.push(config.id);
|
|
116
|
+
|
|
117
|
+
this.log.info('Discovered %s (%s) identified as %s (%s)', devices[config.id].name, config.id, devices[config.id].type, config.version);
|
|
118
|
+
|
|
119
|
+
const device = new TuyaAccessory({
|
|
120
|
+
...devices[config.id], ...config,
|
|
121
|
+
log: this.log,
|
|
122
|
+
UUID: UUID.generate(PLUGIN_NAME + ':' + config.id),
|
|
123
|
+
connect: false
|
|
124
|
+
});
|
|
125
|
+
this.addAccessory(device);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
fakeDevices.forEach(config => {
|
|
129
|
+
this.log.info('Adding fake device: %s', config.name);
|
|
130
|
+
this.addAccessory(new TuyaAccessory({
|
|
131
|
+
...config,
|
|
132
|
+
log: this.log,
|
|
133
|
+
UUID: UUID.generate(PLUGIN_NAME + ':fake:' + config.id),
|
|
134
|
+
connect: false
|
|
135
|
+
}));
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
setTimeout(() => {
|
|
139
|
+
deviceIds.forEach(deviceId => {
|
|
140
|
+
if (connectedDevices.includes(deviceId)) return;
|
|
141
|
+
|
|
142
|
+
if (devices[deviceId].ip) {
|
|
143
|
+
|
|
144
|
+
this.log.info('Failed to discover %s (%s) in time but will connect via %s.', devices[deviceId].name, deviceId, devices[deviceId].ip);
|
|
145
|
+
|
|
146
|
+
const device = new TuyaAccessory({
|
|
147
|
+
...devices[deviceId],
|
|
148
|
+
log: this.log,
|
|
149
|
+
UUID: UUID.generate(PLUGIN_NAME + ':' + deviceId),
|
|
150
|
+
connect: false
|
|
151
|
+
});
|
|
152
|
+
this.addAccessory(device);
|
|
153
|
+
} else {
|
|
154
|
+
this.log.warn('Failed to discover %s (%s) in time but will keep looking.', devices[deviceId].name, deviceId);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}, 60000);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
registerPlatformAccessories(platformAccessories) {
|
|
161
|
+
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, Array.isArray(platformAccessories) ? platformAccessories : [platformAccessories]);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
configureAccessory(accessory) {
|
|
165
|
+
// also checks null objects or empty config - this._expectedUUIDs
|
|
166
|
+
if (accessory instanceof PlatformAccessory && this._expectedUUIDs && this._expectedUUIDs.includes(accessory.UUID)) {
|
|
167
|
+
this.cachedAccessories.set(accessory.UUID, accessory);
|
|
168
|
+
accessory.services.forEach(service => {
|
|
169
|
+
if (service.UUID === Service.AccessoryInformation.UUID) return;
|
|
170
|
+
service.characteristics.some(characteristic => {
|
|
171
|
+
if (!characteristic.props ||
|
|
172
|
+
!Array.isArray(characteristic.props.perms) ||
|
|
173
|
+
characteristic.props.perms.length !== 3 ||
|
|
174
|
+
!(characteristic.props.perms.includes(Characteristic.Perms.WRITE) && characteristic.props.perms.includes(Characteristic.Perms.NOTIFY))
|
|
175
|
+
) return;
|
|
176
|
+
|
|
177
|
+
this.log.info('Marked %s unreachable by faulting Service.%s.%s', accessory.displayName, service.displayName, characteristic.displayName);
|
|
178
|
+
|
|
179
|
+
characteristic.updateValue(new Error('Unreachable'));
|
|
180
|
+
return true;
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
} else {
|
|
184
|
+
/*
|
|
185
|
+
* Irrespective of this unregistering, Homebridge continues
|
|
186
|
+
* to "_prepareAssociatedHAPAccessory" and "addBridgedAccessory".
|
|
187
|
+
* This timeout will hopefully remove the accessory after that has happened.
|
|
188
|
+
*/
|
|
189
|
+
setTimeout(() => {
|
|
190
|
+
this.removeAccessory(accessory);
|
|
191
|
+
}, 1000);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
addAccessory(device) {
|
|
196
|
+
const deviceConfig = device.context;
|
|
197
|
+
const type = (deviceConfig.type || '').toLowerCase();
|
|
198
|
+
|
|
199
|
+
const Accessory = CLASS_DEF[type];
|
|
200
|
+
|
|
201
|
+
let accessory = this.cachedAccessories.get(deviceConfig.UUID),
|
|
202
|
+
isCached = true;
|
|
203
|
+
|
|
204
|
+
if (accessory && accessory.category !== Accessory.getCategory(Categories)) {
|
|
205
|
+
this.log.info("%s has a different type (%s vs %s)", accessory.displayName, accessory.category, Accessory.getCategory(Categories));
|
|
206
|
+
this.removeAccessory(accessory);
|
|
207
|
+
accessory = null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!accessory) {
|
|
211
|
+
accessory = new PlatformAccessory(deviceConfig.name, deviceConfig.UUID, Accessory.getCategory(Categories));
|
|
212
|
+
accessory.getService(Service.AccessoryInformation)
|
|
213
|
+
.setCharacteristic(Characteristic.Manufacturer, deviceConfig.manufacturer || "Unknown")
|
|
214
|
+
.setCharacteristic(Characteristic.Model, deviceConfig.model || "Unknown")
|
|
215
|
+
.setCharacteristic(Characteristic.SerialNumber, deviceConfig.id.slice(8));
|
|
216
|
+
|
|
217
|
+
isCached = false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (accessory && accessory.displayName !== deviceConfig.name) {
|
|
221
|
+
this.log.info(
|
|
222
|
+
"Configuration name %s differs from cached displayName %s. Updating cached displayName to %s ",
|
|
223
|
+
deviceConfig.name, accessory.displayName, deviceConfig.name);
|
|
224
|
+
accessory.displayName = deviceConfig.name;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
this.cachedAccessories.set(deviceConfig.UUID, new Accessory(this, accessory, device, !isCached));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
removeAccessory(homebridgeAccessory) {
|
|
231
|
+
if (!homebridgeAccessory) return;
|
|
232
|
+
|
|
233
|
+
this.log.warn('Unregistering', homebridgeAccessory.displayName);
|
|
234
|
+
|
|
235
|
+
delete this.cachedAccessories[homebridgeAccessory.UUID];
|
|
236
|
+
this.api.unregisterPlatformAccessories(PLATFORM_NAME, PLATFORM_NAME, [homebridgeAccessory]);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
removeAccessoryByUUID(uuid) {
|
|
240
|
+
if (uuid) this.removeAccessory(this.cachedAccessories.get(uuid));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
const BaseAccessory = require('./BaseAccessory');
|
|
2
|
+
|
|
3
|
+
const STATE_OTHER = 9;
|
|
4
|
+
|
|
5
|
+
class AirConditionerAccessory extends BaseAccessory {
|
|
6
|
+
static getCategory(Categories) {
|
|
7
|
+
return Categories.AIR_CONDITIONER;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
constructor(...props) {
|
|
11
|
+
super(...props);
|
|
12
|
+
|
|
13
|
+
this.cmdCool = 'COOL';
|
|
14
|
+
if (this.device.context.cmdCool) {
|
|
15
|
+
if (/^c[a-z]+$/i.test(this.device.context.cmdCool)) this.cmdCool = ('' + this.device.context.cmdCool).trim();
|
|
16
|
+
else throw new Error('The cmdCool doesn\'t appear to be valid: ' + this.device.context.cmdCool);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
this.cmdHeat = 'HEAT';
|
|
20
|
+
if (this.device.context.cmdHeat) {
|
|
21
|
+
if (/^h[a-z]+$/i.test(this.device.context.cmdHeat)) this.cmdHeat = ('' + this.device.context.cmdHeat).trim();
|
|
22
|
+
else throw new Error('The cmdHeat doesn\'t appear to be valid: ' + this.device.context.cmdHeat);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
this.cmdAuto = 'AUTO';
|
|
26
|
+
if (this.device.context.cmdAuto) {
|
|
27
|
+
if (/^a[a-z]+$/i.test(this.device.context.cmdAuto)) this.cmdAuto = ('' + this.device.context.cmdAuto).trim();
|
|
28
|
+
else throw new Error('The cmdAuto doesn\'t appear to be valid: ' + this.device.context.cmdAuto);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Disabling auto mode because I have not found a Tuya device config that has a temperature range for AUTO
|
|
32
|
+
this.device.context.noAuto = true;
|
|
33
|
+
|
|
34
|
+
if (!this.device.context.noRotationSpeed) {
|
|
35
|
+
const fanSpeedSteps = (this.device.context.fanSpeedSteps && isFinite(this.device.context.fanSpeedSteps) && this.device.context.fanSpeedSteps > 0 && this.device.context.fanSpeedSteps < 100) ? this.device.context.fanSpeedSteps : 100;
|
|
36
|
+
this._rotationSteps = [0];
|
|
37
|
+
this._rotationStops = {0: 0};
|
|
38
|
+
for (let i = 0; i++ < 100;) {
|
|
39
|
+
const _rotationStep = Math.floor(fanSpeedSteps * (i - 1) / 100) + 1;
|
|
40
|
+
this._rotationSteps.push(_rotationStep);
|
|
41
|
+
this._rotationStops[_rotationStep] = i;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_registerPlatformAccessory() {
|
|
47
|
+
const {Service} = this.hap;
|
|
48
|
+
|
|
49
|
+
this.accessory.addService(Service.HeaterCooler, this.device.context.name);
|
|
50
|
+
|
|
51
|
+
super._registerPlatformAccessory();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_registerCharacteristics(dps) {
|
|
55
|
+
const {Service, Characteristic} = this.hap;
|
|
56
|
+
const service = this.accessory.getService(Service.HeaterCooler);
|
|
57
|
+
this._checkServiceName(service, this.device.context.name);
|
|
58
|
+
|
|
59
|
+
this.dpActive = this._getCustomDP(this.device.context.dpActive) || '1';
|
|
60
|
+
this.dpThreshold = this._getCustomDP(this.device.context.dpThreshold) || '2';
|
|
61
|
+
this.dpCurrentTemperature = this._getCustomDP(this.device.context.dpCurrentTemperature) || '3';
|
|
62
|
+
this.dpMode = this._getCustomDP(this.device.context.dpMode) || '4';
|
|
63
|
+
this.dpRotationSpeed = this._getCustomDP(this.device.context.dpRotationSpeed) || '5';
|
|
64
|
+
this.dpChildLock = this._getCustomDP(this.device.context.dpChildLock) || '6';
|
|
65
|
+
this.dpTempUnits = this._getCustomDP(this.device.context.dpTempUnits) || '19';
|
|
66
|
+
this.dpSwingMode = this._getCustomDP(this.device.context.dpSwingMode) || '104';
|
|
67
|
+
|
|
68
|
+
const characteristicActive = service.getCharacteristic(Characteristic.Active)
|
|
69
|
+
.updateValue(this._getActive(this.dpActive))
|
|
70
|
+
.on('get', this.getActive.bind(this))
|
|
71
|
+
.on('set', this.setActive.bind(this));
|
|
72
|
+
|
|
73
|
+
const characteristicCurrentHeaterCoolerState = service.getCharacteristic(Characteristic.CurrentHeaterCoolerState)
|
|
74
|
+
.updateValue(this._getCurrentHeaterCoolerState(dps))
|
|
75
|
+
.on('get', this.getCurrentHeaterCoolerState.bind(this));
|
|
76
|
+
|
|
77
|
+
const _validTargetHeaterCoolerStateValues = [STATE_OTHER];
|
|
78
|
+
if (!this.device.context.noCool) _validTargetHeaterCoolerStateValues.unshift(Characteristic.TargetHeaterCoolerState.COOL);
|
|
79
|
+
if (!this.device.context.noHeat) _validTargetHeaterCoolerStateValues.unshift(Characteristic.TargetHeaterCoolerState.HEAT);
|
|
80
|
+
if (!this.device.context.noAuto) _validTargetHeaterCoolerStateValues.unshift(Characteristic.TargetHeaterCoolerState.AUTO);
|
|
81
|
+
|
|
82
|
+
const characteristicTargetHeaterCoolerState = service.getCharacteristic(Characteristic.TargetHeaterCoolerState)
|
|
83
|
+
.setProps({
|
|
84
|
+
maxValue: 9,
|
|
85
|
+
validValues: _validTargetHeaterCoolerStateValues
|
|
86
|
+
})
|
|
87
|
+
.updateValue(this._getTargetHeaterCoolerState(dps[this.dpMode]))
|
|
88
|
+
.on('get', this.getTargetHeaterCoolerState.bind(this))
|
|
89
|
+
.on('set', this.setTargetHeaterCoolerState.bind(this));
|
|
90
|
+
|
|
91
|
+
const characteristicCurrentTemperature = service.getCharacteristic(Characteristic.CurrentTemperature)
|
|
92
|
+
.updateValue(dps[this.dpSwingMode])
|
|
93
|
+
.on('get', this.getState.bind(this, this.dpSwingMode));
|
|
94
|
+
|
|
95
|
+
let characteristicSwingMode;
|
|
96
|
+
if (!this.device.context.noSwing) {
|
|
97
|
+
characteristicSwingMode = service.getCharacteristic(Characteristic.SwingMode)
|
|
98
|
+
.updateValue(this._getSwingMode(dps[this.dpSwingMode]))
|
|
99
|
+
.on('get', this.getSwingMode.bind(this))
|
|
100
|
+
.on('set', this.setSwingMode.bind(this));
|
|
101
|
+
} else this._removeCharacteristic(service, Characteristic.SwingMode);
|
|
102
|
+
|
|
103
|
+
let characteristicLockPhysicalControls;
|
|
104
|
+
if (!this.device.context.noChildLock) {
|
|
105
|
+
characteristicLockPhysicalControls = service.getCharacteristic(Characteristic.LockPhysicalControls)
|
|
106
|
+
.updateValue(this._getLockPhysicalControls(dps[this.dpChildLock]))
|
|
107
|
+
.on('get', this.getLockPhysicalControls.bind(this))
|
|
108
|
+
.on('set', this.setLockPhysicalControls.bind(this));
|
|
109
|
+
} else this._removeCharacteristic(service, Characteristic.LockPhysicalControls);
|
|
110
|
+
|
|
111
|
+
let characteristicCoolingThresholdTemperature;
|
|
112
|
+
if (!this.device.context.noCool) {
|
|
113
|
+
characteristicCoolingThresholdTemperature = service.getCharacteristic(Characteristic.CoolingThresholdTemperature)
|
|
114
|
+
.setProps({
|
|
115
|
+
minValue: this.device.context.minTemperature || 10,
|
|
116
|
+
maxValue: this.device.context.maxTemperature || 35,
|
|
117
|
+
minStep: this.device.context.minTemperatureSteps || 1
|
|
118
|
+
})
|
|
119
|
+
.updateValue(this.dpThreshold)
|
|
120
|
+
.on('get', this.getState.bind(this, this.dpThreshold))
|
|
121
|
+
.on('set', this.setTargetThresholdTemperature.bind(this, 'cool'));
|
|
122
|
+
} else this._removeCharacteristic(service, Characteristic.CoolingThresholdTemperature);
|
|
123
|
+
|
|
124
|
+
let characteristicHeatingThresholdTemperature;
|
|
125
|
+
if (!this.device.context.noHeat) {
|
|
126
|
+
characteristicHeatingThresholdTemperature = service.getCharacteristic(Characteristic.HeatingThresholdTemperature)
|
|
127
|
+
.setProps({
|
|
128
|
+
minValue: this.device.context.minTemperature || 10,
|
|
129
|
+
maxValue: this.device.context.maxTemperature || 35,
|
|
130
|
+
minStep: this.device.context.minTemperatureSteps || 1
|
|
131
|
+
})
|
|
132
|
+
.updateValue(dps[this.dpThreshold])
|
|
133
|
+
.on('get', this.getState.bind(this, this.dpThreshold))
|
|
134
|
+
.on('set', this.setTargetThresholdTemperature.bind(this, 'heat'));
|
|
135
|
+
} else this._removeCharacteristic(service, Characteristic.HeatingThresholdTemperature);
|
|
136
|
+
|
|
137
|
+
const characteristicTemperatureDisplayUnits = service.getCharacteristic(Characteristic.TemperatureDisplayUnits)
|
|
138
|
+
.updateValue(this._getTemperatureDisplayUnits(dps[this.dpTempUnits]))
|
|
139
|
+
.on('get', this.getTemperatureDisplayUnits.bind(this))
|
|
140
|
+
.on('set', this.setTemperatureDisplayUnits.bind(this));
|
|
141
|
+
|
|
142
|
+
let characteristicRotationSpeed;
|
|
143
|
+
if (!this.device.context.noRotationSpeed) {
|
|
144
|
+
characteristicRotationSpeed = service.getCharacteristic(Characteristic.RotationSpeed)
|
|
145
|
+
.updateValue(this._getRotationSpeed(dps))
|
|
146
|
+
.on('get', this.getRotationSpeed.bind(this))
|
|
147
|
+
.on('set', this.setRotationSpeed.bind(this));
|
|
148
|
+
} else this._removeCharacteristic(service, Characteristic.RotationSpeed);
|
|
149
|
+
|
|
150
|
+
this.characteristicCoolingThresholdTemperature = characteristicCoolingThresholdTemperature;
|
|
151
|
+
this.characteristicHeatingThresholdTemperature = characteristicHeatingThresholdTemperature;
|
|
152
|
+
|
|
153
|
+
this.device.on('change', (changes, state) => {
|
|
154
|
+
if (changes.hasOwnProperty(this.dpActive)) {
|
|
155
|
+
const newActive = this._getActive(changes[this.dpActive]);
|
|
156
|
+
if (characteristicActive.value !== newActive) {
|
|
157
|
+
characteristicActive.updateValue(newActive);
|
|
158
|
+
|
|
159
|
+
if (!changes.hasOwnProperty(this.dpMode)) {
|
|
160
|
+
characteristicCurrentHeaterCoolerState.updateValue(this._getCurrentHeaterCoolerState(state));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!changes.hasOwnProperty(this.dpRotationSpeed)) {
|
|
164
|
+
characteristicRotationSpeed.updateValue(this._getRotationSpeed(state));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (characteristicLockPhysicalControls && changes.hasOwnProperty(this.dpChildLock)) {
|
|
170
|
+
const newLockPhysicalControls = this._getLockPhysicalControls(changes[this.dpChildLock]);
|
|
171
|
+
if (characteristicLockPhysicalControls.value !== newLockPhysicalControls) {
|
|
172
|
+
characteristicLockPhysicalControls.updateValue(newLockPhysicalControls);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (changes.hasOwnProperty(this.dpThreshold)) {
|
|
177
|
+
if (!this.device.context.noCool && characteristicCoolingThresholdTemperature && characteristicCoolingThresholdTemperature.value !== changes[this.dpThreshold])
|
|
178
|
+
characteristicCoolingThresholdTemperature.updateValue(changes[this.dpThreshold]);
|
|
179
|
+
if (!this.device.context.noHeat && characteristicHeatingThresholdTemperature && characteristicHeatingThresholdTemperature.value !== changes[this.dpThreshold])
|
|
180
|
+
characteristicHeatingThresholdTemperature.updateValue(changes[this.dpThreshold]);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (changes.hasOwnProperty(this.dpCurrentTemperature) && characteristicCurrentTemperature.value !== changes[this.dpCurrentTemperature]) characteristicCurrentTemperature.updateValue(changes[this.dpCurrentTemperature]);
|
|
184
|
+
|
|
185
|
+
if (changes.hasOwnProperty(this.dpMode)) {
|
|
186
|
+
const newTargetHeaterCoolerState = this._getTargetHeaterCoolerState(changes[this.dpMode]);
|
|
187
|
+
const newCurrentHeaterCoolerState = this._getCurrentHeaterCoolerState(state);
|
|
188
|
+
if (characteristicTargetHeaterCoolerState.value !== newTargetHeaterCoolerState) characteristicTargetHeaterCoolerState.updateValue(newTargetHeaterCoolerState);
|
|
189
|
+
if (characteristicCurrentHeaterCoolerState.value !== newCurrentHeaterCoolerState) characteristicCurrentHeaterCoolerState.updateValue(newCurrentHeaterCoolerState);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (changes.hasOwnProperty(this.dpSwingMode)) {
|
|
193
|
+
const newSwingMode = this._getSwingMode(changes[this.dpSwingMode]);
|
|
194
|
+
if (characteristicSwingMode.value !== newSwingMode) characteristicSwingMode.updateValue(newSwingMode);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (changes.hasOwnProperty(this.dpTempUnits)) {
|
|
198
|
+
const newTemperatureDisplayUnits = this._getTemperatureDisplayUnits(changes[this.dpTempUnits]);
|
|
199
|
+
if (characteristicTemperatureDisplayUnits.value !== newTemperatureDisplayUnits) characteristicTemperatureDisplayUnits.updateValue(newTemperatureDisplayUnits);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (changes.hasOwnProperty(this.dpRotationSpeed)) {
|
|
203
|
+
const newRotationSpeed = this._getRotationSpeed(state);
|
|
204
|
+
if (characteristicRotationSpeed.value !== newRotationSpeed) characteristicRotationSpeed.updateValue(newRotationSpeed);
|
|
205
|
+
|
|
206
|
+
if (!changes.hasOwnProperty(this.dpMode)) {
|
|
207
|
+
characteristicCurrentHeaterCoolerState.updateValue(this._getCurrentHeaterCoolerState(state));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
getActive(callback) {
|
|
214
|
+
this.getState(this.dpActive, (err, dp) => {
|
|
215
|
+
if (err) return callback(err);
|
|
216
|
+
|
|
217
|
+
callback(null, this._getActive(dp));
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
_getActive(dp) {
|
|
222
|
+
const {Characteristic} = this.hap;
|
|
223
|
+
|
|
224
|
+
return dp ? Characteristic.Active.ACTIVE : Characteristic.Active.INACTIVE;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
setActive(value, callback) {
|
|
228
|
+
const {Characteristic} = this.hap;
|
|
229
|
+
|
|
230
|
+
switch (value) {
|
|
231
|
+
case Characteristic.Active.ACTIVE:
|
|
232
|
+
return this.setState(this.dpActive, true, callback);
|
|
233
|
+
|
|
234
|
+
case Characteristic.Active.INACTIVE:
|
|
235
|
+
return this.setState(this.dpActive, false, callback);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
callback();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
getLockPhysicalControls(callback) {
|
|
242
|
+
this.getState(this.dpChildLock, (err, dp) => {
|
|
243
|
+
if (err) return callback(err);
|
|
244
|
+
|
|
245
|
+
callback(null, this._getLockPhysicalControls(dp));
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
_getLockPhysicalControls(dp) {
|
|
250
|
+
const {Characteristic} = this.hap;
|
|
251
|
+
|
|
252
|
+
return dp ? Characteristic.LockPhysicalControls.CONTROL_LOCK_ENABLED : Characteristic.LockPhysicalControls.CONTROL_LOCK_DISABLED;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
setLockPhysicalControls(value, callback) {
|
|
256
|
+
const {Characteristic} = this.hap;
|
|
257
|
+
|
|
258
|
+
switch (value) {
|
|
259
|
+
case Characteristic.LockPhysicalControls.CONTROL_LOCK_ENABLED:
|
|
260
|
+
return this.setState(this.dpChildLock, true, callback);
|
|
261
|
+
|
|
262
|
+
case Characteristic.LockPhysicalControls.CONTROL_LOCK_DISABLED:
|
|
263
|
+
return this.setState(this.dpChildLock, false, callback);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
callback();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
getCurrentHeaterCoolerState(callback) {
|
|
270
|
+
this.getState([this.dpActive, this.dpMode], (err, dps) => {
|
|
271
|
+
if (err) return callback(err);
|
|
272
|
+
|
|
273
|
+
callback(null, this._getCurrentHeaterCoolerState(dps));
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
_getCurrentHeaterCoolerState(dps) {
|
|
278
|
+
const {Characteristic} = this.hap;
|
|
279
|
+
if (!dps[this.dpActive]) return Characteristic.CurrentHeaterCoolerState.INACTIVE;
|
|
280
|
+
|
|
281
|
+
switch (dps[this.dpMode]) {
|
|
282
|
+
case this.cmdCool:
|
|
283
|
+
return Characteristic.CurrentHeaterCoolerState.COOLING;
|
|
284
|
+
|
|
285
|
+
case this.cmdHeat:
|
|
286
|
+
return Characteristic.CurrentHeaterCoolerState.HEATING;
|
|
287
|
+
|
|
288
|
+
default:
|
|
289
|
+
return Characteristic.CurrentHeaterCoolerState.IDLE;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
getTargetHeaterCoolerState(callback) {
|
|
294
|
+
this.getState(this.dpMode, (err, dp) => {
|
|
295
|
+
if (err) return callback(err);
|
|
296
|
+
|
|
297
|
+
callback(null, this._getTargetHeaterCoolerState(dp));
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
_getTargetHeaterCoolerState(dp) {
|
|
302
|
+
const {Characteristic} = this.hap;
|
|
303
|
+
|
|
304
|
+
switch (dp) {
|
|
305
|
+
case this.cmdCool:
|
|
306
|
+
if (this.device.context.noCool) return STATE_OTHER;
|
|
307
|
+
return Characteristic.TargetHeaterCoolerState.COOL;
|
|
308
|
+
|
|
309
|
+
case this.cmdHeat:
|
|
310
|
+
if (this.device.context.noHeat) return STATE_OTHER;
|
|
311
|
+
return Characteristic.TargetHeaterCoolerState.HEAT;
|
|
312
|
+
|
|
313
|
+
case this.cmdAuto:
|
|
314
|
+
if (this.device.context.noAuto) return STATE_OTHER;
|
|
315
|
+
return Characteristic.TargetHeaterCoolerState.AUTO;
|
|
316
|
+
|
|
317
|
+
default:
|
|
318
|
+
return STATE_OTHER;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
setTargetHeaterCoolerState(value, callback) {
|
|
323
|
+
const {Characteristic} = this.hap;
|
|
324
|
+
|
|
325
|
+
switch (value) {
|
|
326
|
+
case Characteristic.TargetHeaterCoolerState.COOL:
|
|
327
|
+
if (this.device.context.noCool) return callback();
|
|
328
|
+
return this.setState(this.dpMode, this.cmdCool, callback);
|
|
329
|
+
|
|
330
|
+
case Characteristic.TargetHeaterCoolerState.HEAT:
|
|
331
|
+
if (this.device.context.noHeat) return callback();
|
|
332
|
+
return this.setState(this.dpMode, this.cmdHeat, callback);
|
|
333
|
+
|
|
334
|
+
case Characteristic.TargetHeaterCoolerState.AUTO:
|
|
335
|
+
if (this.device.context.noAuto) return callback();
|
|
336
|
+
return this.setState(this.dpMode, this.cmdAuto, callback);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
callback();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
getSwingMode(callback) {
|
|
343
|
+
this.getState(this.dpSwingMode, (err, dp) => {
|
|
344
|
+
if (err) return callback(err);
|
|
345
|
+
|
|
346
|
+
callback(null, this._getSwingMode(dp));
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
_getSwingMode(dp) {
|
|
351
|
+
const {Characteristic} = this.hap;
|
|
352
|
+
|
|
353
|
+
return dp ? Characteristic.SwingMode.SWING_ENABLED : Characteristic.SwingMode.SWING_DISABLED;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
setSwingMode(value, callback) {
|
|
357
|
+
if (this.device.context.noSwing) return callback();
|
|
358
|
+
|
|
359
|
+
const {Characteristic} = this.hap;
|
|
360
|
+
|
|
361
|
+
switch (value) {
|
|
362
|
+
case Characteristic.SwingMode.SWING_ENABLED:
|
|
363
|
+
return this.setState(this.dpSwingMode, true, callback);
|
|
364
|
+
|
|
365
|
+
case Characteristic.SwingMode.SWING_DISABLED:
|
|
366
|
+
return this.setState(this.dpSwingMode, false, callback);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
callback();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
setTargetThresholdTemperature(mode, value, callback) {
|
|
373
|
+
this.setState(this.dpThreshold, value, err => {
|
|
374
|
+
if (err) return callback(err);
|
|
375
|
+
|
|
376
|
+
if (mode === 'cool' && !this.device.context.noHeat && this.characteristicHeatingThresholdTemperature) {
|
|
377
|
+
this.characteristicHeatingThresholdTemperature.updateValue(value);
|
|
378
|
+
} else if (mode === 'heat' && !this.device.context.noCool && this.characteristicCoolingThresholdTemperature) {
|
|
379
|
+
this.characteristicCoolingThresholdTemperature.updateValue(value);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
callback();
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
getTemperatureDisplayUnits(callback) {
|
|
386
|
+
this.getState(this.dpTempUnits, (err, dp) => {
|
|
387
|
+
if (err) return callback(err);
|
|
388
|
+
|
|
389
|
+
callback(null, this._getTemperatureDisplayUnits(dp));
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
_getTemperatureDisplayUnits(dp) {
|
|
394
|
+
const {Characteristic} = this.hap;
|
|
395
|
+
|
|
396
|
+
return dp === 'F' ? Characteristic.TemperatureDisplayUnits.FAHRENHEIT : Characteristic.TemperatureDisplayUnits.CELSIUS;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
setTemperatureDisplayUnits(value, callback) {
|
|
400
|
+
const {Characteristic} = this.hap;
|
|
401
|
+
|
|
402
|
+
this.setState(this.dpTempUnits, value === Characteristic.TemperatureDisplayUnits.FAHRENHEIT ? 'F' : 'C', callback);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
getRotationSpeed(callback) {
|
|
406
|
+
this.getState([this.dpActive, this.dpRotationSpeed], (err, dps) => {
|
|
407
|
+
if (err) return callback(err);
|
|
408
|
+
|
|
409
|
+
callback(null, this._getRotationSpeed(dps));
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
_getRotationSpeed(dps) {
|
|
414
|
+
if (!dps[this.dpActive]) return 0;
|
|
415
|
+
|
|
416
|
+
if (this._hkRotationSpeed) {
|
|
417
|
+
const currntRotationSpeed = this.convertRotationSpeedFromHomeKitToTuya(this._hkRotationSpeed);
|
|
418
|
+
|
|
419
|
+
return currntRotationSpeed === dps[this.dpRotationSpeed] ? this._hkRotationSpeed : this.convertRotationSpeedFromTuyaToHomeKit(dps[this.dpRotationSpeed]);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return this._hkRotationSpeed = this.convertRotationSpeedFromTuyaToHomeKit(dps[this.dpRotationSpeed]);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
setRotationSpeed(value, callback) {
|
|
426
|
+
const {Characteristic} = this.hap;
|
|
427
|
+
|
|
428
|
+
if (value === 0) {
|
|
429
|
+
this.setActive(Characteristic.Active.INACTIVE, callback);
|
|
430
|
+
} else {
|
|
431
|
+
this._hkRotationSpeed = value;
|
|
432
|
+
this.setMultiState({[this.dpActive]: true, [this.dpRotationSpeed]: this.convertRotationSpeedFromHomeKitToTuya(value)}, callback);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
convertRotationSpeedFromTuyaToHomeKit(value) {
|
|
437
|
+
return this._rotationStops[parseInt(value)];
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
convertRotationSpeedFromHomeKitToTuya(value) {
|
|
441
|
+
return this.device.context.fanSpeedSteps ? '' + this._rotationSteps[value] : this._rotationSteps[value];
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
module.exports = AirConditionerAccessory;
|