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,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
+ }