@switchbot/homebridge-switchbot 5.0.0 → 5.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/.github/npm-version-script-esm.js +97 -0
  2. package/CHANGELOG.md +21 -8
  3. package/MIGRATION.md +5 -0
  4. package/README.md +25 -0
  5. package/config.schema.json +11 -0
  6. package/dist/SwitchBotHAPPlatform.d.ts.map +1 -1
  7. package/dist/SwitchBotHAPPlatform.js +33 -4
  8. package/dist/SwitchBotHAPPlatform.js.map +1 -1
  9. package/dist/SwitchBotMatterPlatform.d.ts +2 -0
  10. package/dist/SwitchBotMatterPlatform.d.ts.map +1 -1
  11. package/dist/SwitchBotMatterPlatform.js +61 -5
  12. package/dist/SwitchBotMatterPlatform.js.map +1 -1
  13. package/dist/homebridge-ui/endpoints/config.d.ts.map +1 -1
  14. package/dist/homebridge-ui/endpoints/config.js +9 -2
  15. package/dist/homebridge-ui/endpoints/config.js.map +1 -1
  16. package/dist/homebridge-ui/homebridge-ui/endpoints/config.js +9 -2
  17. package/dist/homebridge-ui/homebridge-ui/endpoints/config.js.map +1 -1
  18. package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js +8 -0
  19. package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js.map +1 -1
  20. package/dist/homebridge-ui/homebridge-ui/utils/v4-detection.js +23 -0
  21. package/dist/homebridge-ui/homebridge-ui/utils/v4-detection.js.map +1 -0
  22. package/dist/homebridge-ui/public/css/styles.css +61 -1
  23. package/dist/homebridge-ui/public/index.html +25 -2
  24. package/dist/homebridge-ui/public/js/api.d.ts +16 -0
  25. package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
  26. package/dist/homebridge-ui/public/js/api.js +48 -0
  27. package/dist/homebridge-ui/public/js/api.js.map +1 -1
  28. package/dist/homebridge-ui/public/js/api.ts +61 -1
  29. package/dist/homebridge-ui/public/js/app.js +237 -111
  30. package/dist/homebridge-ui/public/js/app.js.map +4 -4
  31. package/dist/homebridge-ui/public/js/app.ts +22 -4
  32. package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -1
  33. package/dist/homebridge-ui/public/js/devices.js +6 -0
  34. package/dist/homebridge-ui/public/js/devices.js.map +1 -1
  35. package/dist/homebridge-ui/public/js/devices.ts +6 -0
  36. package/dist/homebridge-ui/public/js/modals.d.ts +1 -0
  37. package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -1
  38. package/dist/homebridge-ui/public/js/modals.js +53 -0
  39. package/dist/homebridge-ui/public/js/modals.js.map +1 -1
  40. package/dist/homebridge-ui/public/js/modals.ts +58 -0
  41. package/dist/homebridge-ui/public/js/render.d.ts.map +1 -1
  42. package/dist/homebridge-ui/public/js/render.js +2 -1
  43. package/dist/homebridge-ui/public/js/render.js.map +1 -1
  44. package/dist/homebridge-ui/public/js/render.ts +2 -1
  45. package/dist/homebridge-ui/settings.js.map +1 -1
  46. package/dist/homebridge-ui/utils/config-parser.d.ts +8 -0
  47. package/dist/homebridge-ui/utils/config-parser.d.ts.map +1 -1
  48. package/dist/homebridge-ui/utils/config-parser.js +8 -0
  49. package/dist/homebridge-ui/utils/config-parser.js.map +1 -1
  50. package/dist/homebridge-ui/utils/v4-detection.d.ts +12 -0
  51. package/dist/homebridge-ui/utils/v4-detection.d.ts.map +1 -0
  52. package/dist/homebridge-ui/utils/v4-detection.js +23 -0
  53. package/dist/homebridge-ui/utils/v4-detection.js.map +1 -0
  54. package/dist/settings.d.ts +11 -0
  55. package/dist/settings.d.ts.map +1 -1
  56. package/dist/settings.js.map +1 -1
  57. package/dist/utils.d.ts +5 -0
  58. package/dist/utils.d.ts.map +1 -1
  59. package/dist/utils.js +27 -0
  60. package/dist/utils.js.map +1 -1
  61. package/docs/assets/highlight.css +15 -15
  62. package/docs/assets/icons.js +1 -1
  63. package/docs/assets/icons.svg +1 -1
  64. package/docs/assets/main.js +3 -3
  65. package/docs/index.html +22 -5
  66. package/docs/variables/default.html +1 -1
  67. package/package.json +6 -6
  68. package/scripts/build-ui.js +30 -0
  69. package/src/SwitchBotHAPPlatform.ts +31 -6
  70. package/src/SwitchBotMatterPlatform.ts +61 -7
  71. package/src/homebridge-ui/endpoints/config.ts +10 -2
  72. package/src/homebridge-ui/public/css/styles.css +61 -1
  73. package/src/homebridge-ui/public/index.html +23 -0
  74. package/src/homebridge-ui/public/js/api.ts +61 -1
  75. package/src/homebridge-ui/public/js/app.ts +22 -4
  76. package/src/homebridge-ui/public/js/devices.ts +6 -0
  77. package/src/homebridge-ui/public/js/modals.ts +58 -0
  78. package/src/homebridge-ui/public/js/render.ts +2 -1
  79. package/src/homebridge-ui/utils/config-parser.ts +9 -0
  80. package/src/homebridge-ui/utils/v4-detection.ts +23 -0
  81. package/src/settings.ts +12 -0
  82. package/src/utils.ts +31 -0
  83. package/test/homebridge-ui/config-parser.spec.ts +61 -0
  84. package/test/platform/matter-integration.spec.ts +50 -0
  85. package/test/utils/utils.spec.ts +15 -0
