@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
@@ -24,51 +24,69 @@ const CLIMATE_CONTROL_IDS = ['climateControl', 'climateControlMainZone', 'climat
24
24
  // =============================================================================
25
25
 
26
26
  const SSLUtils = {
27
- isIPAddress(str) {
28
- const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
29
- const ipv6Pattern = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/;
30
- return ipv4Pattern.test(str) || ipv6Pattern.test(str);
31
- },
32
-
33
- generateCert(hostname, certDir) {
34
- const keyPath = resolve(certDir, 'server.key');
35
- const certPath = resolve(certDir, 'server.crt');
36
-
37
- if (!fs.existsSync(certDir)) {
38
- fs.mkdirSync(certDir, { recursive: true });
39
- }
27
+ isIPAddress(str) {
28
+ const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
29
+ const ipv6Pattern = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/;
30
+ return ipv4Pattern.test(str) || ipv6Pattern.test(str);
31
+ },
32
+
33
+ /**
34
+ * Validate that a hostname is safe for use in shell commands.
35
+ * Only allows alphanumeric, dots, hyphens, and colons (for IPv6).
36
+ */
37
+ validateHostname(hostname) {
38
+ if (!hostname || typeof hostname !== 'string') {
39
+ throw new Error('Hostname is required');
40
+ }
41
+ if (!/^[a-zA-Z0-9.:_-]+$/.test(hostname)) {
42
+ throw new Error('Invalid hostname: contains disallowed characters');
43
+ }
44
+ if (hostname.length > 253) {
45
+ throw new Error('Hostname too long (max 253 characters)');
46
+ }
47
+ },
40
48
 
41
- if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
42
- try {
43
- return {
44
- key: fs.readFileSync(keyPath, 'utf8'),
45
- cert: fs.readFileSync(certPath, 'utf8'),
46
- };
47
- } catch (e) {
48
- // Regenerate if can't read
49
- }
50
- }
49
+ generateCert(hostname, certDir) {
50
+ this.validateHostname(hostname);
51
51
 
52
- try {
53
- execSync(`openssl genrsa -out "${keyPath}" 2048`, { stdio: 'pipe' });
52
+ const keyPath = resolve(certDir, 'server.key');
53
+ const certPath = resolve(certDir, 'server.crt');
54
+
55
+ if (!fs.existsSync(certDir)) {
56
+ fs.mkdirSync(certDir, { recursive: true });
57
+ }
58
+
59
+ if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
60
+ try {
61
+ return {
62
+ key: fs.readFileSync(keyPath, 'utf8'),
63
+ cert: fs.readFileSync(certPath, 'utf8'),
64
+ };
65
+ } catch (e) {
66
+ // Regenerate if can't read
67
+ }
68
+ }
54
69
 
55
- const isIP = this.isIPAddress(hostname);
56
- const sanValue = isIP ? `IP:${hostname}` : `DNS:${hostname}`;
57
- const subj = `/CN=${hostname}/O=Homebridge Daikin Cloud/C=US`;
70
+ try {
71
+ execSync(`openssl genrsa -out "${keyPath}" 2048`, { stdio: 'pipe' });
58
72
 
59
- execSync(
60
- `openssl req -new -x509 -key "${keyPath}" -out "${certPath}" -days 365 -subj "${subj}" -addext "subjectAltName=${sanValue}"`,
61
- { stdio: 'pipe' }
62
- );
73
+ const isIP = this.isIPAddress(hostname);
74
+ const sanValue = isIP ? `IP:${hostname}` : `DNS:${hostname}`;
75
+ const subj = `/CN=${hostname}/O=Homebridge Daikin Cloud/C=US`;
63
76
 
64
- return {
65
- key: fs.readFileSync(keyPath, 'utf8'),
66
- cert: fs.readFileSync(certPath, 'utf8'),
67
- };
68
- } catch (error) {
69
- throw new Error(`Failed to generate SSL certificate: ${error.message}`);
70
- }
71
- },
77
+ execSync(
78
+ `openssl req -new -x509 -key "${keyPath}" -out "${certPath}" -days 365 -subj "${subj}" -addext "subjectAltName=${sanValue}"`,
79
+ { stdio: 'pipe' },
80
+ );
81
+
82
+ return {
83
+ key: fs.readFileSync(keyPath, 'utf8'),
84
+ cert: fs.readFileSync(certPath, 'utf8'),
85
+ };
86
+ } catch (error) {
87
+ throw new Error(`Failed to generate SSL certificate: ${error.message}`);
88
+ }
89
+ },
72
90
  };
73
91
 
74
92
  // =============================================================================
