homebridge-tuya-without-developer-account 1.0.2 → 1.0.4
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/CHANGELOG.md +15 -0
- package/README.md +56 -2
- package/config.schema.json +29 -0
- package/dist/platform.js +28 -0
- package/dist/shared/accessories/AirConditionerAccessory.js +41 -16
- package/homebridge-ui/public/index.html +305 -12
- package/homebridge-ui/server.js +213 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.4
|
|
4
|
+
|
|
5
|
+
- Added a Homebridge settings UI helper for air conditioner temperature overrides.
|
|
6
|
+
- Users can now select a detected Tuya device by name instead of manually finding and pasting the device ID.
|
|
7
|
+
- Added a backend UI endpoint that reads the cached Tuya device list from Homebridge `persist/TuyaDeviceList*.json`.
|
|
8
|
+
- AC-looking devices are listed first when metadata suggests they are air conditioners.
|
|
9
|
+
- The UI writes the correct `deviceOverrides[].id` automatically and saves `airConditioner.minTemperature`, `airConditioner.maxTemperature`, and `airConditioner.temperatureStep`.
|
|
10
|
+
|
|
11
|
+
## 1.0.3
|
|
12
|
+
|
|
13
|
+
- Added user-friendly air conditioner temperature limit overrides under `deviceOverrides[].airConditioner`.
|
|
14
|
+
- Allows per-device HomeKit AC setpoint limits such as 16-31 °C or 17-31 °C.
|
|
15
|
+
- Allows `temperatureStep: 1` to suppress 0.5 °C steps in the Home app.
|
|
16
|
+
- Values are always configured in Celsius; Fahrenheit users see the Home app converted values automatically.
|
|
17
|
+
|
|
3
18
|
## 1.0.2
|
|
4
19
|
|
|
5
20
|
- Fixed startup abort when Homebridge UI saves an empty or incomplete `deviceOverrides` row. Invalid override rows without `id` are now skipped with a warning instead of stopping QR cloud startup.
|
package/README.md
CHANGED
|
@@ -187,8 +187,62 @@ Optional. Use only when a device is discovered with the wrong category or requir
|
|
|
187
187
|
}
|
|
188
188
|
```
|
|
189
189
|
|
|
190
|
-
Use `
|
|
191
|
-
|
|
190
|
+
Use `global` as the override ID to apply an override globally.
|
|
191
|
+
|
|
192
|
+
### Air conditioner temperature limits
|
|
193
|
+
|
|
194
|
+
Optional. For Wi-Fi AC units, you can limit the Home app setpoint range and step size. Values are always configured in Celsius. If the iPhone/Home app is set to Fahrenheit, HomeKit converts the values automatically.
|
|
195
|
+
|
|
196
|
+
The preferred method is the Homebridge plugin settings UI:
|
|
197
|
+
|
|
198
|
+
1. Authenticate and let the plugin discover devices at least once.
|
|
199
|
+
2. Open **Plugins → Tuya without developer account for Homebridge → Settings**.
|
|
200
|
+
3. In **Air Conditioner Temperature Overrides**, click **Load Detected Devices**.
|
|
201
|
+
4. Select the AC device by name, for example **Bedroom AC**.
|
|
202
|
+
5. Enter:
|
|
203
|
+
|
|
204
|
+
```text
|
|
205
|
+
Min Temperature: 17
|
|
206
|
+
Max Temperature: 31
|
|
207
|
+
Step: 1
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
6. Click **Add / Update AC Override**.
|
|
211
|
+
7. Click **Save Configuration** and restart Homebridge.
|
|
212
|
+
|
|
213
|
+
The UI automatically saves the correct Tuya device ID. Users no longer need to manually find and paste the device ID for this AC override.
|
|
214
|
+
|
|
215
|
+
The saved config looks like this internally:
|
|
216
|
+
|
|
217
|
+
```json
|
|
218
|
+
{
|
|
219
|
+
"options": {
|
|
220
|
+
"userCode": "YOUR_TUYA_USER_CODE",
|
|
221
|
+
"deviceOverrides": [
|
|
222
|
+
{
|
|
223
|
+
"id": "THE_SELECTED_AC_DEVICE_ID",
|
|
224
|
+
"airConditioner": {
|
|
225
|
+
"minTemperature": 17,
|
|
226
|
+
"maxTemperature": 31,
|
|
227
|
+
"temperatureStep": 1
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
]
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
For ACs that support 16 °C minimum, set **Min Temperature** to `16`.
|
|
236
|
+
|
|
237
|
+
Fahrenheit display examples:
|
|
238
|
+
|
|
239
|
+
```text
|
|
240
|
+
16 °C ≈ 61 °F
|
|
241
|
+
17 °C ≈ 63 °F
|
|
242
|
+
31 °C ≈ 88 °F
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
HomeKit stores temperature characteristic metadata in Celsius. Do not enter Fahrenheit values in the plugin config.
|
|
192
246
|
|
|
193
247
|
## Troubleshooting
|
|
194
248
|
|
package/config.schema.json
CHANGED
|
@@ -81,6 +81,35 @@
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
+
},
|
|
85
|
+
"airConditioner": {
|
|
86
|
+
"type": "object",
|
|
87
|
+
"title": "Air Conditioner Temperature Limits",
|
|
88
|
+
"description": "Optional HomeKit temperature range override for Wi-Fi air conditioners. Values are always Celsius; Home app converts to Fahrenheit automatically for users using \u00b0F.",
|
|
89
|
+
"properties": {
|
|
90
|
+
"minTemperature": {
|
|
91
|
+
"type": "number",
|
|
92
|
+
"title": "Minimum temperature (\u00b0C)",
|
|
93
|
+
"description": "Lowest setpoint exposed to HomeKit. Common values are 16 or 17. Leave empty to use Tuya schema.",
|
|
94
|
+
"minimum": 0,
|
|
95
|
+
"maximum": 50
|
|
96
|
+
},
|
|
97
|
+
"maxTemperature": {
|
|
98
|
+
"type": "number",
|
|
99
|
+
"title": "Maximum temperature (\u00b0C)",
|
|
100
|
+
"description": "Highest setpoint exposed to HomeKit. Common value is 31. Leave empty to use Tuya schema.",
|
|
101
|
+
"minimum": 0,
|
|
102
|
+
"maximum": 60
|
|
103
|
+
},
|
|
104
|
+
"temperatureStep": {
|
|
105
|
+
"type": "number",
|
|
106
|
+
"title": "Temperature step (\u00b0C)",
|
|
107
|
+
"description": "Set to 1 to suppress 0.5 \u00b0C steps. Values are Celsius even when Home app displays Fahrenheit.",
|
|
108
|
+
"minimum": 0.1,
|
|
109
|
+
"maximum": 5,
|
|
110
|
+
"default": 1
|
|
111
|
+
}
|
|
112
|
+
}
|
|
84
113
|
}
|
|
85
114
|
},
|
|
86
115
|
"required": [
|
package/dist/platform.js
CHANGED
|
@@ -95,6 +95,33 @@ class TuyaPlatform {
|
|
|
95
95
|
continue;
|
|
96
96
|
}
|
|
97
97
|
item.id = id;
|
|
98
|
+
if (item.airConditioner && typeof item.airConditioner === 'object') {
|
|
99
|
+
const normalizedAirConditioner = {};
|
|
100
|
+
const minTemperature = Number(item.airConditioner.minTemperature);
|
|
101
|
+
const maxTemperature = Number(item.airConditioner.maxTemperature);
|
|
102
|
+
const temperatureStep = Number(item.airConditioner.temperatureStep);
|
|
103
|
+
if (Number.isFinite(minTemperature)) {
|
|
104
|
+
normalizedAirConditioner.minTemperature = minTemperature;
|
|
105
|
+
}
|
|
106
|
+
if (Number.isFinite(maxTemperature)) {
|
|
107
|
+
normalizedAirConditioner.maxTemperature = maxTemperature;
|
|
108
|
+
}
|
|
109
|
+
if (Number.isFinite(temperatureStep) && temperatureStep > 0) {
|
|
110
|
+
normalizedAirConditioner.temperatureStep = temperatureStep;
|
|
111
|
+
}
|
|
112
|
+
if (Number.isFinite(normalizedAirConditioner.minTemperature) && Number.isFinite(normalizedAirConditioner.maxTemperature) && normalizedAirConditioner.minTemperature > normalizedAirConditioner.maxTemperature) {
|
|
113
|
+
this.log.warn('[Tuya QR] Air conditioner override for id "%s" has minTemperature greater than maxTemperature. Swapping values.', id);
|
|
114
|
+
const oldMin = normalizedAirConditioner.minTemperature;
|
|
115
|
+
normalizedAirConditioner.minTemperature = normalizedAirConditioner.maxTemperature;
|
|
116
|
+
normalizedAirConditioner.maxTemperature = oldMin;
|
|
117
|
+
}
|
|
118
|
+
if (Object.keys(normalizedAirConditioner).length > 0) {
|
|
119
|
+
item.airConditioner = normalizedAirConditioner;
|
|
120
|
+
} else {
|
|
121
|
+
this.log.warn('[Tuya QR] Ignoring invalid airConditioner override for id "%s" because no numeric temperature values were provided.', id);
|
|
122
|
+
delete item.airConditioner;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
98
125
|
seenIds.add(id);
|
|
99
126
|
validOverrides.push(item);
|
|
100
127
|
}
|
|
@@ -221,6 +248,7 @@ class TuyaPlatform {
|
|
|
221
248
|
customCategory: deviceConfig?.category,
|
|
222
249
|
unbridged: deviceConfig?.unbridged ?? false,
|
|
223
250
|
schemaOverrides: deviceConfig?.schema ? JSON.stringify(deviceConfig.schema) : undefined,
|
|
251
|
+
airConditioner: deviceConfig?.airConditioner ? JSON.stringify(deviceConfig.airConditioner) : undefined,
|
|
224
252
|
adaptiveLighting: deviceConfig?.adaptiveLighting ?? false,
|
|
225
253
|
};
|
|
226
254
|
const { changed: configChanged } = this.configHash.hasConfigChanged(device.id, configToHash);
|
|
@@ -234,18 +234,49 @@ class AirConditionerAccessory extends BaseAccessory_1.default {
|
|
|
234
234
|
})
|
|
235
235
|
.setProps({ validValues });
|
|
236
236
|
}
|
|
237
|
+
getAirConditionerTemperatureProps(schema) {
|
|
238
|
+
const property = schema.property || {};
|
|
239
|
+
const multiple = Math.pow(10, property.scale || 0);
|
|
240
|
+
const props = {
|
|
241
|
+
minValue: Number.isFinite(Number(property.min)) ? Number(property.min) / multiple : 16,
|
|
242
|
+
maxValue: Number.isFinite(Number(property.max)) ? Number(property.max) / multiple : 31,
|
|
243
|
+
minStep: Math.max(0.1, Number.isFinite(Number(property.step)) ? Number(property.step) / multiple : 1),
|
|
244
|
+
};
|
|
245
|
+
const deviceConfig = this.platform.getDeviceConfig?.(this.device);
|
|
246
|
+
const airConditioner = deviceConfig?.airConditioner;
|
|
247
|
+
if (airConditioner && typeof airConditioner === 'object') {
|
|
248
|
+
const minTemperature = Number(airConditioner.minTemperature);
|
|
249
|
+
const maxTemperature = Number(airConditioner.maxTemperature);
|
|
250
|
+
const temperatureStep = Number(airConditioner.temperatureStep);
|
|
251
|
+
if (Number.isFinite(minTemperature)) {
|
|
252
|
+
props.minValue = minTemperature;
|
|
253
|
+
}
|
|
254
|
+
if (Number.isFinite(maxTemperature)) {
|
|
255
|
+
props.maxValue = maxTemperature;
|
|
256
|
+
}
|
|
257
|
+
if (Number.isFinite(temperatureStep) && temperatureStep > 0) {
|
|
258
|
+
props.minStep = Math.max(0.1, temperatureStep);
|
|
259
|
+
}
|
|
260
|
+
if (props.minValue > props.maxValue) {
|
|
261
|
+
this.log.warn('Invalid airConditioner temperature override: minTemperature %s is greater than maxTemperature %s. Swapping values.', props.minValue, props.maxValue);
|
|
262
|
+
const oldMin = props.minValue;
|
|
263
|
+
props.minValue = props.maxValue;
|
|
264
|
+
props.maxValue = oldMin;
|
|
265
|
+
}
|
|
266
|
+
this.log.info('Using air conditioner HomeKit temperature override: min=%s°C, max=%s°C, step=%s°C. Fahrenheit users will see the Home app converted values automatically.', props.minValue, props.maxValue, props.minStep);
|
|
267
|
+
}
|
|
268
|
+
return { props, multiple };
|
|
269
|
+
}
|
|
270
|
+
normalizeTemperatureCommandValue(value, props, multiple) {
|
|
271
|
+
const clamped = (0, util_1.limit)(Number(value), props.minValue, props.maxValue);
|
|
272
|
+
return Math.round(clamped * multiple);
|
|
273
|
+
}
|
|
237
274
|
configureCoolingThreshouldTemp() {
|
|
238
275
|
const schema = this.getSchema(...SCHEMA_CODE.TARGET_TEMP);
|
|
239
276
|
if (!schema) {
|
|
240
277
|
return;
|
|
241
278
|
}
|
|
242
|
-
const
|
|
243
|
-
const multiple = Math.pow(10, property.scale);
|
|
244
|
-
const props = {
|
|
245
|
-
minValue: property.min / multiple,
|
|
246
|
-
maxValue: property.max / multiple,
|
|
247
|
-
minStep: Math.max(0.1, property.step / multiple),
|
|
248
|
-
};
|
|
279
|
+
const { props, multiple } = this.getAirConditionerTemperatureProps(schema);
|
|
249
280
|
this.log.debug('Set props for CoolingThresholdTemperature:', props);
|
|
250
281
|
this.mainService().getCharacteristic(this.Characteristic.CoolingThresholdTemperature)
|
|
251
282
|
.onGet(() => {
|
|
@@ -264,7 +295,7 @@ class AirConditionerAccessory extends BaseAccessory_1.default {
|
|
|
264
295
|
.updateValue(props.minValue);
|
|
265
296
|
return;
|
|
266
297
|
}
|
|
267
|
-
await this.sendCommands([{ code: schema.code, value: value
|
|
298
|
+
await this.sendCommands([{ code: schema.code, value: this.normalizeTemperatureCommandValue(value, props, multiple) }], true);
|
|
268
299
|
})
|
|
269
300
|
.setProps(props);
|
|
270
301
|
}
|
|
@@ -273,13 +304,7 @@ class AirConditionerAccessory extends BaseAccessory_1.default {
|
|
|
273
304
|
if (!schema) {
|
|
274
305
|
return;
|
|
275
306
|
}
|
|
276
|
-
const
|
|
277
|
-
const multiple = Math.pow(10, property.scale);
|
|
278
|
-
const props = {
|
|
279
|
-
minValue: property.min / multiple,
|
|
280
|
-
maxValue: property.max / multiple,
|
|
281
|
-
minStep: Math.max(0.1, property.step / multiple),
|
|
282
|
-
};
|
|
307
|
+
const { props, multiple } = this.getAirConditionerTemperatureProps(schema);
|
|
283
308
|
this.log.debug('Set props for HeatingThresholdTemperature:', props);
|
|
284
309
|
this.mainService().getCharacteristic(this.Characteristic.HeatingThresholdTemperature)
|
|
285
310
|
.onGet(() => {
|
|
@@ -298,7 +323,7 @@ class AirConditionerAccessory extends BaseAccessory_1.default {
|
|
|
298
323
|
.updateValue(props.maxValue);
|
|
299
324
|
return;
|
|
300
325
|
}
|
|
301
|
-
await this.sendCommands([{ code: schema.code, value: value
|
|
326
|
+
await this.sendCommands([{ code: schema.code, value: this.normalizeTemperatureCommandValue(value, props, multiple) }], true);
|
|
302
327
|
})
|
|
303
328
|
.setProps(props);
|
|
304
329
|
}
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
font-size: 0.875rem;
|
|
40
40
|
opacity: 0.85;
|
|
41
41
|
}
|
|
42
|
-
.tuya-nodev-status
|
|
42
|
+
.tuya-nodev-status,
|
|
43
|
+
.tuya-nodev-ac-status {
|
|
43
44
|
margin-top: 12px;
|
|
44
45
|
}
|
|
45
46
|
.tuya-nodev-raw {
|
|
@@ -49,6 +50,32 @@
|
|
|
49
50
|
font-size: 0.8rem;
|
|
50
51
|
margin-top: 8px;
|
|
51
52
|
}
|
|
53
|
+
.tuya-nodev-grid {
|
|
54
|
+
display: grid;
|
|
55
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
56
|
+
gap: 12px;
|
|
57
|
+
}
|
|
58
|
+
.tuya-nodev-table-wrap {
|
|
59
|
+
overflow-x: auto;
|
|
60
|
+
margin-top: 12px;
|
|
61
|
+
}
|
|
62
|
+
.tuya-nodev-table {
|
|
63
|
+
width: 100%;
|
|
64
|
+
border-collapse: collapse;
|
|
65
|
+
font-size: 0.9rem;
|
|
66
|
+
}
|
|
67
|
+
.tuya-nodev-table th,
|
|
68
|
+
.tuya-nodev-table td {
|
|
69
|
+
border-bottom: 1px solid rgba(127, 127, 127, 0.2);
|
|
70
|
+
padding: 8px;
|
|
71
|
+
vertical-align: middle;
|
|
72
|
+
}
|
|
73
|
+
.tuya-nodev-table th {
|
|
74
|
+
text-align: left;
|
|
75
|
+
}
|
|
76
|
+
.tuya-nodev-muted-option {
|
|
77
|
+
opacity: 0.75;
|
|
78
|
+
}
|
|
52
79
|
</style>
|
|
53
80
|
|
|
54
81
|
<div class="tuya-nodev-card">
|
|
@@ -87,12 +114,71 @@
|
|
|
87
114
|
</div>
|
|
88
115
|
</div>
|
|
89
116
|
|
|
117
|
+
<div class="tuya-nodev-card">
|
|
118
|
+
<div class="tuya-nodev-title">Air Conditioner Temperature Overrides</div>
|
|
119
|
+
<p class="tuya-nodev-small mb-3">
|
|
120
|
+
Select a discovered Tuya AC device and set the Home app temperature range. Values are always Celsius; iOS/Home automatically displays Fahrenheit for users who use °F.
|
|
121
|
+
</p>
|
|
122
|
+
|
|
123
|
+
<div class="form-group">
|
|
124
|
+
<label for="tuyaNodevAcDevice">Select Device</label>
|
|
125
|
+
<select id="tuyaNodevAcDevice" class="form-control">
|
|
126
|
+
<option value="">Load devices first...</option>
|
|
127
|
+
</select>
|
|
128
|
+
<small class="form-text text-muted">AC-looking devices are listed first when the cached Tuya device list contains enough metadata.</small>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<div class="tuya-nodev-grid">
|
|
132
|
+
<div class="form-group">
|
|
133
|
+
<label for="tuyaNodevAcMin">Min Temperature (°C)</label>
|
|
134
|
+
<input id="tuyaNodevAcMin" class="form-control" type="number" min="0" max="50" step="0.1" value="17">
|
|
135
|
+
</div>
|
|
136
|
+
<div class="form-group">
|
|
137
|
+
<label for="tuyaNodevAcMax">Max Temperature (°C)</label>
|
|
138
|
+
<input id="tuyaNodevAcMax" class="form-control" type="number" min="0" max="60" step="0.1" value="31">
|
|
139
|
+
</div>
|
|
140
|
+
<div class="form-group">
|
|
141
|
+
<label for="tuyaNodevAcStep">Step (°C)</label>
|
|
142
|
+
<input id="tuyaNodevAcStep" class="form-control" type="number" min="0.1" max="5" step="0.1" value="1">
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<div class="tuya-nodev-actions">
|
|
147
|
+
<button id="tuyaNodevLoadDevices" class="btn btn-outline-primary" type="button">Load Detected Devices</button>
|
|
148
|
+
<button id="tuyaNodevApplyAc" class="btn btn-primary" type="button">Add / Update AC Override</button>
|
|
149
|
+
<button id="tuyaNodevRemoveAc" class="btn btn-outline-danger" type="button">Remove Selected AC Override</button>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div id="tuyaNodevAcStatus" class="tuya-nodev-ac-status alert alert-secondary">
|
|
153
|
+
Load detected devices after the plugin has authenticated and discovered your Tuya devices at least once.
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div class="tuya-nodev-table-wrap">
|
|
157
|
+
<table class="tuya-nodev-table">
|
|
158
|
+
<thead>
|
|
159
|
+
<tr>
|
|
160
|
+
<th>Device</th>
|
|
161
|
+
<th>Device ID</th>
|
|
162
|
+
<th>Min</th>
|
|
163
|
+
<th>Max</th>
|
|
164
|
+
<th>Step</th>
|
|
165
|
+
<th></th>
|
|
166
|
+
</tr>
|
|
167
|
+
</thead>
|
|
168
|
+
<tbody id="tuyaNodevAcOverrideRows">
|
|
169
|
+
<tr><td colspan="6" class="tuya-nodev-small">No AC overrides configured.</td></tr>
|
|
170
|
+
</tbody>
|
|
171
|
+
</table>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
90
175
|
<script>
|
|
91
176
|
(() => {
|
|
92
177
|
const PLATFORM = 'TuyaNoDeveloperAccount';
|
|
93
178
|
let currentConfig = null;
|
|
94
179
|
let pollTimer = null;
|
|
95
180
|
let isAuthenticated = false;
|
|
181
|
+
let detectedDevices = [];
|
|
96
182
|
|
|
97
183
|
const $ = (id) => document.getElementById(id);
|
|
98
184
|
|
|
@@ -102,6 +188,12 @@
|
|
|
102
188
|
el.textContent = message;
|
|
103
189
|
}
|
|
104
190
|
|
|
191
|
+
function setAcStatus(message, type = 'secondary') {
|
|
192
|
+
const el = $('tuyaNodevAcStatus');
|
|
193
|
+
el.className = `tuya-nodev-ac-status alert alert-${type}`;
|
|
194
|
+
el.textContent = message;
|
|
195
|
+
}
|
|
196
|
+
|
|
105
197
|
function getUserCode() {
|
|
106
198
|
return $('tuyaNodevUserCode').value.trim();
|
|
107
199
|
}
|
|
@@ -110,13 +202,20 @@
|
|
|
110
202
|
return $('tuyaNodevName').value.trim() || 'Tuya without developer account';
|
|
111
203
|
}
|
|
112
204
|
|
|
205
|
+
function clone(value) {
|
|
206
|
+
return value && typeof value === 'object' ? JSON.parse(JSON.stringify(value)) : value;
|
|
207
|
+
}
|
|
208
|
+
|
|
113
209
|
function normaliseConfig(base) {
|
|
114
|
-
const cfg = base && typeof base === 'object' ?
|
|
210
|
+
const cfg = base && typeof base === 'object' ? clone(base) : {};
|
|
115
211
|
cfg.platform = PLATFORM;
|
|
116
212
|
cfg.name = getPlatformName();
|
|
117
213
|
cfg.options = cfg.options && typeof cfg.options === 'object' ? cfg.options : {};
|
|
118
214
|
cfg.options.userCode = getUserCode();
|
|
119
215
|
cfg.options.projectType = '3';
|
|
216
|
+
if (!Array.isArray(cfg.options.deviceOverrides)) {
|
|
217
|
+
cfg.options.deviceOverrides = [];
|
|
218
|
+
}
|
|
120
219
|
delete cfg.options.accessId;
|
|
121
220
|
delete cfg.options.accessKey;
|
|
122
221
|
delete cfg.options.username;
|
|
@@ -128,9 +227,15 @@
|
|
|
128
227
|
return cfg;
|
|
129
228
|
}
|
|
130
229
|
|
|
131
|
-
|
|
230
|
+
function ensureConfig() {
|
|
132
231
|
currentConfig = normaliseConfig(currentConfig);
|
|
232
|
+
return currentConfig;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function syncConfigToUi() {
|
|
236
|
+
ensureConfig();
|
|
133
237
|
await homebridge.updatePluginConfig([currentConfig]);
|
|
238
|
+
renderAcOverrides();
|
|
134
239
|
}
|
|
135
240
|
|
|
136
241
|
function stopPolling() {
|
|
@@ -150,6 +255,195 @@
|
|
|
150
255
|
if (homebridge.enableSaveButton) homebridge.enableSaveButton();
|
|
151
256
|
}
|
|
152
257
|
|
|
258
|
+
function getDeviceName(id) {
|
|
259
|
+
const device = detectedDevices.find((item) => item.id === id);
|
|
260
|
+
return device ? device.name : '';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function getAcOverrides() {
|
|
264
|
+
const cfg = ensureConfig();
|
|
265
|
+
return (cfg.options.deviceOverrides || []).filter((item) => item && item.id && item.airConditioner && typeof item.airConditioner === 'object');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function isOnlyAcOverride(item) {
|
|
269
|
+
const keys = Object.keys(item || {}).filter((key) => item[key] !== undefined && item[key] !== null && item[key] !== '');
|
|
270
|
+
return keys.every((key) => ['id', 'airConditioner'].includes(key));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function renderAcOverrides() {
|
|
274
|
+
const tbody = $('tuyaNodevAcOverrideRows');
|
|
275
|
+
const overrides = getAcOverrides();
|
|
276
|
+
if (!overrides.length) {
|
|
277
|
+
tbody.innerHTML = '<tr><td colspan="6" class="tuya-nodev-small">No AC overrides configured.</td></tr>';
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
tbody.innerHTML = overrides.map((override) => {
|
|
282
|
+
const ac = override.airConditioner || {};
|
|
283
|
+
const name = getDeviceName(override.id) || 'Unknown / not in detected cache';
|
|
284
|
+
return `
|
|
285
|
+
<tr>
|
|
286
|
+
<td>${escapeHtml(name)}</td>
|
|
287
|
+
<td><code>${escapeHtml(override.id)}</code></td>
|
|
288
|
+
<td>${escapeHtml(ac.minTemperature ?? '')} °C</td>
|
|
289
|
+
<td>${escapeHtml(ac.maxTemperature ?? '')} °C</td>
|
|
290
|
+
<td>${escapeHtml(ac.temperatureStep ?? '')} °C</td>
|
|
291
|
+
<td><button class="btn btn-sm btn-outline-secondary tuya-nodev-edit-ac" type="button" data-id="${escapeHtml(override.id)}">Edit</button></td>
|
|
292
|
+
</tr>`;
|
|
293
|
+
}).join('');
|
|
294
|
+
|
|
295
|
+
tbody.querySelectorAll('.tuya-nodev-edit-ac').forEach((button) => {
|
|
296
|
+
button.addEventListener('click', () => editAcOverride(button.getAttribute('data-id')));
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function escapeHtml(value) {
|
|
301
|
+
return String(value ?? '')
|
|
302
|
+
.replace(/&/g, '&')
|
|
303
|
+
.replace(/</g, '<')
|
|
304
|
+
.replace(/>/g, '>')
|
|
305
|
+
.replace(/"/g, '"')
|
|
306
|
+
.replace(/'/g, ''');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function parseNumberInput(id, label) {
|
|
310
|
+
const raw = $(id).value;
|
|
311
|
+
const value = Number(raw);
|
|
312
|
+
if (!Number.isFinite(value)) {
|
|
313
|
+
throw new Error(`${label} must be a number.`);
|
|
314
|
+
}
|
|
315
|
+
return value;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function populateDeviceSelect(devices) {
|
|
319
|
+
const select = $('tuyaNodevAcDevice');
|
|
320
|
+
const current = select.value;
|
|
321
|
+
if (!devices.length) {
|
|
322
|
+
select.innerHTML = '<option value="">No detected devices found</option>';
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
select.innerHTML = '<option value="">Select a Tuya device...</option>' + devices.map((device) => {
|
|
327
|
+
const tag = device.likelyAirConditioner ? 'AC candidate' : (device.category || 'device');
|
|
328
|
+
const text = `${device.name} — ${tag} — ${device.id}`;
|
|
329
|
+
return `<option value="${escapeHtml(device.id)}">${escapeHtml(text)}</option>`;
|
|
330
|
+
}).join('');
|
|
331
|
+
|
|
332
|
+
if (current && devices.some((device) => device.id === current)) {
|
|
333
|
+
select.value = current;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function loadDetectedDevices(showToast = true) {
|
|
338
|
+
try {
|
|
339
|
+
homebridge.showSpinner();
|
|
340
|
+
const res = await homebridge.request('/devices/list', {});
|
|
341
|
+
detectedDevices = Array.isArray(res.devices) ? res.devices : [];
|
|
342
|
+
populateDeviceSelect(detectedDevices);
|
|
343
|
+
renderAcOverrides();
|
|
344
|
+
const acCount = detectedDevices.filter((device) => device.likelyAirConditioner).length;
|
|
345
|
+
setAcStatus(`${res.message || 'Device list loaded.'} ${acCount ? `${acCount} AC candidate(s) listed first.` : ''}`.trim(), detectedDevices.length ? 'success' : 'warning');
|
|
346
|
+
if (showToast) homebridge.toast.success(`Loaded ${detectedDevices.length} detected Tuya device(s).`, 'Tuya');
|
|
347
|
+
} catch (e) {
|
|
348
|
+
setAcStatus(e.message || 'Failed to load detected devices.', 'danger');
|
|
349
|
+
if (showToast) homebridge.toast.error(e.message || 'Failed to load detected devices.', 'Tuya');
|
|
350
|
+
} finally {
|
|
351
|
+
homebridge.hideSpinner();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function editAcOverride(id) {
|
|
356
|
+
const override = getAcOverrides().find((item) => item.id === id);
|
|
357
|
+
if (!override) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const device = detectedDevices.find((item) => item.id === id);
|
|
361
|
+
if (!device) {
|
|
362
|
+
const select = $('tuyaNodevAcDevice');
|
|
363
|
+
const option = document.createElement('option');
|
|
364
|
+
option.value = id;
|
|
365
|
+
option.textContent = `${id} — not currently in detected device cache`;
|
|
366
|
+
select.appendChild(option);
|
|
367
|
+
}
|
|
368
|
+
$('tuyaNodevAcDevice').value = id;
|
|
369
|
+
$('tuyaNodevAcMin').value = override.airConditioner.minTemperature ?? 17;
|
|
370
|
+
$('tuyaNodevAcMax').value = override.airConditioner.maxTemperature ?? 31;
|
|
371
|
+
$('tuyaNodevAcStep').value = override.airConditioner.temperatureStep ?? 1;
|
|
372
|
+
setAcStatus(`Editing AC override for ${device?.name || id}.`, 'info');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function addOrUpdateAcOverride() {
|
|
376
|
+
try {
|
|
377
|
+
const id = $('tuyaNodevAcDevice').value.trim();
|
|
378
|
+
if (!id) {
|
|
379
|
+
setAcStatus('Select a Tuya device first.', 'warning');
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const minTemperature = parseNumberInput('tuyaNodevAcMin', 'Minimum temperature');
|
|
383
|
+
const maxTemperature = parseNumberInput('tuyaNodevAcMax', 'Maximum temperature');
|
|
384
|
+
const temperatureStep = parseNumberInput('tuyaNodevAcStep', 'Temperature step');
|
|
385
|
+
if (temperatureStep <= 0) {
|
|
386
|
+
throw new Error('Temperature step must be greater than 0.');
|
|
387
|
+
}
|
|
388
|
+
if (minTemperature > maxTemperature) {
|
|
389
|
+
throw new Error('Minimum temperature cannot be greater than maximum temperature.');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const cfg = ensureConfig();
|
|
393
|
+
const overrides = cfg.options.deviceOverrides;
|
|
394
|
+
let override = overrides.find((item) => item && String(item.id || '').trim() === id);
|
|
395
|
+
if (!override) {
|
|
396
|
+
override = { id };
|
|
397
|
+
overrides.push(override);
|
|
398
|
+
}
|
|
399
|
+
override.id = id;
|
|
400
|
+
override.airConditioner = { minTemperature, maxTemperature, temperatureStep };
|
|
401
|
+
|
|
402
|
+
await syncConfigToUi();
|
|
403
|
+
const name = getDeviceName(id) || id;
|
|
404
|
+
setAcStatus(`AC override saved in plugin config for ${name}: ${minTemperature}–${maxTemperature} °C, step ${temperatureStep} °C. Click Save Configuration when ready.`, 'success');
|
|
405
|
+
homebridge.toast.success('AC temperature override added to config.', 'Tuya');
|
|
406
|
+
if (isAuthenticated) {
|
|
407
|
+
enableSaving();
|
|
408
|
+
}
|
|
409
|
+
} catch (e) {
|
|
410
|
+
setAcStatus(e.message || 'Failed to add AC override.', 'danger');
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function removeSelectedAcOverride() {
|
|
415
|
+
const id = $('tuyaNodevAcDevice').value.trim();
|
|
416
|
+
if (!id) {
|
|
417
|
+
setAcStatus('Select a Tuya device first.', 'warning');
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const cfg = ensureConfig();
|
|
422
|
+
const before = cfg.options.deviceOverrides.length;
|
|
423
|
+
cfg.options.deviceOverrides = cfg.options.deviceOverrides.flatMap((item) => {
|
|
424
|
+
if (!item || String(item.id || '').trim() !== id) {
|
|
425
|
+
return [item];
|
|
426
|
+
}
|
|
427
|
+
if (isOnlyAcOverride(item)) {
|
|
428
|
+
return [];
|
|
429
|
+
}
|
|
430
|
+
const copy = clone(item);
|
|
431
|
+
delete copy.airConditioner;
|
|
432
|
+
return [copy];
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
await syncConfigToUi();
|
|
436
|
+
if (cfg.options.deviceOverrides.length === before) {
|
|
437
|
+
setAcStatus('No AC override existed for the selected device.', 'warning');
|
|
438
|
+
} else {
|
|
439
|
+
setAcStatus('Selected AC override was removed from the plugin config. Click Save Configuration when ready.', 'success');
|
|
440
|
+
homebridge.toast.success('AC temperature override removed from config.', 'Tuya');
|
|
441
|
+
}
|
|
442
|
+
if (isAuthenticated) {
|
|
443
|
+
enableSaving();
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
153
447
|
async function checkAuth(showSuccessToast = false) {
|
|
154
448
|
const userCode = getUserCode();
|
|
155
449
|
if (!userCode) {
|
|
@@ -286,12 +580,9 @@
|
|
|
286
580
|
platform: PLATFORM,
|
|
287
581
|
name: 'Tuya without developer account',
|
|
288
582
|
mode: 'cloud',
|
|
289
|
-
options: { projectType: '3' },
|
|
583
|
+
options: { projectType: '3', deviceOverrides: [] },
|
|
290
584
|
};
|
|
291
|
-
|
|
292
|
-
currentConfig.mode = 'cloud';
|
|
293
|
-
currentConfig.options = currentConfig.options || {};
|
|
294
|
-
currentConfig.options.projectType = '3';
|
|
585
|
+
ensureConfig();
|
|
295
586
|
$('tuyaNodevName').value = currentConfig.name || 'Tuya without developer account';
|
|
296
587
|
$('tuyaNodevUserCode').value = currentConfig.options?.userCode || '';
|
|
297
588
|
|
|
@@ -301,12 +592,10 @@
|
|
|
301
592
|
const block = Array.isArray(data) ? data[0] : data;
|
|
302
593
|
if (block && typeof block === 'object') {
|
|
303
594
|
currentConfig = block;
|
|
304
|
-
|
|
305
|
-
currentConfig.mode = 'cloud';
|
|
306
|
-
currentConfig.options = currentConfig.options || {};
|
|
307
|
-
currentConfig.options.projectType = '3';
|
|
595
|
+
ensureConfig();
|
|
308
596
|
if (block.name) $('tuyaNodevName').value = block.name;
|
|
309
597
|
if (block.options?.userCode) $('tuyaNodevUserCode').value = block.options.userCode;
|
|
598
|
+
renderAcOverrides();
|
|
310
599
|
}
|
|
311
600
|
});
|
|
312
601
|
|
|
@@ -320,8 +609,12 @@
|
|
|
320
609
|
disableSaving();
|
|
321
610
|
syncConfigToUi();
|
|
322
611
|
});
|
|
612
|
+
$('tuyaNodevLoadDevices').addEventListener('click', () => loadDetectedDevices(true));
|
|
613
|
+
$('tuyaNodevApplyAc').addEventListener('click', addOrUpdateAcOverride);
|
|
614
|
+
$('tuyaNodevRemoveAc').addEventListener('click', removeSelectedAcOverride);
|
|
323
615
|
|
|
324
616
|
await syncConfigToUi();
|
|
617
|
+
await loadDetectedDevices(false);
|
|
325
618
|
if (getUserCode()) {
|
|
326
619
|
await checkAuth(false);
|
|
327
620
|
} else {
|
package/homebridge-ui/server.js
CHANGED
|
@@ -13,6 +13,161 @@ function normaliseUserCode(userCode) {
|
|
|
13
13
|
return String(userCode || '').trim();
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
function firstString(...values) {
|
|
17
|
+
for (const value of values) {
|
|
18
|
+
if (value === undefined || value === null) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const text = String(value).trim();
|
|
22
|
+
if (text) {
|
|
23
|
+
return text;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return '';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function looksLikeAirConditioner(device) {
|
|
30
|
+
const haystack = [
|
|
31
|
+
device.name,
|
|
32
|
+
device.category,
|
|
33
|
+
device.productName,
|
|
34
|
+
device.productId,
|
|
35
|
+
device.model,
|
|
36
|
+
].filter(Boolean).join(' ').toLowerCase();
|
|
37
|
+
|
|
38
|
+
return [
|
|
39
|
+
'air conditioner',
|
|
40
|
+
'airconditioner',
|
|
41
|
+
'aircon',
|
|
42
|
+
'a/c',
|
|
43
|
+
'ac ',
|
|
44
|
+
' ac',
|
|
45
|
+
'clima',
|
|
46
|
+
'climă',
|
|
47
|
+
'aer conditionat',
|
|
48
|
+
'aer condiționat',
|
|
49
|
+
'hvac',
|
|
50
|
+
].some((needle) => haystack.includes(needle))
|
|
51
|
+
|| ['kt', 'wk', 'air_conditioner', 'airconditioner'].includes(String(device.category || '').toLowerCase());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function collectDevicesFromObject(root) {
|
|
55
|
+
const byId = new Map();
|
|
56
|
+
|
|
57
|
+
function addDevice(obj) {
|
|
58
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const id = firstString(
|
|
63
|
+
obj.id,
|
|
64
|
+
obj.devId,
|
|
65
|
+
obj.dev_id,
|
|
66
|
+
obj.deviceId,
|
|
67
|
+
obj.device_id,
|
|
68
|
+
obj.uid,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const name = firstString(
|
|
72
|
+
obj.name,
|
|
73
|
+
obj.deviceName,
|
|
74
|
+
obj.device_name,
|
|
75
|
+
obj.customName,
|
|
76
|
+
obj.custom_name,
|
|
77
|
+
obj.title,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
if (!id || !name) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Avoid adding automation scenes as selectable devices.
|
|
85
|
+
if (obj.scene_id || obj.sceneId || obj.rule_id || obj.ruleId) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const category = firstString(
|
|
90
|
+
obj.category,
|
|
91
|
+
obj.categoryCode,
|
|
92
|
+
obj.category_code,
|
|
93
|
+
obj.productCategory,
|
|
94
|
+
obj.product_category,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const productName = firstString(
|
|
98
|
+
obj.productName,
|
|
99
|
+
obj.product_name,
|
|
100
|
+
obj.product,
|
|
101
|
+
obj.productTitle,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const productId = firstString(
|
|
105
|
+
obj.productId,
|
|
106
|
+
obj.product_id,
|
|
107
|
+
obj.pid,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const model = firstString(obj.model, obj.modelId, obj.model_id);
|
|
111
|
+
|
|
112
|
+
const status = Array.isArray(obj.status) ? obj.status : [];
|
|
113
|
+
const statusCodes = status
|
|
114
|
+
.map((item) => item && typeof item === 'object' ? firstString(item.code) : '')
|
|
115
|
+
.filter(Boolean);
|
|
116
|
+
|
|
117
|
+
const schema = Array.isArray(obj.schema) ? obj.schema : Array.isArray(obj.schemas) ? obj.schemas : [];
|
|
118
|
+
const schemaCodes = schema
|
|
119
|
+
.map((item) => item && typeof item === 'object' ? firstString(item.code) : '')
|
|
120
|
+
.filter(Boolean);
|
|
121
|
+
|
|
122
|
+
const existing = byId.get(id) || {};
|
|
123
|
+
const merged = {
|
|
124
|
+
id,
|
|
125
|
+
name: existing.name || name,
|
|
126
|
+
category: existing.category || category || null,
|
|
127
|
+
productName: existing.productName || productName || null,
|
|
128
|
+
productId: existing.productId || productId || null,
|
|
129
|
+
model: existing.model || model || null,
|
|
130
|
+
online: typeof obj.online === 'boolean' ? obj.online : existing.online,
|
|
131
|
+
statusCodes: Array.from(new Set([...(existing.statusCodes || []), ...statusCodes])).sort(),
|
|
132
|
+
schemaCodes: Array.from(new Set([...(existing.schemaCodes || []), ...schemaCodes])).sort(),
|
|
133
|
+
};
|
|
134
|
+
merged.likelyAirConditioner = looksLikeAirConditioner(merged)
|
|
135
|
+
|| merged.statusCodes.includes('temp_set')
|
|
136
|
+
|| merged.schemaCodes.includes('temp_set');
|
|
137
|
+
merged.label = `${merged.name} (${merged.id})`;
|
|
138
|
+
byId.set(id, merged);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function walk(value) {
|
|
142
|
+
if (Array.isArray(value)) {
|
|
143
|
+
for (const item of value) {
|
|
144
|
+
walk(item);
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (!value || typeof value !== 'object') {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
addDevice(value);
|
|
153
|
+
|
|
154
|
+
for (const child of Object.values(value)) {
|
|
155
|
+
if (child && typeof child === 'object') {
|
|
156
|
+
walk(child);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
walk(root);
|
|
162
|
+
|
|
163
|
+
return Array.from(byId.values()).sort((a, b) => {
|
|
164
|
+
if (a.likelyAirConditioner !== b.likelyAirConditioner) {
|
|
165
|
+
return a.likelyAirConditioner ? -1 : 1;
|
|
166
|
+
}
|
|
167
|
+
return String(a.name).localeCompare(String(b.name));
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
16
171
|
(async () => {
|
|
17
172
|
const { HomebridgePluginUiServer, RequestError } = await import('@homebridge/plugin-ui-utils');
|
|
18
173
|
|
|
@@ -24,6 +179,7 @@ function normaliseUserCode(userCode) {
|
|
|
24
179
|
this.onRequest('/qr/status', this.qrStatus.bind(this));
|
|
25
180
|
this.onRequest('/auth/status', this.authStatus.bind(this));
|
|
26
181
|
this.onRequest('/auth/clear', this.clearAuth.bind(this));
|
|
182
|
+
this.onRequest('/devices/list', this.listDevices.bind(this));
|
|
27
183
|
this.ready();
|
|
28
184
|
}
|
|
29
185
|
|
|
@@ -58,6 +214,63 @@ function normaliseUserCode(userCode) {
|
|
|
58
214
|
return file;
|
|
59
215
|
}
|
|
60
216
|
|
|
217
|
+
async listDevices() {
|
|
218
|
+
const persistDir = path.join(this.homebridgeStoragePath, 'persist');
|
|
219
|
+
let entries;
|
|
220
|
+
try {
|
|
221
|
+
entries = await fs.promises.readdir(persistDir, { withFileTypes: true });
|
|
222
|
+
} catch (err) {
|
|
223
|
+
if (err && err.code === 'ENOENT') {
|
|
224
|
+
return { devices: [], files: [], message: 'No Homebridge persist directory found yet. Authenticate and restart Homebridge once so the plugin can save a device list.' };
|
|
225
|
+
}
|
|
226
|
+
throw err;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const candidates = [];
|
|
230
|
+
for (const entry of entries) {
|
|
231
|
+
if (!entry.isFile()) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (!/^TuyaDeviceList.*\.json$/i.test(entry.name)) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const file = path.join(persistDir, entry.name);
|
|
238
|
+
const stat = await fs.promises.stat(file);
|
|
239
|
+
candidates.push({ file, mtimeMs: stat.mtimeMs });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
243
|
+
|
|
244
|
+
const allDevices = new Map();
|
|
245
|
+
const errors = [];
|
|
246
|
+
for (const candidate of candidates) {
|
|
247
|
+
try {
|
|
248
|
+
const data = JSON.parse(await fs.promises.readFile(candidate.file, 'utf8'));
|
|
249
|
+
for (const device of collectDevicesFromObject(data)) {
|
|
250
|
+
if (!allDevices.has(device.id)) {
|
|
251
|
+
allDevices.set(device.id, device);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
} catch (err) {
|
|
255
|
+
errors.push({ file: candidate.file, message: err.message });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const devices = Array.from(allDevices.values()).sort((a, b) => {
|
|
260
|
+
if (a.likelyAirConditioner !== b.likelyAirConditioner) {
|
|
261
|
+
return a.likelyAirConditioner ? -1 : 1;
|
|
262
|
+
}
|
|
263
|
+
return String(a.name).localeCompare(String(b.name));
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
devices,
|
|
268
|
+
files: candidates.map((item) => item.file),
|
|
269
|
+
errors,
|
|
270
|
+
message: devices.length ? `Loaded ${devices.length} Tuya device(s) from Homebridge persist cache.` : 'No devices found in TuyaDeviceList cache yet. Authenticate and restart Homebridge once, then reopen this settings page.',
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
61
274
|
async authStatus(payload = {}) {
|
|
62
275
|
const userCode = normaliseUserCode(payload.userCode);
|
|
63
276
|
if (!userCode) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-tuya-without-developer-account",
|
|
3
3
|
"displayName": "Tuya without developer account for Homebridge",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.4",
|
|
5
5
|
"description": "Homebridge plugin for Tuya and Smart Life devices using QR cloud authentication without a Tuya IoT developer account.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Kosztyk",
|