@@ -1,5 +1,6 @@
1
1
  // Fetch the list of configured devices from the Homebridge UI API
2
2
  import { isValidDeviceType, normalizeDeviceType } from '../../../device-types.js'
3
+ import { isV4Config } from '../../utils/v4-detection.js'
3
4
  import './types.js'
4
5
  import { uiLog } from './logger.js'
5
6
  import { toastError } from './toast.js'
@@ -22,6 +23,35 @@ export async function fetchDevices(): Promise<any[]> {
22
23
  }
23
24
  }
24
25
 
26
+ /**
27
+ * Detect whether the current plugin config is in the legacy v4 format.
28
+ *
29
+ * v4 configs stored credentials under a `credentials` sub-object and devices
30
+ * under `options.devices`. v5+ flattened these to `openApiToken`,
31
+ * `openApiSecret`, and a root-level `devices` array.
32
+ *
33
+ * Returns true when a v4 config block is found, false otherwise.
34
+ */
35
+ export async function detectV4Config(): Promise<boolean> {
36
+ try {
37
+ if (typeof homebridge.getPluginConfig !== 'function') {
38
+ return false
39
+ }
40
+ const configArr = await homebridge.getPluginConfig()
41
+ if (!Array.isArray(configArr)) {
42
+ return false
43
+ }
44
+ const config = configArr.find(isSwitchBotPlatformConfig)
45
+ if (!config || typeof config !== 'object') {
46
+ return false
47
+ }
48
+ return isV4Config(config)
49
+ } catch (e) {
50
+ uiLog.warn('Error detecting v4 config:', e)
51
+ return false
52
+ }
53
+ }
54
+
25
55
  /**
26
56
  * Validate and auto-correct device types in the config array before saving.
27
57
  * Returns an array of errors for devices that cannot be fixed.
@@ -189,7 +219,19 @@ export async function addDevice(
189
219
  deviceId: string,
190
220
  name: string,
191
221
  type: string,
192
- options?: { address?: string, model?: string, rssi?: number, encryptionKey?: string, keyId?: string },
222
+ options?: {
223
+ address?: string
224
+ model?: string
225
+ rssi?: number
226
+ connectionPreference?: string
227
+ externalPublishProtocol?: string
228
+ room?: string
229
+ encryptionKey?: string
230
+ keyId?: string
231
+ refreshRate?: number
232
+ blePollingEnabled?: boolean
233
+ blePollIntervalMs?: number
234
+ },
193
235
  ): Promise<any> {
194
236
  if (typeof homebridge.getPluginConfig !== 'function' || typeof homebridge.updatePluginConfig !== 'function') {
195
237
  throw new TypeError('Homebridge UI API not available')
@@ -224,6 +266,24 @@ export async function addDevice(
224
266
  if (options?.keyId) {
225
267
  newDevice.keyId = options.keyId
226
268
  }
269
+ if (options?.connectionPreference) {
270
+ newDevice.connectionPreference = options.connectionPreference
271
+ }
272
+ if (options?.externalPublishProtocol) {
273
+ newDevice.externalPublishProtocol = options.externalPublishProtocol
274
+ }
275
+ if (options?.room) {
276
+ newDevice.room = options.room
277
+ }
278
+ if (typeof options?.refreshRate === 'number' && Number.isFinite(options.refreshRate)) {
279
+ newDevice.refreshRate = options.refreshRate
280
+ }
281
+ if (typeof options?.blePollingEnabled === 'boolean') {
282
+ newDevice.blePollingEnabled = options.blePollingEnabled
283
+ }
284
+ if (typeof options?.blePollIntervalMs === 'number' && Number.isFinite(options.blePollIntervalMs)) {
285
+ newDevice.blePollIntervalMs = options.blePollIntervalMs
286
+ }
227
287
  config.devices.push(newDevice)
228
288
  await homebridge.updatePluginConfig(configArr)
229
289
  if (typeof homebridge.savePluginConfig === 'function') {
@@ -8,111 +8,6 @@ var __export = (target, all) => {
8
8
  __defProp(target, name, { get: all[name], enumerable: true });
9
9
  };
10
10
 
11
- // src/homebridge-ui/public/js/logger.ts
12
- var PREFIX, uiLog;
13
- var init_logger = __esm({
14
- "src/homebridge-ui/public/js/logger.ts"() {
15
- "use strict";
16
- PREFIX = "[SwitchBot UI/html]";
17
- uiLog = {
18
- info: (message, ...parameters) => {
19
- console.log(PREFIX, message, ...parameters);
20
- },
21
- warn: (message, ...parameters) => {
22
- console.warn(PREFIX, message, ...parameters);
23
- },
24
- error: (message, ...parameters) => {
25
- console.error(PREFIX, message, ...parameters);
26
- },
27
- debug: (message, ...parameters) => {
28
- console.debug(PREFIX, message, ...parameters);
29
- }
30
- };
31
- }
32
- });
33
-
34
- // src/homebridge-ui/public/js/types.ts
35
- var init_types = __esm({
36
- "src/homebridge-ui/public/js/types.ts"() {
37
- "use strict";
38
- }
39
- });
40
-
41
- // src/homebridge-ui/public/js/modal.ts
42
- function callUiMethod(name, ...args) {
43
- try {
44
- if (typeof homebridge?.[name] === "function") {
45
- uiLog.info(`[callUiMethod] Invoking homebridge.${String(name)}()`);
46
- const fn = homebridge?.[name];
47
- if (typeof fn === "function") {
48
- uiLog.info(`[callUiMethod] Invoking homebridge.${String(name)}()`);
49
- fn.apply(homebridge, args);
50
- } else {
51
- uiLog.warn(`[callUiMethod] homebridge[${String(name)}] is not a function.`);
52
- }
53
- } else {
54
- uiLog.warn(`[callUiMethod] homebridge[${String(name)}] is not a function.`);
55
- }
56
- } catch (e) {
57
- uiLog.warn(`Homebridge UI method ${String(name)} failed:`, e);
58
- }
59
- }
60
- function showBusyUi() {
61
- callUiMethod("disableSaveButton");
62
- callUiMethod("showSpinner");
63
- }
64
- function hideBusyUi() {
65
- callUiMethod("hideSpinner");
66
- callUiMethod("enableSaveButton");
67
- }
68
- var init_modal = __esm({
69
- "src/homebridge-ui/public/js/modal.ts"() {
70
- "use strict";
71
- init_types();
72
- init_logger();
73
- }
74
- });
75
-
76
- // src/homebridge-ui/public/js/toast.ts
77
- function showToast(method, message, title = "SwitchBot") {
78
- try {
79
- const hb = typeof window !== "undefined" ? window.homebridge : void 0;
80
- const toast = hb && typeof hb.toast === "object" ? hb.toast : void 0;
81
- const fn = toast && typeof toast[method] === "function" ? toast[method] : void 0;
82
- if (fn) {
83
- try {
84
- fn(message, title);
85
- return;
86
- } catch (err) {
87
- uiLog.warn(`Toast ${method} threw:`, err);
88
- }
89
- }
90
- uiLog.info(`[Toast:${method}] ${title} - ${message}`);
91
- } catch (e) {
92
- uiLog.warn(`Toast ${method} outer error:`, e);
93
- uiLog.info(`[Toast:${method}] ${title} - ${message}`);
94
- }
95
- }
96
- function toastSuccess(message, title) {
97
- showToast("success", message, title);
98
- }
99
- function toastError(message, title) {
100
- showToast("error", message, title);
101
- }
102
- function toastWarning(message, title) {
103
- showToast("warning", message, title);
104
- }
105
- function toastInfo(message, title) {
106
- showToast("info", message, title);
107
- }
108
- var init_toast = __esm({
109
- "src/homebridge-ui/public/js/toast.ts"() {
110
- "use strict";
111
- init_types();
112
- init_logger();
113
- }
114
- });
115
-
116
11
  // src/device-types.js
117
12
  function getValidDeviceTypes() {
118
13
  const validTypes = /* @__PURE__ */ new Set();
