homebridge-tuya-plus 3.14.0-dev.5 → 3.14.0-dev.52
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/workflows/publish-dev.yml +298 -31
- package/AGENTS.md +162 -0
- package/CLAUDE.md +1 -0
- package/Changelog.md +16 -3
- package/Readme.MD +8 -32
- package/config.schema.json +79 -79
- package/index.js +599 -358
- package/lib/AirPurifierAccessory.js +1 -1
- package/lib/BaseAccessory.js +54 -17
- package/lib/ConvectorAccessory.js +1 -1
- package/lib/CustomMultiOutletAccessory.js +2 -1
- package/lib/DoorbellAccessory.js +9 -16
- package/lib/GarageDoorAccessory.js +1 -1
- package/lib/IrrigationSystemAccessory.js +260 -120
- package/lib/MultiOutletAccessory.js +3 -2
- package/lib/OilDiffuserAccessory.js +10 -8
- package/lib/RGBTWLightAccessory.js +13 -11
- package/lib/RGBTWOutletAccessory.js +11 -9
- package/lib/SimpleBlindsAccessory.js +5 -5
- package/lib/SimpleDimmer2Accessory.js +1 -1
- package/lib/SimpleDimmerAccessory.js +10 -6
- package/lib/SimpleGarageDoorAccessory.js +122 -26
- package/lib/SimpleHeaterAccessory.js +4 -4
- package/lib/SimpleLightAccessory.js +1 -1
- package/lib/SwitchAccessory.js +3 -2
- package/lib/TWLightAccessory.js +2 -2
- package/lib/TuyaAccessory.js +75 -42
- package/lib/TuyaCloudApi.js +121 -8
- package/lib/TuyaCloudDevice.js +434 -31
- package/lib/TuyaCloudMessaging.js +34 -41
- package/lib/TuyaDevice.js +269 -0
- package/lib/TuyaDiscovery.js +25 -9
- package/lib/ValveAccessory.js +16 -12
- package/lib/VerticalBlindsWithTilt.js +27 -25
- package/lib/WledDimmerAccessory.js +46 -81
- package/package.json +5 -6
- package/scripts/cleanup-pr-tags.js +122 -0
- package/test/BaseAccessory.test.js +94 -15
- package/test/IrrigationSystemAccessory.test.js +293 -67
- package/test/MultiOutletAccessory.test.js +18 -3
- package/test/RGBTWLightAccessory.test.js +3 -3
- package/test/SimpleFanLightAccessory.test.js +3 -3
- package/test/SimpleGarageDoorAccessory.test.js +193 -19
- package/test/TuyaAccessory.protocol.test.js +76 -3
- package/test/TuyaCloudApi.test.js +110 -1
- package/test/TuyaCloudDevice.test.js +564 -2
- package/test/TuyaCloudMessaging.test.js +24 -5
- package/test/TuyaDevice.test.js +266 -0
- package/test/getCategory.test.js +1 -0
- package/test/index.test.js +271 -0
- package/test/support/mocks.js +13 -0
- package/wiki/Supported-Device-Types.md +64 -34
- package/wiki/Tuya-Cloud-Setup.md +31 -108
|
@@ -80,7 +80,7 @@ class AirPurifierAccessory extends BaseAccessory {
|
|
|
80
80
|
_addAirQualityService() {
|
|
81
81
|
const {Service} = this.hap;
|
|
82
82
|
const nameAirQuality = this.device.context.nameAirQuality || 'Air Quality';
|
|
83
|
-
this.log.
|
|
83
|
+
this.log.debug('Adding air quality sensor: %s', nameAirQuality);
|
|
84
84
|
this.accessory.addService(Service.AirQualitySensor, nameAirQuality);
|
|
85
85
|
}
|
|
86
86
|
|
package/lib/BaseAccessory.js
CHANGED
|
@@ -24,7 +24,7 @@ class BaseAccessory {
|
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
this.device.once('change', () => {
|
|
27
|
-
this.log(`Ready to handle ${this.device.context.name} (${this.device.context.type}:${this.device.context.version}) with signature ${JSON.stringify(this.device.state)}`);
|
|
27
|
+
this.log.debug(`Ready to handle ${this.device.context.name} (${this.device.context.type}:${this.device.context.version}) with signature ${JSON.stringify(this.device.state)}`);
|
|
28
28
|
|
|
29
29
|
this._registerCharacteristics(this.device.state);
|
|
30
30
|
});
|
|
@@ -65,8 +65,18 @@ class BaseAccessory {
|
|
|
65
65
|
return typeof b === 'boolean' ? b : (typeof b === 'string' ? b.toLowerCase().trim() === 'true' : (typeof b === 'number' ? b !== 0 : df));
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
// A HapStatusError to throw from a get/set handler when the value can't be
|
|
69
|
+
// served (device unreachable, or an unusable reading). HomeKit shows the
|
|
70
|
+
// accessory as "No Response". Preferred over a plain `throw new Error(...)`,
|
|
71
|
+
// which newer Homebridge logs a characteristic warning for before falling
|
|
72
|
+
// back to this same status code (SERVICE_COMMUNICATION_FAILURE, -70402).
|
|
73
|
+
_commError() {
|
|
74
|
+
const {HapStatusError, HAPStatus} = this.hap;
|
|
75
|
+
return new HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE);
|
|
76
|
+
}
|
|
77
|
+
|
|
68
78
|
getState(dp, callback) {
|
|
69
|
-
if (!this.device.connected) return callback(
|
|
79
|
+
if (!this.device.connected) return callback(this._commError());
|
|
70
80
|
const _callback = () => {
|
|
71
81
|
if (Array.isArray(dp)) {
|
|
72
82
|
const ret = {};
|
|
@@ -91,36 +101,37 @@ class BaseAccessory {
|
|
|
91
101
|
//For devices like the DETA Smart Fan Controller Switch that by default set the speed as 3 the new code in the setMultiState function causes issues.
|
|
92
102
|
if (!this.device.connected) {
|
|
93
103
|
this.log.debug(`${this.device.context.name}: skipping write, device not connected`);
|
|
94
|
-
return callback && callback();
|
|
104
|
+
return callback && callback(this._commError());
|
|
95
105
|
}
|
|
96
106
|
const ret = this.device.update(dps);
|
|
97
|
-
callback && callback(
|
|
107
|
+
callback && callback(ret === false ? this._commError() : null);
|
|
98
108
|
}
|
|
99
109
|
|
|
100
110
|
setMultiState(dps, callback) {
|
|
101
111
|
if (!this.device.connected) {
|
|
102
112
|
this.log.debug(`${this.device.context.name}: skipping write, device not connected`);
|
|
103
|
-
return callback && callback();
|
|
113
|
+
return callback && callback(this._commError());
|
|
104
114
|
}
|
|
115
|
+
let failed = false;
|
|
105
116
|
for (const dp in dps) {
|
|
106
117
|
if (dps.hasOwnProperty(dp) && dps[dp] !== this.device.state[dp]){
|
|
107
|
-
|
|
118
|
+
if (this.device.update({[dp.toString()] : dps[dp]}) === false) failed = true;
|
|
108
119
|
}
|
|
109
120
|
}
|
|
110
|
-
callback && callback(
|
|
121
|
+
callback && callback(failed ? this._commError() : null);
|
|
111
122
|
}
|
|
112
123
|
|
|
113
124
|
getDividedState(dp, divisor, callback) {
|
|
114
125
|
this.getState(dp, (err, data) => {
|
|
115
126
|
if (err) return callback(err);
|
|
116
|
-
if (!isFinite(data)) return callback(
|
|
127
|
+
if (!isFinite(data)) return callback(this._commError());
|
|
117
128
|
|
|
118
129
|
callback(null, this._getDividedState(data, divisor));
|
|
119
130
|
});
|
|
120
131
|
}
|
|
121
132
|
|
|
122
133
|
getStateAsync(dp) {
|
|
123
|
-
if (!this.device.connected) throw
|
|
134
|
+
if (!this.device.connected) throw this._commError();
|
|
124
135
|
if (Array.isArray(dp)) {
|
|
125
136
|
const ret = {};
|
|
126
137
|
dp.forEach(p => { ret[p] = this.device.state[p]; });
|
|
@@ -133,29 +144,55 @@ class BaseAccessory {
|
|
|
133
144
|
return this.setMultiStateAsync({[dp.toString()]: value});
|
|
134
145
|
}
|
|
135
146
|
|
|
136
|
-
|
|
147
|
+
// Throws a "No Response" HapStatusError when the device is unreachable or the
|
|
148
|
+
// write is rejected, so a get/set handler that returns this promise surfaces
|
|
149
|
+
// the failure to HomeKit instead of silently pretending the write succeeded.
|
|
150
|
+
// `device.update` returns a boolean for LAN devices and an awaitable boolean
|
|
151
|
+
// for cloud devices (false == the command did not go through).
|
|
152
|
+
async setMultiStateAsync(dps) {
|
|
137
153
|
if (!this.device.connected) {
|
|
138
154
|
this.log.debug(`${this.device.context.name}: skipping write, device not connected`);
|
|
139
|
-
|
|
155
|
+
throw this._commError();
|
|
140
156
|
}
|
|
157
|
+
const writes = [];
|
|
141
158
|
for (const dp in dps) {
|
|
142
159
|
if (dps.hasOwnProperty(dp) && dps[dp] !== this.device.state[dp]) {
|
|
143
|
-
this.device.update({[dp.toString()]: dps[dp]});
|
|
160
|
+
writes.push(this.device.update({[dp.toString()]: dps[dp]}));
|
|
144
161
|
}
|
|
145
162
|
}
|
|
163
|
+
const results = await Promise.all(writes);
|
|
164
|
+
if (results.some(ok => ok === false)) throw this._commError();
|
|
146
165
|
}
|
|
147
166
|
|
|
148
|
-
setMultiStateLegacyAsync(dps) {
|
|
167
|
+
async setMultiStateLegacyAsync(dps) {
|
|
149
168
|
if (!this.device.connected) {
|
|
150
169
|
this.log.debug(`${this.device.context.name}: skipping write, device not connected`);
|
|
151
|
-
|
|
170
|
+
throw this._commError();
|
|
152
171
|
}
|
|
153
|
-
this.device.update(dps);
|
|
172
|
+
if (await this.device.update(dps) === false) throw this._commError();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Non-throwing write helpers for callers that are NOT inside a HomeKit
|
|
176
|
+
// get/set handler — setTimeout callbacks, multi-step command sequences, etc.
|
|
177
|
+
// The async helpers above reject on a disconnected/failed write so HomeKit
|
|
178
|
+
// can show "No Response"; in a detached/background context there is no
|
|
179
|
+
// HomeKit request to fail and an unhandled rejection would crash Homebridge,
|
|
180
|
+
// so these swallow the error (the helper has already logged it).
|
|
181
|
+
setStateInBackground(dp, value) {
|
|
182
|
+
this.setStateAsync(dp, value).catch(() => {});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
setMultiStateInBackground(dps) {
|
|
186
|
+
this.setMultiStateAsync(dps).catch(() => {});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
setMultiStateLegacyInBackground(dps) {
|
|
190
|
+
this.setMultiStateLegacyAsync(dps).catch(() => {});
|
|
154
191
|
}
|
|
155
192
|
|
|
156
193
|
getDividedStateAsync(dp, divisor) {
|
|
157
194
|
const data = this.getStateAsync(dp);
|
|
158
|
-
if (!isFinite(data)) throw
|
|
195
|
+
if (!isFinite(data)) throw this._commError();
|
|
159
196
|
return this._getDividedState(data, divisor);
|
|
160
197
|
}
|
|
161
198
|
|
|
@@ -167,7 +204,7 @@ class BaseAccessory {
|
|
|
167
204
|
this.colorFunction = this.device.context.colorFunction && {HSB: 'HSB', HEXHSB: 'HEXHSB'}[this.device.context.colorFunction.toUpperCase()];
|
|
168
205
|
if (!this.colorFunction && value) {
|
|
169
206
|
this.colorFunction = {12: 'HSB', 14: 'HEXHSB'}[value.length] || 'Unknown';
|
|
170
|
-
if (this.colorFunction) this.log.
|
|
207
|
+
if (this.colorFunction) this.log.debug(`Color format for ${this.device.context.name} (${this.device.context.version}) identified as ${this.colorFunction} (length: ${value.length}).`);
|
|
171
208
|
}
|
|
172
209
|
if (!this.colorFunction) {
|
|
173
210
|
this.colorFunction = 'Unknown';
|
|
@@ -55,7 +55,7 @@ class ConvectorAccessory extends BaseAccessory {
|
|
|
55
55
|
service.getCharacteristic(Characteristic.TargetHeaterCoolerState)
|
|
56
56
|
.setProps({ minValue: 1, maxValue: 1, validValues: [Characteristic.TargetHeaterCoolerState.HEAT] })
|
|
57
57
|
.updateValue(this._getTargetHeaterCoolerState())
|
|
58
|
-
.onGet(() => this._getTargetHeaterCoolerState())
|
|
58
|
+
.onGet(() => { if (!this.device.connected) throw this._commError(); return this._getTargetHeaterCoolerState(); })
|
|
59
59
|
.onSet(() => this.setStateAsync(this.dpActive, true));
|
|
60
60
|
|
|
61
61
|
const characteristicCurrentTemperature = service.getCharacteristic(Characteristic.CurrentTemperature)
|
|
@@ -74,6 +74,7 @@ class CustomMultiOutletAccessory extends BaseAccessory {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
setPower(dp, value) {
|
|
77
|
+
if (!this.device.connected) throw this._commError();
|
|
77
78
|
if (!this._pendingPower) {
|
|
78
79
|
this._pendingPower = {props: {}, resolvers: []};
|
|
79
80
|
}
|
|
@@ -87,7 +88,7 @@ class CustomMultiOutletAccessory extends BaseAccessory {
|
|
|
87
88
|
this._pendingPower.timer = setTimeout(() => {
|
|
88
89
|
const {props, resolvers} = this._pendingPower;
|
|
89
90
|
this._pendingPower = null;
|
|
90
|
-
this.
|
|
91
|
+
this.setMultiStateInBackground(props);
|
|
91
92
|
resolvers.forEach(r => r());
|
|
92
93
|
}, 500);
|
|
93
94
|
});
|
package/lib/DoorbellAccessory.js
CHANGED
|
@@ -11,7 +11,8 @@ const LOCK_TIMEOUT = 5000;
|
|
|
11
11
|
|
|
12
12
|
class DoorbellDelegate /*extends CameraStreamingDelegate*/ {
|
|
13
13
|
|
|
14
|
-
constructor() {
|
|
14
|
+
constructor(log) {
|
|
15
|
+
this.log = log || console;
|
|
15
16
|
this.cameraImage = undefined;
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -27,7 +28,7 @@ class DoorbellDelegate /*extends CameraStreamingDelegate*/ {
|
|
|
27
28
|
var body=Buffer.alloc(0);
|
|
28
29
|
|
|
29
30
|
if (res.statusCode!==200) {
|
|
30
|
-
return
|
|
31
|
+
return me.log.error('Doorbell image fetch failed: HTTP %s', res.statusCode);
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
res.on('data', function(chunk) {
|
|
@@ -36,13 +37,12 @@ class DoorbellDelegate /*extends CameraStreamingDelegate*/ {
|
|
|
36
37
|
|
|
37
38
|
res.on('end', function() {
|
|
38
39
|
me.setCameraImage(body);
|
|
39
|
-
|
|
40
|
+
me.log.debug('Doorbell image stored.');
|
|
40
41
|
callback();
|
|
41
|
-
console.log("Callback after image store complete");
|
|
42
42
|
});
|
|
43
43
|
|
|
44
44
|
res.on('error', function(err) {
|
|
45
|
-
|
|
45
|
+
me.log.error('Doorbell image fetch error:', err);
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
48
|
);
|
|
@@ -96,12 +96,9 @@ class DoorbellAccessory extends BaseAccessory {
|
|
|
96
96
|
|
|
97
97
|
|
|
98
98
|
this.device.on('change', (changes, state) => {
|
|
99
|
-
//console.log(`Changes: ${JSON.stringify(changes)}, State: ${JSON.stringify(state)}`);
|
|
100
|
-
|
|
101
99
|
if (changes.hasOwnProperty(DP_DOORBELL)) {
|
|
102
100
|
const urlBase64 = changes[DP_DOORBELL];
|
|
103
101
|
const strUrl = Buffer.from(urlBase64, 'base64');
|
|
104
|
-
//console.log(`Image URL: ${strUrl}`);
|
|
105
102
|
|
|
106
103
|
this.streamingDelegate.storeImage(strUrl.toString(), () => {
|
|
107
104
|
service.updateCharacteristic(Characteristic.ProgrammableSwitchEvent, Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS);
|
|
@@ -109,18 +106,18 @@ class DoorbellAccessory extends BaseAccessory {
|
|
|
109
106
|
}
|
|
110
107
|
|
|
111
108
|
if (changes.hasOwnProperty(DP_DOOR)) {
|
|
112
|
-
|
|
109
|
+
this.log.debug('Door button pressed');
|
|
113
110
|
}
|
|
114
111
|
|
|
115
112
|
if (changes.hasOwnProperty(DP_GATE)) {
|
|
116
|
-
|
|
113
|
+
this.log.debug('Gate button pressed');
|
|
117
114
|
}
|
|
118
115
|
});
|
|
119
116
|
}
|
|
120
117
|
|
|
121
118
|
configureCamera() {
|
|
122
|
-
this.streamingDelegate = new DoorbellDelegate();
|
|
123
|
-
|
|
119
|
+
this.streamingDelegate = new DoorbellDelegate(this.log);
|
|
120
|
+
this.log.debug('Created camera streaming delegate.');
|
|
124
121
|
const options = {
|
|
125
122
|
cameraStreamCount: 1,
|
|
126
123
|
delegate: this.streamingDelegate,
|
|
@@ -157,12 +154,8 @@ class DoorbellAccessory extends BaseAccessory {
|
|
|
157
154
|
},
|
|
158
155
|
};
|
|
159
156
|
|
|
160
|
-
//console.log("created options");
|
|
161
157
|
const cameraController = new this.hap.CameraController(options);
|
|
162
|
-
//console.log("created controller");
|
|
163
|
-
|
|
164
158
|
this.accessory.configureController(cameraController);
|
|
165
|
-
//console.log("configured controller with accessory");
|
|
166
159
|
}
|
|
167
160
|
|
|
168
161
|
pushDoor(callback) {
|
|
@@ -105,7 +105,7 @@ class GarageDoorAccessory extends BaseAccessory {
|
|
|
105
105
|
.onGet(() => this.getCurrentDoorState());
|
|
106
106
|
|
|
107
107
|
this.device.on('change', changes => {
|
|
108
|
-
this.
|
|
108
|
+
this._debugLog('changed:' + JSON.stringify(changes));
|
|
109
109
|
|
|
110
110
|
if (changes.hasOwnProperty(this.dpStatus)) {
|
|
111
111
|
const newCurrentDoorState = this._getCurrentDoorState(changes[this.dpStatus]);
|