@mp-consulting/homebridge-daikin-cloud 1.3.5 → 1.3.7

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 (234) hide show
  1. package/LICENSE +39 -1
  2. package/README.md +5 -3
  3. package/dist/src/accessories/air-conditioning-accessory.d.ts +2 -2
  4. package/dist/src/accessories/air-conditioning-accessory.d.ts.map +1 -1
  5. package/dist/src/accessories/air-conditioning-accessory.js.map +1 -1
  6. package/dist/src/accessories/altherma-accessory.d.ts +2 -2
  7. package/dist/src/accessories/altherma-accessory.d.ts.map +1 -1
  8. package/dist/src/accessories/altherma-accessory.js.map +1 -1
  9. package/dist/src/accessories/base-accessory.d.ts +6 -6
  10. package/dist/src/accessories/base-accessory.d.ts.map +1 -1
  11. package/dist/src/accessories/base-accessory.js +15 -15
  12. package/dist/src/accessories/base-accessory.js.map +1 -1
  13. package/dist/src/api/daikin-api.d.ts +26 -26
  14. package/dist/src/api/daikin-api.d.ts.map +1 -1
  15. package/dist/src/api/daikin-api.js +68 -42
  16. package/dist/src/api/daikin-api.js.map +1 -1
  17. package/dist/src/api/daikin-cloud.repository.d.ts.map +1 -1
  18. package/dist/src/api/daikin-cloud.repository.js +22 -14
  19. package/dist/src/api/daikin-cloud.repository.js.map +1 -1
  20. package/dist/src/api/daikin-controller.d.ts +41 -47
  21. package/dist/src/api/daikin-controller.d.ts.map +1 -1
  22. package/dist/src/api/daikin-controller.js +40 -64
  23. package/dist/src/api/daikin-controller.js.map +1 -1
  24. package/dist/src/api/daikin-device.d.ts +36 -31
  25. package/dist/src/api/daikin-device.d.ts.map +1 -1
  26. package/dist/src/api/daikin-device.js +45 -31
  27. package/dist/src/api/daikin-device.js.map +1 -1
  28. package/dist/src/api/daikin-mobile-oauth.d.ts +20 -20
  29. package/dist/src/api/daikin-mobile-oauth.d.ts.map +1 -1
  30. package/dist/src/api/daikin-mobile-oauth.js +49 -44
  31. package/dist/src/api/daikin-mobile-oauth.js.map +1 -1
  32. package/dist/src/api/daikin-oauth.d.ts +32 -32
  33. package/dist/src/api/daikin-oauth.d.ts.map +1 -1
  34. package/dist/src/api/daikin-oauth.js +64 -56
  35. package/dist/src/api/daikin-oauth.js.map +1 -1
  36. package/dist/src/api/daikin-schemas.d.ts +476 -351
  37. package/dist/src/api/daikin-schemas.d.ts.map +1 -1
  38. package/dist/src/api/daikin-schemas.js +11 -42
  39. package/dist/src/api/daikin-schemas.js.map +1 -1
  40. package/dist/src/api/daikin-types.d.ts +5 -1
  41. package/dist/src/api/daikin-types.d.ts.map +1 -1
  42. package/dist/src/api/daikin-types.js.map +1 -1
  43. package/dist/src/api/daikin-websocket.d.ts +31 -32
  44. package/dist/src/api/daikin-websocket.d.ts.map +1 -1
  45. package/dist/src/api/daikin-websocket.js +55 -35
  46. package/dist/src/api/daikin-websocket.js.map +1 -1
  47. package/dist/src/api/index.d.ts +2 -1
  48. package/dist/src/api/index.d.ts.map +1 -1
  49. package/dist/src/api/index.js +3 -1
  50. package/dist/src/api/index.js.map +1 -1
  51. package/dist/src/api/token-storage.d.ts +21 -0
  52. package/dist/src/api/token-storage.d.ts.map +1 -0
  53. package/dist/src/api/token-storage.js +90 -0
  54. package/dist/src/api/token-storage.js.map +1 -0
  55. package/dist/src/config/config-manager.d.ts +33 -33
  56. package/dist/src/config/config-manager.d.ts.map +1 -1
  57. package/dist/src/config/config-manager.js +33 -33
  58. package/dist/src/config/config-manager.js.map +1 -1
  59. package/dist/src/constants/api.constants.d.ts +4 -0
  60. package/dist/src/constants/api.constants.d.ts.map +1 -1
  61. package/dist/src/constants/api.constants.js +5 -1
  62. package/dist/src/constants/api.constants.js.map +1 -1
  63. package/dist/src/constants/device.constants.d.ts +4 -0
  64. package/dist/src/constants/device.constants.d.ts.map +1 -1
  65. package/dist/src/constants/device.constants.js +5 -1
  66. package/dist/src/constants/device.constants.js.map +1 -1
  67. package/dist/src/device/accessory-factory.d.ts +10 -10
  68. package/dist/src/device/accessory-factory.d.ts.map +1 -1
  69. package/dist/src/device/accessory-factory.js +7 -7
  70. package/dist/src/device/accessory-factory.js.map +1 -1
  71. package/dist/src/device/capability-detector.d.ts +8 -8
  72. package/dist/src/device/capability-detector.d.ts.map +1 -1
  73. package/dist/src/device/capability-detector.js +6 -6
  74. package/dist/src/device/capability-detector.js.map +1 -1
  75. package/dist/src/device/capability-docs.d.ts +1 -9
  76. package/dist/src/device/capability-docs.d.ts.map +1 -1
  77. package/dist/src/device/capability-docs.js +19 -73
  78. package/dist/src/device/capability-docs.js.map +1 -1
  79. package/dist/src/device/profiles/device-profile.d.ts +1 -1
  80. package/dist/src/device/profiles/device-profile.d.ts.map +1 -1
  81. package/dist/src/device/profiles/device-profile.js +4 -4
  82. package/dist/src/device/profiles/device-profile.js.map +1 -1
  83. package/dist/src/features/base-feature.d.ts +2 -2
  84. package/dist/src/features/base-feature.d.ts.map +1 -1
  85. package/dist/src/features/base-feature.js +2 -3
  86. package/dist/src/features/base-feature.js.map +1 -1
  87. package/dist/src/features/feature-manager.d.ts +8 -16
  88. package/dist/src/features/feature-manager.d.ts.map +1 -1
  89. package/dist/src/features/feature-manager.js +5 -17
  90. package/dist/src/features/feature-manager.js.map +1 -1
  91. package/dist/src/features/modes/dry-operation-mode.feature.d.ts +1 -1
  92. package/dist/src/features/modes/dry-operation-mode.feature.d.ts.map +1 -1
  93. package/dist/src/features/modes/dry-operation-mode.feature.js.map +1 -1
  94. package/dist/src/features/modes/econo-mode.feature.d.ts +1 -1
  95. package/dist/src/features/modes/econo-mode.feature.d.ts.map +1 -1
  96. package/dist/src/features/modes/econo-mode.feature.js.map +1 -1
  97. package/dist/src/features/modes/fan-only-operation-mode.feature.d.ts +1 -1
  98. package/dist/src/features/modes/fan-only-operation-mode.feature.d.ts.map +1 -1
  99. package/dist/src/features/modes/fan-only-operation-mode.feature.js.map +1 -1
  100. package/dist/src/features/modes/indoor-silent-mode.feature.d.ts +1 -1
  101. package/dist/src/features/modes/indoor-silent-mode.feature.d.ts.map +1 -1
  102. package/dist/src/features/modes/indoor-silent-mode.feature.js.map +1 -1
  103. package/dist/src/features/modes/outdoor-silent-mode.feature.d.ts +1 -1
  104. package/dist/src/features/modes/outdoor-silent-mode.feature.d.ts.map +1 -1
  105. package/dist/src/features/modes/outdoor-silent-mode.feature.js.map +1 -1
  106. package/dist/src/features/modes/powerful-mode.feature.d.ts +1 -1
  107. package/dist/src/features/modes/powerful-mode.feature.d.ts.map +1 -1
  108. package/dist/src/features/modes/powerful-mode.feature.js.map +1 -1
  109. package/dist/src/features/modes/streamer-mode.feature.d.ts +1 -1
  110. package/dist/src/features/modes/streamer-mode.feature.d.ts.map +1 -1
  111. package/dist/src/features/modes/streamer-mode.feature.js.map +1 -1
  112. package/dist/src/index.d.ts +1 -1
  113. package/dist/src/index.d.ts.map +1 -1
  114. package/dist/src/index.js.map +1 -1
  115. package/dist/src/platform.d.ts +11 -8
  116. package/dist/src/platform.d.ts.map +1 -1
  117. package/dist/src/platform.js +64 -15
  118. package/dist/src/platform.js.map +1 -1
  119. package/dist/src/services/climate-control.service.d.ts +8 -2
  120. package/dist/src/services/climate-control.service.d.ts.map +1 -1
  121. package/dist/src/services/climate-control.service.js +59 -58
  122. package/dist/src/services/climate-control.service.js.map +1 -1
  123. package/dist/src/services/hot-water-tank.service.d.ts +6 -2
  124. package/dist/src/services/hot-water-tank.service.d.ts.map +1 -1
  125. package/dist/src/services/hot-water-tank.service.js +33 -31
  126. package/dist/src/services/hot-water-tank.service.js.map +1 -1
  127. package/dist/src/types/daikin-enums.js +12 -12
  128. package/dist/src/types/daikin-enums.js.map +1 -1
  129. package/dist/src/types/device-capabilities.d.ts +1 -1
  130. package/dist/src/types/device-capabilities.d.ts.map +1 -1
  131. package/dist/src/utils/log-context.d.ts +23 -23
  132. package/dist/src/utils/log-context.d.ts.map +1 -1
  133. package/dist/src/utils/log-context.js +28 -28
  134. package/dist/src/utils/log-context.js.map +1 -1
  135. package/dist/src/utils/strings.d.ts.map +1 -1
  136. package/dist/src/utils/strings.js.map +1 -1
  137. package/dist/src/utils/update-mapper.d.ts +16 -16
  138. package/dist/src/utils/update-mapper.d.ts.map +1 -1
  139. package/dist/src/utils/update-mapper.js +14 -14
  140. package/dist/src/utils/update-mapper.js.map +1 -1
  141. package/homebridge-ui/public/index.html +2 -2
  142. package/homebridge-ui/public/script.js +957 -898
  143. package/homebridge-ui/server.js +746 -678
  144. package/package.json +29 -27
  145. package/.claude/settings.json +0 -3
  146. package/.claude/settings.local.json +0 -29
  147. package/CHANGELOG.md +0 -103
  148. package/CLAUDE.md +0 -269
  149. package/config.md +0 -2
  150. package/dist/src/api/daikin-device-tracker.d.ts +0 -97
  151. package/dist/src/api/daikin-device-tracker.d.ts.map +0 -1
  152. package/dist/src/api/daikin-device-tracker.js +0 -136
  153. package/dist/src/api/daikin-device-tracker.js.map +0 -1
  154. package/dist/src/api/http-interceptor.d.ts +0 -99
  155. package/dist/src/api/http-interceptor.d.ts.map +0 -1
  156. package/dist/src/api/http-interceptor.js +0 -177
  157. package/dist/src/api/http-interceptor.js.map +0 -1
  158. package/dist/src/di/service-container.d.ts +0 -92
  159. package/dist/src/di/service-container.d.ts.map +0 -1
  160. package/dist/src/di/service-container.js +0 -156
  161. package/dist/src/di/service-container.js.map +0 -1
  162. package/dist/src/features/feature-registry.d.ts +0 -100
  163. package/dist/src/features/feature-registry.d.ts.map +0 -1
  164. package/dist/src/features/feature-registry.js +0 -142
  165. package/dist/src/features/feature-registry.js.map +0 -1
  166. package/dist/src/services/service-factory.d.ts +0 -46
  167. package/dist/src/services/service-factory.d.ts.map +0 -1
  168. package/dist/src/services/service-factory.js +0 -72
  169. package/dist/src/services/service-factory.js.map +0 -1
  170. package/dist/src/utils/error-handler.d.ts +0 -101
  171. package/dist/src/utils/error-handler.d.ts.map +0 -1
  172. package/dist/src/utils/error-handler.js +0 -251
  173. package/dist/src/utils/error-handler.js.map +0 -1
  174. package/dist/src/utils/retry.d.ts +0 -42
  175. package/dist/src/utils/retry.d.ts.map +0 -1
  176. package/dist/src/utils/retry.js +0 -70
  177. package/dist/src/utils/retry.js.map +0 -1
  178. package/docs/ARCHITECTURE.md +0 -645
  179. package/docs/IMPLEMENTATION_GUIDE.md +0 -899
  180. package/docs/IMPROVEMENTS_SUMMARY.md +0 -415
  181. package/docs/NEXT_STEPS.md +0 -368
  182. package/docs/Screenshot 2024-07-04 at 18.41.28.png +0 -0
  183. package/docs/TROUBLESHOOTING.md +0 -475
  184. package/docs/api-response-for-BRP069A8x.json +0 -520
  185. package/docs/api-response-for-BRP069C4x-2.json +0 -881
  186. package/docs/api-response-for-BRP069C4x.json +0 -916
  187. package/docs/api-response-for-altherma.json +0 -759
  188. package/docs/api-response-for-altherma2.json +0 -2735
  189. package/docs/api-response-with-multiple-devices-incl-heatpump.json +0 -2544
  190. package/docs/cr-insance-altherma-id-0.json +0 -834
  191. package/docs/mock-air-to-air-dx23.json +0 -759
  192. package/docs/mock-air-to-air-dx4.json +0 -1134
  193. package/docs/mock-airpurifier-with-humidifier.json +0 -732
  194. package/docs/mock-airpurifier.json +0 -450
  195. package/docs/mock-altherma-air-to-water-lan.json +0 -845
  196. package/docs/mock-altherma-air-to-water-wlan.json +0 -845
  197. package/docs/mock-d2cnd-gas-boiler.json +0 -649
  198. package/docs/setpointmode-vs-controlmode-vs-setpoints-vs-sensorydata.txt +0 -6
  199. package/images/fan-speed.jpeg +0 -0
  200. package/images/homekit-controls.jpeg +0 -0
  201. package/images/homekit-settings.jpeg +0 -0
  202. package/images/swing-mode.png +0 -0
  203. package/jest.config.ts +0 -13
  204. package/test/fixtures/altherma-crSense-2.ts +0 -834
  205. package/test/fixtures/altherma-fraction.ts +0 -718
  206. package/test/fixtures/altherma-heat-pump-2.ts +0 -479
  207. package/test/fixtures/altherma-heat-pump.ts +0 -757
  208. package/test/fixtures/altherma-miladcerkic-off.ts +0 -524
  209. package/test/fixtures/altherma-miladcerkic.ts +0 -524
  210. package/test/fixtures/altherma-v1ckoeln.ts +0 -644
  211. package/test/fixtures/altherma-with-embedded-id-zero.ts +0 -834
  212. package/test/fixtures/dx23-airco-2.ts +0 -343
  213. package/test/fixtures/dx23-airco.ts +0 -518
  214. package/test/fixtures/dx4-airco.ts +0 -914
  215. package/test/fixtures/unknown-jan.ts +0 -488
  216. package/test/fixtures/unknown-kitchen-guests.ts +0 -488
  217. package/test/helpers/test-isolation.ts +0 -228
  218. package/test/integration/air-conditioning.test.ts +0 -410
  219. package/test/integration/altherma.test.ts +0 -289
  220. package/test/integration/platform.test.ts +0 -118
  221. package/test/mocks/index.ts +0 -27
  222. package/test/test-gigya-auth.js +0 -443
  223. package/test/test-mobile-oauth.js +0 -175
  224. package/test/test-websocket-mobile.js +0 -123
  225. package/test/test-websocket.js +0 -116
  226. package/test/unit/api/__snapshots__/daikinCloud.test.ts.snap +0 -1320
  227. package/test/unit/api/daikin-api.test.ts +0 -384
  228. package/test/unit/api/daikin-oauth.test.ts +0 -214
  229. package/test/unit/api/daikinCloud.test.ts +0 -12
  230. package/test/unit/config/config-manager.test.ts +0 -271
  231. package/test/unit/device/daikin-device.test.ts +0 -79
  232. package/test/unit/services/hot-water-tank.service.test.ts +0 -123
  233. package/test/unit/utils/error-handler.test.ts +0 -274
  234. package/test/unit/utils/log-context.test.ts +0 -271