@@ -354,6 +249,91 @@ var init_device_types = __esm({
354
249
  }
355
250
  });
356
251
 
252
+ // src/homebridge-ui/utils/v4-detection.ts
253
+ function isV4Config(platform) {
254
+ if (!platform || typeof platform !== "object") {
255
+ return false;
256
+ }
257
+ const hasCredentials = platform.credentials !== void 0 && typeof platform.credentials === "object";
258
+ const hasOptionsDevices = platform.options !== void 0 && typeof platform.options === "object" && Array.isArray(platform.options.devices);
259
+ return hasCredentials || hasOptionsDevices;
260
+ }
261
+ var init_v4_detection = __esm({
262
+ "src/homebridge-ui/utils/v4-detection.ts"() {
263
+ "use strict";
264
+ }
265
+ });
266
+
267
+ // src/homebridge-ui/public/js/types.ts
268
+ var init_types = __esm({
269
+ "src/homebridge-ui/public/js/types.ts"() {
270
+ "use strict";
271
+ }
272
+ });
273
+
274
+ // src/homebridge-ui/public/js/logger.ts
275
+ var PREFIX, uiLog;
276
+ var init_logger = __esm({
277
+ "src/homebridge-ui/public/js/logger.ts"() {
278
+ "use strict";
279
+ PREFIX = "[SwitchBot UI/html]";
280
+ uiLog = {
281
+ info: (message, ...parameters) => {
282
+ console.log(PREFIX, message, ...parameters);
283
+ },
284
+ warn: (message, ...parameters) => {
285
+ console.warn(PREFIX, message, ...parameters);
286
+ },
287
+ error: (message, ...parameters) => {
288
+ console.error(PREFIX, message, ...parameters);
289
+ },
290
+ debug: (message, ...parameters) => {
291
+ console.debug(PREFIX, message, ...parameters);
292
+ }
293
+ };
294
+ }
295
+ });
296
+
297
+ // src/homebridge-ui/public/js/toast.ts
298
+ function showToast(method, message, title = "SwitchBot") {
299
+ try {
300
+ const hb = typeof window !== "undefined" ? window.homebridge : void 0;
301
+ const toast = hb && typeof hb.toast === "object" ? hb.toast : void 0;
302
+ const fn = toast && typeof toast[method] === "function" ? toast[method] : void 0;
303
+ if (fn) {
304
+ try {
305
+ fn(message, title);
306
+ return;
307
+ } catch (err) {
308
+ uiLog.warn(`Toast ${method} threw:`, err);
309
+ }
310
+ }
311
+ uiLog.info(`[Toast:${method}] ${title} - ${message}`);
312
+ } catch (e) {
313
+ uiLog.warn(`Toast ${method} outer error:`, e);
314
+ uiLog.info(`[Toast:${method}] ${title} - ${message}`);
315
+ }
316
+ }
317
+ function toastSuccess(message, title) {
318
+ showToast("success", message, title);
319
+ }
320
+ function toastError(message, title) {
321
+ showToast("error", message, title);
322
+ }
323
+ function toastWarning(message, title) {
324
+ showToast("warning", message, title);
325
+ }
326
+ function toastInfo(message, title) {
327
+ showToast("info", message, title);
328
+ }
329
+ var init_toast = __esm({
330
+ "src/homebridge-ui/public/js/toast.ts"() {
331
+ "use strict";
332
+ init_types();
333
+ init_logger();
334
+ }
335
+ });
336
+
357
337
  // src/homebridge-ui/public/js/api.ts
