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,165 @@
|
|
|
1
|
+
const dgram = require('dgram');
|
|
2
|
+
const crypto = require('crypto');
|
|
3
|
+
const EventEmitter = require('events');
|
|
4
|
+
|
|
5
|
+
const UDP_KEY = Buffer.from('6c1ec8e2bb9bb59ab50b0daf649b410a', 'hex');
|
|
6
|
+
|
|
7
|
+
class TuyaDiscovery extends EventEmitter {
|
|
8
|
+
constructor() {
|
|
9
|
+
super();
|
|
10
|
+
|
|
11
|
+
this.discovered = new Map();
|
|
12
|
+
this.limitedIds = [];
|
|
13
|
+
this._servers = {};
|
|
14
|
+
this._running = false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
start(props) {
|
|
18
|
+
this.log = props.log;
|
|
19
|
+
|
|
20
|
+
const opts = props || {};
|
|
21
|
+
|
|
22
|
+
if (opts.clear) {
|
|
23
|
+
this.removeAllListeners();
|
|
24
|
+
this.discovered.clear();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
this.limitedIds.splice(0);
|
|
28
|
+
if (Array.isArray(opts.ids)) [].push.apply(this.limitedIds, opts.ids);
|
|
29
|
+
|
|
30
|
+
this._running = true;
|
|
31
|
+
this._start(6666);
|
|
32
|
+
this._start(6667);
|
|
33
|
+
|
|
34
|
+
return this;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
stop() {
|
|
38
|
+
this._running = false;
|
|
39
|
+
this._stop(6666);
|
|
40
|
+
this._stop(6667);
|
|
41
|
+
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
end() {
|
|
46
|
+
this.stop();
|
|
47
|
+
process.nextTick(() => {
|
|
48
|
+
this.removeAllListeners();
|
|
49
|
+
this.discovered.clear();
|
|
50
|
+
this.log.info('Discovery ended.');
|
|
51
|
+
this.emit('end');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
_start(port) {
|
|
58
|
+
this._stop(port);
|
|
59
|
+
|
|
60
|
+
const server = this._servers[port] = dgram.createSocket({type: 'udp4', reuseAddr: true});
|
|
61
|
+
server.on('error', this._onDgramError.bind(this, port));
|
|
62
|
+
server.on('close', this._onDgramClose.bind(this, port));
|
|
63
|
+
server.on('message', this._onDgramMessage.bind(this, port));
|
|
64
|
+
|
|
65
|
+
server.bind(port, () => {
|
|
66
|
+
this.log.info(`Discovery - Discovery started on port ${port}.`);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_stop(port) {
|
|
71
|
+
if (this._servers[port]) {
|
|
72
|
+
this._servers[port].removeAllListeners();
|
|
73
|
+
this._servers[port].close();
|
|
74
|
+
this._servers[port] = null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
_onDgramError(port, err) {
|
|
79
|
+
this._stop(port);
|
|
80
|
+
|
|
81
|
+
if (err && err.code === 'EADDRINUSE') {
|
|
82
|
+
this.log.warn(`Discovery - Port ${port} is in use. Will retry in 15 seconds.`);
|
|
83
|
+
|
|
84
|
+
setTimeout(() => {
|
|
85
|
+
this._start(port);
|
|
86
|
+
}, 15000);
|
|
87
|
+
} else {
|
|
88
|
+
this.log.error(`Discovery - Port ${port} failed:\n${err.stack}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
_onDgramClose(port) {
|
|
93
|
+
this._stop(port);
|
|
94
|
+
|
|
95
|
+
this.log.info(`Discovery - Port ${port} closed.${this._running ? ' Restarting...' : ''}`);
|
|
96
|
+
if (this._running)
|
|
97
|
+
setTimeout(() => {
|
|
98
|
+
this._start(port);
|
|
99
|
+
}, 1000);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
_onDgramMessage(port, msg, info) {
|
|
103
|
+
const len = msg.length;
|
|
104
|
+
// this.log.info(`Discovery - UDP from ${info.address}:${port} 0x${msg.readUInt32BE(0).toString(16).padStart(8, '0')}...0x${msg.readUInt32BE(len - 4).toString(16).padStart(8, '0')}`);
|
|
105
|
+
if (len < 16 ||
|
|
106
|
+
msg.readUInt32BE(0) !== 0x000055aa ||
|
|
107
|
+
msg.readUInt32BE(len - 4) !== 0x0000aa55
|
|
108
|
+
) {
|
|
109
|
+
this.log.error(`Discovery - UDP from ${info.address}:${port}`, msg.toString('hex'));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const size = msg.readUInt32BE(12);
|
|
114
|
+
if (len - size < 8) {
|
|
115
|
+
this.log.error(`Discovery - UDP from ${info.address}:${port} size ${len - size}`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
//const result = {cmd: msg.readUInt32BE(8)};
|
|
120
|
+
const cleanMsg = msg.slice(len - size + 4, len - 8);
|
|
121
|
+
|
|
122
|
+
let decryptedMsg;
|
|
123
|
+
if (port === 6667) {
|
|
124
|
+
try {
|
|
125
|
+
const decipher = crypto.createDecipheriv('aes-128-ecb', UDP_KEY, '');
|
|
126
|
+
decryptedMsg = decipher.update(cleanMsg, 'utf8', 'utf8');
|
|
127
|
+
decryptedMsg += decipher.final('utf8');
|
|
128
|
+
} catch (ex) {}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!decryptedMsg) decryptedMsg = cleanMsg.toString('utf8');
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const result = JSON.parse(decryptedMsg);
|
|
135
|
+
if (result && result.gwId && result.ip) this._onDiscover(result);
|
|
136
|
+
else this.log.error(`Discovery - UDP from ${info.address}:${port} decrypted`, cleanMsg.toString('hex'));
|
|
137
|
+
} catch (ex) {
|
|
138
|
+
this.log.error(`Discovery - Failed to parse discovery response on port ${port}: ${decryptedMsg}`);
|
|
139
|
+
this.log.error(`Discovery - Failed to parse discovery raw message on port ${port}: ${msg.toString('hex')}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
_onDiscover(data) {
|
|
144
|
+
if (this.discovered.has(data.gwId)) return;
|
|
145
|
+
|
|
146
|
+
data.id = data.gwId;
|
|
147
|
+
delete data.gwId;
|
|
148
|
+
|
|
149
|
+
this.discovered.set(data.id, data.ip);
|
|
150
|
+
|
|
151
|
+
this.emit('discover', data);
|
|
152
|
+
|
|
153
|
+
if (this.limitedIds.length &&
|
|
154
|
+
this.limitedIds.includes(data.id) && // Just to avoid checking the rest unnecessarily
|
|
155
|
+
this.limitedIds.length <= this.discovered.size &&
|
|
156
|
+
this.limitedIds.every(id => this.discovered.has(id))
|
|
157
|
+
) {
|
|
158
|
+
process.nextTick(() => {
|
|
159
|
+
this.end();
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = new TuyaDiscovery();
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
const BaseAccessory = require('./BaseAccessory');
|
|
2
|
+
|
|
3
|
+
class ValveAccessory extends BaseAccessory {
|
|
4
|
+
static getCategory(Categories) {
|
|
5
|
+
return Categories.FAUCET;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
constructor(...props) {
|
|
9
|
+
super(...props);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
_registerPlatformAccessory() {
|
|
13
|
+
const {Service} = this.hap;
|
|
14
|
+
|
|
15
|
+
this.accessory.addService(Service.Valve, this.device.context.name);
|
|
16
|
+
|
|
17
|
+
super._registerPlatformAccessory();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
_registerCharacteristics(dps) {
|
|
21
|
+
const {Service, Characteristic} = this.hap;
|
|
22
|
+
const service = this.accessory.getService(Service.Valve);
|
|
23
|
+
this._checkServiceName(service, this.device.context.name);
|
|
24
|
+
this.setDuration = this.device.context.defaultDuration || 600;
|
|
25
|
+
this.noTimer = this.device.context.noTimer;
|
|
26
|
+
this.lastActivationTime = null
|
|
27
|
+
this.timer = null;
|
|
28
|
+
|
|
29
|
+
switch (this.device.context.valveType) {
|
|
30
|
+
case 'IRRIGATION':
|
|
31
|
+
service.getCharacteristic(Characteristic.ValveType).updateValue(1);
|
|
32
|
+
break;
|
|
33
|
+
case 'SHOWER_HEAD':
|
|
34
|
+
service.getCharacteristic(Characteristic.ValveType).updateValue(2);
|
|
35
|
+
break;
|
|
36
|
+
case 'WATER_FAUCET':
|
|
37
|
+
service.getCharacteristic(Characteristic.ValveType).updateValue(3);
|
|
38
|
+
break;
|
|
39
|
+
default:
|
|
40
|
+
service.getCharacteristic(Characteristic.ValveType).updateValue(0);
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.dpPower = this._getCustomDP(this.device.context.dpPower) || '1';
|
|
45
|
+
|
|
46
|
+
const characteristicActive = service.getCharacteristic(Characteristic.Active)
|
|
47
|
+
.updateValue(dps[this.dpPower])
|
|
48
|
+
.on('get', this.getState.bind(this, this.dpPower))
|
|
49
|
+
.on('set', this.setState.bind(this, this.dpPower));
|
|
50
|
+
|
|
51
|
+
const characteristicInUse = service.getCharacteristic(Characteristic.InUse)
|
|
52
|
+
.on('get', (next) => {
|
|
53
|
+
next(null, characteristicActive.value)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if (!this.noTimer) {
|
|
58
|
+
service.getCharacteristic(Characteristic.SetDuration)
|
|
59
|
+
.on('get', (next) => {
|
|
60
|
+
next(null, this.setDuration)
|
|
61
|
+
})
|
|
62
|
+
.on('change', (data)=> {
|
|
63
|
+
this.log.info("Water Valve Time Duration Set to: " + data.newValue/60 + " Minutes")
|
|
64
|
+
this.setDuration = data.newValue
|
|
65
|
+
|
|
66
|
+
if(service.getCharacteristic(Characteristic.InUse).value) {
|
|
67
|
+
this.lastActivationTime = (new Date()).getTime();
|
|
68
|
+
service.getCharacteristic(Characteristic.RemainingDuration)
|
|
69
|
+
.updateValue(data.newValue);
|
|
70
|
+
|
|
71
|
+
clearTimeout(this.timer); // clear any existing timer
|
|
72
|
+
this.timer = setTimeout( ()=> {
|
|
73
|
+
this.log.info("Water Valve Timer Expired. Shutting OFF Valve");
|
|
74
|
+
service.getCharacteristic(Characteristic.Active).setValue(0);
|
|
75
|
+
service.getCharacteristic(Characteristic.InUse).updateValue(0);
|
|
76
|
+
this.lastActivationTime = null;
|
|
77
|
+
}, (data.newValue *1000));
|
|
78
|
+
}
|
|
79
|
+
}); // end .on('change' ...
|
|
80
|
+
|
|
81
|
+
service.getCharacteristic(Characteristic.RemainingDuration)
|
|
82
|
+
.on('get', (next) => {
|
|
83
|
+
var remainingTime = this.setDuration - Math.floor(((new Date()).getTime() - this.lastActivationTime) / 1000)
|
|
84
|
+
if (!remainingTime || remainingTime < 0)
|
|
85
|
+
remainingTime = 0
|
|
86
|
+
next(null, remainingTime)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
service.getCharacteristic(Characteristic.InUse)
|
|
90
|
+
.on('change', (data) => {
|
|
91
|
+
switch(data.newValue) {
|
|
92
|
+
case 0:
|
|
93
|
+
this.lastActivationTime = null
|
|
94
|
+
service.getCharacteristic(Characteristic.RemainingDuration).updateValue(0);
|
|
95
|
+
service.getCharacteristic(Characteristic.Active).updateValue(0);
|
|
96
|
+
clearTimeout(this.timer); // clear the timer if it was used!
|
|
97
|
+
this.log.info("Water Valve is OFF!");
|
|
98
|
+
break;
|
|
99
|
+
case 1:
|
|
100
|
+
this.lastActivationTime = (new Date()).getTime();
|
|
101
|
+
service.getCharacteristic(Characteristic.RemainingDuration).updateValue(this.setDuration);
|
|
102
|
+
service.getCharacteristic(Characteristic.Active).updateValue(1);
|
|
103
|
+
this.log.info("Water Valve Turning ON with Timer Set to: "+ this.setDuration/60 + " Minutes");
|
|
104
|
+
clearTimeout(this.timer); // clear any existing timer
|
|
105
|
+
this.timer = setTimeout(()=> {
|
|
106
|
+
this.log.info("Water Valve Timer Expired. Shutting OFF Valve");
|
|
107
|
+
// use 'setvalue' when the timer ends so it triggers the .on('set'...) event
|
|
108
|
+
service.getCharacteristic(Characteristic.Active).setValue(0);
|
|
109
|
+
service.getCharacteristic(Characteristic.InUse).updateValue(0);
|
|
110
|
+
this.lastActivationTime = null;
|
|
111
|
+
}, (this.setDuration *1000));
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}); // end .on('change' ...
|
|
115
|
+
|
|
116
|
+
// If Homebridge crash when valve is on the timer reset
|
|
117
|
+
if (dps[this.dpPower]) {
|
|
118
|
+
this.lastActivationTime = (new Date()).getTime();
|
|
119
|
+
service.getCharacteristic(Characteristic.RemainingDuration).updateValue(this.setDuration);
|
|
120
|
+
service.getCharacteristic(Characteristic.Active).updateValue(1);
|
|
121
|
+
service.getCharacteristic(Characteristic.InUse).updateValue(1);
|
|
122
|
+
this.log.info("Water Valve is ON After Restart. Setting Timer to: "+ this.setDuration/60 + " Minutes");
|
|
123
|
+
clearTimeout(this.timer); // clear any existing timer
|
|
124
|
+
this.timer = setTimeout(()=> {
|
|
125
|
+
this.log.info("Water Valve Timer Expired. Shutting OFF Valve");
|
|
126
|
+
// use 'setvalue' when the timer ends so it triggers the .on('set'...) event
|
|
127
|
+
service.getCharacteristic(Characteristic.Active).setValue(0);
|
|
128
|
+
this.lastActivationTime = null;
|
|
129
|
+
}, (this.setDuration *1000));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
} // end if(!this.noTimer)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
this.device.on('change', changes => {
|
|
136
|
+
if (changes.hasOwnProperty(this.dpPower) && characteristicActive.value !== changes[this.dpPower]) characteristicActive.updateValue(changes[this.dpPower]);
|
|
137
|
+
if (changes.hasOwnProperty(this.dpPower) && characteristicInUse.value !== changes[this.dpPower]) characteristicInUse.setValue(changes[this.dpPower]);
|
|
138
|
+
|
|
139
|
+
if (!this.noTimer) {
|
|
140
|
+
if (changes.hasOwnProperty(this.dpPower) && !changes[this.dpPower]){
|
|
141
|
+
this.lastActivationTime = null;
|
|
142
|
+
service.getCharacteristic(Characteristic.RemainingDuration).updateValue(0);
|
|
143
|
+
clearTimeout(this.timer);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = ValveAccessory;
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "homebridge-tuya-plus",
|
|
3
|
+
"version": "3.1.2",
|
|
4
|
+
"description": "A community-maintained Homebridge plugin for controlling Tuya devices locally over LAN. Includes new features, fixes, and updated device support.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
8
|
+
"lint": "eslint index.js lib/**.js bin/**.js"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"tuya-lan": "./bin/cli.js",
|
|
12
|
+
"tuya-lan-find": "./bin/cli-find.js",
|
|
13
|
+
"tuya-lan-decode": "./bin/cli-decode.js"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/adrianjagielak/homebridge-tuya-plus.git"
|
|
18
|
+
},
|
|
19
|
+
"author": "Rayan Khan",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/adrianjagielak/homebridge-tuya-plus/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/adrianjagielak/homebridge-tuya-plus#readme",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"async": "^3.2.2",
|
|
27
|
+
"commander": "^3.0.1",
|
|
28
|
+
"fs-extra": "^8.1.0",
|
|
29
|
+
"http-mitm-proxy": "^1.1.0",
|
|
30
|
+
"json5": "^2.2.0",
|
|
31
|
+
"qrcode": "^1.4.1",
|
|
32
|
+
"yaml": "^1.6.0"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"homebridge-plugin",
|
|
36
|
+
"homebridge",
|
|
37
|
+
"homebridge-tuya",
|
|
38
|
+
"tuya"
|
|
39
|
+
],
|
|
40
|
+
"engines": {
|
|
41
|
+
"homebridge": ">=0.4.0",
|
|
42
|
+
"node": ">=8.6.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@eslint/js": "^9.30.1",
|
|
46
|
+
"eslint": "^9.30.1",
|
|
47
|
+
"globals": "^16.3.0"
|
|
48
|
+
}
|
|
49
|
+
}
|