@@ -76,48 +94,50 @@ const SSLUtils = {
76
94
  // =============================================================================
77
95
 
78
96
  const TokenManager = {
79
- load(filePath) {
80
- try {
81
- if (fs.existsSync(filePath)) {
82
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
83
- }
84
- } catch (error) {
85
- console.error('Error loading token set:', error.message);
86
- }
87
- return null;
88
- },
89
-
90
- save(filePath, tokenSet) {
91
- fs.writeFileSync(filePath, JSON.stringify(tokenSet, null, 2), 'utf8');
92
- },
93
-
94
- delete(filePath) {
95
- try {
96
- fs.unlinkSync(filePath);
97
- } catch (error) {
98
- if (error.code !== 'ENOENT') throw error;
99
- }
100
- },
101
-
102
- getStatus(tokenSet) {
103
- if (!tokenSet || !tokenSet.access_token) {
104
- return { authenticated: false, message: 'Not authenticated' };
105
- }
97
+ load(filePath) {
98
+ try {
99
+ if (fs.existsSync(filePath)) {
100
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
101
+ }
102
+ } catch (error) {
103
+ console.error('Error loading token set:', error.message);
104
+ }
105
+ return null;
106
+ },
107
+
108
+ save(filePath, tokenSet) {
109
+ fs.writeFileSync(filePath, JSON.stringify(tokenSet, null, 2), { encoding: 'utf8', mode: 0o600 });
110
+ },
111
+
112
+ delete(filePath) {
113
+ try {
114
+ fs.unlinkSync(filePath);
115
+ } catch (error) {
116
+ if (error.code !== 'ENOENT') {
117
+ throw error;
118
+ }
119
+ }
120
+ },
106
121
 
107
- const expiresAt = tokenSet.expires_at ? new Date(tokenSet.expires_at * 1000) : null;
108
- const isExpired = expiresAt ? expiresAt < new Date() : false;
109
- const hasRefreshToken = !!tokenSet.refresh_token;
122
+ getStatus(tokenSet) {
123
+ if (!tokenSet || !tokenSet.access_token) {
124
+ return { authenticated: false, message: 'Not authenticated' };
125
+ }
110
126
 
111
- return {
112
- authenticated: true,
113
- isExpired,
114
- canRefresh: hasRefreshToken,
115
- expiresAt: expiresAt ? expiresAt.toISOString() : null,
116
- message: isExpired
117
- ? (hasRefreshToken ? 'Token expired, will refresh automatically' : 'Token expired, re-authentication required')
118
- : 'Authenticated',
119
- };
120
- },
127
+ const expiresAt = tokenSet.expires_at ? new Date(tokenSet.expires_at * 1000) : null;
128
+ const isExpired = expiresAt ? expiresAt < new Date() : false;
129
+ const hasRefreshToken = !!tokenSet.refresh_token;
130
+
131
+ return {
132
+ authenticated: true,
133
+ isExpired,
134
+ canRefresh: hasRefreshToken,
135
+ expiresAt: expiresAt ? expiresAt.toISOString() : null,
136
+ message: isExpired
137
+ ? (hasRefreshToken ? 'Token expired, will refresh automatically' : 'Token expired, re-authentication required')
138
+ : 'Authenticated',
139
+ };
140
+ },
121
141
  };
122
142
 
123
143
  // =============================================================================
@@ -125,98 +145,122 @@ const TokenManager = {
125
145
  // =============================================================================
126
146
 
127
147
  const DeviceExtractor = {
128
- getManagementPoint(device, embeddedId) {
129
- return device.managementPoints?.find(mp => mp.embeddedId === embeddedId) || null;
130
- },
131
-
132
- getClimateControlPoint(device) {
133
- for (const id of CLIMATE_CONTROL_IDS) {
134
- const mp = this.getManagementPoint(device, id);
135
- if (mp) return mp;
136
- }
137
- return null;
138
- },
139
-
140
- extractName(device) {
141
- const climateControl = this.getClimateControlPoint(device);
142
- return climateControl?.name?.value || device.id || 'Unknown Device';
143
- },
144
-
145
- extractModel(device) {
146
- const gateway = this.getManagementPoint(device, 'gateway');
147
- return gateway?.modelInfo?.value || device.deviceModel || 'Unknown Model';
148
- },
148
+ getManagementPoint(device, embeddedId) {
149
+ return device.managementPoints?.find(mp => mp.embeddedId === embeddedId) || null;
150
+ },
151
+
152
+ getClimateControlPoint(device) {
153
+ for (const id of CLIMATE_CONTROL_IDS) {
154
+ const mp = this.getManagementPoint(device, id);
155
+ if (mp) {
156
+ return mp;
157
+ }
158
+ }
159
+ return null;
160
+ },
161
+
162
+ extractName(device) {
163
+ const climateControl = this.getClimateControlPoint(device);
164
+ return climateControl?.name?.value || device.id || 'Unknown Device';
165
+ },
166
+
167
+ extractModel(device) {
168
+ const gateway = this.getManagementPoint(device, 'gateway');
169
+ return gateway?.modelInfo?.value || device.deviceModel || 'Unknown Model';
170
+ },
171
+
172
+ extractType(device) {
173
+ if (!device.managementPoints) {
174
+ return device.type || 'Unknown Type';
175
+ }
149
176
 
150
- extractType(device) {
151
- if (!device.managementPoints) return device.type || 'Unknown Type';
177
+ for (const mp of device.managementPoints) {
178
+ if (mp.embeddedId === 'climateControl') {
179
+ return 'Climate Control';
180
+ }
181
+ if (mp.embeddedId === 'domesticHotWaterTank') {
182
+ return 'Hot Water Tank';
183
+ }
184
+ }
185
+ return device.type || 'Unknown Type';
186
+ },
187
+
188
+ isOnline(device) {
189
+ return device.isCloudConnectionUp?.value ?? false;
190
+ },
191
+
192
+ extractRoomTemp(device) {
193
+ const climateControl = this.getClimateControlPoint(device);
194
+ const roomTemp = climateControl?.sensoryData?.value?.roomTemperature;
195
+ return roomTemp?.value !== undefined ? `${roomTemp.value}${roomTemp.unit || '°C'}` : null;
196
+ },
197
+
198
+ extractOutdoorTemp(device) {
199
+ const climateControl = this.getClimateControlPoint(device);
200
+ const outdoorTemp = climateControl?.sensoryData?.value?.outdoorTemperature;
201
+ return outdoorTemp?.value !== undefined ? `${outdoorTemp.value}${outdoorTemp.unit || '°C'}` : null;
202
+ },
203
+
204
+ extractOperationMode(device) {
205
+ const climateControl = this.getClimateControlPoint(device);
206
+ return climateControl?.operationMode?.value || null;
207
+ },
208
+
209
+ extractPowerState(device) {
210
+ const climateControl = this.getClimateControlPoint(device);
211
+ return climateControl?.onOffMode?.value || null;
212
+ },
213
+
214
+ extractFeatures(device) {
215
+ const features = [];
216
+ if (!device.managementPoints) {
217
+ return features;
218
+ }
152
219
 
153
- for (const mp of device.managementPoints) {
154
- if (mp.embeddedId === 'climateControl') return 'Climate Control';
155
- if (mp.embeddedId === 'domesticHotWaterTank') return 'Hot Water Tank';
220
+ for (const mp of device.managementPoints) {
221
+ if (CLIMATE_CONTROL_IDS.includes(mp.embeddedId)) {
222
+ if (mp.onOffMode) {
223
+ features.push('Power');
156
224
  }
157
- return device.type || 'Unknown Type';
158
- },
159
-
160
- isOnline(device) {
161
- return device.isCloudConnectionUp?.value ?? false;
162
- },
163
-
164
- extractRoomTemp(device) {
165
- const climateControl = this.getClimateControlPoint(device);
166
- const roomTemp = climateControl?.sensoryData?.value?.roomTemperature;
167
- return roomTemp?.value !== undefined ? `${roomTemp.value}${roomTemp.unit || '°C'}` : null;
168
- },
169
-
170
- extractOutdoorTemp(device) {
171
- const climateControl = this.getClimateControlPoint(device);
172
- const outdoorTemp = climateControl?.sensoryData?.value?.outdoorTemperature;
173
- return outdoorTemp?.value !== undefined ? `${outdoorTemp.value}${outdoorTemp.unit || '°C'}` : null;
174
- },
175
-
176
- extractOperationMode(device) {
177
- const climateControl = this.getClimateControlPoint(device);
178
- return climateControl?.operationMode?.value || null;
179
- },
180
-
181
- extractPowerState(device) {
182
- const climateControl = this.getClimateControlPoint(device);
183
- return climateControl?.onOffMode?.value || null;
184
- },
185
-
186
- extractFeatures(device) {
187
- const features = [];
188
- if (!device.managementPoints) return features;
189
-
190
- for (const mp of device.managementPoints) {
191
- if (CLIMATE_CONTROL_IDS.includes(mp.embeddedId)) {
192
- if (mp.onOffMode) features.push('Power');
193
- if (mp.temperatureControl) features.push('Temperature');
194
- if (mp.operationMode) features.push('Mode');
195
- if (mp.fanControl) features.push('Fan');
196
- if (mp.sensoryData) features.push('Sensors');
197
- }
198
- if (mp.embeddedId === 'domesticHotWaterTank') {
199
- if (mp.onOffMode) features.push('Hot Water');
200
- if (mp.temperatureControl) features.push('Water Temp');
201
- }
225
+ if (mp.temperatureControl) {
226
+ features.push('Temperature');
202
227
  }
203
- return features;
204
- },
205
-
206
- extractAll(device) {
207
- return {
208
- id: device.id,
209
- name: this.extractName(device),
210
- model: this.extractModel(device),
211
- type: this.extractType(device),
212
- online: this.isOnline(device),
213
- features: this.extractFeatures(device),
214
- roomTemp: this.extractRoomTemp(device),
215
- outdoorTemp: this.extractOutdoorTemp(device),
216
- operationMode: this.extractOperationMode(device),
217
- powerState: this.extractPowerState(device),
218
- };
219
- },
228
+ if (mp.operationMode) {
229
+ features.push('Mode');
230
+ }
231
+ if (mp.fanControl) {
232
+ features.push('Fan');
233
+ }
234
+ if (mp.sensoryData) {
235
+ features.push('Sensors');
236
+ }
237
+ }
238
+ if (mp.embeddedId === 'domesticHotWaterTank') {
239
+ if (mp.onOffMode) {
240
+ features.push('Hot Water');
241
+ }
242
+ if (mp.temperatureControl) {
243
+ features.push('Water Temp');
244
+ }
245
+ }
246
+ }
247
+ return features;
248
+ },
249
+
250
+ extractAll(device) {
251
+ return {
252
+ id: device.id,
253
+ name: this.extractName(device),
254
+ model: this.extractModel(device),
255
+ type: this.extractType(device),
256
+ online: this.isOnline(device),
257
+ features: this.extractFeatures(device),
258
+ roomTemp: this.extractRoomTemp(device),
259
+ outdoorTemp: this.extractOutdoorTemp(device),
260
+ operationMode: this.extractOperationMode(device),
261
+ powerState: this.extractPowerState(device),
262
+ };
263
+ },
220
264
  };
221
265
 
222
266
  // =============================================================================
@@ -224,83 +268,83 @@ const DeviceExtractor = {
224
268
  // =============================================================================
225
269
 
226
270
  class CallbackServer {
227
- constructor() {
228
- this.server = null;
229
- this.port = null;
230
- this.connections = new Set();
231
- }
232
-
233
- async start(port, hostname, certDir, requestHandler) {
234
- await this.stop();
235
-
236
- return new Promise((resolve, reject) => {
237
- const tryStart = (attempt = 1) => {
238
- try {
239
- const { key, cert } = SSLUtils.generateCert(hostname, certDir);
240
-
241
- this.server = https.createServer({ key, cert }, requestHandler);
242
-
243
- this.server.on('connection', (conn) => {
244
- this.connections.add(conn);
245
- conn.on('close', () => this.connections.delete(conn));
246
- });
247
-
248
- this.server.on('error', (err) => {
249
- if (err.code === 'EADDRINUSE' && attempt < 3) {
250
- console.warn(`Port ${port} in use, retrying in 1 second (attempt ${attempt}/3)...`);
251
- setTimeout(() => tryStart(attempt + 1), 1000);
252
- } else {
253
- console.error('Callback server error:', err.message);
254
- reject(err);
255
- }
256
- });
257
-
258
- this.server.listen(port, '0.0.0.0', () => {
259
- console.log(`HTTPS callback server listening on port ${port}`);
260
- this.port = port;
261
- resolve({ success: true, port });
262
- });
263
- } catch (error) {
264
- reject(error);
265
- }
266
- };
267
-
268
- tryStart();
269
- });
270
- }
271
-
272
- async stop() {
273
- return new Promise((resolve) => {
274
- if (!this.server) {
275
- resolve({ success: true });
276
- return;
277
- }
271
+ constructor() {
272
+ this.server = null;
273
+ this.port = null;
274
+ this.connections = new Set();
275
+ }
276
+
277
+ async start(port, hostname, certDir, requestHandler) {
278
+ await this.stop();
278
279
 
279
- for (const conn of this.connections) {
280
- conn.destroy();
280
+ return new Promise((resolve, reject) => {
281
+ const tryStart = (attempt = 1) => {
282
+ try {
283
+ const { key, cert } = SSLUtils.generateCert(hostname, certDir);
284
+
285
+ this.server = https.createServer({ key, cert }, requestHandler);
286
+
287
+ this.server.on('connection', (conn) => {
288
+ this.connections.add(conn);
289
+ conn.on('close', () => this.connections.delete(conn));
290
+ });
291
+
292
+ this.server.on('error', (err) => {
293
+ if (err.code === 'EADDRINUSE' && attempt < 3) {
294
+ console.warn(`Port ${port} in use, retrying in 1 second (attempt ${attempt}/3)...`);
295
+ setTimeout(() => tryStart(attempt + 1), 1000);
296
+ } else {
297
+ console.error('Callback server error:', err.message);
298
+ reject(err);
281
299
  }
282
- this.connections.clear();
300
+ });
301
+
302
+ this.server.listen(port, '0.0.0.0', () => {
303
+ console.log(`HTTPS callback server listening on port ${port}`);
304
+ this.port = port;
305
+ resolve({ success: true, port });
306
+ });
307
+ } catch (error) {
308
+ reject(error);
309
+ }
310
+ };
283
311
 
284
- const timeout = setTimeout(() => {
285
- console.warn('HTTPS callback server close timed out, forcing cleanup');
286
- this.server = null;
287
- this.port = null;
288
- resolve({ success: true });
289
- }, 2000);
312
+ tryStart();
313
+ });
314
+ }
290
315
 
291
- this.server.close(() => {
292
- clearTimeout(timeout);
293
- console.log('HTTPS callback server stopped');
294
- this.server = null;
295
- this.port = null;
296
- resolve({ success: true });
297
- });
298
- });
299
- }
316
+ async stop() {
317
+ return new Promise((resolve) => {
318
+ if (!this.server) {
319
+ resolve({ success: true });
320
+ return;
321
+ }
300
322
 
301
- get isRunning() {
302
- return this.server !== null;
303
- }
323
+ for (const conn of this.connections) {
324
+ conn.destroy();
325
+ }
326
+ this.connections.clear();
327
+
328
+ const timeout = setTimeout(() => {
329
+ console.warn('HTTPS callback server close timed out, forcing cleanup');
330
+ this.server = null;
331
+ this.port = null;
332
+ resolve({ success: true });
333
+ }, 2000);
334
+
335
+ this.server.close(() => {
336
+ clearTimeout(timeout);
337
+ console.log('HTTPS callback server stopped');
338
+ this.server = null;
339
+ this.port = null;
340
+ resolve({ success: true });
341
+ });
342
+ });
343
+ }
344
+
345
+ get isRunning() {
346
+ return this.server !== null;
347
+ }
304
348
  }
305
349
 
306
350
  // =============================================================================
@@ -308,12 +352,12 @@ class CallbackServer {
308
352
  // =============================================================================
309
353
 
310
354
  const HtmlTemplates = {
311
- callbackResponse(success, message) {
312
- const icon = success
313
- ? '<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="#4caf50" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>'
314
- : '<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="#f44336" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>';
355
+ callbackResponse(success, message) {
356
+ const icon = success
357
+ ? '<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="#4caf50" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>'
358
+ : '<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="#f44336" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>';
315
359
 
316
- return `<!DOCTYPE html>
360
+ return `<!DOCTYPE html>
317
361
  <html>
318
362
  <head>
319
363
  <meta charset="UTF-8">
@@ -369,7 +413,7 @@ const HtmlTemplates = {
369
413
  </div>
370
414
  </body>
371
415
  </html>`;
372
- },
416
+ },
373
417
  };
374
418
 
375
419
  // =============================================================================
@@ -377,498 +421,522 @@ const HtmlTemplates = {
377
421
  // =============================================================================
378
422
 
379
423
  class DaikinCloudUiServer extends HomebridgePluginUiServer {
380
- constructor() {
381
- super();
382
-
383
- this.pendingAuth = null;
384
- this.authResult = null;
385
- this.callbackServer = new CallbackServer();
386
-
387
- this.registerHandlers();
388
- this.ready();
424
+ constructor() {
425
+ super();
426
+
427
+ this.pendingAuth = null;
428
+ this.authResult = null;
429
+ this.callbackServer = new CallbackServer();
430
+
431
+ this.registerHandlers();
432
+ this.ready();
433
+ }
434
+
435
+ getTokenFilePath() {
436
+ return resolve(this.homebridgeStoragePath || process.env.UIX_STORAGE_PATH || '', '.daikin-controller-cloud-tokenset');
437
+ }
438
+
439
+ getMobileTokenFilePath() {
440
+ return resolve(this.homebridgeStoragePath || process.env.UIX_STORAGE_PATH || '', '.daikin-mobile-tokenset');
441
+ }
442
+
443
+ getCertDir() {
444
+ return resolve(this.homebridgeStoragePath || process.env.UIX_STORAGE_PATH || '', 'daikin-cloud-certs');
445
+ }
446
+
447
+ getActiveTokenSet() {
448
+ // Check mobile token first (takes precedence)
449
+ const mobileTokenSet = TokenManager.load(this.getMobileTokenFilePath());
450
+ if (mobileTokenSet?.access_token) {
451
+ return mobileTokenSet;
389
452
  }
390
-
391
- getTokenFilePath() {
392
- return resolve(this.homebridgeStoragePath || process.env.UIX_STORAGE_PATH || '', '.daikin-controller-cloud-tokenset');
453
+ // Fall back to developer portal token
454
+ return TokenManager.load(this.getTokenFilePath());
455
+ }
456
+
457
+ registerHandlers() {
458
+ this.onRequest('/auth/status', this.handleGetAuthStatus.bind(this));
459
+ this.onRequest('/auth/start', this.handleStartAuth.bind(this));
460
+ this.onRequest('/auth/', this.handleCallback.bind(this));
461
+ this.onRequest('/auth/revoke', this.handleRevokeAuth.bind(this));
462
+ this.onRequest('/auth/test', this.handleTestConnection.bind(this));
463
+ this.onRequest('/auth/poll', this.handlePollAuthResult.bind(this));
464
+ this.onRequest('/auth/stop-server', this.handleStopServer.bind(this));
465
+ this.onRequest('/auth/mobile-test', this.handleMobileAuthTest.bind(this));
466
+ this.onRequest('/config/validate', this.handleValidateConfig.bind(this));
467
+ this.onRequest('/devices/list', this.handleListDevices.bind(this));
468
+ this.onRequest('/api/rate-limit', this.handleGetRateLimit.bind(this));
469
+ this.onRequest('/server/info', this.handleGetServerInfo.bind(this));
470
+ }
471
+
472
+ // -------------------------------------------------------------------------
473
+ // Server Info Handler
474
+ // -------------------------------------------------------------------------
475
+
476
+ async handleGetServerInfo() {
477
+ const ipAddresses = this.getServerIpAddresses();
478
+ return {
479
+ ipAddresses,
480
+ primaryIp: ipAddresses[0] || null,
481
+ hostname: os.hostname(),
482
+ };
483
+ }
484
+
485
+ getServerIpAddresses() {
486
+ const interfaces = os.networkInterfaces();
487
+ const addresses = [];
488
+
489
+ for (const nets of Object.values(interfaces)) {
490
+ if (!nets) {
491
+ continue;
492
+ }
493
+ for (const net of nets) {
494
+ // Skip internal and non-IPv4 addresses
495
+ if (net.internal || net.family !== 'IPv4') {
496
+ continue;
497
+ }
498
+ addresses.push(net.address);
499
+ }
393
500
  }
394
501
 
395
- getMobileTokenFilePath() {
396
- return resolve(this.homebridgeStoragePath || process.env.UIX_STORAGE_PATH || '', '.daikin-mobile-tokenset');
397
- }
398
-
399
- getCertDir() {
400
- return resolve(this.homebridgeStoragePath || process.env.UIX_STORAGE_PATH || '', 'daikin-cloud-certs');
401
- }
402
-
403
- getActiveTokenSet() {
404
- // Check mobile token first (takes precedence)
405
- const mobileTokenSet = TokenManager.load(this.getMobileTokenFilePath());
406
- if (mobileTokenSet?.access_token) {
407
- return mobileTokenSet;
408
- }
409
- // Fall back to developer portal token
410
- return TokenManager.load(this.getTokenFilePath());
411
- }
412
-
413
- registerHandlers() {
414
- this.onRequest('/auth/status', this.handleGetAuthStatus.bind(this));
415
- this.onRequest('/auth/start', this.handleStartAuth.bind(this));
416
- this.onRequest('/auth/', this.handleCallback.bind(this));
417
- this.onRequest('/auth/revoke', this.handleRevokeAuth.bind(this));
418
- this.onRequest('/auth/test', this.handleTestConnection.bind(this));
419
- this.onRequest('/auth/poll', this.handlePollAuthResult.bind(this));
420
- this.onRequest('/auth/stop-server', this.handleStopServer.bind(this));
421
- this.onRequest('/auth/mobile-test', this.handleMobileAuthTest.bind(this));
422
- this.onRequest('/config/validate', this.handleValidateConfig.bind(this));
423
- this.onRequest('/devices/list', this.handleListDevices.bind(this));
424
- this.onRequest('/api/rate-limit', this.handleGetRateLimit.bind(this));
425
- this.onRequest('/server/info', this.handleGetServerInfo.bind(this));
426
- }
427
-
428
- // -------------------------------------------------------------------------
429
- // Server Info Handler
430
- // -------------------------------------------------------------------------
431
-
432
- async handleGetServerInfo() {
433
- const ipAddresses = this.getServerIpAddresses();
434
- return {
435
- ipAddresses,
436
- primaryIp: ipAddresses[0] || null,
437
- hostname: os.hostname(),
438
- };
502
+ return addresses;
503
+ }
504
+
505
+ // -------------------------------------------------------------------------
506
+ // Auth Status Handler
507
+ // -------------------------------------------------------------------------
508
+
509
+ async handleGetAuthStatus() {
510
+ try {
511
+ // Check both token files - mobile takes precedence if exists
512
+ const mobileTokenSet = TokenManager.load(this.getMobileTokenFilePath());
513
+ const devPortalTokenSet = TokenManager.load(this.getTokenFilePath());
514
+
515
+ if (mobileTokenSet?.access_token) {
516
+ const status = TokenManager.getStatus(mobileTokenSet);
517
+ status.authMode = 'mobile_app';
518
+ return status;
519
+ }
520
+
521
+ if (devPortalTokenSet?.access_token) {
522
+ const status = TokenManager.getStatus(devPortalTokenSet);
523
+ status.authMode = 'developer_portal';
524
+ return status;
525
+ }
526
+
527
+ return { authenticated: false, message: 'Not authenticated' };
528
+ } catch (error) {
529
+ return { authenticated: false, error: error.message, message: 'Error reading token status' };
439
530
  }
531
+ }
440
532
 
441
- getServerIpAddresses() {
442
- const interfaces = os.networkInterfaces();
443
- const addresses = [];
533
+ // -------------------------------------------------------------------------
534
+ // Start Auth Handler
535
+ // -------------------------------------------------------------------------
444
536
 
445
- for (const nets of Object.values(interfaces)) {
446
- if (!nets) continue;
447
- for (const net of nets) {
448
- // Skip internal and non-IPv4 addresses
449
- if (net.internal || net.family !== 'IPv4') continue;
450
- addresses.push(net.address);
451
- }
452
- }
537
+ async handleStartAuth(payload) {
538
+ const { clientId, clientSecret, callbackServerExternalAddress, callbackServerPort } = payload;
453
539
 
454
- return addresses;
540
+ if (!clientId || !clientSecret) {
541
+ throw new Error('Client ID and Client Secret are required');
455
542
  }
456
-
457
- // -------------------------------------------------------------------------
458
- // Auth Status Handler
459
- // -------------------------------------------------------------------------
460
-
461
- async handleGetAuthStatus() {
462
- try {
463
- // Check both token files - mobile takes precedence if exists
464
- const mobileTokenSet = TokenManager.load(this.getMobileTokenFilePath());
465
- const devPortalTokenSet = TokenManager.load(this.getTokenFilePath());
466
-
467
- if (mobileTokenSet?.access_token) {
468
- const status = TokenManager.getStatus(mobileTokenSet);
469
- status.authMode = 'mobile_app';
470
- return status;
471
- }
472
-
473
- if (devPortalTokenSet?.access_token) {
474
- const status = TokenManager.getStatus(devPortalTokenSet);
475
- status.authMode = 'developer_portal';
476
- return status;
477
- }
478
-
479
- return { authenticated: false, message: 'Not authenticated' };
480
- } catch (error) {
481
- return { authenticated: false, error: error.message, message: 'Error reading token status' };
482
- }
543
+ if (!callbackServerExternalAddress) {
544
+ throw new Error('Callback Server Address is required');
483
545
  }
484
546
 
485
- // -------------------------------------------------------------------------
486
- // Start Auth Handler
487
- // -------------------------------------------------------------------------
488
-
489
- async handleStartAuth(payload) {
490
- const { clientId, clientSecret, callbackServerExternalAddress, callbackServerPort } = payload;
491
-
492
- if (!clientId || !clientSecret) throw new Error('Client ID and Client Secret are required');
493
- if (!callbackServerExternalAddress) throw new Error('Callback Server Address is required');
494
-
495
- const port = parseInt(callbackServerPort || '8582', 10);
496
- const redirectUri = `https://${callbackServerExternalAddress}:${port}`;
497
- const state = crypto.randomBytes(32).toString('hex');
547
+ const port = parseInt(callbackServerPort || '8582', 10);
548
+ const redirectUri = `https://${callbackServerExternalAddress}:${port}`;
549
+ const state = crypto.randomBytes(32).toString('hex');
550
+
551
+ this.pendingAuth = { state, clientId, clientSecret, redirectUri, port, createdAt: Date.now() };
552
+ this.authResult = null;
553
+
554
+ // Use static method from compiled src/api
555
+ const authUrl = DaikinOAuth.buildAuthUrlStatic(clientId, redirectUri, state);
556
+ console.log('[DaikinCloud] Generated auth URL:', authUrl);
557
+ console.log('[DaikinCloud] Redirect URI:', redirectUri);
558
+
559
+ // Try to start callback server for automatic code capture
560
+ let callbackServerRunning = false;
561
+ let callbackServerError = null;
562
+
563
+ try {
564
+ await this.callbackServer.start(
565
+ port,
566
+ callbackServerExternalAddress,
567
+ this.getCertDir(),
568
+ this.handleHttpsCallback.bind(this),
569
+ );
570
+ callbackServerRunning = true;
571
+ console.log('[DaikinCloud] Callback server started successfully');
572
+ } catch (error) {
573
+ callbackServerError = error.message;
574
+ console.warn('[DaikinCloud] Failed to start callback server:', error.message);
575
+ }
498
576
 
499
- this.pendingAuth = { state, clientId, clientSecret, redirectUri, port, createdAt: Date.now() };
500
- this.authResult = null;
577
+ return {
578
+ authUrl,
579
+ state,
580
+ redirectUri,
581
+ callbackServerRunning,
582
+ callbackServerError,
583
+ message: callbackServerRunning
584
+ ? 'Callback server is running. Authentication will complete automatically.'
585
+ : 'Could not start callback server. After authenticating, copy the full callback URL and paste it below.',
586
+ };
587
+ }
588
+
589
+ // -------------------------------------------------------------------------
590
+ // HTTPS Callback Handler
591
+ // -------------------------------------------------------------------------
592
+
593
+ handleHttpsCallback(req, res) {
594
+ const url = new URL(req.url, `https://${req.headers.host}`);
595
+
596
+ const code = url.searchParams.get('code');
597
+ const state = url.searchParams.get('state');
598
+ const error = url.searchParams.get('error');
599
+ const errorDescription = url.searchParams.get('error_description');
600
+
601
+ if (error) {
602
+ this.authResult = { success: false, error: errorDescription || error };
603
+ this.sendCallbackResponse(res, false, errorDescription || error);
604
+ return;
605
+ }
501
606
 
502
- // Use static method from compiled src/api
503
- const authUrl = DaikinOAuth.buildAuthUrlStatic(clientId, redirectUri, state);
504
- console.log('[DaikinCloud] Generated auth URL:', authUrl);
505
- console.log('[DaikinCloud] Redirect URI:', redirectUri);
607
+ if (!code || !state) {
608
+ this.authResult = { success: false, error: 'Missing code or state parameter' };
609
+ this.sendCallbackResponse(res, false, 'Missing authorization code');
610
+ return;
611
+ }
506
612
 
507
- // Try to start callback server for automatic code capture
508
- let callbackServerRunning = false;
509
- let callbackServerError = null;
613
+ if (!this.pendingAuth || state !== this.pendingAuth.state) {
614
+ this.authResult = { success: false, error: 'Invalid state parameter' };
615
+ this.sendCallbackResponse(res, false, 'Invalid state parameter');
616
+ return;
617
+ }
510
618
 
511
- try {
512
- await this.callbackServer.start(
513
- port,
514
- callbackServerExternalAddress,
515
- this.getCertDir(),
516
- this.handleHttpsCallback.bind(this)
517
- );
518
- callbackServerRunning = true;
519
- console.log('[DaikinCloud] Callback server started successfully');
520
- } catch (error) {
521
- callbackServerError = error.message;
522
- console.warn('[DaikinCloud] Failed to start callback server:', error.message);
523
- }
619
+ const { clientId, clientSecret, redirectUri } = this.pendingAuth;
524
620
 
525
- return {
526
- authUrl,
527
- state,
528
- redirectUri,
529
- callbackServerRunning,
530
- callbackServerError,
531
- message: callbackServerRunning
532
- ? 'Callback server is running. Authentication will complete automatically.'
533
- : 'Could not start callback server. After authenticating, copy the full callback URL and paste it below.',
621
+ // Use static method from compiled src/api
622
+ DaikinOAuth.exchangeCodeStatic(code, clientId, clientSecret, redirectUri)
623
+ .then((tokenSet) => {
624
+ TokenManager.save(this.getTokenFilePath(), tokenSet);
625
+ this.authResult = {
626
+ success: true,
627
+ message: 'Authentication successful!',
628
+ expiresAt: tokenSet.expires_at ? new Date(tokenSet.expires_at * 1000).toISOString() : null,
534
629
  };
630
+ this.pendingAuth = null;
631
+ this.sendCallbackResponse(res, true, 'Authentication successful! You can close this window.');
632
+ this.callbackServer.stop().catch(() => {});
633
+ })
634
+ .catch((err) => {
635
+ this.authResult = { success: false, error: err.message };
636
+ this.sendCallbackResponse(res, false, `Token exchange failed: ${err.message}`);
637
+ });
638
+ }
639
+
640
+ sendCallbackResponse(res, success, message) {
641
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
642
+ res.end(HtmlTemplates.callbackResponse(success, message));
643
+ }
644
+
645
+ // -------------------------------------------------------------------------
646
+ // Manual Callback Handler
647
+ // -------------------------------------------------------------------------
648
+
649
+ async handleCallback(payload) {
650
+ let { code, state, callbackUrl } = payload;
651
+
652
+ if (callbackUrl) {
653
+ try {
654
+ const url = new URL(callbackUrl);
655
+ code = url.searchParams.get('code');
656
+ state = url.searchParams.get('state');
657
+ } catch (e) {
658
+ throw new Error('Invalid callback URL format');
659
+ }
535
660
  }
536
661
 
537
- // -------------------------------------------------------------------------
538
- // HTTPS Callback Handler
539
- // -------------------------------------------------------------------------
540
-
541
- handleHttpsCallback(req, res) {
542
- const url = new URL(req.url, `https://${req.headers.host}`);
543
-
544
- const code = url.searchParams.get('code');
545
- const state = url.searchParams.get('state');
546
- const error = url.searchParams.get('error');
547
- const errorDescription = url.searchParams.get('error_description');
548
-
549
- if (error) {
550
- this.authResult = { success: false, error: errorDescription || error };
551
- this.sendCallbackResponse(res, false, errorDescription || error);
552
- return;
553
- }
554
-
555
- if (!code || !state) {
556
- this.authResult = { success: false, error: 'Missing code or state parameter' };
557
- this.sendCallbackResponse(res, false, 'Missing authorization code');
558
- return;
559
- }
560
-
561
- if (!this.pendingAuth || state !== this.pendingAuth.state) {
562
- this.authResult = { success: false, error: 'Invalid state parameter' };
563
- this.sendCallbackResponse(res, false, 'Invalid state parameter');
564
- return;
565
- }
566
-
567
- const { clientId, clientSecret, redirectUri } = this.pendingAuth;
568
-
569
- // Use static method from compiled src/api
570
- DaikinOAuth.exchangeCodeStatic(code, clientId, clientSecret, redirectUri)
571
- .then((tokenSet) => {
572
- TokenManager.save(this.getTokenFilePath(), tokenSet);
573
- this.authResult = {
574
- success: true,
575
- message: 'Authentication successful!',
576
- expiresAt: tokenSet.expires_at ? new Date(tokenSet.expires_at * 1000).toISOString() : null,
577
- };
578
- this.pendingAuth = null;
579
- this.sendCallbackResponse(res, true, 'Authentication successful! You can close this window.');
580
- this.callbackServer.stop().catch(() => {});
581
- })
582
- .catch((err) => {
583
- this.authResult = { success: false, error: err.message };
584
- this.sendCallbackResponse(res, false, `Token exchange failed: ${err.message}`);
585
- });
586
- }
587
-
588
- sendCallbackResponse(res, success, message) {
589
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
590
- res.end(HtmlTemplates.callbackResponse(success, message));
591
- }
592
-
593
- // -------------------------------------------------------------------------
594
- // Manual Callback Handler
595
- // -------------------------------------------------------------------------
596
-
597
- async handleCallback(payload) {
598
- let { code, state, callbackUrl } = payload;
599
-
600
- if (callbackUrl) {
601
- try {
602
- const url = new URL(callbackUrl);
603
- code = url.searchParams.get('code');
604
- state = url.searchParams.get('state');
605
- } catch (e) {
606
- throw new Error('Invalid callback URL format');
607
- }
608
- }
609
-
610
- if (!code) throw new Error('Authorization code is required');
611
- if (!this.pendingAuth) throw new Error('No pending authorization. Please start the auth flow again.');
612
- if (state && state !== this.pendingAuth.state) throw new Error('Invalid state parameter. Please try again.');
613
- if (Date.now() - this.pendingAuth.createdAt > 10 * 60 * 1000) {
614
- this.pendingAuth = null;
615
- throw new Error('Authorization request expired. Please try again.');
616
- }
617
-
618
- const { clientId, clientSecret, redirectUri } = this.pendingAuth;
619
-
620
- try {
621
- // Use static method from compiled src/api
622
- const tokenSet = await DaikinOAuth.exchangeCodeStatic(code, clientId, clientSecret, redirectUri);
623
- TokenManager.save(this.getTokenFilePath(), tokenSet);
624
- this.pendingAuth = null;
625
-
626
- await this.callbackServer.stop();
627
-
628
- return {
629
- success: true,
630
- message: 'Authentication successful! Restart Homebridge to apply.',
631
- expiresAt: tokenSet.expires_at ? new Date(tokenSet.expires_at * 1000).toISOString() : null,
632
- };
633
- } catch (error) {
634
- throw new Error(`Token exchange failed: ${error.message}`);
635
- }
662
+ if (!code) {
663
+ throw new Error('Authorization code is required');
664
+ }
665
+ if (!this.pendingAuth) {
666
+ throw new Error('No pending authorization. Please start the auth flow again.');
667
+ }
668
+ if (state && state !== this.pendingAuth.state) {
669
+ throw new Error('Invalid state parameter. Please try again.');
670
+ }
671
+ if (Date.now() - this.pendingAuth.createdAt > 10 * 60 * 1000) {
672
+ this.pendingAuth = null;
673
+ throw new Error('Authorization request expired. Please try again.');
636
674
  }
637
675
 
638
- // -------------------------------------------------------------------------
639
- // Poll Auth Result Handler
640
- // -------------------------------------------------------------------------
676
+ const { clientId, clientSecret, redirectUri } = this.pendingAuth;
641
677
 
642
- async handlePollAuthResult() {
643
- if (this.authResult) {
644
- const result = { ...this.authResult };
645
- if (result.success) {
646
- this.authResult = null;
647
- await this.callbackServer.stop();
648
- }
649
- return result;
650
- }
678
+ try {
679
+ // Use static method from compiled src/api
680
+ const tokenSet = await DaikinOAuth.exchangeCodeStatic(code, clientId, clientSecret, redirectUri);
681
+ TokenManager.save(this.getTokenFilePath(), tokenSet);
682
+ this.pendingAuth = null;
651
683
 
652
- const tokenSet = TokenManager.load(this.getTokenFilePath());
653
- if (tokenSet && tokenSet.access_token) {
654
- return {
655
- success: true,
656
- message: 'Authentication successful!',
657
- expiresAt: tokenSet.expires_at ? new Date(tokenSet.expires_at * 1000).toISOString() : null,
658
- };
659
- }
660
- return { pending: true };
684
+ await this.callbackServer.stop();
685
+
686
+ return {
687
+ success: true,
688
+ message: 'Authentication successful! Restart Homebridge to apply.',
689
+ expiresAt: tokenSet.expires_at ? new Date(tokenSet.expires_at * 1000).toISOString() : null,
690
+ };
691
+ } catch (error) {
692
+ throw new Error(`Token exchange failed: ${error.message}`);
661
693
  }
694
+ }
662
695
 
663
- // -------------------------------------------------------------------------
664
- // Stop Server Handler
665
- // -------------------------------------------------------------------------
696
+ // -------------------------------------------------------------------------
697
+ // Poll Auth Result Handler
698
+ // -------------------------------------------------------------------------
666
699
 
667
- async handleStopServer() {
668
- this.pendingAuth = null;
700
+ async handlePollAuthResult() {
701
+ if (this.authResult) {
702
+ const result = { ...this.authResult };
703
+ if (result.success) {
669
704
  this.authResult = null;
670
705
  await this.callbackServer.stop();
671
- return { success: true };
706
+ }
707
+ return result;
672
708
  }
673
709
 
674
- // -------------------------------------------------------------------------
675
- // Mobile Auth Test Handler
676
- // -------------------------------------------------------------------------
710
+ const tokenSet = TokenManager.load(this.getTokenFilePath());
711
+ if (tokenSet && tokenSet.access_token) {
712
+ return {
713
+ success: true,
714
+ message: 'Authentication successful!',
715
+ expiresAt: tokenSet.expires_at ? new Date(tokenSet.expires_at * 1000).toISOString() : null,
716
+ };
717
+ }
718
+ return { pending: true };
719
+ }
677
720
 
678
- async handleMobileAuthTest(payload) {
679
- const { email, password } = payload;
721
+ // -------------------------------------------------------------------------
722
+ // Stop Server Handler
723
+ // -------------------------------------------------------------------------
680
724
 
681
- if (!email || !password) {
682
- return { success: false, message: 'Email and password are required' };
683
- }
725
+ async handleStopServer() {
726
+ this.pendingAuth = null;
727
+ this.authResult = null;
728
+ await this.callbackServer.stop();
729
+ return { success: true };
730
+ }
684
731
 
685
- const tokenFilePath = this.getMobileTokenFilePath();
732
+ // -------------------------------------------------------------------------
733
+ // Mobile Auth Test Handler
734
+ // -------------------------------------------------------------------------
686
735
 
687
- try {
688
- // Create a temporary mobile OAuth client
689
- const mobileOAuth = new DaikinMobileOAuth({
690
- email,
691
- password,
692
- tokenFilePath,
693
- });
694
-
695
- // Perform authentication
696
- console.log('[DaikinCloud] Testing mobile app authentication...');
697
- const tokenSet = await mobileOAuth.authenticate();
698
- console.log('[DaikinCloud] Mobile authentication successful');
699
-
700
- // Test API access and get device count
701
- let deviceCount = 0;
702
- let rateLimit = null;
703
-
704
- try {
705
- const result = await DaikinApi.requestStatic('/v1/gateway-devices', tokenSet.access_token);
706
- deviceCount = Array.isArray(result.data) ? result.data.length : 0;
707
- rateLimit = result.rateLimit;
708
- } catch (apiError) {
709
- console.warn('[DaikinCloud] API test failed:', apiError.message);
710
- }
736
+ async handleMobileAuthTest(payload) {
737
+ const { email, password } = payload;
711
738
 
712
- return {
713
- success: true,
714
- message: 'Authentication successful!',
715
- deviceCount,
716
- rateLimit,
717
- expiresAt: tokenSet.expires_at ? new Date(tokenSet.expires_at * 1000).toISOString() : null,
718
- };
719
- } catch (error) {
720
- console.error('[DaikinCloud] Mobile auth test failed:', error.message);
721
- return {
722
- success: false,
723
- message: error.message || 'Authentication failed',
724
- };
725
- }
739
+ if (!email || !password) {
740
+ return { success: false, message: 'Email and password are required' };
726
741
  }
727
742
 
728
- // -------------------------------------------------------------------------
729
- // Revoke Auth Handler
730
- // -------------------------------------------------------------------------
731
-
732
- async handleRevokeAuth(payload) {
733
- const tokenSet = TokenManager.load(this.getTokenFilePath());
734
- if (!tokenSet) return { success: true, message: 'No tokens to revoke' };
735
-
736
- const { clientId, clientSecret } = payload;
737
-
738
- if (tokenSet.refresh_token && clientId && clientSecret) {
739
- try {
740
- // Use static method from compiled src/api
741
- await DaikinOAuth.revokeTokenStatic(tokenSet.refresh_token, clientId, clientSecret);
742
- } catch (error) {
743
- console.warn('Failed to revoke token at server:', error.message);
744
- }
745
- }
746
-
747
- TokenManager.delete(this.getTokenFilePath());
748
- return { success: true, message: 'Authentication revoked. You will need to re-authenticate.' };
743
+ const tokenFilePath = this.getMobileTokenFilePath();
744
+
745
+ try {
746
+ // Create a temporary mobile OAuth client
747
+ const mobileOAuth = new DaikinMobileOAuth({
748
+ email,
749
+ password,
750
+ tokenFilePath,
751
+ });
752
+
753
+ // Perform authentication
754
+ console.log('[DaikinCloud] Testing mobile app authentication...');
755
+ const tokenSet = await mobileOAuth.authenticate();
756
+ console.log('[DaikinCloud] Mobile authentication successful');
757
+
758
+ // Test API access and get device count
759
+ let deviceCount = 0;
760
+ let rateLimit = null;
761
+
762
+ try {
763
+ const result = await DaikinApi.requestStatic('/v1/gateway-devices', tokenSet.access_token);
764
+ deviceCount = Array.isArray(result.data) ? result.data.length : 0;
765
+ rateLimit = result.rateLimit;
766
+ } catch (apiError) {
767
+ console.warn('[DaikinCloud] API test failed:', apiError.message);
768
+ }
769
+
770
+ return {
771
+ success: true,
772
+ message: 'Authentication successful!',
773
+ deviceCount,
774
+ rateLimit,
775
+ expiresAt: tokenSet.expires_at ? new Date(tokenSet.expires_at * 1000).toISOString() : null,
776
+ };
777
+ } catch (error) {
778
+ console.error('[DaikinCloud] Mobile auth test failed:', error.message);
779
+ return {
780
+ success: false,
781
+ message: error.message || 'Authentication failed',
782
+ };
749
783
  }
784
+ }
750
785
 
751
- // -------------------------------------------------------------------------
752
- // Test Connection Handler
753
- // -------------------------------------------------------------------------
786
+ // -------------------------------------------------------------------------
787
+ // Revoke Auth Handler
788
+ // -------------------------------------------------------------------------
754
789
 
755
- async handleTestConnection() {
756
- const tokenSet = this.getActiveTokenSet();
757
- if (!tokenSet?.access_token) {
758
- return { success: false, message: 'Not authenticated. Please authenticate first.' };
759
- }
790
+ async handleRevokeAuth(payload) {
791
+ const devPortalTokenSet = TokenManager.load(this.getTokenFilePath());
792
+ const mobileTokenSet = TokenManager.load(this.getMobileTokenFilePath());
760
793
 
761
- try {
762
- // Use static method from compiled src/api
763
- const result = await DaikinApi.requestStatic('/v1/gateway-devices', tokenSet.access_token);
764
- const devices = result.data;
765
- return {
766
- success: true,
767
- message: `Connection successful! Found ${Array.isArray(devices) ? devices.length : 0} device(s).`,
768
- deviceCount: Array.isArray(devices) ? devices.length : 0,
769
- };
770
- } catch (error) {
771
- return { success: false, message: `Connection failed: ${error.message}`, error: error.message };
772
- }
794
+ if (!devPortalTokenSet && !mobileTokenSet) {
795
+ return { success: true, message: 'No tokens to revoke' };
773
796
  }
774
797
 
775
- // -------------------------------------------------------------------------
776
- // List Devices Handler
777
- // -------------------------------------------------------------------------
778
-
779
- async handleListDevices(payload) {
780
- const mode = payload?.mode;
798
+ const { clientId, clientSecret } = payload;
781
799
 
782
- // Get token based on mode parameter, or fall back to active token
783
- let tokenSet;
784
- if (mode === 'mobile_app') {
785
- tokenSet = TokenManager.load(this.getMobileTokenFilePath());
786
- } else if (mode === 'developer_portal') {
787
- tokenSet = TokenManager.load(this.getTokenFilePath());
788
- } else {
789
- tokenSet = this.getActiveTokenSet();
790
- }
800
+ if (devPortalTokenSet?.refresh_token && clientId && clientSecret) {
801
+ try {
802
+ // Use static method from compiled src/api
803
+ await DaikinOAuth.revokeTokenStatic(devPortalTokenSet.refresh_token, clientId, clientSecret);
804
+ } catch (error) {
805
+ console.warn('Failed to revoke token at server:', error.message);
806
+ }
807
+ }
791
808
 
792
- // If the requested mode's token doesn't exist or is invalid, fall back to active token
793
- if (!tokenSet?.access_token) {
794
- tokenSet = this.getActiveTokenSet();
795
- }
809
+ // Delete both token files
810
+ TokenManager.delete(this.getTokenFilePath());
811
+ TokenManager.delete(this.getMobileTokenFilePath());
812
+ return { success: true, message: 'Authentication revoked. You will need to re-authenticate.' };
813
+ }
796
814
 
797
- if (!tokenSet?.access_token) {
798
- return { success: false, devices: [], message: 'Not authenticated. Please authenticate first.' };
799
- }
815
+ // -------------------------------------------------------------------------
816
+ // Test Connection Handler
817
+ // -------------------------------------------------------------------------
800
818
 
801
- try {
802
- // Use static method from compiled src/api
803
- const result = await DaikinApi.requestStatic('/v1/gateway-devices', tokenSet.access_token);
804
- const gatewayDevices = result.data;
805
- const devices = Array.isArray(gatewayDevices)
806
- ? gatewayDevices.map(device => DeviceExtractor.extractAll(device))
807
- : [];
808
-
809
- return { success: true, devices, message: `Found ${devices.length} device(s).` };
810
- } catch (error) {
811
- return { success: false, devices: [], message: `Failed to fetch devices: ${error.message}`, error: error.message };
812
- }
819
+ async handleTestConnection() {
820
+ const tokenSet = this.getActiveTokenSet();
821
+ if (!tokenSet?.access_token) {
822
+ return { success: false, message: 'Not authenticated. Please authenticate first.' };
813
823
  }
814
824
 
815
- // -------------------------------------------------------------------------
816
- // Get Rate Limit Handler
817
- // -------------------------------------------------------------------------
825
+ try {
826
+ // Use static method from compiled src/api
827
+ const result = await DaikinApi.requestStatic('/v1/gateway-devices', tokenSet.access_token);
828
+ const devices = result.data;
829
+ return {
830
+ success: true,
831
+ message: `Connection successful! Found ${Array.isArray(devices) ? devices.length : 0} device(s).`,
832
+ deviceCount: Array.isArray(devices) ? devices.length : 0,
833
+ };
834
+ } catch (error) {
835
+ return { success: false, message: `Connection failed: ${error.message}`, error: error.message };
836
+ }
837
+ }
838
+
839
+ // -------------------------------------------------------------------------
840
+ // List Devices Handler
841
+ // -------------------------------------------------------------------------
842
+
843
+ async handleListDevices(payload) {
844
+ const mode = payload?.mode;
845
+
846
+ // Get token based on mode parameter, or fall back to active token
847
+ let tokenSet;
848
+ if (mode === 'mobile_app') {
849
+ tokenSet = TokenManager.load(this.getMobileTokenFilePath());
850
+ } else if (mode === 'developer_portal') {
851
+ tokenSet = TokenManager.load(this.getTokenFilePath());
852
+ } else {
853
+ tokenSet = this.getActiveTokenSet();
854
+ }
818
855
 
819
- async handleGetRateLimit(payload) {
820
- const mode = payload?.mode;
856
+ // If the requested mode's token doesn't exist or is invalid, fall back to active token
857
+ if (!tokenSet?.access_token) {
858
+ tokenSet = this.getActiveTokenSet();
859
+ }
821
860
 
822
- // Get token based on mode parameter, or fall back to active token
823
- let tokenSet;
824
- if (mode === 'mobile_app') {
825
- tokenSet = TokenManager.load(this.getMobileTokenFilePath());
826
- } else if (mode === 'developer_portal') {
827
- tokenSet = TokenManager.load(this.getTokenFilePath());
828
- } else {
829
- tokenSet = this.getActiveTokenSet();
830
- }
861
+ if (!tokenSet?.access_token) {
862
+ return { success: false, devices: [], message: 'Not authenticated. Please authenticate first.' };
863
+ }
831
864
 
832
- if (!tokenSet?.access_token) {
833
- return { success: false, message: 'Not authenticated' };
834
- }
865
+ try {
866
+ // Use static method from compiled src/api
867
+ const result = await DaikinApi.requestStatic('/v1/gateway-devices', tokenSet.access_token);
868
+ const gatewayDevices = result.data;
869
+ const devices = Array.isArray(gatewayDevices)
870
+ ? gatewayDevices.map(device => DeviceExtractor.extractAll(device))
871
+ : [];
872
+
873
+ return { success: true, devices, message: `Found ${devices.length} device(s).` };
874
+ } catch (error) {
875
+ return { success: false, devices: [], message: `Failed to fetch devices: ${error.message}`, error: error.message };
876
+ }
877
+ }
878
+
879
+ // -------------------------------------------------------------------------
880
+ // Get Rate Limit Handler
881
+ // -------------------------------------------------------------------------
882
+
883
+ async handleGetRateLimit(payload) {
884
+ const mode = payload?.mode;
885
+
886
+ // Get token based on mode parameter, or fall back to active token
887
+ let tokenSet;
888
+ if (mode === 'mobile_app') {
889
+ tokenSet = TokenManager.load(this.getMobileTokenFilePath());
890
+ } else if (mode === 'developer_portal') {
891
+ tokenSet = TokenManager.load(this.getTokenFilePath());
892
+ } else {
893
+ tokenSet = this.getActiveTokenSet();
894
+ }
835
895
 
836
- try {
837
- // Use static method from compiled src/api
838
- const result = await DaikinApi.requestStatic('/v1/gateway-devices', tokenSet.access_token);
839
- return { success: true, rateLimit: result.rateLimit };
840
- } catch (error) {
841
- return { success: false, message: error.message };
842
- }
896
+ if (!tokenSet?.access_token) {
897
+ return { success: false, message: 'Not authenticated' };
843
898
  }
844
899
 
845
- // -------------------------------------------------------------------------
846
- // Validate Config Handler
847
- // -------------------------------------------------------------------------
900
+ try {
901
+ // Use static method from compiled src/api
902
+ const result = await DaikinApi.requestStatic('/v1/gateway-devices', tokenSet.access_token);
903
+ return { success: true, rateLimit: result.rateLimit };
904
+ } catch (error) {
905
+ return { success: false, message: error.message };
906
+ }
907
+ }
848
908
 
849
- async handleValidateConfig(payload) {
850
- const errors = [];
851
- const warnings = [];
852
- const { clientId, clientSecret, callbackServerExternalAddress, callbackServerPort } = payload;
909
+ // -------------------------------------------------------------------------
910
+ // Validate Config Handler
911
+ // -------------------------------------------------------------------------
853
912
 
854
- if (!clientId) errors.push('Client ID is required. Get it from the Daikin Developer Portal.');
855
- if (!clientSecret) errors.push('Client Secret is required. Get it from the Daikin Developer Portal.');
913
+ async handleValidateConfig(payload) {
914
+ const errors = [];
915
+ const warnings = [];
916
+ const { clientId, clientSecret, callbackServerExternalAddress, callbackServerPort } = payload;
856
917
 
857
- if (!callbackServerExternalAddress) {
858
- errors.push('Callback Server External Address is required.');
859
- } else if (callbackServerExternalAddress === 'localhost' || callbackServerExternalAddress === '127.0.0.1') {
860
- errors.push('Callback address cannot be localhost. Use your external IP or domain.');
861
- }
918
+ if (!clientId) {
919
+ errors.push('Client ID is required. Get it from the Daikin Developer Portal.');
920
+ }
921
+ if (!clientSecret) {
922
+ errors.push('Client Secret is required. Get it from the Daikin Developer Portal.');
923
+ }
862
924
 
863
- const port = parseInt(callbackServerPort || '8582', 10);
864
- if (isNaN(port) || port < 1 || port > 65535) {
865
- errors.push('Invalid port number. Must be between 1 and 65535.');
866
- } else if (port < 1024) {
867
- warnings.push('Using a privileged port (< 1024) may require root permissions.');
868
- }
925
+ if (!callbackServerExternalAddress) {
926
+ errors.push('Callback Server External Address is required.');
927
+ } else if (callbackServerExternalAddress === 'localhost' || callbackServerExternalAddress === '127.0.0.1') {
928
+ errors.push('Callback address cannot be localhost. Use your external IP or domain.');
929
+ }
869
930
 
870
- return { valid: errors.length === 0, errors, warnings };
931
+ const port = parseInt(callbackServerPort || '8582', 10);
932
+ if (isNaN(port) || port < 1 || port > 65535) {
933
+ errors.push('Invalid port number. Must be between 1 and 65535.');
934
+ } else if (port < 1024) {
935
+ warnings.push('Using a privileged port (< 1024) may require root permissions.');
871
936
  }
937
+
938
+ return { valid: errors.length === 0, errors, warnings };
939
+ }
872
940
  }
873
941
 
874
942
  // =============================================================================