358
338
  var api_exports = {};
359
339
  __export(api_exports, {
@@ -361,11 +341,12 @@ __export(api_exports, {
361
341
  addDevicesInBulk: () => addDevicesInBulk,
362
342
  deleteAllDevices: () => deleteAllDevices,
363
343
  deleteDevice: () => deleteDevice,
344
+ detectV4Config: () => detectV4Config,
364
345
  discoverDevices: () => discoverDevices,
365
346
  fetchBluetoothStatus: () => fetchBluetoothStatus,
366
347
  fetchCredentialStatus: () => fetchCredentialStatus,
367
348
  fetchDevices: () => fetchDevices,
368
- saveCredentials: () => saveCredentials2,
349
+ saveCredentials: () => saveCredentials,
369
350
  syncParentPluginConfigFromDisk: () => syncParentPluginConfigFromDisk,
370
351
  testDeviceConnection: () => testDeviceConnection,
371
352
  updateDevice: () => updateDevice,
@@ -388,6 +369,25 @@ async function fetchDevices() {
388
369
  return [];
389
370
  }
390
371
  }
372
+ async function detectV4Config() {
373
+ try {
374
+ if (typeof homebridge.getPluginConfig !== "function") {
375
+ return false;
376
+ }
377
+ const configArr = await homebridge.getPluginConfig();
378
+ if (!Array.isArray(configArr)) {
379
+ return false;
380
+ }
381
+ const config = configArr.find(isSwitchBotPlatformConfig);
382
+ if (!config || typeof config !== "object") {
383
+ return false;
384
+ }
385
+ return isV4Config(config);
386
+ } catch (e) {
387
+ uiLog.warn("Error detecting v4 config:", e);
388
+ return false;
389
+ }
390
+ }
391
391
  function validateAndFixDeviceTypes(devices) {
392
392
  const errors = [];
393
393
  for (const d of devices) {
@@ -457,7 +457,7 @@ async function fetchCredentialStatus() {
457
457
  return null;
458
458
  }
459
459
  }
460
- async function saveCredentials2(token, secret) {
460
+ async function saveCredentials(token, secret) {
461
461
  uiLog.info("Saving credentials...");
462
462
  const resp = await homebridge.request("/credentials", { token, secret });
463
463
  uiLog.info("Save response:", resp);
@@ -532,6 +532,24 @@ async function addDevice(deviceId, name, type, options) {
532
532
  if (options?.keyId) {
533
533
  newDevice.keyId = options.keyId;
534
534
  }
535
+ if (options?.connectionPreference) {
536
+ newDevice.connectionPreference = options.connectionPreference;
537
+ }
538
+ if (options?.externalPublishProtocol) {
539
+ newDevice.externalPublishProtocol = options.externalPublishProtocol;
540
+ }
541
+ if (options?.room) {
542
+ newDevice.room = options.room;
543
+ }
544
+ if (typeof options?.refreshRate === "number" && Number.isFinite(options.refreshRate)) {
545
+ newDevice.refreshRate = options.refreshRate;
546
+ }
547
+ if (typeof options?.blePollingEnabled === "boolean") {
548
+ newDevice.blePollingEnabled = options.blePollingEnabled;
549
+ }
550
+ if (typeof options?.blePollIntervalMs === "number" && Number.isFinite(options.blePollIntervalMs)) {
551
+ newDevice.blePollIntervalMs = options.blePollIntervalMs;
552
+ }
535
553
  config.devices.push(newDevice);
536
554
  await homebridge.updatePluginConfig(configArr);
537
555
  if (typeof homebridge.savePluginConfig === "function") {
@@ -639,12 +657,48 @@ var init_api = __esm({
639
657
  "src/homebridge-ui/public/js/api.ts"() {
640
658
  "use strict";
641
659
  init_device_types();
660
+ init_v4_detection();
642
661
  init_types();
643
662
  init_logger();
644
663
  init_toast();
645
664
  }
646
665
  });
647
666
 
667
+ // src/homebridge-ui/public/js/modal.ts
668
+ function callUiMethod(name, ...args) {
669
+ try {
670
+ if (typeof homebridge?.[name] === "function") {
671
+ uiLog.info(`[callUiMethod] Invoking homebridge.${String(name)}()`);
672
+ const fn = homebridge?.[name];
673
+ if (typeof fn === "function") {
674
+ uiLog.info(`[callUiMethod] Invoking homebridge.${String(name)}()`);
675
+ fn.apply(homebridge, args);
676
+ } else {
677
+ uiLog.warn(`[callUiMethod] homebridge[${String(name)}] is not a function.`);
678
+ }
679
+ } else {
680
+ uiLog.warn(`[callUiMethod] homebridge[${String(name)}] is not a function.`);
681
+ }
682
+ } catch (e) {
683
+ uiLog.warn(`Homebridge UI method ${String(name)} failed:`, e);
684
+ }
685
+ }
686
+ function showBusyUi() {
687
+ callUiMethod("disableSaveButton");
688
+ callUiMethod("showSpinner");
689
+ }
690
+ function hideBusyUi() {
691
+ callUiMethod("hideSpinner");
692
+ callUiMethod("enableSaveButton");
693
+ }
694
+ var init_modal = __esm({
695
+ "src/homebridge-ui/public/js/modal.ts"() {
696
+ "use strict";
697
+ init_types();
698
+ init_logger();
699
+ }
700
+ });
701
+
648
702
  // src/homebridge-ui/public/js/discovery.ts
649
703
  var discovery_exports = {};
650
704
  __export(discovery_exports, {
@@ -1892,6 +1946,27 @@ async function importDiscoveredDevice(device) {
1892
1946
  opt.selected = (device.connectionPreference || "auto") === val;
1893
1947
  connectionPrefSelect.appendChild(opt);
1894
1948
  });
1949
+ const externalPublishLabel = document.createElement("label");
1950
+ externalPublishLabel.textContent = "Publish as External Device";
1951
+ externalPublishLabel.style.display = "block";
1952
+ externalPublishLabel.style.marginBottom = "6px";
1953
+ externalPublishLabel.style.fontWeight = "500";
1954
+ externalPublishLabel.style.fontSize = "12px";
1955
+ externalPublishLabel.style.color = "#6b7280";
1956
+ const externalPublishSelect = document.createElement("select");
1957
+ externalPublishSelect.style.width = "100%";
1958
+ externalPublishSelect.style.marginBottom = "12px";
1959
+ externalPublishSelect.style.padding = "8px 10px";
1960
+ externalPublishSelect.style.borderRadius = "6px";
1961
+ externalPublishSelect.style.fontSize = "14px";
1962
+ externalPublishSelect.style.boxSizing = "border-box";
1963
+ ["none", "hap", "matter"].forEach((val) => {
1964
+ const opt = document.createElement("option");
1965
+ opt.value = val;
1966
+ opt.text = val === "none" ? "None" : val.toUpperCase();
1967
+ opt.selected = (device.externalPublishProtocol || "none") === val;
1968
+ externalPublishSelect.appendChild(opt);
1969
+ });
1895
1970
  const roomLabel = document.createElement("label");
1896
1971
  roomLabel.textContent = "Room";
1897
1972
  roomLabel.style.display = "block";
@@ -2023,6 +2098,7 @@ async function importDiscoveredDevice(device) {
2023
2098
  configDeviceType: typeSelect.value || device.type,
2024
2099
  address: macInput.value || void 0,
2025
2100
  connectionPreference: connectionPrefSelect.value || void 0,
2101
+ externalPublishProtocol: externalPublishSelect.value || "none",
2026
2102
  room: roomInput.value || void 0,
2027
2103
  encryptionKey: encryptionKeyInput.value || void 0,
2028
2104
  keyId: keyIdInput.value || void 0,
@@ -2044,6 +2120,8 @@ async function importDiscoveredDevice(device) {
2044
2120
  contentDiv.appendChild(typeSelect);
2045
2121
  contentDiv.appendChild(connectionPrefLabel);
2046
2122
  contentDiv.appendChild(connectionPrefSelect);
2123
+ contentDiv.appendChild(externalPublishLabel);
2124
+ contentDiv.appendChild(externalPublishSelect);
2047
2125
  contentDiv.appendChild(roomLabel);
2048
2126
  contentDiv.appendChild(roomInput);
2049
2127
  contentDiv.appendChild(macLabel);
@@ -2249,6 +2327,27 @@ async function editDevice(device) {
2249
2327
  opt.selected = (device.connectionPreference || "auto") === val;
2250
2328
  connectionPrefSelect.appendChild(opt);
2251
2329
  });
2330
+ const externalPublishLabel = document.createElement("label");
2331
+ externalPublishLabel.textContent = "Publish as External Device";
2332
+ externalPublishLabel.style.display = "block";
2333
+ externalPublishLabel.style.marginBottom = "6px";
2334
+ externalPublishLabel.style.fontWeight = "500";
2335
+ externalPublishLabel.style.fontSize = "12px";
2336
+ externalPublishLabel.style.color = "#6b7280";
2337
+ const externalPublishSelect = document.createElement("select");
2338
+ externalPublishSelect.style.width = "100%";
2339
+ externalPublishSelect.style.marginBottom = "12px";
2340
+ externalPublishSelect.style.padding = "8px 10px";
2341
+ externalPublishSelect.style.borderRadius = "6px";
2342
+ externalPublishSelect.style.fontSize = "14px";
2343
+ externalPublishSelect.style.boxSizing = "border-box";
2344
+ ["none", "hap", "matter"].forEach((val) => {
2345
+ const opt = document.createElement("option");
2346
+ opt.value = val;
2347
+ opt.text = val === "none" ? "None" : val.toUpperCase();
2348
+ opt.selected = (device.externalPublishProtocol || "none") === val;
2349
+ externalPublishSelect.appendChild(opt);
2350
+ });
2252
2351
  const roomLabel = document.createElement("label");
2253
2352
  roomLabel.textContent = "Room";
2254
2353
  roomLabel.style.display = "block";
@@ -2337,6 +2436,7 @@ async function editDevice(device) {
2337
2436
  configDeviceName: nameInput.value || void 0,
2338
2437
  configDeviceType: typeSelect.value,
2339
2438
  connectionPreference: connectionPrefSelect.value,
2439
+ externalPublishProtocol: externalPublishSelect.value,
2340
2440
  room: roomInput.value || void 0,
2341
2441
  encryptionKey: encryptionKeyInput.value || void 0,
2342
2442
  keyId: keyIdInput.value || void 0,
@@ -2351,6 +2451,9 @@ async function editDevice(device) {
2351
2451
  if (params.room !== void 0) {
2352
2452
  options.room = params.room;
2353
2453
  }
2454
+ if (params.externalPublishProtocol !== void 0) {
2455
+ options.externalPublishProtocol = params.externalPublishProtocol;
2456
+ }
2354
2457
  if (params.encryptionKey !== void 0) {
2355
2458
  options.encryptionKey = params.encryptionKey;
2356
2459
  }
@@ -2395,6 +2498,8 @@ async function editDevice(device) {
2395
2498
  contentDiv.appendChild(typeSelect);
2396
2499
  contentDiv.appendChild(connectionPrefLabel);
2397
2500
  contentDiv.appendChild(connectionPrefSelect);
2501
+ contentDiv.appendChild(externalPublishLabel);
2502
+ contentDiv.appendChild(externalPublishSelect);
2398
2503
  contentDiv.appendChild(roomLabel);
2399
2504
  contentDiv.appendChild(roomInput);
2400
2505
  contentDiv.appendChild(encryptionKeyLabel);
@@ -3439,8 +3544,9 @@ function renderDeviceList(list) {
3439
3544
  meta.style.fontSize = "11px";
3440
3545
  const typeText = d.type ? `type: ${d.type}` : "";
3441
3546
  const connText = d.connectionPreference ? `conn: ${d.connectionPreference}` : "";
3547
+ const extText = d.externalPublishProtocol && d.externalPublishProtocol !== "none" ? `external: ${d.externalPublishProtocol}` : "";
3442
3548
  const roomText = d.room ? `room: ${d.room}` : "";
3443
- meta.textContent = [typeText, connText, roomText].filter(Boolean).join(" | ");
3549
+ meta.textContent = [typeText, connText, extText, roomText].filter(Boolean).join(" | ");
3444
3550
  info.appendChild(nameContainer);
3445
3551
  info.appendChild(meta);
3446
3552
  info.appendChild(expandedDetails);
@@ -3537,8 +3643,14 @@ async function addDeviceToConfig2(device, options = {}) {
3537
3643
  address: importValues.address,
3538
3644
  model: device.model,
3539
3645
  rssi: device.rssi,
3646
+ connectionPreference: importValues.connectionPreference,
3647
+ externalPublishProtocol: importValues.externalPublishProtocol,
3648
+ room: importValues.room,
3540
3649
  encryptionKey: importValues.encryptionKey,
3541
- keyId: importValues.keyId
3650
+ keyId: importValues.keyId,
3651
+ refreshRate: importValues.refreshRate,
3652
+ blePollingEnabled: importValues.blePollingEnabled,
3653
+ blePollIntervalMs: importValues.blePollIntervalMs
3542
3654
  });
3543
3655
  uiLog.info("Add device response:", resp);
3544
3656
  const alreadyExists = !!resp?.alreadyExists;
@@ -3609,6 +3721,9 @@ var init_devices = __esm({
3609
3721
  }
3610
3722
  });
3611
3723
 
3724
+ // src/homebridge-ui/public/js/app.ts
3725
+ init_api();
3726
+
3612
3727
  // src/homebridge-ui/public/js/credentials.ts
3613
3728
  init_logger();
3614
3729
  init_modal();
@@ -3646,7 +3761,7 @@ async function loadCredentialStatus() {
3646
3761
  uiLog.error("Error loading credentials:", e);
3647
3762
  }
3648
3763
  }
3649
- async function saveCredentials() {
3764
+ async function saveCredentials2() {
3650
3765
  const token = document.getElementById("token")?.value;
3651
3766
  const secret = document.getElementById("secret")?.value;
3652
3767
  const saveStatus = document.getElementById("saveStatus");
@@ -3706,9 +3821,20 @@ async function saveCredentials() {
3706
3821
  init_devices();
3707
3822
  init_discovery();
3708
3823
  window.loadCredentialStatus = loadCredentialStatus;
3709
- window.saveCredentials = saveCredentials;
3824
+ window.saveCredentials = saveCredentials2;
3710
3825
  window.discoverDevices = discoverDevices2;
3826
+ async function checkV4Config() {
3827
+ const banner = document.getElementById("v4ConfigWarning");
3828
+ if (!banner) {
3829
+ return;
3830
+ }
3831
+ const isV4 = await detectV4Config();
3832
+ if (isV4) {
3833
+ banner.style.display = "block";
3834
+ }
3835
+ }
3711
3836
  async function init() {
3837
+ await checkV4Config();
3712
3838
  await loadCredentialStatus();
3713
3839
  await initializeDiscoverySettings();
3714
3840
  await loadConfiguredDevices();