@@ -12,47 +12,55 @@ const $$ = (selector) => document.querySelectorAll(selector);
12
12
  const $id = (id) => document.getElementById(id);
13
13
 
14
14
  const DOM = {
15
- show: (el) => el?.classList.remove('d-none'),
16
- hide: (el) => el?.classList.add('d-none'),
17
- toggle: (el, show) => el?.classList.toggle('d-none', !show),
18
- setValid: (el, valid) => el?.classList.toggle('is-invalid', !valid),
15
+ show: (el) => el?.classList.remove('d-none'),
16
+ hide: (el) => el?.classList.add('d-none'),
17
+ toggle: (el, show) => el?.classList.toggle('d-none', !show),
18
+ setValid: (el, valid) => el?.classList.toggle('is-invalid', !valid),
19
19
  };
20
20
 
21
21
  const Utils = {
22
- capitalize: (str) => str ? str.charAt(0).toUpperCase() + str.slice(1) : '',
23
-
24
- escapeHtml(text) {
25
- const div = document.createElement('div');
26
- div.textContent = text;
27
- return div.innerHTML;
28
- },
29
-
30
- isValidPort(port) {
31
- const num = parseInt(port, 10);
32
- return !isNaN(num) && num >= 1 && num <= 65535;
33
- },
34
-
35
- isValidNumber(value, min, max) {
36
- const num = parseInt(value, 10);
37
- return !isNaN(num) && num >= min && num <= max;
38
- },
39
-
40
- isValidIPv4(ip) {
41
- if (!ip) return true;
42
- return /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.){3}(25[0-5]|(2[0-4]|1\d|[1-9]|)\d)$/.test(ip);
43
- },
44
-
45
- formatTime(ms) {
46
- const days = Math.floor(ms / 86400000);
47
- const hours = Math.floor((ms % 86400000) / 3600000);
48
- const minutes = Math.floor((ms % 3600000) / 60000);
49
- const seconds = Math.floor((ms % 60000) / 1000);
50
-
51
- if (days > 0) return `${days}d ${hours}h ${minutes}m`;
52
- if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
53
- if (minutes > 0) return `${minutes}m ${seconds}s`;
54
- return `${seconds}s`;
55
- },
22
+ capitalize: (str) => str ? str.charAt(0).toUpperCase() + str.slice(1) : '',
23
+
24
+ escapeHtml(text) {
25
+ const div = document.createElement('div');
26
+ div.textContent = text;
27
+ return div.innerHTML;
28
+ },
29
+
30
+ isValidPort(port) {
31
+ const num = parseInt(port, 10);
32
+ return !isNaN(num) && num >= 1 && num <= 65535;
33
+ },
34
+
35
+ isValidNumber(value, min, max) {
36
+ const num = parseInt(value, 10);
37
+ return !isNaN(num) && num >= min && num <= max;
38
+ },
39
+
40
+ isValidIPv4(ip) {
41
+ if (!ip) {
42
+ return true;
43
+ }
44
+ return /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.){3}(25[0-5]|(2[0-4]|1\d|[1-9]|)\d)$/.test(ip);
45
+ },
46
+
47
+ formatTime(ms) {
48
+ const days = Math.floor(ms / 86400000);
49
+ const hours = Math.floor((ms % 86400000) / 3600000);
50
+ const minutes = Math.floor((ms % 3600000) / 60000);
51
+ const seconds = Math.floor((ms % 60000) / 1000);
52
+
53
+ if (days > 0) {
54
+ return `${days}d ${hours}h ${minutes}m`;
55
+ }
56
+ if (hours > 0) {
57
+ return `${hours}h ${minutes}m ${seconds}s`;
58
+ }
59
+ if (minutes > 0) {
60
+ return `${minutes}m ${seconds}s`;
61
+ }
62
+ return `${seconds}s`;
63
+ },
56
64
  };
57
65
 
58
66
  // ============================================================================
