homebridge-tuya-without-developer-account 1.0.12 → 1.0.14
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 +18 -0
- package/README.md +52 -1
- package/SUPPORTED_DEVICES.md +6 -0
- package/config.schema.json +27 -7
- package/dist/platform.js +43 -0
- package/dist/shared/accessories/PetFeederAccessory.js +92 -11
- package/dist/shared/accessories/SwitchAccessory.js +9 -2
- package/dist/shared/accessories/characteristic/Name.js +66 -10
- package/homebridge-ui/public/index.html +257 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.14
|
|
4
|
+
|
|
5
|
+
- Preserves service names changed by users in Apple Home/Homebridge instead of resetting multi-gang switch and outlet names on every restart.
|
|
6
|
+
- Added global `options.preserveHomeKitNames` setting, enabled by default.
|
|
7
|
+
- Added per-device `deviceOverrides[].preserveHomeKitNames` override.
|
|
8
|
+
- Added explicit `deviceOverrides[].switchNames` channel-name overrides keyed by Tuya schema code, such as `switch_1`, `switch_2`, and `switch_usb1`.
|
|
9
|
+
- Added a Homebridge custom UI section for preserving HomeKit names and assigning explicit names to detected multi-switch/outlet channels.
|
|
10
|
+
- Name priority is now: explicit `switchNames` override → existing HomeKit `ConfiguredName` → plugin-generated default.
|
|
11
|
+
- Added `nameOverride` as a compatibility alias for `preserveHomeKitNames`.
|
|
12
|
+
|
|
13
|
+
## 1.0.13
|
|
14
|
+
|
|
15
|
+
- Changed Smart Pet Feeder presentation to use a refined HomeKit `Valve` service as the main **Feed Now** control.
|
|
16
|
+
- `Active` triggers the configured `manual_feed` amount when available, falling back to `quick_feed` when needed.
|
|
17
|
+
- `InUse` reflects `feed_state` so HomeKit can show when the feeder is currently feeding.
|
|
18
|
+
- Kept the dedicated Quick Feed switch, optional Slow Feed switch, Battery service, and Feeding occupancy/status sensor.
|
|
19
|
+
- Removes the old cached Manual Feed switch from feeder accessories when reconfigured, since the Valve now handles manual feeding.
|
|
20
|
+
|
|
3
21
|
## 1.0.12
|
|
4
22
|
|
|
5
23
|
- Stopped exposing `switch_inching` as a HomeKit switch because it is an internal Tuya inching/timer configuration DP, not a user-facing relay.
|
package/README.md
CHANGED
|
@@ -192,6 +192,57 @@ Optional. Use only when a device is discovered with the wrong category or requir
|
|
|
192
192
|
|
|
193
193
|
Use `global` as the override ID to apply an override globally.
|
|
194
194
|
|
|
195
|
+
### Preserve HomeKit names and name multi-gang channels
|
|
196
|
+
|
|
197
|
+
Version 1.0.14 preserves names that users assign to individual services in Apple Home or the Homebridge Accessories page. This is especially useful for multi-gang switches and outlets that would otherwise return to generated names such as `Brilliant 1` and `Brilliant 2` after a restart.
|
|
198
|
+
|
|
199
|
+
The setting is enabled by default and is available in the custom plugin UI:
|
|
200
|
+
|
|
201
|
+
```text
|
|
202
|
+
HomeKit Names → Preserve names changed in HomeKit
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Equivalent configuration:
|
|
206
|
+
|
|
207
|
+
```json
|
|
208
|
+
{
|
|
209
|
+
"options": {
|
|
210
|
+
"userCode": "YOUR_TUYA_USER_CODE",
|
|
211
|
+
"preserveHomeKitNames": true
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
You can also assign deterministic names to individual Tuya switch channels. The Homebridge UI can load detected devices and save these channel names automatically. The internal configuration looks like this:
|
|
217
|
+
|
|
218
|
+
```json
|
|
219
|
+
{
|
|
220
|
+
"options": {
|
|
221
|
+
"userCode": "YOUR_TUYA_USER_CODE",
|
|
222
|
+
"deviceOverrides": [
|
|
223
|
+
{
|
|
224
|
+
"id": "YOUR_MULTI_SWITCH_DEVICE_ID",
|
|
225
|
+
"switchNames": {
|
|
226
|
+
"switch_1": "Coffee Machine",
|
|
227
|
+
"switch_2": "Kitchen Lamp",
|
|
228
|
+
"switch_usb1": "USB Ports"
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
]
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Name priority is:
|
|
237
|
+
|
|
238
|
+
```text
|
|
239
|
+
Explicit switchNames override
|
|
240
|
+
→ Existing HomeKit ConfiguredName
|
|
241
|
+
→ Generated device/channel name
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Set `deviceOverrides[].preserveHomeKitNames` to `false` for a specific device when you want the plugin to reapply generated names at every restart.
|
|
245
|
+
|
|
195
246
|
### Air conditioner temperature limits
|
|
196
247
|
|
|
197
248
|
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.
|
|
@@ -351,6 +402,6 @@ Version **1.0.5** adds support for DP10-style Tuya dimmer plugs that expose `swi
|
|
|
351
402
|
|
|
352
403
|
## Version 1.0.7 device support
|
|
353
404
|
|
|
354
|
-
This release adds native support for Tuya Smart Pet Feeders and Tuya alarm panels that expose `master_mode`. Pet feeders expose
|
|
405
|
+
This release adds native support for Tuya Smart Pet Feeders and Tuya alarm panels that expose `master_mode`. Pet feeders expose a refined HomeKit Valve-style **Feed Now** control, Quick Feed switch, optional Slow Feed switch, feed-state sensor, and battery when available. Alarm panels are exposed as HomeKit Security System accessories, with optional extra switches controlled through `deviceOverrides[].alarm`.
|
|
355
406
|
|
|
356
407
|
Aroma diffuser devices whose Tuya QR cloud schema is empty remain visible as unsupported direct devices, but any diffuser scenes returned by Tuya are still exposed separately.
|
package/SUPPORTED_DEVICES.md
CHANGED
|
@@ -222,6 +222,8 @@ They are exposed to HomeKit as a Lightbulb with On and Brightness. After upgradi
|
|
|
222
222
|
|
|
223
223
|
Supported when Tuya exposes one or more of: `quick_feed`, `manual_feed`, `slow_feed`, `feed_state`, `battery_percentage`, `charge_state`.
|
|
224
224
|
|
|
225
|
+
From v1.0.13 the feeder uses a refined HomeKit `Valve` service as the main **Feed Now** control. Activating it sends the configured `manual_feed` amount when available, or falls back to `quick_feed`. `InUse` reflects `feed_state`. The plugin also keeps the Quick Feed switch, optional Slow Feed switch, Feeding occupancy/status sensor, and Battery service.
|
|
226
|
+
|
|
225
227
|
### Alarm / Security System
|
|
226
228
|
|
|
227
229
|
Supported when Tuya exposes `master_mode`. Alarm-triggered state is detected from `master_state=alarm`, `sos_state=true`, or `master_mode=sos` when available. Optional extra switches can be enabled through `deviceOverrides[].alarm`.
|
|
@@ -235,3 +237,7 @@ If Tuya QR cloud returns an empty schema for the diffuser device, direct device
|
|
|
235
237
|
Adaptive Lighting is supported for eligible Tuya light accessories that expose both brightness and real white color-temperature datapoints, for example CCT, CW, or RGBCW lights.
|
|
236
238
|
|
|
237
239
|
Not eligible: simple on/off lights, outlets, switches, brightness-only dimmers, DP10 dimmer plugs, and RGB-only lights without a real white-temperature datapoint.
|
|
240
|
+
## Multi-gang switch and outlet names
|
|
241
|
+
|
|
242
|
+
From v1.0.14, names changed in Apple Home/Homebridge are preserved by default for multi-service accessories. Optional `deviceOverrides[].switchNames` entries can assign explicit names to Tuya channels such as `switch_1`, `switch_2`, and `switch_usb1`.
|
|
243
|
+
|
package/config.schema.json
CHANGED
|
@@ -42,6 +42,12 @@
|
|
|
42
42
|
"description": "Optional. Enables HomeKit Adaptive Lighting only for Tuya lights that expose both brightness and real white color-temperature controls. RGB-only lights and dimmer plugs are skipped.",
|
|
43
43
|
"default": false
|
|
44
44
|
},
|
|
45
|
+
"preserveHomeKitNames": {
|
|
46
|
+
"type": "boolean",
|
|
47
|
+
"title": "Preserve names changed in HomeKit",
|
|
48
|
+
"description": "Recommended. Prevents Homebridge restarts from replacing names you assigned to individual services in Apple Home, especially multi-gang switches and outlets. Explicit switchNames overrides still take priority.",
|
|
49
|
+
"default": true
|
|
50
|
+
},
|
|
45
51
|
"deviceOverrides": {
|
|
46
52
|
"type": "array",
|
|
47
53
|
"title": "Device Overrides",
|
|
@@ -62,6 +68,19 @@
|
|
|
62
68
|
"title": "Expose as external accessory",
|
|
63
69
|
"default": false
|
|
64
70
|
},
|
|
71
|
+
"preserveHomeKitNames": {
|
|
72
|
+
"type": "boolean",
|
|
73
|
+
"title": "Preserve HomeKit names for this device",
|
|
74
|
+
"description": "Optional per-device override of the global setting. True preserves names changed in Apple Home; false lets the plugin reapply generated/default names on restart."
|
|
75
|
+
},
|
|
76
|
+
"switchNames": {
|
|
77
|
+
"type": "object",
|
|
78
|
+
"title": "Multi-switch channel names",
|
|
79
|
+
"description": "Optional explicit names keyed by Tuya schema code. These names take priority over preserved HomeKit names and generated defaults. Example: {\"switch_1\":\"Coffee Machine\",\"switch_2\":\"Kitchen Lamp\",\"switch_usb1\":\"USB Ports\"}.",
|
|
80
|
+
"additionalProperties": {
|
|
81
|
+
"type": "string"
|
|
82
|
+
}
|
|
83
|
+
},
|
|
65
84
|
"adaptiveLighting": {
|
|
66
85
|
"title": "Adaptive Lighting Override",
|
|
67
86
|
"description": "Optional per-device override. Use enabled=true to force-enable for this device, or enabled=false to disable it even when global Adaptive Lighting is enabled.",
|
|
@@ -106,26 +125,26 @@
|
|
|
106
125
|
"airConditioner": {
|
|
107
126
|
"type": "object",
|
|
108
127
|
"title": "Air Conditioner Temperature Limits",
|
|
109
|
-
"description": "Optional HomeKit temperature range override for Wi-Fi air conditioners. Values are always Celsius; Home app converts to Fahrenheit automatically for users using
|
|
128
|
+
"description": "Optional HomeKit temperature range override for Wi-Fi air conditioners. Values are always Celsius; Home app converts to Fahrenheit automatically for users using °F.",
|
|
110
129
|
"properties": {
|
|
111
130
|
"minTemperature": {
|
|
112
131
|
"type": "number",
|
|
113
|
-
"title": "Minimum temperature (
|
|
132
|
+
"title": "Minimum temperature (°C)",
|
|
114
133
|
"description": "Lowest setpoint exposed to HomeKit. Common values are 16 or 17. Leave empty to use Tuya schema.",
|
|
115
134
|
"minimum": 0,
|
|
116
135
|
"maximum": 50
|
|
117
136
|
},
|
|
118
137
|
"maxTemperature": {
|
|
119
138
|
"type": "number",
|
|
120
|
-
"title": "Maximum temperature (
|
|
139
|
+
"title": "Maximum temperature (°C)",
|
|
121
140
|
"description": "Highest setpoint exposed to HomeKit. Common value is 31. Leave empty to use Tuya schema.",
|
|
122
141
|
"minimum": 0,
|
|
123
142
|
"maximum": 60
|
|
124
143
|
},
|
|
125
144
|
"temperatureStep": {
|
|
126
145
|
"type": "number",
|
|
127
|
-
"title": "Temperature step (
|
|
128
|
-
"description": "Set to 1 to suppress 0.5
|
|
146
|
+
"title": "Temperature step (°C)",
|
|
147
|
+
"description": "Set to 1 to suppress 0.5 °C steps. Values are Celsius even when Home app displays Fahrenheit.",
|
|
129
148
|
"minimum": 0.1,
|
|
130
149
|
"maximum": 5,
|
|
131
150
|
"default": 1
|
|
@@ -135,12 +154,12 @@
|
|
|
135
154
|
"petFeeder": {
|
|
136
155
|
"type": "object",
|
|
137
156
|
"title": "Smart Pet Feeder Options",
|
|
138
|
-
"description": "Optional settings for Tuya pet feeders. The plugin exposes
|
|
157
|
+
"description": "Optional settings for Tuya pet feeders. The plugin exposes a refined HomeKit Valve-style Feed Now control, Quick Feed switch, optional Slow Feed switch, feeding-state sensor, and battery when supported.",
|
|
139
158
|
"properties": {
|
|
140
159
|
"manualFeedAmount": {
|
|
141
160
|
"type": "integer",
|
|
142
161
|
"title": "Manual feed amount",
|
|
143
|
-
"description": "Portions sent when the
|
|
162
|
+
"description": "Portions sent when the Valve-style Feed Now control is activated.",
|
|
144
163
|
"minimum": 1,
|
|
145
164
|
"maximum": 12,
|
|
146
165
|
"default": 1
|
|
@@ -216,6 +235,7 @@
|
|
|
216
235
|
"items": [
|
|
217
236
|
"options.homeWhitelist",
|
|
218
237
|
"options.enableAdaptiveLighting",
|
|
238
|
+
"options.preserveHomeKitNames",
|
|
219
239
|
"options.deviceOverrides",
|
|
220
240
|
"options.debug",
|
|
221
241
|
"options.debugLevel"
|
package/dist/platform.js
CHANGED
|
@@ -48,6 +48,11 @@ class TuyaPlatform {
|
|
|
48
48
|
this.config.mode = "cloud";
|
|
49
49
|
this.options.projectType = "3";
|
|
50
50
|
this.options.enableAdaptiveLighting = this.options.enableAdaptiveLighting === true;
|
|
51
|
+
// Preserve names changed by users in Apple Home/Homebridge by default.
|
|
52
|
+
// nameOverride is accepted as a compatibility alias used by some plugins.
|
|
53
|
+
this.options.preserveHomeKitNames = typeof this.options.preserveHomeKitNames === 'boolean'
|
|
54
|
+
? this.options.preserveHomeKitNames
|
|
55
|
+
: (typeof this.options.nameOverride === 'boolean' ? this.options.nameOverride : true);
|
|
51
56
|
|
|
52
57
|
if (!this.options.userCode || String(this.options.userCode).trim().length === 0) {
|
|
53
58
|
this.log.error("[Tuya QR] Missing Tuya User Code. Open Homebridge UI → Plugins → Tuya without developer account for Homebridge → Settings, generate/scan the QR code, then save.");
|
|
@@ -151,6 +156,41 @@ class TuyaPlatform {
|
|
|
151
156
|
delete item.alarm;
|
|
152
157
|
}
|
|
153
158
|
}
|
|
159
|
+
if (item.preserveHomeKitNames === undefined && typeof item.nameOverride === 'boolean') {
|
|
160
|
+
item.preserveHomeKitNames = item.nameOverride;
|
|
161
|
+
}
|
|
162
|
+
if (item.preserveHomeKitNames !== undefined && typeof item.preserveHomeKitNames !== 'boolean') {
|
|
163
|
+
this.log.warn('[Tuya QR] Ignoring invalid preserveHomeKitNames override for id "%s". Use true or false.', id);
|
|
164
|
+
delete item.preserveHomeKitNames;
|
|
165
|
+
}
|
|
166
|
+
delete item.nameOverride;
|
|
167
|
+
if (item.switchNames !== undefined) {
|
|
168
|
+
if (!item.switchNames || typeof item.switchNames !== 'object' || Array.isArray(item.switchNames)) {
|
|
169
|
+
this.log.warn('[Tuya QR] Ignoring invalid switchNames override for id "%s" because it is not an object.', id);
|
|
170
|
+
delete item.switchNames;
|
|
171
|
+
} else {
|
|
172
|
+
const normalizedSwitchNames = {};
|
|
173
|
+
for (const [rawCode, rawName] of Object.entries(item.switchNames)) {
|
|
174
|
+
const code = String(rawCode || '').trim();
|
|
175
|
+
const requestedName = String(rawName || '').trim();
|
|
176
|
+
if (!code || !requestedName) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const fallbackName = requestedName.replace(/[^A-Za-z0-9 '\s]/g, ' ').replace(/\s+/g, ' ').trim();
|
|
180
|
+
const safeName = sanitizeName(requestedName) ?? fallbackName;
|
|
181
|
+
if (!safeName) {
|
|
182
|
+
this.log.warn('[Tuya QR] Ignoring invalid switch name for code "%s" on device id "%s".', code, id);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
normalizedSwitchNames[code] = safeName;
|
|
186
|
+
}
|
|
187
|
+
if (Object.keys(normalizedSwitchNames).length > 0) {
|
|
188
|
+
item.switchNames = normalizedSwitchNames;
|
|
189
|
+
} else {
|
|
190
|
+
delete item.switchNames;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
154
194
|
if (item.adaptiveLighting !== undefined) {
|
|
155
195
|
if (typeof item.adaptiveLighting === 'boolean') {
|
|
156
196
|
item.adaptiveLighting = { enabled: item.adaptiveLighting };
|
|
@@ -292,6 +332,9 @@ class TuyaPlatform {
|
|
|
292
332
|
alarm: deviceConfig?.alarm ? JSON.stringify(deviceConfig.alarm) : undefined,
|
|
293
333
|
globalAdaptiveLighting: !!this.options.enableAdaptiveLighting,
|
|
294
334
|
adaptiveLighting: deviceConfig?.adaptiveLighting ? JSON.stringify(deviceConfig.adaptiveLighting) : undefined,
|
|
335
|
+
globalPreserveHomeKitNames: this.options.preserveHomeKitNames !== false,
|
|
336
|
+
preserveHomeKitNames: typeof deviceConfig?.preserveHomeKitNames === 'boolean' ? deviceConfig.preserveHomeKitNames : undefined,
|
|
337
|
+
switchNames: deviceConfig?.switchNames ? JSON.stringify(deviceConfig.switchNames) : undefined,
|
|
295
338
|
};
|
|
296
339
|
const { changed: configChanged } = this.configHash.hasConfigChanged(device.id, configToHash);
|
|
297
340
|
device.configChanged = configChanged;
|
|
@@ -27,10 +27,92 @@ class PetFeederAccessory extends BaseAccessory_1.default {
|
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
29
|
configureServices() {
|
|
30
|
+
this.configureFeedNowValve();
|
|
30
31
|
this.configureQuickFeed();
|
|
31
|
-
this.configureManualFeed();
|
|
32
32
|
this.configureSlowFeed();
|
|
33
33
|
this.configureFeedState();
|
|
34
|
+
this.removeLegacyManualFeedSwitch();
|
|
35
|
+
}
|
|
36
|
+
isFeeding() {
|
|
37
|
+
const schema = this.getSchema(...SCHEMA_CODE.FEED_STATE);
|
|
38
|
+
if (!schema) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
const status = this.getStatus(schema.code);
|
|
42
|
+
if (!status) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const value = status.value;
|
|
46
|
+
if (typeof value === 'boolean') {
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
if (typeof value === 'number') {
|
|
50
|
+
return value > 0;
|
|
51
|
+
}
|
|
52
|
+
const text = String(value ?? '').trim().toLowerCase();
|
|
53
|
+
if (!text) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
return !['0', 'false', 'idle', 'standby', 'normal', 'done', 'finish', 'finished', 'complete', 'completed', 'none', 'ready'].includes(text);
|
|
57
|
+
}
|
|
58
|
+
getFeedNowCommand() {
|
|
59
|
+
const manualSchema = this.getSchema(...SCHEMA_CODE.MANUAL_FEED);
|
|
60
|
+
if (manualSchema) {
|
|
61
|
+
const { manualFeedAmount } = this.getPetFeederConfig();
|
|
62
|
+
return [{ code: manualSchema.code, value: manualFeedAmount }];
|
|
63
|
+
}
|
|
64
|
+
const quickSchema = this.getSchema(...SCHEMA_CODE.QUICK_FEED);
|
|
65
|
+
if (quickSchema) {
|
|
66
|
+
return [{ code: quickSchema.code, value: true }];
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
configureFeedNowValve() {
|
|
71
|
+
const commands = this.getFeedNowCommand();
|
|
72
|
+
if (!commands) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const name = `${this.device?.name || 'Pet Feeder'} Feed Now`;
|
|
76
|
+
const service = this.accessory.getServiceById(this.Service.Valve, 'feed_now')
|
|
77
|
+
|| this.accessory.addService(this.Service.Valve, name, 'feed_now');
|
|
78
|
+
(0, Name_1.configureName)(this, service, name);
|
|
79
|
+
const valveType = this.Characteristic.ValveType.GENERIC_VALVE ?? this.Characteristic.ValveType.IRRIGATION;
|
|
80
|
+
service.setCharacteristic(this.Characteristic.ValveType, valveType);
|
|
81
|
+
const { ACTIVE, INACTIVE } = this.Characteristic.Active;
|
|
82
|
+
const { IN_USE, NOT_IN_USE } = this.Characteristic.InUse;
|
|
83
|
+
service.getCharacteristic(this.Characteristic.Active)
|
|
84
|
+
.onGet(() => {
|
|
85
|
+
this.checkOnlineStatus();
|
|
86
|
+
return this.isFeeding() ? ACTIVE : INACTIVE;
|
|
87
|
+
})
|
|
88
|
+
.onSet(async (value) => {
|
|
89
|
+
this.checkOnlineStatus();
|
|
90
|
+
if (value === ACTIVE) {
|
|
91
|
+
await this.sendCommands(commands, true);
|
|
92
|
+
service.getCharacteristic(this.Characteristic.Active).updateValue(ACTIVE);
|
|
93
|
+
service.getCharacteristic(this.Characteristic.InUse).updateValue(IN_USE);
|
|
94
|
+
setTimeout(() => {
|
|
95
|
+
if (!this.isFeeding()) {
|
|
96
|
+
service.getCharacteristic(this.Characteristic.Active).updateValue(INACTIVE);
|
|
97
|
+
service.getCharacteristic(this.Characteristic.InUse).updateValue(NOT_IN_USE);
|
|
98
|
+
}
|
|
99
|
+
}, 2500);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
// Tuya pet feeders usually expose manual_feed/quick_feed as momentary actions,
|
|
103
|
+
// not cancellable feed sessions. Show HomeKit as inactive but do not send a
|
|
104
|
+
// fake cancel command that the device does not support.
|
|
105
|
+
service.getCharacteristic(this.Characteristic.Active).updateValue(INACTIVE);
|
|
106
|
+
if (!this.isFeeding()) {
|
|
107
|
+
service.getCharacteristic(this.Characteristic.InUse).updateValue(NOT_IN_USE);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
service.getCharacteristic(this.Characteristic.InUse)
|
|
112
|
+
.onGet(() => {
|
|
113
|
+
this.checkOnlineStatus();
|
|
114
|
+
return this.isFeeding() ? IN_USE : NOT_IN_USE;
|
|
115
|
+
});
|
|
34
116
|
}
|
|
35
117
|
configureActionSwitch(schema, name, subtype, onSet) {
|
|
36
118
|
if (!schema) {
|
|
@@ -42,7 +124,7 @@ class PetFeederAccessory extends BaseAccessory_1.default {
|
|
|
42
124
|
service.getCharacteristic(this.Characteristic.On)
|
|
43
125
|
.onGet(() => {
|
|
44
126
|
this.checkOnlineStatus();
|
|
45
|
-
// quick_feed
|
|
127
|
+
// quick_feed is a momentary action. Always display it as off.
|
|
46
128
|
return false;
|
|
47
129
|
})
|
|
48
130
|
.onSet(async (value) => {
|
|
@@ -74,13 +156,6 @@ class PetFeederAccessory extends BaseAccessory_1.default {
|
|
|
74
156
|
await this.sendCommands([{ code: schema.code, value: true }], true);
|
|
75
157
|
});
|
|
76
158
|
}
|
|
77
|
-
configureManualFeed() {
|
|
78
|
-
const schema = this.getSchema(...SCHEMA_CODE.MANUAL_FEED);
|
|
79
|
-
const { manualFeedAmount } = this.getPetFeederConfig();
|
|
80
|
-
this.configureActionSwitch(schema, `${this.device?.name || 'Pet Feeder'} Manual Feed`, 'manual_feed', async () => {
|
|
81
|
-
await this.sendCommands([{ code: schema.code, value: manualFeedAmount }], true);
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
159
|
configureSlowFeed() {
|
|
85
160
|
const schema = this.getSchema(...SCHEMA_CODE.SLOW_FEED);
|
|
86
161
|
const { exposeSlowFeed } = this.getPetFeederConfig();
|
|
@@ -101,10 +176,16 @@ class PetFeederAccessory extends BaseAccessory_1.default {
|
|
|
101
176
|
service.getCharacteristic(this.Characteristic.OccupancyDetected)
|
|
102
177
|
.onGet(() => {
|
|
103
178
|
this.checkOnlineStatus();
|
|
104
|
-
|
|
105
|
-
return value === 'feeding' ? OCCUPANCY_DETECTED : OCCUPANCY_NOT_DETECTED;
|
|
179
|
+
return this.isFeeding() ? OCCUPANCY_DETECTED : OCCUPANCY_NOT_DETECTED;
|
|
106
180
|
});
|
|
107
181
|
}
|
|
182
|
+
removeLegacyManualFeedSwitch() {
|
|
183
|
+
const service = this.accessory.getServiceById(this.Service.Switch, 'manual_feed');
|
|
184
|
+
if (service) {
|
|
185
|
+
this.log.warn(`Removing old pet feeder Manual Feed switch from cache: ${service.displayName}`);
|
|
186
|
+
this.accessory.removeService(service);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
108
189
|
}
|
|
109
190
|
exports.default = PetFeederAccessory;
|
|
110
191
|
//# sourceMappingURL=PetFeederAccessory.js.map
|
|
@@ -102,7 +102,7 @@ class SwitchAccessory extends BaseAccessory_1.default {
|
|
|
102
102
|
const name = schemata.length === 1 ? this.device.name : `${this.device.name} ${switchNum}`;
|
|
103
103
|
// Extract schema info from the cached service's current state
|
|
104
104
|
// In this case we reuse the service without re-adding it
|
|
105
|
-
(0, Name_1.configureName)(this, service, name);
|
|
105
|
+
(0, Name_1.configureName)(this, service, name, { overrideName: this.getSwitchNameOverride(subtype) });
|
|
106
106
|
}
|
|
107
107
|
// Other
|
|
108
108
|
(0, CurrentTemperature_1.configureCurrentTemperature)(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_TEMP));
|
|
@@ -118,10 +118,17 @@ class SwitchAccessory extends BaseAccessory_1.default {
|
|
|
118
118
|
mainService() {
|
|
119
119
|
return this.Service.Switch;
|
|
120
120
|
}
|
|
121
|
+
getSwitchNameOverride(schemaCode) {
|
|
122
|
+
const config = typeof this.platform.getDeviceConfig === 'function'
|
|
123
|
+
? this.platform.getDeviceConfig(this.device)
|
|
124
|
+
: undefined;
|
|
125
|
+
const value = config?.switchNames?.[schemaCode];
|
|
126
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
127
|
+
}
|
|
121
128
|
configureSwitch(schema, name) {
|
|
122
129
|
const service = this.accessory.getService(schema.code)
|
|
123
130
|
|| this.accessory.addService(this.mainService(), name, schema.code);
|
|
124
|
-
(0, Name_1.configureName)(this, service, name);
|
|
131
|
+
(0, Name_1.configureName)(this, service, name, { overrideName: this.getSwitchNameOverride(schema.code) });
|
|
125
132
|
(0, On_1.configureOn)(this, service, schema);
|
|
126
133
|
if (schema.code === this.getSchema(...SCHEMA_CODE.ON)?.code) {
|
|
127
134
|
(0, EnergyUsage_1.configureEnergyUsage)(this.platform.api, this, service, this.getSchema(...SCHEMA_CODE.CURRENT), this.getSchema(...SCHEMA_CODE.POWER), this.getSchema(...SCHEMA_CODE.VOLTAGE), this.getSchema(...SCHEMA_CODE.TOTAL_POWER));
|
|
@@ -2,14 +2,70 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.configureName = configureName;
|
|
4
4
|
const util_1 = require("../../util/util");
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
service.setCharacteristic(accessory.Characteristic.ConfiguredName, safeName);
|
|
5
|
+
|
|
6
|
+
function toSafeName(name, fallback = 'Tuya Service') {
|
|
7
|
+
const raw = String(name ?? '');
|
|
8
|
+
const asciiFallback = raw
|
|
9
|
+
.replace(/[^A-Za-z0-9 '\s]/g, ' ')
|
|
10
|
+
.replace(/\s+/g, ' ')
|
|
11
|
+
.trim();
|
|
12
|
+
return (0, util_1.sanitizeName)(raw) ?? (asciiFallback || fallback);
|
|
14
13
|
}
|
|
15
|
-
|
|
14
|
+
|
|
15
|
+
function getPreserveHomeKitNames(accessory) {
|
|
16
|
+
const globalSetting = accessory?.platform?.options?.preserveHomeKitNames !== false;
|
|
17
|
+
const device = accessory?.device;
|
|
18
|
+
const deviceConfig = device && typeof accessory?.platform?.getDeviceConfig === 'function'
|
|
19
|
+
? accessory.platform.getDeviceConfig(device)
|
|
20
|
+
: undefined;
|
|
21
|
+
if (typeof deviceConfig?.preserveHomeKitNames === 'boolean') {
|
|
22
|
+
return deviceConfig.preserveHomeKitNames;
|
|
23
|
+
}
|
|
24
|
+
return globalSetting;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Configure a HomeKit service name without overwriting a user-renamed
|
|
29
|
+
* ConfiguredName on every Homebridge restart.
|
|
30
|
+
*
|
|
31
|
+
* Priority:
|
|
32
|
+
* 1. Explicit override supplied by the plugin configuration.
|
|
33
|
+
* 2. Existing HomeKit ConfiguredName when preservation is enabled.
|
|
34
|
+
* 3. Plugin-generated default name.
|
|
35
|
+
*/
|
|
36
|
+
function configureName(accessory, service, name, options = {}) {
|
|
37
|
+
const explicitOverride = typeof options.overrideName === 'string' && options.overrideName.trim()
|
|
38
|
+
? options.overrideName.trim()
|
|
39
|
+
: undefined;
|
|
40
|
+
const generatedName = toSafeName(name);
|
|
41
|
+
const forcedName = explicitOverride ? toSafeName(explicitOverride, generatedName) : undefined;
|
|
42
|
+
const preserveExisting = options.preserveExisting ?? getPreserveHomeKitNames(accessory);
|
|
43
|
+
|
|
44
|
+
const hadConfiguredName = service.testCharacteristic(accessory.Characteristic.ConfiguredName);
|
|
45
|
+
if (!hadConfiguredName) {
|
|
46
|
+
service.addOptionalCharacteristic(accessory.Characteristic.ConfiguredName);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const configuredCharacteristic = service.getCharacteristic(accessory.Characteristic.ConfiguredName);
|
|
50
|
+
const currentConfiguredName = hadConfiguredName && typeof configuredCharacteristic.value === 'string'
|
|
51
|
+
? configuredCharacteristic.value.trim()
|
|
52
|
+
: '';
|
|
53
|
+
|
|
54
|
+
let targetName;
|
|
55
|
+
if (forcedName) {
|
|
56
|
+
targetName = forcedName;
|
|
57
|
+
}
|
|
58
|
+
else if (preserveExisting && currentConfiguredName) {
|
|
59
|
+
// Preserve names changed by the user in Apple Home/Homebridge. If an
|
|
60
|
+
// older cached name is invalid, correct it once rather than reverting
|
|
61
|
+
// to the generated default.
|
|
62
|
+
targetName = toSafeName(currentConfiguredName, generatedName);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
targetName = generatedName;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
service.updateCharacteristic(accessory.Characteristic.Name, targetName);
|
|
69
|
+
service.updateCharacteristic(accessory.Characteristic.ConfiguredName, targetName);
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=Name.js.map
|
|
@@ -127,6 +127,55 @@
|
|
|
127
127
|
<small class="form-text text-muted">HomeKit automatic mode may send color-temperature updates roughly once per minute while Adaptive Lighting is active.</small>
|
|
128
128
|
</div>
|
|
129
129
|
|
|
130
|
+
<div class="tuya-nodev-card">
|
|
131
|
+
<div class="tuya-nodev-title">HomeKit Names</div>
|
|
132
|
+
<p class="tuya-nodev-small mb-3">
|
|
133
|
+
Preserve names assigned in Apple Home so multi-gang switches and outlets do not return to generated names such as <em>Brilliant 1</em> and <em>Brilliant 2</em> after every Homebridge restart. You can also set explicit channel names below.
|
|
134
|
+
</p>
|
|
135
|
+
<div class="form-check mb-3">
|
|
136
|
+
<input id="tuyaNodevPreserveNames" class="form-check-input" type="checkbox" checked>
|
|
137
|
+
<label class="form-check-label" for="tuyaNodevPreserveNames">Preserve names changed in HomeKit</label>
|
|
138
|
+
</div>
|
|
139
|
+
<small class="form-text text-muted">Explicit channel names take priority over preserved HomeKit names. Disable preservation only when you want the plugin to reapply generated names at every restart.</small>
|
|
140
|
+
|
|
141
|
+
<div class="form-group mt-3">
|
|
142
|
+
<label for="tuyaNodevSwitchDevice">Select multi-switch / outlet device</label>
|
|
143
|
+
<select id="tuyaNodevSwitchDevice" class="form-control">
|
|
144
|
+
<option value="">Load devices first...</option>
|
|
145
|
+
</select>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<div id="tuyaNodevSwitchChannels" class="tuya-nodev-table-wrap">
|
|
149
|
+
<div class="tuya-nodev-small">Select a detected multi-switch device to configure channel names.</div>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div class="tuya-nodev-actions">
|
|
153
|
+
<button id="tuyaNodevLoadSwitchDevices" class="btn btn-outline-primary" type="button">Load Detected Devices</button>
|
|
154
|
+
<button id="tuyaNodevApplySwitchNames" class="btn btn-primary" type="button">Add / Update Channel Names</button>
|
|
155
|
+
<button id="tuyaNodevRemoveSwitchNames" class="btn btn-outline-danger" type="button">Remove Channel Name Overrides</button>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<div id="tuyaNodevSwitchStatus" class="tuya-nodev-ac-status alert alert-secondary">
|
|
159
|
+
Names changed directly in Apple Home are preserved automatically when the option above is enabled.
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<div class="tuya-nodev-table-wrap">
|
|
163
|
+
<table class="tuya-nodev-table">
|
|
164
|
+
<thead>
|
|
165
|
+
<tr>
|
|
166
|
+
<th>Device</th>
|
|
167
|
+
<th>Device ID</th>
|
|
168
|
+
<th>Configured channel names</th>
|
|
169
|
+
<th></th>
|
|
170
|
+
</tr>
|
|
171
|
+
</thead>
|
|
172
|
+
<tbody id="tuyaNodevSwitchOverrideRows">
|
|
173
|
+
<tr><td colspan="4" class="tuya-nodev-small">No explicit channel-name overrides configured.</td></tr>
|
|
174
|
+
</tbody>
|
|
175
|
+
</table>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
130
179
|
<div class="tuya-nodev-card">
|
|
131
180
|
<div class="tuya-nodev-title">Air Conditioner Temperature Overrides</div>
|
|
132
181
|
<p class="tuya-nodev-small mb-3">
|
|
@@ -209,6 +258,12 @@
|
|
|
209
258
|
el.textContent = message;
|
|
210
259
|
}
|
|
211
260
|
|
|
261
|
+
function setSwitchStatus(message, type = 'secondary') {
|
|
262
|
+
const el = $('tuyaNodevSwitchStatus');
|
|
263
|
+
el.className = `tuya-nodev-ac-status alert alert-${type}`;
|
|
264
|
+
el.textContent = message;
|
|
265
|
+
}
|
|
266
|
+
|
|
212
267
|
function firstNonEmpty(...values) {
|
|
213
268
|
for (const value of values) {
|
|
214
269
|
if (value === undefined || value === null) {
|
|
@@ -263,6 +318,7 @@
|
|
|
263
318
|
}
|
|
264
319
|
cfg.options.projectType = '3';
|
|
265
320
|
cfg.options.enableAdaptiveLighting = $('tuyaNodevAdaptiveLighting')?.checked === true;
|
|
321
|
+
cfg.options.preserveHomeKitNames = $('tuyaNodevPreserveNames')?.checked !== false;
|
|
266
322
|
if (!Array.isArray(cfg.options.deviceOverrides)) {
|
|
267
323
|
cfg.options.deviceOverrides = [];
|
|
268
324
|
}
|
|
@@ -286,6 +342,7 @@
|
|
|
286
342
|
ensureConfig();
|
|
287
343
|
await homebridge.updatePluginConfig([currentConfig]);
|
|
288
344
|
renderAcOverrides();
|
|
345
|
+
renderSwitchNameOverrides();
|
|
289
346
|
}
|
|
290
347
|
|
|
291
348
|
function stopPolling() {
|
|
@@ -310,6 +367,191 @@
|
|
|
310
367
|
enableSaving();
|
|
311
368
|
}
|
|
312
369
|
|
|
370
|
+
function getSwitchCodes(device) {
|
|
371
|
+
const codes = Array.from(new Set([
|
|
372
|
+
...(Array.isArray(device?.schemaCodes) ? device.schemaCodes : []),
|
|
373
|
+
...(Array.isArray(device?.statusCodes) ? device.statusCodes : []),
|
|
374
|
+
].map((code) => String(code || '').trim()).filter(Boolean)));
|
|
375
|
+
|
|
376
|
+
return codes.filter((code) => {
|
|
377
|
+
if (code === 'switch_inching') return false;
|
|
378
|
+
return code === 'switch'
|
|
379
|
+
|| /^switch_\d+$/.test(code)
|
|
380
|
+
|| /^switch_usb_?\d+$/.test(code);
|
|
381
|
+
}).sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function getSwitchNameOverrides() {
|
|
385
|
+
const cfg = ensureConfig();
|
|
386
|
+
return (cfg.options.deviceOverrides || []).filter((item) => item && item.id && item.switchNames && typeof item.switchNames === 'object' && !Array.isArray(item.switchNames));
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function findDeviceOverride(id) {
|
|
390
|
+
const cfg = ensureConfig();
|
|
391
|
+
return (cfg.options.deviceOverrides || []).find((item) => item && item.id === id);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function ensureDeviceOverride(id) {
|
|
395
|
+
const cfg = ensureConfig();
|
|
396
|
+
let override = (cfg.options.deviceOverrides || []).find((item) => item && item.id === id);
|
|
397
|
+
if (!override) {
|
|
398
|
+
override = { id };
|
|
399
|
+
cfg.options.deviceOverrides.push(override);
|
|
400
|
+
}
|
|
401
|
+
return override;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function removeEmptyDeviceOverride(id) {
|
|
405
|
+
const cfg = ensureConfig();
|
|
406
|
+
const index = cfg.options.deviceOverrides.findIndex((item) => item && item.id === id);
|
|
407
|
+
if (index < 0) return;
|
|
408
|
+
const item = cfg.options.deviceOverrides[index];
|
|
409
|
+
const meaningfulKeys = Object.keys(item).filter((key) => key !== 'id' && item[key] !== undefined && item[key] !== null && item[key] !== '');
|
|
410
|
+
if (meaningfulKeys.length === 0) {
|
|
411
|
+
cfg.options.deviceOverrides.splice(index, 1);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function populateSwitchDeviceSelect(devices) {
|
|
416
|
+
const select = $('tuyaNodevSwitchDevice');
|
|
417
|
+
const current = select.value;
|
|
418
|
+
const candidates = devices
|
|
419
|
+
.map((device) => ({ ...device, switchCodes: getSwitchCodes(device) }))
|
|
420
|
+
.filter((device) => device.switchCodes.length > 0)
|
|
421
|
+
.sort((a, b) => {
|
|
422
|
+
if ((a.switchCodes.length > 1) !== (b.switchCodes.length > 1)) {
|
|
423
|
+
return a.switchCodes.length > 1 ? -1 : 1;
|
|
424
|
+
}
|
|
425
|
+
return String(a.name).localeCompare(String(b.name));
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
if (!candidates.length) {
|
|
429
|
+
select.innerHTML = '<option value="">No switch/outlet devices found</option>';
|
|
430
|
+
renderSwitchChannelEditor();
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
select.innerHTML = '<option value="">Select a switch/outlet device...</option>' + candidates.map((device) => {
|
|
435
|
+
const label = `${device.name} — ${device.switchCodes.length} channel${device.switchCodes.length === 1 ? '' : 's'} — ${device.id}`;
|
|
436
|
+
return `<option value="${escapeHtml(device.id)}">${escapeHtml(label)}</option>`;
|
|
437
|
+
}).join('');
|
|
438
|
+
|
|
439
|
+
if (current && candidates.some((device) => device.id === current)) {
|
|
440
|
+
select.value = current;
|
|
441
|
+
}
|
|
442
|
+
renderSwitchChannelEditor();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function renderSwitchChannelEditor() {
|
|
446
|
+
const container = $('tuyaNodevSwitchChannels');
|
|
447
|
+
const id = $('tuyaNodevSwitchDevice')?.value || '';
|
|
448
|
+
if (!id) {
|
|
449
|
+
container.innerHTML = '<div class="tuya-nodev-small">Select a detected multi-switch device to configure channel names.</div>';
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const device = detectedDevices.find((item) => item.id === id);
|
|
453
|
+
const override = findDeviceOverride(id);
|
|
454
|
+
const configured = override?.switchNames || {};
|
|
455
|
+
const codes = getSwitchCodes(device || { schemaCodes: Object.keys(configured) });
|
|
456
|
+
const allCodes = Array.from(new Set([...codes, ...Object.keys(configured)])).sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
|
|
457
|
+
if (!allCodes.length) {
|
|
458
|
+
container.innerHTML = '<div class="tuya-nodev-small">No controllable switch channels were found for this device.</div>';
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
container.innerHTML = `
|
|
462
|
+
<table class="tuya-nodev-table">
|
|
463
|
+
<thead><tr><th>Tuya channel</th><th>HomeKit name</th></tr></thead>
|
|
464
|
+
<tbody>${allCodes.map((code) => `
|
|
465
|
+
<tr>
|
|
466
|
+
<td><code>${escapeHtml(code)}</code></td>
|
|
467
|
+
<td><input class="form-control tuya-nodev-switch-name-input" type="text" data-code="${escapeHtml(code)}" value="${escapeHtml(configured[code] || '')}" placeholder="Leave blank to preserve the HomeKit name"></td>
|
|
468
|
+
</tr>`).join('')}</tbody>
|
|
469
|
+
</table>`;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function renderSwitchNameOverrides() {
|
|
473
|
+
const tbody = $('tuyaNodevSwitchOverrideRows');
|
|
474
|
+
if (!tbody) return;
|
|
475
|
+
const overrides = getSwitchNameOverrides();
|
|
476
|
+
if (!overrides.length) {
|
|
477
|
+
tbody.innerHTML = '<tr><td colspan="4" class="tuya-nodev-small">No explicit channel-name overrides configured.</td></tr>';
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
tbody.innerHTML = overrides.map((override) => {
|
|
481
|
+
const name = getDeviceName(override.id) || 'Unknown / not in detected cache';
|
|
482
|
+
const names = Object.entries(override.switchNames || {})
|
|
483
|
+
.map(([code, value]) => `<code>${escapeHtml(code)}</code>: ${escapeHtml(value)}`)
|
|
484
|
+
.join('<br>');
|
|
485
|
+
return `
|
|
486
|
+
<tr>
|
|
487
|
+
<td>${escapeHtml(name)}</td>
|
|
488
|
+
<td><code>${escapeHtml(override.id)}</code></td>
|
|
489
|
+
<td>${names}</td>
|
|
490
|
+
<td><button class="btn btn-sm btn-outline-secondary tuya-nodev-edit-switch-names" type="button" data-id="${escapeHtml(override.id)}">Edit</button></td>
|
|
491
|
+
</tr>`;
|
|
492
|
+
}).join('');
|
|
493
|
+
tbody.querySelectorAll('.tuya-nodev-edit-switch-names').forEach((button) => {
|
|
494
|
+
button.addEventListener('click', () => editSwitchNameOverride(button.getAttribute('data-id')));
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function editSwitchNameOverride(id) {
|
|
499
|
+
const select = $('tuyaNodevSwitchDevice');
|
|
500
|
+
if (!detectedDevices.some((device) => device.id === id)) {
|
|
501
|
+
const option = document.createElement('option');
|
|
502
|
+
option.value = id;
|
|
503
|
+
option.textContent = `${id} — not currently in detected device cache`;
|
|
504
|
+
select.appendChild(option);
|
|
505
|
+
}
|
|
506
|
+
select.value = id;
|
|
507
|
+
renderSwitchChannelEditor();
|
|
508
|
+
select.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async function addOrUpdateSwitchNames() {
|
|
512
|
+
const id = $('tuyaNodevSwitchDevice')?.value || '';
|
|
513
|
+
if (!id) {
|
|
514
|
+
setSwitchStatus('Select a switch/outlet device first.', 'warning');
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
const inputs = Array.from(document.querySelectorAll('.tuya-nodev-switch-name-input'));
|
|
518
|
+
const switchNames = {};
|
|
519
|
+
for (const input of inputs) {
|
|
520
|
+
const code = String(input.getAttribute('data-code') || '').trim();
|
|
521
|
+
const value = String(input.value || '').trim();
|
|
522
|
+
if (code && value) switchNames[code] = value;
|
|
523
|
+
}
|
|
524
|
+
const override = ensureDeviceOverride(id);
|
|
525
|
+
if (Object.keys(switchNames).length > 0) {
|
|
526
|
+
override.switchNames = switchNames;
|
|
527
|
+
setSwitchStatus('Explicit channel names staged. Click Save Configuration, then restart Homebridge.', 'success');
|
|
528
|
+
} else {
|
|
529
|
+
delete override.switchNames;
|
|
530
|
+
removeEmptyDeviceOverride(id);
|
|
531
|
+
setSwitchStatus('No explicit names entered. HomeKit-renamed services will still be preserved by the global option.', 'info');
|
|
532
|
+
}
|
|
533
|
+
await syncConfigToUi();
|
|
534
|
+
markDirty();
|
|
535
|
+
renderSwitchChannelEditor();
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async function removeSelectedSwitchNames() {
|
|
539
|
+
const id = $('tuyaNodevSwitchDevice')?.value || '';
|
|
540
|
+
if (!id) {
|
|
541
|
+
setSwitchStatus('Select a switch/outlet device first.', 'warning');
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
const override = findDeviceOverride(id);
|
|
545
|
+
if (override) {
|
|
546
|
+
delete override.switchNames;
|
|
547
|
+
removeEmptyDeviceOverride(id);
|
|
548
|
+
}
|
|
549
|
+
await syncConfigToUi();
|
|
550
|
+
markDirty();
|
|
551
|
+
renderSwitchChannelEditor();
|
|
552
|
+
setSwitchStatus('Explicit channel-name overrides removed. Preserved HomeKit names will be used.', 'success');
|
|
553
|
+
}
|
|
554
|
+
|
|
313
555
|
function getDeviceName(id) {
|
|
314
556
|
const device = detectedDevices.find((item) => item.id === id);
|
|
315
557
|
return device ? device.name : '';
|
|
@@ -375,6 +617,7 @@
|
|
|
375
617
|
const current = select.value;
|
|
376
618
|
if (!devices.length) {
|
|
377
619
|
select.innerHTML = '<option value="">No detected devices found</option>';
|
|
620
|
+
populateSwitchDeviceSelect(devices);
|
|
378
621
|
return;
|
|
379
622
|
}
|
|
380
623
|
|
|
@@ -387,6 +630,7 @@
|
|
|
387
630
|
if (current && devices.some((device) => device.id === current)) {
|
|
388
631
|
select.value = current;
|
|
389
632
|
}
|
|
633
|
+
populateSwitchDeviceSelect(devices);
|
|
390
634
|
}
|
|
391
635
|
|
|
392
636
|
async function loadDetectedDevices(showToast = true) {
|
|
@@ -760,6 +1004,7 @@
|
|
|
760
1004
|
$('tuyaNodevName').value = currentConfig.name || 'Tuya without developer account';
|
|
761
1005
|
if (currentConfig.options?.userCode) setUserCode(currentConfig.options.userCode);
|
|
762
1006
|
$('tuyaNodevAdaptiveLighting').checked = currentConfig.options?.enableAdaptiveLighting === true;
|
|
1007
|
+
$('tuyaNodevPreserveNames').checked = currentConfig.options?.preserveHomeKitNames !== false;
|
|
763
1008
|
ensureConfig();
|
|
764
1009
|
|
|
765
1010
|
if (homebridge.showSchemaForm) homebridge.showSchemaForm();
|
|
@@ -775,8 +1020,11 @@
|
|
|
775
1020
|
if (block.options?.userCode) setUserCode(block.options.userCode);
|
|
776
1021
|
else if (getUserCode()) setUserCode(getUserCode());
|
|
777
1022
|
$('tuyaNodevAdaptiveLighting').checked = block.options?.enableAdaptiveLighting === true;
|
|
1023
|
+
$('tuyaNodevPreserveNames').checked = block.options?.preserveHomeKitNames !== false;
|
|
778
1024
|
ensureConfig();
|
|
779
1025
|
renderAcOverrides();
|
|
1026
|
+
renderSwitchNameOverrides();
|
|
1027
|
+
renderSwitchChannelEditor();
|
|
780
1028
|
}
|
|
781
1029
|
});
|
|
782
1030
|
|
|
@@ -793,6 +1041,11 @@
|
|
|
793
1041
|
markDirty();
|
|
794
1042
|
setStatus('Adaptive Lighting setting changed. Click Save Configuration when ready.', 'info');
|
|
795
1043
|
});
|
|
1044
|
+
$('tuyaNodevPreserveNames').addEventListener('change', async () => {
|
|
1045
|
+
await syncConfigToUi();
|
|
1046
|
+
markDirty();
|
|
1047
|
+
setSwitchStatus('HomeKit name preservation setting changed. Click Save Configuration, then restart Homebridge.', 'info');
|
|
1048
|
+
});
|
|
796
1049
|
$('tuyaNodevUserCode').addEventListener('input', () => {
|
|
797
1050
|
isAuthenticated = false;
|
|
798
1051
|
disableSaving();
|
|
@@ -801,6 +1054,10 @@
|
|
|
801
1054
|
$('tuyaNodevLoadDevices').addEventListener('click', () => loadDetectedDevices(true));
|
|
802
1055
|
$('tuyaNodevApplyAc').addEventListener('click', addOrUpdateAcOverride);
|
|
803
1056
|
$('tuyaNodevRemoveAc').addEventListener('click', removeSelectedAcOverride);
|
|
1057
|
+
$('tuyaNodevLoadSwitchDevices').addEventListener('click', () => loadDetectedDevices(true));
|
|
1058
|
+
$('tuyaNodevSwitchDevice').addEventListener('change', renderSwitchChannelEditor);
|
|
1059
|
+
$('tuyaNodevApplySwitchNames').addEventListener('click', addOrUpdateSwitchNames);
|
|
1060
|
+
$('tuyaNodevRemoveSwitchNames').addEventListener('click', removeSelectedSwitchNames);
|
|
804
1061
|
|
|
805
1062
|
if (!getUserCode()) {
|
|
806
1063
|
await discoverExistingAuth(false);
|
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.14",
|
|
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",
|