@@ -60,17 +68,17 @@ const Utils = {
60
68
  // ============================================================================
61
69
 
62
70
  const State = {
63
- currentTab: 'devices',
64
- currentStep: 1,
65
- authState: null,
66
- authUrl: null,
67
- tokenExpiresAt: null,
68
- isValidating: false,
69
- intervals: {
70
- poll: null,
71
- countdown: null,
72
- statusRefresh: null,
73
- },
71
+ currentTab: 'devices',
72
+ currentStep: 1,
73
+ authState: null,
74
+ authUrl: null,
75
+ tokenExpiresAt: null,
76
+ isValidating: false,
77
+ intervals: {
78
+ poll: null,
79
+ countdown: null,
80
+ statusRefresh: null,
81
+ },
74
82
  };
75
83
 
76
84
  // Element cache (populated on init)
@@ -81,27 +89,29 @@ let El = {};
81
89
  // ============================================================================
82
90
 
83
91
  const UI = {
84
- showLoading: () => DOM.show(El.loading),
85
- hideLoading: () => DOM.hide(El.loading),
86
-
87
- showError(message) {
88
- if (El.globalError) {
89
- El.globalError.innerHTML = `<div class="alert alert-danger">${Utils.escapeHtml(message)}</div>`;
90
- DOM.show(El.globalError);
91
- setTimeout(() => DOM.hide(El.globalError), 10000);
92
- }
93
- },
94
-
95
- setButtonLoading(btn, loading, loadingText = 'Loading...', normalText = null) {
96
- if (!btn) return;
97
- btn.disabled = loading;
98
- if (loading) {
99
- btn.dataset.originalText = btn.innerHTML;
100
- btn.innerHTML = `<span class="spinner-border spinner-border-sm"></span> ${loadingText}`;
101
- } else {
102
- btn.innerHTML = normalText || btn.dataset.originalText || btn.innerHTML;
103
- }
104
- },
92
+ showLoading: () => DOM.show(El.loading),
93
+ hideLoading: () => DOM.hide(El.loading),
94
+
95
+ showError(message) {
96
+ if (El.globalError) {
97
+ El.globalError.innerHTML = `<div class="alert alert-danger">${Utils.escapeHtml(message)}</div>`;
98
+ DOM.show(El.globalError);
99
+ setTimeout(() => DOM.hide(El.globalError), 10000);
100
+ }
101
+ },
102
+
103
+ setButtonLoading(btn, loading, loadingText = 'Loading...', normalText = null) {
104
+ if (!btn) {
105
+ return;
106
+ }
107
+ btn.disabled = loading;
108
+ if (loading) {
109
+ btn.dataset.originalText = btn.innerHTML;
110
+ btn.innerHTML = `<span class="spinner-border spinner-border-sm"></span> ${loadingText}`;
111
+ } else {
112
+ btn.innerHTML = normalText || btn.dataset.originalText || btn.innerHTML;
113
+ }
114
+ },
105
115
  };
106
116
 
107
117
  // ============================================================================
@@ -109,18 +119,18 @@ const UI = {
109
119
  // ============================================================================
110
120
 
111
121
  function switchTab(tabName) {
112
- State.currentTab = tabName;
113
- $$('.nav-pills .nav-link').forEach(btn =>
114
- btn.classList.toggle('active', btn.dataset.tab === tabName));
115
- $$('.tab-pane').forEach(panel =>
116
- panel.classList.toggle('active', panel.id === `tab-${tabName}`));
122
+ State.currentTab = tabName;
123
+ $$('.nav-pills .nav-link').forEach(btn =>
124
+ btn.classList.toggle('active', btn.dataset.tab === tabName));
125
+ $$('.tab-pane').forEach(panel =>
126
+ panel.classList.toggle('active', panel.id === `tab-${tabName}`));
117
127
  }
118
128
 
119
129
  function switchSettingsSubtab(subtabName) {
120
- $$('.nav-tabs .nav-link').forEach(btn =>
121
- btn.classList.toggle('active', btn.dataset.subtab === subtabName));
122
- $$('.settings-subtab-content').forEach(panel =>
123
- DOM.toggle(panel, panel.id === `subtab-${subtabName}`));
130
+ $$('.nav-tabs .nav-link').forEach(btn =>
131
+ btn.classList.toggle('active', btn.dataset.subtab === subtabName));
132
+ $$('.settings-subtab-content').forEach(panel =>
133
+ DOM.toggle(panel, panel.id === `subtab-${subtabName}`));
124
134
  }
125
135
 
126
136
  // ============================================================================
@@ -128,43 +138,47 @@ function switchSettingsSubtab(subtabName) {
128
138
  // ============================================================================
129
139
 
130
140
  const Countdown = {
131
- start() {
132
- this.stop();
133
- this.update();
134
- State.intervals.countdown = setInterval(() => this.update(), 1000);
135
- },
136
-
137
- stop() {
138
- if (State.intervals.countdown) {
139
- clearInterval(State.intervals.countdown);
140
- State.intervals.countdown = null;
141
- }
142
- },
143
-
144
- update() {
145
- if (!State.tokenExpiresAt) return;
146
-
147
- const diff = State.tokenExpiresAt - Date.now();
148
-
149
- if (diff <= 0) {
150
- [El.tokenExpires, El.authTokenExpires].forEach(el => {
151
- if (el) el.textContent = 'Expired';
152
- });
153
- this.stop();
154
- Auth.loadStatus();
155
- return;
141
+ start() {
142
+ this.stop();
143
+ this.update();
144
+ State.intervals.countdown = setInterval(() => this.update(), 1000);
145
+ },
146
+
147
+ stop() {
148
+ if (State.intervals.countdown) {
149
+ clearInterval(State.intervals.countdown);
150
+ State.intervals.countdown = null;
151
+ }
152
+ },
153
+
154
+ update() {
155
+ if (!State.tokenExpiresAt) {
156
+ return;
157
+ }
158
+
159
+ const diff = State.tokenExpiresAt - Date.now();
160
+
161
+ if (diff <= 0) {
162
+ [El.tokenExpires, El.authTokenExpires].forEach(el => {
163
+ if (el) {
164
+ el.textContent = 'Expired';
156
165
  }
157
-
158
- const formatted = Utils.formatTime(diff);
159
- const color = diff < 300000 ? 'var(--bs-danger)' : diff < 1800000 ? 'var(--bs-warning)' : '';
160
-
161
- [El.tokenExpires, El.authTokenExpires].forEach(el => {
162
- if (el) {
163
- el.textContent = formatted;
164
- el.style.color = color;
165
- }
166
- });
167
- },
166
+ });
167
+ this.stop();
168
+ Auth.loadStatus();
169
+ return;
170
+ }
171
+
172
+ const formatted = Utils.formatTime(diff);
173
+ const color = diff < 300000 ? 'var(--bs-danger)' : diff < 1800000 ? 'var(--bs-warning)' : '';
174
+
175
+ [El.tokenExpires, El.authTokenExpires].forEach(el => {
176
+ if (el) {
177
+ el.textContent = formatted;
178
+ el.style.color = color;
179
+ }
180
+ });
181
+ },
168
182
  };
169
183
 
170
184
  // ============================================================================
@@ -172,141 +186,141 @@ const Countdown = {
172
186
  // ============================================================================
173
187
 
174
188
  const Auth = {
175
- STATUS_REFRESH_INTERVAL: 60000,
176
- _lastStatusCheck: 0,
177
- _statusPromise: null,
178
-
179
- async loadStatus() {
180
- // Debounce: skip if called within 500ms of last call
181
- const now = Date.now();
182
- if (now - this._lastStatusCheck < 500 && this._statusPromise) {
183
- return this._statusPromise;
184
- }
185
- this._lastStatusCheck = now;
186
-
187
- this._statusPromise = (async () => {
188
- try {
189
- const response = await homebridge.request('/auth/status');
190
- this.updateUI(response);
191
- } catch (error) {
192
- this.updateUI({ authenticated: false, error: error.message });
193
- }
194
- })();
195
-
196
- return this._statusPromise;
197
- },
198
-
199
- startStatusRefresh() {
200
- this.stopStatusRefresh();
201
- State.intervals.statusRefresh = setInterval(() => this.checkTokenRenewal(), this.STATUS_REFRESH_INTERVAL);
202
- },
203
-
204
- stopStatusRefresh() {
205
- if (State.intervals.statusRefresh) {
206
- clearInterval(State.intervals.statusRefresh);
207
- State.intervals.statusRefresh = null;
208
- }
209
- },
210
-
211
- async checkTokenRenewal() {
212
- try {
213
- const response = await homebridge.request('/auth/status');
214
- if (response.authenticated && response.expiresAt) {
215
- const newExpires = new Date(response.expiresAt).getTime();
216
- if (!State.tokenExpiresAt || newExpires !== State.tokenExpiresAt.getTime()) {
217
- this.updateUI(response);
218
- }
219
- }
220
- } catch (error) {
221
- console.error('Token check failed:', error);
222
- }
223
- },
224
-
225
- updateUI(status) {
226
- if (status.authenticated) {
227
- this.setAuthenticated(status);
228
- } else {
229
- this.setUnauthenticated(status);
230
- }
231
- },
232
-
233
- setAuthenticated(status) {
234
- const isExpired = status.isExpired;
235
-
236
- El.statusBadge.className = `badge ${isExpired ? 'expired' : 'authenticated'}`;
237
- El.statusIndicator.className = `status-dot ${isExpired ? 'yellow' : 'green'}`;
238
- El.statusText.textContent = isExpired ? 'Expired' : 'Connected';
239
- El.authStatus.textContent = isExpired
240
- ? (status.canRefresh ? 'Token expired (will auto-refresh)' : 'Token expired')
241
- : 'Authenticated and ready';
242
-
243
- if (status.expiresAt) {
244
- State.tokenExpiresAt = new Date(status.expiresAt);
245
- DOM.show(El.tokenExpiresLabel);
246
- DOM.show(El.expiresRow);
247
- Countdown.start();
248
- }
249
-
250
- this.startStatusRefresh();
251
- El.btnAuthenticate.textContent = 'Re-authenticate';
252
- DOM.show(El.btnRevoke);
253
- El.btnTest.disabled = false;
254
- },
255
-
256
- setUnauthenticated(status) {
257
- El.statusBadge.className = 'badge not-authenticated';
258
- El.statusIndicator.className = 'status-dot red';
259
- El.statusText.textContent = 'Not Connected';
260
- El.authStatus.textContent = status.error || 'Authentication required';
261
-
262
- DOM.hide(El.tokenExpiresLabel);
263
- DOM.hide(El.expiresRow);
264
- El.tokenExpires.textContent = '';
265
- El.btnAuthenticate.textContent = 'Authenticate';
266
- DOM.hide(El.btnRevoke);
267
- El.btnTest.disabled = true;
268
-
269
- Countdown.stop();
270
- this.stopStatusRefresh();
271
- },
272
-
273
- async startAuth(config) {
274
- const result = await homebridge.request('/auth/start', config);
275
- State.authState = result.state;
276
- State.authUrl = result.authUrl;
277
- return result;
278
- },
279
-
280
- async revoke() {
281
- UI.showLoading();
282
- try {
283
- const config = await homebridge.getPluginConfig();
284
- const { clientId, clientSecret } = config[0] || {};
285
- await homebridge.request('/auth/revoke', { clientId, clientSecret });
286
- this.loadStatus();
287
- } catch (error) {
288
- UI.showError('Failed to revoke: ' + error.message);
289
- }
290
- UI.hideLoading();
291
- },
292
-
293
- async testConnection() {
294
- UI.setButtonLoading(El.btnTest, true, 'Testing...');
295
- DOM.hide(El.testResult);
296
-
297
- try {
298
- const result = await homebridge.request('/auth/test');
299
- El.testResult.className = `alert ${result.success ? 'alert-success' : 'alert-danger'}`;
300
- El.testResult.textContent = result.message;
301
- DOM.show(El.testResult);
302
- } catch (error) {
303
- El.testResult.className = 'alert alert-danger';
304
- El.testResult.textContent = 'Test failed: ' + error.message;
305
- DOM.show(El.testResult);
189
+ STATUS_REFRESH_INTERVAL: 60000,
190
+ _lastStatusCheck: 0,
191
+ _statusPromise: null,
192
+
193
+ async loadStatus() {
194
+ // Debounce: skip if called within 500ms of last call
195
+ const now = Date.now();
196
+ if (now - this._lastStatusCheck < 500 && this._statusPromise) {
197
+ return this._statusPromise;
198
+ }
199
+ this._lastStatusCheck = now;
200
+
201
+ this._statusPromise = (async () => {
202
+ try {
203
+ const response = await homebridge.request('/auth/status');
204
+ this.updateUI(response);
205
+ } catch (error) {
206
+ this.updateUI({ authenticated: false, error: error.message });
207
+ }
208
+ })();
209
+
210
+ return this._statusPromise;
211
+ },
212
+
213
+ startStatusRefresh() {
214
+ this.stopStatusRefresh();
215
+ State.intervals.statusRefresh = setInterval(() => this.checkTokenRenewal(), this.STATUS_REFRESH_INTERVAL);
216
+ },
217
+
218
+ stopStatusRefresh() {
219
+ if (State.intervals.statusRefresh) {
220
+ clearInterval(State.intervals.statusRefresh);
221
+ State.intervals.statusRefresh = null;
222
+ }
223
+ },
224
+
225
+ async checkTokenRenewal() {
226
+ try {
227
+ const response = await homebridge.request('/auth/status');
228
+ if (response.authenticated && response.expiresAt) {
229
+ const newExpires = new Date(response.expiresAt).getTime();
230
+ if (!State.tokenExpiresAt || newExpires !== State.tokenExpiresAt.getTime()) {
231
+ this.updateUI(response);
306
232
  }
307
-
308
- UI.setButtonLoading(El.btnTest, false, null, 'Test Connection');
309
- },
233
+ }
234
+ } catch (error) {
235
+ console.error('Token check failed:', error);
236
+ }
237
+ },
238
+
239
+ updateUI(status) {
240
+ if (status.authenticated) {
241
+ this.setAuthenticated(status);
242
+ } else {
243
+ this.setUnauthenticated(status);
244
+ }
245
+ },
246
+
247
+ setAuthenticated(status) {
248
+ const isExpired = status.isExpired;
249
+
250
+ El.statusBadge.className = `badge ${isExpired ? 'expired' : 'authenticated'}`;
251
+ El.statusIndicator.className = `status-dot ${isExpired ? 'yellow' : 'green'}`;
252
+ El.statusText.textContent = isExpired ? 'Expired' : 'Connected';
253
+ El.authStatus.textContent = isExpired
254
+ ? (status.canRefresh ? 'Token expired (will auto-refresh)' : 'Token expired')
255
+ : 'Authenticated and ready';
256
+
257
+ if (status.expiresAt) {
258
+ State.tokenExpiresAt = new Date(status.expiresAt);
259
+ DOM.show(El.tokenExpiresLabel);
260
+ DOM.show(El.expiresRow);
261
+ Countdown.start();
262
+ }
263
+
264
+ this.startStatusRefresh();
265
+ El.btnAuthenticate.textContent = 'Re-authenticate';
266
+ DOM.show(El.btnRevoke);
267
+ El.btnTest.disabled = false;
268
+ },
269
+
270
+ setUnauthenticated(status) {
271
+ El.statusBadge.className = 'badge not-authenticated';
272
+ El.statusIndicator.className = 'status-dot red';
273
+ El.statusText.textContent = 'Not Connected';
274
+ El.authStatus.textContent = status.error || 'Authentication required';
275
+
276
+ DOM.hide(El.tokenExpiresLabel);
277
+ DOM.hide(El.expiresRow);
278
+ El.tokenExpires.textContent = '';
279
+ El.btnAuthenticate.textContent = 'Authenticate';
280
+ DOM.hide(El.btnRevoke);
281
+ El.btnTest.disabled = true;
282
+
283
+ Countdown.stop();
284
+ this.stopStatusRefresh();
285
+ },
286
+
287
+ async startAuth(config) {
288
+ const result = await homebridge.request('/auth/start', config);
289
+ State.authState = result.state;
290
+ State.authUrl = result.authUrl;
291
+ return result;
292
+ },
293
+
294
+ async revoke() {
295
+ UI.showLoading();
296
+ try {
297
+ const config = await homebridge.getPluginConfig();
298
+ const { clientId, clientSecret } = config[0] || {};
299
+ await homebridge.request('/auth/revoke', { clientId, clientSecret });
300
+ this.loadStatus();
301
+ } catch (error) {
302
+ UI.showError('Failed to revoke: ' + error.message);
303
+ }
304
+ UI.hideLoading();
305
+ },
306
+
307
+ async testConnection() {
308
+ UI.setButtonLoading(El.btnTest, true, 'Testing...');
309
+ DOM.hide(El.testResult);
310
+
311
+ try {
312
+ const result = await homebridge.request('/auth/test');
313
+ El.testResult.className = `alert ${result.success ? 'alert-success' : 'alert-danger'}`;
314
+ El.testResult.textContent = result.message;
315
+ DOM.show(El.testResult);
316
+ } catch (error) {
317
+ El.testResult.className = 'alert alert-danger';
318
+ El.testResult.textContent = 'Test failed: ' + error.message;
319
+ DOM.show(El.testResult);
320
+ }
321
+
322
+ UI.setButtonLoading(El.btnTest, false, null, 'Test Connection');
323
+ },
310
324
  };
311
325
 
312
326
  // ============================================================================
@@ -314,122 +328,133 @@ const Auth = {
314
328
  // ============================================================================
315
329
 
316
330
  const Wizard = {
317
- show() {
318
- DOM.show(El.wizard);
319
- DOM.hide(El.authStatusCard);
320
- this.goToStep(1);
321
- },
322
-
323
- hide() {
324
- Polling.stop();
325
- homebridge.request('/auth/stop-server').catch(() => {});
326
- DOM.hide(El.wizard);
327
- DOM.show(El.authStatusCard);
328
- DOM.hide(El.callbackServerStatus);
329
- },
330
-
331
- goToStep(step) {
332
- State.currentStep = step;
333
- for (let i = 1; i <= 3; i++) {
334
- const stepEl = $id(`step-${i}`);
335
- const contentEl = $id(`content-${i}`);
336
- stepEl.classList.remove('active', 'completed');
337
- DOM.toggle(contentEl, i === step);
338
- if (i < step) stepEl.classList.add('completed');
339
- else if (i === step) stepEl.classList.add('active');
340
- }
341
- },
342
-
343
- async validateAndProceed() {
344
- if (State.isValidating) return;
345
- State.isValidating = true;
346
-
347
- const config = this.getFormConfig();
348
- const portInput = $id('callbackServerPort');
349
-
350
- if (!Utils.isValidPort(config.callbackServerPort)) {
351
- DOM.setValid(portInput, false);
352
- El.validationErrors.innerHTML = '<div>Port must be between 1 and 65535</div>';
353
- DOM.show(El.validationErrors);
354
- State.isValidating = false;
355
- return;
356
- }
357
- DOM.setValid(portInput, true);
358
-
359
- UI.showLoading();
360
- try {
361
- const validation = await homebridge.request('/config/validate', config);
362
- if (!validation.valid) {
363
- El.validationErrors.innerHTML = validation.errors.map(e => `<div>${e}</div>`).join('');
364
- DOM.show(El.validationErrors);
365
- UI.hideLoading();
366
- State.isValidating = false;
367
- return;
368
- }
369
-
370
- DOM.hide(El.validationErrors);
371
- const authResult = await Auth.startAuth(config);
372
-
373
- El.authUrlDisplay.textContent = authResult.authUrl;
374
- DOM.show(El.authUrlContainer);
375
-
376
- const manualCollapse = $id('manual-callback-collapse');
377
- DOM.toggle(El.callbackServerStatus, authResult.callbackServerRunning);
378
- if (manualCollapse) {
379
- manualCollapse.classList.toggle('show', !authResult.callbackServerRunning);
380
- }
381
-
382
- Polling.start();
383
- this.goToStep(2);
384
- } catch (error) {
385
- El.validationErrors.innerHTML = `<div>${error.message}</div>`;
386
- DOM.show(El.validationErrors);
387
- }
388
-
331
+ show() {
332
+ DOM.show(El.wizard);
333
+ DOM.hide(El.authStatusCard);
334
+ this.goToStep(1);
335
+ },
336
+
337
+ hide() {
338
+ Polling.stop();
339
+ homebridge.request('/auth/stop-server').catch(() => {});
340
+ DOM.hide(El.wizard);
341
+ DOM.show(El.authStatusCard);
342
+ DOM.hide(El.callbackServerStatus);
343
+ },
344
+
345
+ goToStep(step) {
346
+ State.currentStep = step;
347
+ for (let i = 1; i <= 3; i++) {
348
+ const stepEl = $id(`step-${i}`);
349
+ const contentEl = $id(`content-${i}`);
350
+ stepEl.classList.remove('active', 'completed');
351
+ DOM.toggle(contentEl, i === step);
352
+ if (i < step) {
353
+ stepEl.classList.add('completed');
354
+ } else if (i === step) {
355
+ stepEl.classList.add('active');
356
+ }
357
+ }
358
+ },
359
+
360
+ async validateAndProceed() {
361
+ if (State.isValidating) {
362
+ return;
363
+ }
364
+ State.isValidating = true;
365
+
366
+ const config = this.getFormConfig();
367
+ const portInput = $id('callbackServerPort');
368
+
369
+ if (!Utils.isValidPort(config.callbackServerPort)) {
370
+ DOM.setValid(portInput, false);
371
+ El.validationErrors.innerHTML = '<div>Port must be between 1 and 65535</div>';
372
+ DOM.show(El.validationErrors);
373
+ State.isValidating = false;
374
+ return;
375
+ }
376
+ DOM.setValid(portInput, true);
377
+
378
+ UI.showLoading();
379
+ try {
380
+ const validation = await homebridge.request('/config/validate', config);
381
+ if (!validation.valid) {
382
+ El.validationErrors.innerHTML = validation.errors.map(e => `<div>${e}</div>`).join('');
383
+ DOM.show(El.validationErrors);
389
384
  UI.hideLoading();
390
385
  State.isValidating = false;
391
- },
392
-
393
- getFormConfig() {
394
- return {
395
- clientId: $id('clientId')?.value.trim() || '',
396
- clientSecret: $id('clientSecret')?.value.trim() || '',
397
- callbackServerExternalAddress: $id('callbackServerExternalAddress')?.value.trim() || '',
398
- callbackServerPort: $id('callbackServerPort')?.value.trim() || '8582',
399
- };
400
- },
401
-
402
- async submitCallbackUrl() {
403
- const url = $id('callbackUrl')?.value.trim();
404
- if (!url) return UI.showError('Please paste the callback URL');
405
- if (!url.includes('code=')) return UI.showError('Invalid URL - must contain code parameter');
406
-
407
- UI.showLoading();
408
- try {
409
- const result = await homebridge.request('/auth/', { callbackUrl: url });
410
- if (result.success) {
411
- El.successMessage.textContent = result.message;
412
- this.goToStep(3);
413
- await Config.save();
414
- } else {
415
- UI.showError('Authentication failed: ' + (result.message || 'Unknown error'));
416
- }
417
- } catch (error) {
418
- UI.showError('Authentication failed: ' + error.message);
419
- }
420
- UI.hideLoading();
421
- },
422
-
423
- finish() {
424
- Polling.stop();
425
- this.hide();
426
- Auth.loadStatus();
427
- Devices.load();
428
- },
429
-
430
- openAuthUrl() {
431
- if (State.authUrl) window.open(State.authUrl, '_blank');
432
- },
386
+ return;
387
+ }
388
+
389
+ DOM.hide(El.validationErrors);
390
+ const authResult = await Auth.startAuth(config);
391
+
392
+ El.authUrlDisplay.textContent = authResult.authUrl;
393
+ DOM.show(El.authUrlContainer);
394
+
395
+ const manualCollapse = $id('manual-callback-collapse');
396
+ DOM.toggle(El.callbackServerStatus, authResult.callbackServerRunning);
397
+ if (manualCollapse) {
398
+ manualCollapse.classList.toggle('show', !authResult.callbackServerRunning);
399
+ }
400
+
401
+ Polling.start();
402
+ this.goToStep(2);
403
+ } catch (error) {
404
+ El.validationErrors.innerHTML = `<div>${error.message}</div>`;
405
+ DOM.show(El.validationErrors);
406
+ }
407
+
408
+ UI.hideLoading();
409
+ State.isValidating = false;
410
+ },
411
+
412
+ getFormConfig() {
413
+ return {
414
+ clientId: $id('clientId')?.value.trim() || '',
415
+ clientSecret: $id('clientSecret')?.value.trim() || '',
416
+ callbackServerExternalAddress: $id('callbackServerExternalAddress')?.value.trim() || '',
417
+ callbackServerPort: $id('callbackServerPort')?.value.trim() || '8582',
418
+ };
419
+ },
420
+
421
+ async submitCallbackUrl() {
422
+ const url = $id('callbackUrl')?.value.trim();
423
+ if (!url) {
424
+ return UI.showError('Please paste the callback URL');
425
+ }
426
+ if (!url.includes('code=')) {
427
+ return UI.showError('Invalid URL - must contain code parameter');
428
+ }
429
+
430
+ UI.showLoading();
431
+ try {
432
+ const result = await homebridge.request('/auth/', { callbackUrl: url });
433
+ if (result.success) {
434
+ El.successMessage.textContent = result.message;
435
+ this.goToStep(3);
436
+ await Config.save();
437
+ } else {
438
+ UI.showError('Authentication failed: ' + (result.message || 'Unknown error'));
439
+ }
440
+ } catch (error) {
441
+ UI.showError('Authentication failed: ' + error.message);
442
+ }
443
+ UI.hideLoading();
444
+ },
445
+
446
+ finish() {
447
+ Polling.stop();
448
+ this.hide();
449
+ Auth.loadStatus();
450
+ Devices.load();
451
+ },
452
+
453
+ openAuthUrl() {
454
+ if (State.authUrl) {
455
+ window.open(State.authUrl, '_blank');
456
+ }
457
+ },
433
458
  };
434
459
 
435
460
  // ============================================================================
@@ -437,47 +462,49 @@ const Wizard = {
437
462
  // ============================================================================
438
463
 
439
464
  const Polling = {
440
- isPolling: false,
441
-
442
- start() {
465
+ isPolling: false,
466
+
467
+ start() {
468
+ this.stop();
469
+ this.isPolling = true;
470
+ this.poll();
471
+ },
472
+
473
+ stop() {
474
+ this.isPolling = false;
475
+ if (State.intervals.poll) {
476
+ clearTimeout(State.intervals.poll);
477
+ State.intervals.poll = null;
478
+ }
479
+ },
480
+
481
+ async poll() {
482
+ if (!this.isPolling) {
483
+ return;
484
+ }
485
+
486
+ try {
487
+ const result = await homebridge.request('/auth/poll');
488
+ if (!result.pending) {
443
489
  this.stop();
444
- this.isPolling = true;
445
- this.poll();
446
- },
447
-
448
- stop() {
449
- this.isPolling = false;
450
- if (State.intervals.poll) {
451
- clearTimeout(State.intervals.poll);
452
- State.intervals.poll = null;
453
- }
454
- },
455
-
456
- async poll() {
457
- if (!this.isPolling) return;
458
-
459
- try {
460
- const result = await homebridge.request('/auth/poll');
461
- if (!result.pending) {
462
- this.stop();
463
- if (result.success) {
464
- El.successMessage.textContent = result.message || 'Authentication successful!';
465
- Wizard.goToStep(3);
466
- await Config.save();
467
- Devices.load();
468
- } else {
469
- UI.showError('Authentication failed: ' + (result.error || 'Unknown error'));
470
- }
471
- return;
472
- }
473
- } catch (error) {
474
- console.error('Polling error:', error);
475
- }
476
-
477
- if (this.isPolling) {
478
- State.intervals.poll = setTimeout(() => this.poll(), 1500);
490
+ if (result.success) {
491
+ El.successMessage.textContent = result.message || 'Authentication successful!';
492
+ Wizard.goToStep(3);
493
+ await Config.save();
494
+ Devices.load();
495
+ } else {
496
+ UI.showError('Authentication failed: ' + (result.error || 'Unknown error'));
479
497
  }
480
- },
498
+ return;
499
+ }
500
+ } catch (error) {
501
+ console.error('Polling error:', error);
502
+ }
503
+
504
+ if (this.isPolling) {
505
+ State.intervals.poll = setTimeout(() => this.poll(), 1500);
506
+ }
507
+ },
481
508
  };
482
509
 
483
510
  // ============================================================================
@@ -485,51 +512,55 @@ const Polling = {
485
512
  // ============================================================================
486
513
 
487
514
  const Config = {
488
- async load() {
489
- try {
490
- const config = await homebridge.getPluginConfig();
491
- if (config?.[0]) {
492
- this.populateForm(config[0]);
493
- }
494
- await this.prefillServerAddress();
495
- } catch (error) {
496
- console.error('Config load failed:', error);
497
- }
498
- },
499
-
500
- async prefillServerAddress() {
501
- const field = $id('callbackServerExternalAddress');
502
- if (!field || field.value.trim()) return;
503
-
504
- try {
505
- const { primaryIp } = await homebridge.request('/server/info');
506
- if (primaryIp) {
507
- field.value = primaryIp;
508
- field.placeholder = primaryIp;
509
- }
510
- } catch (error) {
511
- console.error('Server IP fetch failed:', error);
512
- }
513
- },
514
-
515
- populateForm(config) {
516
- ['clientId', 'clientSecret', 'callbackServerExternalAddress', 'callbackServerPort'].forEach(field => {
517
- const el = $id(field);
518
- if (el && config[field]) el.value = config[field];
519
- });
520
- },
521
-
522
- async save() {
523
- try {
524
- const config = await homebridge.getPluginConfig();
525
- const platformConfig = config[0] || { platform: 'DaikinCloud' };
526
- Object.assign(platformConfig, Wizard.getFormConfig());
527
- await homebridge.updatePluginConfig([platformConfig]);
528
- await homebridge.savePluginConfig();
529
- } catch (error) {
530
- console.error('Config save failed:', error);
531
- }
532
- },
515
+ async load() {
516
+ try {
517
+ const config = await homebridge.getPluginConfig();
518
+ if (config?.[0]) {
519
+ this.populateForm(config[0]);
520
+ }
521
+ await this.prefillServerAddress();
522
+ } catch (error) {
523
+ console.error('Config load failed:', error);
524
+ }
525
+ },
526
+
527
+ async prefillServerAddress() {
528
+ const field = $id('callbackServerExternalAddress');
529
+ if (!field || field.value.trim()) {
530
+ return;
531
+ }
532
+
533
+ try {
534
+ const { primaryIp } = await homebridge.request('/server/info');
535
+ if (primaryIp) {
536
+ field.value = primaryIp;
537
+ field.placeholder = primaryIp;
538
+ }
539
+ } catch (error) {
540
+ console.error('Server IP fetch failed:', error);
541
+ }
542
+ },
543
+
544
+ populateForm(config) {
545
+ ['clientId', 'clientSecret', 'callbackServerExternalAddress', 'callbackServerPort'].forEach(field => {
546
+ const el = $id(field);
547
+ if (el && config[field]) {
548
+ el.value = config[field];
549
+ }
550
+ });
551
+ },
552
+
553
+ async save() {
554
+ try {
555
+ const config = await homebridge.getPluginConfig();
556
+ const platformConfig = config[0] || { platform: 'DaikinCloud' };
557
+ Object.assign(platformConfig, Wizard.getFormConfig());
558
+ await homebridge.updatePluginConfig([platformConfig]);
559
+ await homebridge.savePluginConfig();
560
+ } catch (error) {
561
+ console.error('Config save failed:', error);
562
+ }
563
+ },
533
564
  };
534
565
 
535
566
  // ============================================================================
@@ -537,133 +568,147 @@ const Config = {
537
568
  // ============================================================================
538
569
 
539
570
  const Settings = {
540
- devices: [],
541
- excludedIds: [],
542
- saveTimeout: null,
543
-
544
- FEATURE_KEYS: [
545
- 'showPowerfulMode', 'showEconoMode', 'showStreamerMode',
546
- 'showOutdoorSilentMode', 'showIndoorSilentMode', 'showDryMode', 'showFanOnlyMode',
547
- ],
548
-
549
- async load() {
550
- try {
551
- const config = await homebridge.getPluginConfig();
552
- if (config?.[0]) {
553
- this.populateForm(config[0]);
554
- this.excludedIds = config[0].excludedDevicesByDeviceId || [];
555
- }
556
- await this.loadDeviceToggles();
557
- this.setupAutoSave();
558
- } catch (error) {
559
- console.error('Settings load failed:', error);
560
- }
561
- },
562
-
563
- setupAutoSave() {
564
- const handler = () => this.autoSave();
565
-
566
- // Feature toggles + WebSocket
567
- [...this.FEATURE_KEYS, 'enableWebSocket'].forEach(id => {
568
- $id(id)?.addEventListener('change', handler);
569
- });
570
-
571
- // Number/text inputs
572
- ['updateIntervalInMinutes', 'forceUpdateDelay', 'oidcCallbackServerBindAddr'].forEach(id => {
573
- const el = $id(id);
574
- el?.addEventListener('change', handler);
575
- el?.addEventListener('input', handler);
576
- });
577
- },
578
-
579
- autoSave() {
580
- clearTimeout(this.saveTimeout);
581
- this.saveTimeout = setTimeout(() => {
582
- if (this.validateInputs()) this.save();
583
- }, 500);
584
- },
585
-
586
- validateInputs() {
587
- const validators = [
588
- ['oidcCallbackServerBindAddr', (v) => Utils.isValidIPv4(v.trim())],
589
- ['updateIntervalInMinutes', (v) => Utils.isValidNumber(v, 1, 60)],
590
- ['forceUpdateDelay', (v) => Utils.isValidNumber(v, 1, 300)],
591
- ];
592
-
593
- return validators.every(([id, validate]) => {
594
- const el = $id(id);
595
- const valid = !el || validate(el.value);
596
- DOM.setValid(el, valid);
597
- return valid;
598
- });
599
- },
600
-
601
- populateForm(config) {
602
- const showAllLegacy = config.showExtraFeatures === true;
603
-
604
- this.FEATURE_KEYS.forEach(key => {
605
- const el = $id(key);
606
- if (el) el.checked = key in config ? config[key] === true : showAllLegacy;
607
- });
608
-
609
- const updateInterval = $id('updateIntervalInMinutes');
610
- if (updateInterval) updateInterval.value = config.updateIntervalInMinutes || 15;
611
-
612
- const forceDelay = $id('forceUpdateDelay');
613
- if (forceDelay) forceDelay.value = Math.round((config.forceUpdateDelay || 60000) / 1000);
614
-
615
- const bindAddr = $id('oidcCallbackServerBindAddr');
616
- if (bindAddr) bindAddr.value = config.oidcCallbackServerBindAddr || '0.0.0.0';
617
-
618
- const enableWS = $id('enableWebSocket');
619
- if (enableWS) enableWS.checked = config.enableWebSocket !== false;
620
- },
621
-
622
- async loadDeviceToggles() {
623
- const loading = $id('device-toggles-loading');
624
- const list = $id('device-toggles-list');
625
- const empty = $id('device-toggles-empty');
626
-
627
- DOM.show(loading);
628
- list.innerHTML = '';
629
- DOM.hide(empty);
630
-
631
- try {
632
- const result = await homebridge.request('/devices/list', { mode: AuthMode.current });
633
- DOM.hide(loading);
634
-
635
- if (!result.success) {
636
- empty.innerHTML = `<p class="mb-0">${result.message || 'No devices found. Authenticate first to see your devices.'}</p>`;
637
- DOM.show(empty);
638
- return;
639
- }
640
-
641
- if (!result.devices.length) {
642
- empty.innerHTML = '<p class="mb-0">No devices found. Authenticate first to see your devices.</p>';
643
- DOM.show(empty);
644
- return;
645
- }
646
-
647
- this.devices = result.devices;
648
- list.innerHTML = this.devices.map((d, i) => this.renderDeviceToggle(d, i)).join('');
649
-
650
- list.addEventListener('change', (e) => {
651
- if (e.target.classList.contains('device-visibility-toggle')) {
652
- const idx = parseInt(e.target.dataset.index, 10);
653
- const device = this.devices[idx];
654
- if (device) this.toggleDevice(device.id, e.target.checked, idx);
655
- }
656
- });
657
- } catch (error) {
658
- DOM.hide(loading);
659
- empty.innerHTML = `<p class="mb-0">Failed to load: ${error.message}</p>`;
660
- DOM.show(empty);
571
+ devices: [],
572
+ excludedIds: [],
573
+ saveTimeout: null,
574
+
575
+ FEATURE_KEYS: [
576
+ 'showPowerfulMode', 'showEconoMode', 'showStreamerMode',
577
+ 'showOutdoorSilentMode', 'showIndoorSilentMode', 'showDryMode', 'showFanOnlyMode',
578
+ ],
579
+
580
+ async load() {
581
+ try {
582
+ const config = await homebridge.getPluginConfig();
583
+ if (config?.[0]) {
584
+ this.populateForm(config[0]);
585
+ this.excludedIds = config[0].excludedDevicesByDeviceId || [];
586
+ }
587
+ await this.loadDeviceToggles();
588
+ this.setupAutoSave();
589
+ } catch (error) {
590
+ console.error('Settings load failed:', error);
591
+ }
592
+ },
593
+
594
+ setupAutoSave() {
595
+ const handler = () => this.autoSave();
596
+
597
+ // Feature toggles + WebSocket
598
+ [...this.FEATURE_KEYS, 'enableWebSocket'].forEach(id => {
599
+ $id(id)?.addEventListener('change', handler);
600
+ });
601
+
602
+ // Number/text inputs
603
+ ['updateIntervalInMinutes', 'forceUpdateDelay', 'oidcCallbackServerBindAddr'].forEach(id => {
604
+ const el = $id(id);
605
+ el?.addEventListener('change', handler);
606
+ el?.addEventListener('input', handler);
607
+ });
608
+ },
609
+
610
+ autoSave() {
611
+ clearTimeout(this.saveTimeout);
612
+ this.saveTimeout = setTimeout(() => {
613
+ if (this.validateInputs()) {
614
+ this.save();
615
+ }
616
+ }, 500);
617
+ },
618
+
619
+ validateInputs() {
620
+ const validators = [
621
+ ['oidcCallbackServerBindAddr', (v) => Utils.isValidIPv4(v.trim())],
622
+ ['updateIntervalInMinutes', (v) => Utils.isValidNumber(v, 1, 60)],
623
+ ['forceUpdateDelay', (v) => Utils.isValidNumber(v, 1, 300)],
624
+ ];
625
+
626
+ return validators.every(([id, validate]) => {
627
+ const el = $id(id);
628
+ const valid = !el || validate(el.value);
629
+ DOM.setValid(el, valid);
630
+ return valid;
631
+ });
632
+ },
633
+
634
+ populateForm(config) {
635
+ const showAllLegacy = config.showExtraFeatures === true;
636
+
637
+ this.FEATURE_KEYS.forEach(key => {
638
+ const el = $id(key);
639
+ if (el) {
640
+ el.checked = key in config ? config[key] === true : showAllLegacy;
641
+ }
642
+ });
643
+
644
+ const updateInterval = $id('updateIntervalInMinutes');
645
+ if (updateInterval) {
646
+ updateInterval.value = config.updateIntervalInMinutes || 15;
647
+ }
648
+
649
+ const forceDelay = $id('forceUpdateDelay');
650
+ if (forceDelay) {
651
+ forceDelay.value = Math.round((config.forceUpdateDelay || 60000) / 1000);
652
+ }
653
+
654
+ const bindAddr = $id('oidcCallbackServerBindAddr');
655
+ if (bindAddr) {
656
+ bindAddr.value = config.oidcCallbackServerBindAddr || '0.0.0.0';
657
+ }
658
+
659
+ const enableWS = $id('enableWebSocket');
660
+ if (enableWS) {
661
+ enableWS.checked = config.enableWebSocket !== false;
662
+ }
663
+ },
664
+
665
+ async loadDeviceToggles() {
666
+ const loading = $id('device-toggles-loading');
667
+ const list = $id('device-toggles-list');
668
+ const empty = $id('device-toggles-empty');
669
+
670
+ DOM.show(loading);
671
+ list.innerHTML = '';
672
+ DOM.hide(empty);
673
+
674
+ try {
675
+ const result = await homebridge.request('/devices/list', { mode: AuthMode.current });
676
+ DOM.hide(loading);
677
+
678
+ if (!result.success) {
679
+ empty.innerHTML = `<p class="mb-0">${result.message || 'No devices found. Authenticate first to see your devices.'}</p>`;
680
+ DOM.show(empty);
681
+ return;
682
+ }
683
+
684
+ if (!result.devices.length) {
685
+ empty.innerHTML = '<p class="mb-0">No devices found. Authenticate first to see your devices.</p>';
686
+ DOM.show(empty);
687
+ return;
688
+ }
689
+
690
+ this.devices = result.devices;
691
+ list.innerHTML = this.devices.map((d, i) => this.renderDeviceToggle(d, i)).join('');
692
+
693
+ list.addEventListener('change', (e) => {
694
+ if (e.target.classList.contains('device-visibility-toggle')) {
695
+ const idx = parseInt(e.target.dataset.index, 10);
696
+ const device = this.devices[idx];
697
+ if (device) {
698
+ this.toggleDevice(device.id, e.target.checked, idx);
699
+ }
661
700
  }
662
- },
663
-
664
- renderDeviceToggle(device, index) {
665
- const visible = !this.excludedIds.includes(device.id);
666
- return `
701
+ });
702
+ } catch (error) {
703
+ DOM.hide(loading);
704
+ empty.innerHTML = `<p class="mb-0">Failed to load: ${error.message}</p>`;
705
+ DOM.show(empty);
706
+ }
707
+ },
708
+
709
+ renderDeviceToggle(device, index) {
710
+ const visible = !this.excludedIds.includes(device.id);
711
+ return `
667
712
  <div class="list-group-item d-flex justify-content-between align-items-center">
668
713
  <div>
669
714
  <div class="fw-medium">${Utils.escapeHtml(device.name)}</div>
@@ -676,66 +721,72 @@ const Settings = {
676
721
  </div>
677
722
  </div>
678
723
  </div>`;
679
- },
680
-
681
- toggleDevice(deviceId, visible, index) {
682
- const label = $(`[data-label-index="${index}"]`);
683
-
684
- if (visible) {
685
- this.excludedIds = this.excludedIds.filter(id => id !== deviceId);
686
- } else if (!this.excludedIds.includes(deviceId)) {
687
- this.excludedIds.push(deviceId);
688
- }
689
-
690
- if (label) {
691
- label.textContent = visible ? 'Visible' : 'Hidden';
692
- label.className = `device-toggle-label small ${visible ? 'visible' : 'hidden-label'}`;
693
- }
724
+ },
725
+
726
+ toggleDevice(deviceId, visible, index) {
727
+ const label = $(`[data-label-index="${index}"]`);
728
+
729
+ if (visible) {
730
+ this.excludedIds = this.excludedIds.filter(id => id !== deviceId);
731
+ } else if (!this.excludedIds.includes(deviceId)) {
732
+ this.excludedIds.push(deviceId);
733
+ }
734
+
735
+ if (label) {
736
+ label.textContent = visible ? 'Visible' : 'Hidden';
737
+ label.className = `device-toggle-label small ${visible ? 'visible' : 'hidden-label'}`;
738
+ }
739
+
740
+ this.autoSave();
741
+ },
742
+
743
+ getFormSettings() {
744
+ const settings = {
745
+ updateIntervalInMinutes: parseInt($id('updateIntervalInMinutes')?.value, 10) || 15,
746
+ forceUpdateDelay: (parseInt($id('forceUpdateDelay')?.value, 10) || 60) * 1000,
747
+ oidcCallbackServerBindAddr: $id('oidcCallbackServerBindAddr')?.value?.trim() || '0.0.0.0',
748
+ excludedDevicesByDeviceId: this.excludedIds,
749
+ enableWebSocket: $id('enableWebSocket')?.checked ?? true,
750
+ };
694
751
 
695
- this.autoSave();
696
- },
697
-
698
- getFormSettings() {
699
- const settings = {
700
- updateIntervalInMinutes: parseInt($id('updateIntervalInMinutes')?.value, 10) || 15,
701
- forceUpdateDelay: (parseInt($id('forceUpdateDelay')?.value, 10) || 60) * 1000,
702
- oidcCallbackServerBindAddr: $id('oidcCallbackServerBindAddr')?.value?.trim() || '0.0.0.0',
703
- excludedDevicesByDeviceId: this.excludedIds,
704
- enableWebSocket: $id('enableWebSocket')?.checked ?? true,
705
- };
706
-
707
- this.FEATURE_KEYS.forEach(key => {
708
- const el = $id(key);
709
- if (el) settings[key] = el.checked;
710
- });
711
-
712
- return settings;
713
- },
714
-
715
- async save() {
716
- const status = $id('settings-status');
717
-
718
- try {
719
- this.showStatus(status, 'saving', 'Saving...');
720
- const config = await homebridge.getPluginConfig();
721
- const platformConfig = config[0] || { platform: 'DaikinCloud' };
722
- Object.assign(platformConfig, this.getFormSettings());
723
- await homebridge.updatePluginConfig([platformConfig]);
724
- await homebridge.savePluginConfig();
725
- this.showStatus(status, 'saved', 'Saved');
726
- } catch (error) {
727
- this.showStatus(status, 'error', 'Failed');
728
- console.error('Settings save failed:', error);
729
- }
730
- },
731
-
732
- showStatus(el, type, message) {
733
- if (!el) return;
734
- el.className = `badge ${type}`;
735
- el.textContent = message;
736
- DOM.show(el);
737
- if (type === 'saved') setTimeout(() => DOM.hide(el), 2000);
738
- },
752
+ this.FEATURE_KEYS.forEach(key => {
753
+ const el = $id(key);
754
+ if (el) {
755
+ settings[key] = el.checked;
756
+ }
757
+ });
758
+
759
+ return settings;
760
+ },
761
+
762
+ async save() {
763
+ const status = $id('settings-status');
764
+
765
+ try {
766
+ this.showStatus(status, 'saving', 'Saving...');
767
+ const config = await homebridge.getPluginConfig();
768
+ const platformConfig = config[0] || { platform: 'DaikinCloud' };
769
+ Object.assign(platformConfig, this.getFormSettings());
770
+ await homebridge.updatePluginConfig([platformConfig]);
771
+ await homebridge.savePluginConfig();
772
+ this.showStatus(status, 'saved', 'Saved');
773
+ } catch (error) {
774
+ this.showStatus(status, 'error', 'Failed');
775
+ console.error('Settings save failed:', error);
776
+ }
777
+ },
778
+
779
+ showStatus(el, type, message) {
780
+ if (!el) {
781
+ return;
782
+ }
783
+ el.className = `badge ${type}`;
784
+ el.textContent = message;
785
+ DOM.show(el);
786
+ if (type === 'saved') {
787
+ setTimeout(() => DOM.hide(el), 2000);
788
+ }
789
+ },
739
790
  };
740
791
 
741
792
  // ============================================================================
@@ -743,56 +794,56 @@ const Settings = {
743
794
  // ============================================================================
744
795
 
745
796
  const Devices = {
746
- async load() {
747
- DOM.show(El.devicesLoading);
748
- El.devicesList.innerHTML = '';
749
- DOM.hide(El.devicesEmpty);
750
- DOM.hide(El.devicesError);
751
-
752
- try {
753
- const result = await homebridge.request('/devices/list', { mode: AuthMode.current });
754
- DOM.hide(El.devicesLoading);
755
-
756
- if (!result.success) {
757
- this.handleError(result);
758
- return;
759
- }
760
-
761
- if (!result.devices.length) {
762
- DOM.show(El.devicesEmpty);
763
- return;
764
- }
765
-
766
- El.devicesList.innerHTML = result.devices.map(d => this.render(d)).join('');
767
- } catch (error) {
768
- DOM.hide(El.devicesLoading);
769
- El.devicesError.textContent = 'Failed to load: ' + error.message;
770
- DOM.show(El.devicesError);
771
- }
772
- },
773
-
774
- handleError(result) {
775
- if (result.message?.includes('Not authenticated')) {
776
- El.devicesEmpty.innerHTML = `
797
+ async load() {
798
+ DOM.show(El.devicesLoading);
799
+ El.devicesList.innerHTML = '';
800
+ DOM.hide(El.devicesEmpty);
801
+ DOM.hide(El.devicesError);
802
+
803
+ try {
804
+ const result = await homebridge.request('/devices/list', { mode: AuthMode.current });
805
+ DOM.hide(El.devicesLoading);
806
+
807
+ if (!result.success) {
808
+ this.handleError(result);
809
+ return;
810
+ }
811
+
812
+ if (!result.devices.length) {
813
+ DOM.show(El.devicesEmpty);
814
+ return;
815
+ }
816
+
817
+ El.devicesList.innerHTML = result.devices.map(d => this.render(d)).join('');
818
+ } catch (error) {
819
+ DOM.hide(El.devicesLoading);
820
+ El.devicesError.textContent = 'Failed to load: ' + error.message;
821
+ DOM.show(El.devicesError);
822
+ }
823
+ },
824
+
825
+ handleError(result) {
826
+ if (result.message?.includes('Not authenticated')) {
827
+ El.devicesEmpty.innerHTML = `
777
828
  <div class="fs-1 mb-2 opacity-50">🔐</div>
778
829
  <p class="mb-1">Please authenticate first</p>
779
830
  <p class="text-muted small">Go to Authentication tab to connect.</p>`;
780
- DOM.show(El.devicesEmpty);
781
- } else {
782
- El.devicesError.textContent = result.message;
783
- DOM.show(El.devicesError);
784
- }
785
- },
786
-
787
- render(device) {
788
- const online = device.online;
789
- const powerOn = device.powerState === 'on';
790
- const mode = device.operationMode ? Utils.capitalize(device.operationMode) : '-';
791
- const features = device.features?.length
792
- ? `<div class="mt-2">${device.features.map(f => `<span class="badge bg-secondary me-1">${f}</span>`).join('')}</div>`
793
- : '';
794
-
795
- return `
831
+ DOM.show(El.devicesEmpty);
832
+ } else {
833
+ El.devicesError.textContent = result.message;
834
+ DOM.show(El.devicesError);
835
+ }
836
+ },
837
+
838
+ render(device) {
839
+ const online = device.online;
840
+ const powerOn = device.powerState === 'on';
841
+ const mode = device.operationMode ? Utils.capitalize(device.operationMode) : '-';
842
+ const features = device.features?.length
843
+ ? `<div class="mt-2">${device.features.map(f => `<span class="badge bg-secondary me-1">${f}</span>`).join('')}</div>`
844
+ : '';
845
+
846
+ return `
796
847
  <div class="list-group-item">
797
848
  <div class="d-flex justify-content-between align-items-center mb-2">
798
849
  <div class="fw-semibold">${Utils.escapeHtml(device.name)}</div>
@@ -809,9 +860,11 @@ const Devices = {
809
860
  </div>
810
861
  ${features}
811
862
  </div>`;
812
- },
863
+ },
813
864
 
814
- refresh() { this.load(); },
865
+ refresh() {
866
+ this.load();
867
+ },
815
868
  };
816
869
 
817
870
  // ============================================================================
@@ -819,26 +872,26 @@ const Devices = {
819
872
  // ============================================================================
820
873
 
821
874
  const RateLimit = {
822
- async check() {
823
- const display = El.rateLimitDisplay;
824
- display.textContent = 'Checking...';
825
-
826
- try {
827
- const result = await homebridge.request('/api/rate-limit', { mode: AuthMode.current });
828
- if (result.success && result.rateLimit) {
829
- const { limitDay, remainingDay, limitMinute, remainingMinute } = result.rateLimit;
830
- let text = remainingDay !== undefined ? `${remainingDay}/${limitDay} daily` : '';
831
- if (remainingMinute !== undefined) {
832
- text += text ? `, ${remainingMinute}/${limitMinute}/min` : `${remainingMinute}/${limitMinute}/min`;
833
- }
834
- display.textContent = text || 'No rate limit headers';
835
- } else {
836
- display.textContent = result.message || 'No info';
837
- }
838
- } catch (error) {
839
- display.textContent = 'Error: ' + error.message;
875
+ async check() {
876
+ const display = El.rateLimitDisplay;
877
+ display.textContent = 'Checking...';
878
+
879
+ try {
880
+ const result = await homebridge.request('/api/rate-limit', { mode: AuthMode.current });
881
+ if (result.success && result.rateLimit) {
882
+ const { limitDay, remainingDay, limitMinute, remainingMinute } = result.rateLimit;
883
+ let text = remainingDay !== undefined ? `${remainingDay}/${limitDay} daily` : '';
884
+ if (remainingMinute !== undefined) {
885
+ text += text ? `, ${remainingMinute}/${limitMinute}/min` : `${remainingMinute}/${limitMinute}/min`;
840
886
  }
841
- },
887
+ display.textContent = text || 'No rate limit headers';
888
+ } else {
889
+ display.textContent = result.message || 'No info';
890
+ }
891
+ } catch (error) {
892
+ display.textContent = 'Error: ' + error.message;
893
+ }
894
+ },
842
895
  };
843
896
 
844
897
  // ============================================================================
@@ -846,85 +899,89 @@ const RateLimit = {
846
899
  // ============================================================================
847
900
 
848
901
  const MobileAuth = {
849
- show() {
850
- DOM.show($id('mobile-auth-form'));
851
- DOM.hide($id('auth-status-card'));
852
- this.loadCredentials();
853
- },
854
-
855
- hide() {
856
- DOM.hide($id('mobile-auth-form'));
857
- DOM.show($id('auth-status-card'));
858
- DOM.hide($id('mobile-auth-errors'));
859
- DOM.hide($id('mobile-auth-success'));
860
- },
861
-
862
- async loadCredentials() {
863
- try {
864
- const config = await homebridge.getPluginConfig();
865
- if (config?.[0]) {
866
- const email = $id('daikinEmail');
867
- const pass = $id('daikinPassword');
868
- if (email && config[0].daikinEmail) email.value = config[0].daikinEmail;
869
- if (pass && config[0].daikinPassword) pass.value = config[0].daikinPassword;
870
- }
871
- } catch (error) {
872
- console.error('Credential load failed:', error);
902
+ show() {
903
+ DOM.show($id('mobile-auth-form'));
904
+ DOM.hide($id('auth-status-card'));
905
+ this.loadCredentials();
906
+ },
907
+
908
+ hide() {
909
+ DOM.hide($id('mobile-auth-form'));
910
+ DOM.show($id('auth-status-card'));
911
+ DOM.hide($id('mobile-auth-errors'));
912
+ DOM.hide($id('mobile-auth-success'));
913
+ },
914
+
915
+ async loadCredentials() {
916
+ try {
917
+ const config = await homebridge.getPluginConfig();
918
+ if (config?.[0]) {
919
+ const email = $id('daikinEmail');
920
+ const pass = $id('daikinPassword');
921
+ if (email && config[0].daikinEmail) {
922
+ email.value = config[0].daikinEmail;
873
923
  }
874
- },
875
-
876
- async test() {
877
- const email = $id('daikinEmail')?.value.trim();
878
- const password = $id('daikinPassword')?.value;
879
- const errors = $id('mobile-auth-errors');
880
- const success = $id('mobile-auth-success');
881
- const btn = $id('btn-test-mobile-auth');
882
-
883
- DOM.hide(errors);
884
- DOM.hide(success);
885
-
886
- if (!email || !password) {
887
- errors.textContent = 'Please enter email and password';
888
- DOM.show(errors);
889
- return;
924
+ if (pass && config[0].daikinPassword) {
925
+ pass.value = config[0].daikinPassword;
890
926
  }
891
-
892
- UI.setButtonLoading(btn, true, 'Testing...');
893
-
894
- try {
895
- const result = await homebridge.request('/auth/mobile-test', { email, password });
896
-
897
- if (result.success) {
898
- await this.saveCredentials(email, password);
899
- success.innerHTML = `
927
+ }
928
+ } catch (error) {
929
+ console.error('Credential load failed:', error);
930
+ }
931
+ },
932
+
933
+ async test() {
934
+ const email = $id('daikinEmail')?.value.trim();
935
+ const password = $id('daikinPassword')?.value;
936
+ const errors = $id('mobile-auth-errors');
937
+ const success = $id('mobile-auth-success');
938
+ const btn = $id('btn-test-mobile-auth');
939
+
940
+ DOM.hide(errors);
941
+ DOM.hide(success);
942
+
943
+ if (!email || !password) {
944
+ errors.textContent = 'Please enter email and password';
945
+ DOM.show(errors);
946
+ return;
947
+ }
948
+
949
+ UI.setButtonLoading(btn, true, 'Testing...');
950
+
951
+ try {
952
+ const result = await homebridge.request('/auth/mobile-test', { email, password });
953
+
954
+ if (result.success) {
955
+ await this.saveCredentials(email, password);
956
+ success.innerHTML = `
900
957
  <strong>Success!</strong> Found ${result.deviceCount || 0} device(s).<br>
901
- Rate limit: ${result.rateLimit?.remainingDay || '?'}/${result.rateLimit?.limitDay || '5000'}/day<br>
958
+ Rate limit: ${result.rateLimit?.remainingDay || '?'}/${result.rateLimit?.limitDay || '3000'}/day<br>
902
959
  <small>Restart Homebridge to apply.</small>`;
903
- DOM.show(success);
904
- setTimeout(() => {
905
- this.hide();
906
- Auth.loadStatus();
907
- Devices.load();
908
- }, 2000);
909
- } else {
910
- errors.textContent = result.message || 'Authentication failed';
911
- DOM.show(errors);
912
- }
913
- } catch (error) {
914
- errors.textContent = 'Failed: ' + error.message;
915
- DOM.show(errors);
916
- }
917
-
918
- UI.setButtonLoading(btn, false, null, 'Test & Save Credentials');
919
- },
920
-
921
- async saveCredentials(email, password) {
922
- const config = await homebridge.getPluginConfig();
923
- const platformConfig = config[0] || { platform: 'DaikinCloud' };
924
- Object.assign(platformConfig, { authMode: 'mobile_app', daikinEmail: email, daikinPassword: password });
925
- await homebridge.updatePluginConfig([platformConfig]);
926
- await homebridge.savePluginConfig();
927
- },
960
+ DOM.show(success);
961
+ setTimeout(() => {
962
+ this.hide();
963
+ Auth.loadStatus();
964
+ Devices.load();
965
+ }, 2000);
966
+ } else {
967
+ errors.textContent = result.message || 'Authentication failed';
968
+ DOM.show(errors);
969
+ }
970
+ } catch (error) {
971
+ errors.textContent = 'Failed: ' + error.message;
972
+ DOM.show(errors);
973
+ }
974
+
975
+ UI.setButtonLoading(btn, false, null, 'Test & Save Credentials');
976
+ },
977
+
978
+ async saveCredentials(email, password) {
979
+ const config = await homebridge.getPluginConfig();
980
+ const platformConfig = config[0] || { platform: 'DaikinCloud' };
981
+ Object.assign(platformConfig, { authMode: 'mobile_app', daikinEmail: email, daikinPassword: password });
982
+ await homebridge.updatePluginConfig([platformConfig]);
983
+ await homebridge.savePluginConfig();
984
+ },
928
985
  };
929
986
 
930
987
  // ============================================================================
@@ -932,94 +989,96 @@ const MobileAuth = {
932
989
  // ============================================================================
933
990
 
934
991
  const AuthMode = {
935
- current: 'developer_portal',
936
- previous: 'developer_portal',
937
-
938
- DEFAULTS: {
939
- developer_portal: { updateIntervalInMinutes: 15, forceUpdateDelay: 60, enableWebSocket: false },
940
- mobile_app: { updateIntervalInMinutes: 5, forceUpdateDelay: 10, enableWebSocket: true },
941
- },
942
-
943
- async init() {
944
- try {
945
- const config = await homebridge.getPluginConfig();
946
- if (config?.[0]?.authMode) {
947
- this.current = this.previous = config[0].authMode;
948
- }
949
- } catch (error) {
950
- console.error('AuthMode init failed:', error);
951
- }
952
- this.updateUI();
953
- },
954
-
955
- onChange() {
956
- const select = $id('authMode');
957
- if (select) {
958
- this.previous = this.current;
959
- this.current = select.value;
960
- this.updateUI();
961
- this.updateDefaults();
962
- this.save();
963
- }
964
- },
965
-
966
- updateUI() {
967
- const isMobile = this.current === 'mobile_app';
968
-
969
- $id('authMode').value = this.current;
970
- $id('auth-mode-hint').textContent = isMobile
971
- ? 'Use your Daikin Onecta account (same as mobile app)'
972
- : 'Requires API credentials from the Daikin Developer Portal';
973
-
974
- DOM.toggle($id('btn-authenticate'), !isMobile);
975
- DOM.toggle($id('btn-authenticate-mobile'), isMobile);
976
-
977
- $id('auth-mode-text').textContent = isMobile ? 'Mobile App' : 'Developer Portal';
978
- $id('rate-limit-display').textContent = isMobile ? '5000 requests/day' : '200 requests/day';
979
- $id('rate-limit-info').textContent = `The Daikin API limits you to ${isMobile ? '5000' : '200'} requests per day.`;
980
-
981
- DOM.toggle($id('websocket-setting-row'), isMobile);
982
-
983
- this.updateHints();
984
- },
985
-
986
- updateHints() {
987
- const isMobile = this.current === 'mobile_app';
988
- const interval = $id('updateIntervalInMinutes');
989
- const delay = $id('forceUpdateDelay');
990
-
991
- if (interval) {
992
- interval.placeholder = isMobile ? '5 (recommended)' : '15 (recommended)';
993
- interval.title = isMobile ? '1-5 min (5000 calls/day)' : '15+ min (200 calls/day)';
994
- }
995
- if (delay) {
996
- delay.placeholder = isMobile ? '10 (recommended)' : '60 (recommended)';
997
- delay.title = isMobile ? '10s recommended' : '60s recommended';
998
- }
999
- },
1000
-
1001
- updateDefaults() {
1002
- if (this.current === this.previous) return;
1003
-
1004
- const defaults = this.DEFAULTS[this.current];
1005
- $id('updateIntervalInMinutes').value = defaults.updateIntervalInMinutes;
1006
- $id('forceUpdateDelay').value = defaults.forceUpdateDelay;
1007
- $id('enableWebSocket').checked = defaults.enableWebSocket;
1008
-
1009
- Settings.autoSave();
1010
- },
1011
-
1012
- async save() {
1013
- try {
1014
- const config = await homebridge.getPluginConfig();
1015
- const platformConfig = config[0] || { platform: 'DaikinCloud' };
1016
- platformConfig.authMode = this.current;
1017
- await homebridge.updatePluginConfig([platformConfig]);
1018
- await homebridge.savePluginConfig();
1019
- } catch (error) {
1020
- console.error('AuthMode save failed:', error);
1021
- }
1022
- },
992
+ current: 'developer_portal',
993
+ previous: 'developer_portal',
994
+
995
+ DEFAULTS: {
996
+ developer_portal: { updateIntervalInMinutes: 15, forceUpdateDelay: 60, enableWebSocket: false },
997
+ mobile_app: { updateIntervalInMinutes: 5, forceUpdateDelay: 10, enableWebSocket: true },
998
+ },
999
+
1000
+ async init() {
1001
+ try {
1002
+ const config = await homebridge.getPluginConfig();
1003
+ if (config?.[0]?.authMode) {
1004
+ this.current = this.previous = config[0].authMode;
1005
+ }
1006
+ } catch (error) {
1007
+ console.error('AuthMode init failed:', error);
1008
+ }
1009
+ this.updateUI();
1010
+ },
1011
+
1012
+ onChange() {
1013
+ const select = $id('authMode');
1014
+ if (select) {
1015
+ this.previous = this.current;
1016
+ this.current = select.value;
1017
+ this.updateUI();
1018
+ this.updateDefaults();
1019
+ this.save();
1020
+ }
1021
+ },
1022
+
1023
+ updateUI() {
1024
+ const isMobile = this.current === 'mobile_app';
1025
+
1026
+ $id('authMode').value = this.current;
1027
+ $id('auth-mode-hint').textContent = isMobile
1028
+ ? 'Use your Daikin Onecta account (same as mobile app)'
1029
+ : 'Requires API credentials from the Daikin Developer Portal';
1030
+
1031
+ DOM.toggle($id('btn-authenticate'), !isMobile);
1032
+ DOM.toggle($id('btn-authenticate-mobile'), isMobile);
1033
+
1034
+ $id('auth-mode-text').textContent = isMobile ? 'Mobile App' : 'Developer Portal';
1035
+ $id('rate-limit-display').textContent = isMobile ? '3000 requests/day' : '200 requests/day';
1036
+ $id('rate-limit-info').textContent = `The Daikin API limits you to ${isMobile ? '3000' : '200'} requests per day.`;
1037
+
1038
+ DOM.toggle($id('websocket-setting-row'), isMobile);
1039
+
1040
+ this.updateHints();
1041
+ },
1042
+
1043
+ updateHints() {
1044
+ const isMobile = this.current === 'mobile_app';
1045
+ const interval = $id('updateIntervalInMinutes');
1046
+ const delay = $id('forceUpdateDelay');
1047
+
1048
+ if (interval) {
1049
+ interval.placeholder = isMobile ? '5 (recommended)' : '15 (recommended)';
1050
+ interval.title = isMobile ? '1-5 min (3000 calls/day)' : '15+ min (200 calls/day)';
1051
+ }
1052
+ if (delay) {
1053
+ delay.placeholder = isMobile ? '10 (recommended)' : '60 (recommended)';
1054
+ delay.title = isMobile ? '10s recommended' : '60s recommended';
1055
+ }
1056
+ },
1057
+
1058
+ updateDefaults() {
1059
+ if (this.current === this.previous) {
1060
+ return;
1061
+ }
1062
+
1063
+ const defaults = this.DEFAULTS[this.current];
1064
+ $id('updateIntervalInMinutes').value = defaults.updateIntervalInMinutes;
1065
+ $id('forceUpdateDelay').value = defaults.forceUpdateDelay;
1066
+ $id('enableWebSocket').checked = defaults.enableWebSocket;
1067
+
1068
+ Settings.autoSave();
1069
+ },
1070
+
1071
+ async save() {
1072
+ try {
1073
+ const config = await homebridge.getPluginConfig();
1074
+ const platformConfig = config[0] || { platform: 'DaikinCloud' };
1075
+ platformConfig.authMode = this.current;
1076
+ await homebridge.updatePluginConfig([platformConfig]);
1077
+ await homebridge.savePluginConfig();
1078
+ } catch (error) {
1079
+ console.error('AuthMode save failed:', error);
1080
+ }
1081
+ },
1023
1082
  };
1024
1083
 
1025
1084
  // ============================================================================
@@ -1047,42 +1106,42 @@ const onAuthModeChange = () => AuthMode.onChange();
1047
1106
  // ============================================================================
1048
1107
 
1049
1108
  function cacheElements() {
1050
- El = {
1051
- statusBadge: $id('status-badge'),
1052
- statusIndicator: $id('status-indicator'),
1053
- statusText: $id('status-text'),
1054
- tokenExpiresLabel: $id('token-expires-label'),
1055
- tokenExpires: $id('token-expires'),
1056
- authStatus: $id('auth-status'),
1057
- authTokenExpires: $id('auth-token-expires'),
1058
- expiresRow: $id('expires-row'),
1059
- btnAuthenticate: $id('btn-authenticate'),
1060
- btnRevoke: $id('btn-revoke'),
1061
- btnTest: $id('btn-test'),
1062
- testResult: $id('test-result'),
1063
- wizard: $id('wizard'),
1064
- authStatusCard: $id('auth-status-card'),
1065
- validationErrors: $id('validation-errors'),
1066
- authUrlContainer: $id('auth-url-container'),
1067
- authUrlDisplay: $id('auth-url'),
1068
- callbackServerStatus: $id('callback-server-status'),
1069
- successMessage: $id('success-message'),
1070
- devicesLoading: $id('devices-loading'),
1071
- devicesList: $id('devices-list'),
1072
- devicesEmpty: $id('devices-empty'),
1073
- devicesError: $id('devices-error'),
1074
- settingsStatus: $id('settings-status'),
1075
- rateLimitDisplay: $id('rate-limit-display'),
1076
- loading: $id('loading'),
1077
- globalError: $id('global-error'),
1078
- };
1109
+ El = {
1110
+ statusBadge: $id('status-badge'),
1111
+ statusIndicator: $id('status-indicator'),
1112
+ statusText: $id('status-text'),
1113
+ tokenExpiresLabel: $id('token-expires-label'),
1114
+ tokenExpires: $id('token-expires'),
1115
+ authStatus: $id('auth-status'),
1116
+ authTokenExpires: $id('auth-token-expires'),
1117
+ expiresRow: $id('expires-row'),
1118
+ btnAuthenticate: $id('btn-authenticate'),
1119
+ btnRevoke: $id('btn-revoke'),
1120
+ btnTest: $id('btn-test'),
1121
+ testResult: $id('test-result'),
1122
+ wizard: $id('wizard'),
1123
+ authStatusCard: $id('auth-status-card'),
1124
+ validationErrors: $id('validation-errors'),
1125
+ authUrlContainer: $id('auth-url-container'),
1126
+ authUrlDisplay: $id('auth-url'),
1127
+ callbackServerStatus: $id('callback-server-status'),
1128
+ successMessage: $id('success-message'),
1129
+ devicesLoading: $id('devices-loading'),
1130
+ devicesList: $id('devices-list'),
1131
+ devicesEmpty: $id('devices-empty'),
1132
+ devicesError: $id('devices-error'),
1133
+ settingsStatus: $id('settings-status'),
1134
+ rateLimitDisplay: $id('rate-limit-display'),
1135
+ loading: $id('loading'),
1136
+ globalError: $id('global-error'),
1137
+ };
1079
1138
  }
1080
1139
 
1081
1140
  document.addEventListener('DOMContentLoaded', async () => {
1082
- cacheElements();
1083
- Auth.loadStatus();
1084
- Config.load();
1085
- await AuthMode.init();
1086
- Settings.load();
1087
- Devices.load();
1141
+ cacheElements();
1142
+ Auth.loadStatus();
1143
+ Config.load();
1144
+ await AuthMode.init();
1145
+ Settings.load();
1146
+ Devices.load();
1088
1147
  });