@mp-consulting/homebridge-daikin-cloud 1.3.6 → 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 (287) hide show
  1. package/LICENSE +39 -1
  2. package/README.md +3 -1
  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 +5 -5
  10. package/dist/src/accessories/base-accessory.d.ts.map +1 -1
  11. package/dist/src/accessories/base-accessory.js +7 -4
  12. package/dist/src/accessories/base-accessory.js.map +1 -1
  13. package/dist/src/api/daikin-api.d.ts +25 -25
  14. package/dist/src/api/daikin-api.d.ts.map +1 -1
  15. package/dist/src/api/daikin-api.js +41 -31
  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.map +1 -1
  19. package/dist/src/api/daikin-controller.d.ts +41 -42
  20. package/dist/src/api/daikin-controller.d.ts.map +1 -1
  21. package/dist/src/api/daikin-controller.js +39 -39
  22. package/dist/src/api/daikin-controller.js.map +1 -1
  23. package/dist/src/api/daikin-device.d.ts +33 -31
  24. package/dist/src/api/daikin-device.d.ts.map +1 -1
  25. package/dist/src/api/daikin-device.js +40 -29
  26. package/dist/src/api/daikin-device.js.map +1 -1
  27. package/dist/src/api/daikin-mobile-oauth.d.ts +16 -16
  28. package/dist/src/api/daikin-mobile-oauth.d.ts.map +1 -1
  29. package/dist/src/api/daikin-mobile-oauth.js +32 -22
  30. package/dist/src/api/daikin-mobile-oauth.js.map +1 -1
  31. package/dist/src/api/daikin-oauth.d.ts +29 -29
  32. package/dist/src/api/daikin-oauth.d.ts.map +1 -1
  33. package/dist/src/api/daikin-oauth.js +45 -35
  34. package/dist/src/api/daikin-oauth.js.map +1 -1
  35. package/dist/src/api/daikin-schemas.d.ts +4 -4
  36. package/dist/src/api/daikin-schemas.js +3 -3
  37. package/dist/src/api/daikin-schemas.js.map +1 -1
  38. package/dist/src/api/daikin-types.js.map +1 -1
  39. package/dist/src/api/daikin-websocket.d.ts +31 -32
  40. package/dist/src/api/daikin-websocket.d.ts.map +1 -1
  41. package/dist/src/api/daikin-websocket.js +30 -30
  42. package/dist/src/api/daikin-websocket.js.map +1 -1
  43. package/dist/src/api/index.d.ts +1 -1
  44. package/dist/src/api/index.d.ts.map +1 -1
  45. package/dist/src/api/index.js +2 -1
  46. package/dist/src/api/index.js.map +1 -1
  47. package/dist/src/api/token-storage.d.ts +1 -1
  48. package/dist/src/api/token-storage.d.ts.map +1 -1
  49. package/dist/src/api/token-storage.js +20 -11
  50. package/dist/src/api/token-storage.js.map +1 -1
  51. package/dist/src/config/config-manager.d.ts +33 -33
  52. package/dist/src/config/config-manager.d.ts.map +1 -1
  53. package/dist/src/config/config-manager.js +33 -33
  54. package/dist/src/config/config-manager.js.map +1 -1
  55. package/dist/src/constants/api.constants.js.map +1 -1
  56. package/dist/src/device/accessory-factory.d.ts +10 -10
  57. package/dist/src/device/accessory-factory.d.ts.map +1 -1
  58. package/dist/src/device/accessory-factory.js +6 -6
  59. package/dist/src/device/accessory-factory.js.map +1 -1
  60. package/dist/src/device/capability-detector.d.ts +8 -8
  61. package/dist/src/device/capability-detector.d.ts.map +1 -1
  62. package/dist/src/device/capability-detector.js +6 -6
  63. package/dist/src/device/capability-detector.js.map +1 -1
  64. package/dist/src/device/capability-docs.d.ts +1 -1
  65. package/dist/src/device/capability-docs.d.ts.map +1 -1
  66. package/dist/src/device/capability-docs.js +1 -2
  67. package/dist/src/device/capability-docs.js.map +1 -1
  68. package/dist/src/device/profiles/device-profile.d.ts +1 -1
  69. package/dist/src/device/profiles/device-profile.d.ts.map +1 -1
  70. package/dist/src/device/profiles/device-profile.js +4 -4
  71. package/dist/src/device/profiles/device-profile.js.map +1 -1
  72. package/dist/src/features/base-feature.d.ts +2 -2
  73. package/dist/src/features/base-feature.d.ts.map +1 -1
  74. package/dist/src/features/base-feature.js +2 -3
  75. package/dist/src/features/base-feature.js.map +1 -1
  76. package/dist/src/features/feature-manager.d.ts +8 -8
  77. package/dist/src/features/feature-manager.d.ts.map +1 -1
  78. package/dist/src/features/feature-manager.js +5 -5
  79. package/dist/src/features/feature-manager.js.map +1 -1
  80. package/dist/src/features/modes/dry-operation-mode.feature.d.ts +1 -1
  81. package/dist/src/features/modes/dry-operation-mode.feature.d.ts.map +1 -1
  82. package/dist/src/features/modes/dry-operation-mode.feature.js.map +1 -1
  83. package/dist/src/features/modes/econo-mode.feature.d.ts +1 -1
  84. package/dist/src/features/modes/econo-mode.feature.d.ts.map +1 -1
  85. package/dist/src/features/modes/econo-mode.feature.js.map +1 -1
  86. package/dist/src/features/modes/fan-only-operation-mode.feature.d.ts +1 -1
  87. package/dist/src/features/modes/fan-only-operation-mode.feature.d.ts.map +1 -1
  88. package/dist/src/features/modes/fan-only-operation-mode.feature.js.map +1 -1
  89. package/dist/src/features/modes/indoor-silent-mode.feature.d.ts +1 -1
  90. package/dist/src/features/modes/indoor-silent-mode.feature.d.ts.map +1 -1
  91. package/dist/src/features/modes/indoor-silent-mode.feature.js.map +1 -1
  92. package/dist/src/features/modes/outdoor-silent-mode.feature.d.ts +1 -1
  93. package/dist/src/features/modes/outdoor-silent-mode.feature.d.ts.map +1 -1
  94. package/dist/src/features/modes/outdoor-silent-mode.feature.js.map +1 -1
  95. package/dist/src/features/modes/powerful-mode.feature.d.ts +1 -1
  96. package/dist/src/features/modes/powerful-mode.feature.d.ts.map +1 -1
  97. package/dist/src/features/modes/powerful-mode.feature.js.map +1 -1
  98. package/dist/src/features/modes/streamer-mode.feature.d.ts +1 -1
  99. package/dist/src/features/modes/streamer-mode.feature.d.ts.map +1 -1
  100. package/dist/src/features/modes/streamer-mode.feature.js.map +1 -1
  101. package/dist/src/index.d.ts +1 -1
  102. package/dist/src/index.d.ts.map +1 -1
  103. package/dist/src/index.js.map +1 -1
  104. package/dist/src/platform.d.ts +6 -5
  105. package/dist/src/platform.d.ts.map +1 -1
  106. package/dist/src/platform.js +2 -2
  107. package/dist/src/platform.js.map +1 -1
  108. package/dist/src/services/climate-control.service.d.ts +8 -2
  109. package/dist/src/services/climate-control.service.d.ts.map +1 -1
  110. package/dist/src/services/climate-control.service.js +53 -59
  111. package/dist/src/services/climate-control.service.js.map +1 -1
  112. package/dist/src/services/hot-water-tank.service.d.ts +6 -2
  113. package/dist/src/services/hot-water-tank.service.d.ts.map +1 -1
  114. package/dist/src/services/hot-water-tank.service.js +31 -34
  115. package/dist/src/services/hot-water-tank.service.js.map +1 -1
  116. package/dist/src/types/daikin-enums.js +12 -12
  117. package/dist/src/types/daikin-enums.js.map +1 -1
  118. package/dist/src/types/device-capabilities.d.ts +1 -1
  119. package/dist/src/types/device-capabilities.d.ts.map +1 -1
  120. package/dist/src/utils/log-context.d.ts +23 -23
  121. package/dist/src/utils/log-context.d.ts.map +1 -1
  122. package/dist/src/utils/log-context.js +28 -28
  123. package/dist/src/utils/log-context.js.map +1 -1
  124. package/dist/src/utils/strings.d.ts.map +1 -1
  125. package/dist/src/utils/strings.js.map +1 -1
  126. package/dist/src/utils/update-mapper.d.ts +16 -16
  127. package/dist/src/utils/update-mapper.d.ts.map +1 -1
  128. package/dist/src/utils/update-mapper.js +14 -14
  129. package/dist/src/utils/update-mapper.js.map +1 -1
  130. package/homebridge-ui/public/script.js +956 -897
  131. package/homebridge-ui/server.js +739 -695
  132. package/package.json +27 -24
  133. package/.claude/settings.json +0 -3
  134. package/.claude/settings.local.json +0 -24
  135. package/CHANGELOG.md +0 -114
  136. package/CLAUDE.md +0 -269
  137. package/config.md +0 -2
  138. package/docs/ARCHITECTURE.md +0 -645
  139. package/docs/IMPLEMENTATION_GUIDE.md +0 -899
  140. package/docs/IMPROVEMENTS_SUMMARY.md +0 -415
  141. package/docs/NEXT_STEPS.md +0 -368
  142. package/docs/Screenshot 2024-07-04 at 18.41.28.png +0 -0
  143. package/docs/TROUBLESHOOTING.md +0 -475
  144. package/docs/api-response-for-BRP069A8x.json +0 -520
  145. package/docs/api-response-for-BRP069C4x-2.json +0 -881
  146. package/docs/api-response-for-BRP069C4x.json +0 -916
  147. package/docs/api-response-for-altherma.json +0 -759
  148. package/docs/api-response-for-altherma2.json +0 -2735
  149. package/docs/api-response-with-multiple-devices-incl-heatpump.json +0 -2544
  150. package/docs/cr-insance-altherma-id-0.json +0 -834
  151. package/docs/mock-air-to-air-dx23.json +0 -759
  152. package/docs/mock-air-to-air-dx4.json +0 -1134
  153. package/docs/mock-airpurifier-with-humidifier.json +0 -732
  154. package/docs/mock-airpurifier.json +0 -450
  155. package/docs/mock-altherma-air-to-water-lan.json +0 -845
  156. package/docs/mock-altherma-air-to-water-wlan.json +0 -845
  157. package/docs/mock-d2cnd-gas-boiler.json +0 -649
  158. package/docs/setpointmode-vs-controlmode-vs-setpoints-vs-sensorydata.txt +0 -6
  159. package/images/fan-speed.jpeg +0 -0
  160. package/images/homekit-controls.jpeg +0 -0
  161. package/images/homekit-settings.jpeg +0 -0
  162. package/images/swing-mode.png +0 -0
  163. package/jest.config.ts +0 -21
  164. package/test/fixtures/altherma-crSense-2.ts +0 -834
  165. package/test/fixtures/altherma-fraction.ts +0 -718
  166. package/test/fixtures/altherma-heat-pump-2.ts +0 -479
  167. package/test/fixtures/altherma-heat-pump.ts +0 -757
  168. package/test/fixtures/altherma-miladcerkic-off.ts +0 -524
  169. package/test/fixtures/altherma-miladcerkic.ts +0 -524
  170. package/test/fixtures/altherma-v1ckoeln.ts +0 -644
  171. package/test/fixtures/altherma-with-embedded-id-zero.ts +0 -834
  172. package/test/fixtures/dx23-airco-2.ts +0 -343
  173. package/test/fixtures/dx23-airco.ts +0 -518
  174. package/test/fixtures/dx4-airco.ts +0 -914
  175. package/test/fixtures/unknown-jan.ts +0 -488
  176. package/test/fixtures/unknown-kitchen-guests.ts +0 -488
  177. package/test/hbConfig/.daikin-mobile-tokenset +0 -8
  178. package/test/hbConfig/.uix-dashboard.json +0 -1
  179. package/test/hbConfig/.uix-secrets +0 -1
  180. package/test/hbConfig/accessories/.cachedAccessories.bak +0 -1
  181. package/test/hbConfig/accessories/cachedAccessories +0 -1
  182. package/test/hbConfig/accessories/uiAccessoriesLayout.json +0 -1
  183. package/test/hbConfig/auth.json +0 -10
  184. package/test/hbConfig/backups/config-backups/config.json.1767953686461 +0 -25
  185. package/test/hbConfig/backups/config-backups/config.json.1767953695236 +0 -29
  186. package/test/hbConfig/backups/config-backups/config.json.1767953814763 +0 -29
  187. package/test/hbConfig/backups/config-backups/config.json.1767953823101 +0 -29
  188. package/test/hbConfig/backups/config-backups/config.json.1767954822835 +0 -29
  189. package/test/hbConfig/backups/config-backups/config.json.1767954859218 +0 -29
  190. package/test/hbConfig/backups/config-backups/config.json.1767960145503 +0 -33
  191. package/test/hbConfig/backups/config-backups/config.json.1767960168068 +0 -44
  192. package/test/hbConfig/backups/config-backups/config.json.1767960170333 +0 -46
  193. package/test/hbConfig/backups/config-backups/config.json.1767960172731 +0 -44
  194. package/test/hbConfig/backups/config-backups/config.json.1767960179323 +0 -44
  195. package/test/hbConfig/backups/config-backups/config.json.1767960182114 +0 -44
  196. package/test/hbConfig/backups/config-backups/config.json.1767960189302 +0 -44
  197. package/test/hbConfig/backups/config-backups/config.json.1767960195194 +0 -44
  198. package/test/hbConfig/backups/config-backups/config.json.1767960197301 +0 -44
  199. package/test/hbConfig/backups/config-backups/config.json.1767960199151 +0 -44
  200. package/test/hbConfig/backups/config-backups/config.json.1767960199667 +0 -44
  201. package/test/hbConfig/backups/config-backups/config.json.1767960329839 +0 -44
  202. package/test/hbConfig/backups/config-backups/config.json.1767960334503 +0 -44
  203. package/test/hbConfig/backups/config-backups/config.json.1767960336208 +0 -44
  204. package/test/hbConfig/backups/config-backups/config.json.1767960338537 +0 -44
  205. package/test/hbConfig/backups/config-backups/config.json.1767963223953 +0 -44
  206. package/test/hbConfig/backups/config-backups/config.json.1767963241753 +0 -44
  207. package/test/hbConfig/backups/config-backups/config.json.1767963252785 +0 -44
  208. package/test/hbConfig/backups/config-backups/config.json.1767963463944 +0 -44
  209. package/test/hbConfig/backups/config-backups/config.json.1767963834475 +0 -44
  210. package/test/hbConfig/backups/config-backups/config.json.1767963838474 +0 -44
  211. package/test/hbConfig/backups/config-backups/config.json.1767963843066 +0 -44
  212. package/test/hbConfig/backups/config-backups/config.json.1767965217715 +0 -44
  213. package/test/hbConfig/backups/config-backups/config.json.1767965419624 +0 -25
  214. package/test/hbConfig/backups/config-backups/config.json.1767965870934 +0 -32
  215. package/test/hbConfig/backups/config-backups/config.json.1767977675045 +0 -32
  216. package/test/hbConfig/backups/config-backups/config.json.1767977677222 +0 -33
  217. package/test/hbConfig/backups/config-backups/config.json.1767977710226 +0 -33
  218. package/test/hbConfig/backups/config-backups/config.json.1767977741397 +0 -33
  219. package/test/hbConfig/backups/config-backups/config.json.1767977977093 +0 -35
  220. package/test/hbConfig/backups/config-backups/config.json.1767977981773 +0 -35
  221. package/test/hbConfig/backups/config-backups/config.json.1767977986514 +0 -35
  222. package/test/hbConfig/backups/config-backups/config.json.1767977991174 +0 -35
  223. package/test/hbConfig/backups/config-backups/config.json.1767979424487 +0 -35
  224. package/test/hbConfig/backups/config-backups/config.json.1767979424987 +0 -35
  225. package/test/hbConfig/backups/config-backups/config.json.1767979432646 +0 -47
  226. package/test/hbConfig/backups/config-backups/config.json.1767979433150 +0 -47
  227. package/test/hbConfig/backups/config-backups/config.json.1767979436933 +0 -47
  228. package/test/hbConfig/backups/config-backups/config.json.1767979437438 +0 -47
  229. package/test/hbConfig/backups/config-backups/config.json.1767979441676 +0 -47
  230. package/test/hbConfig/backups/config-backups/config.json.1767979442180 +0 -47
  231. package/test/hbConfig/backups/config-backups/config.json.1767979466735 +0 -47
  232. package/test/hbConfig/backups/config-backups/config.json.1767979903636 +0 -47
  233. package/test/hbConfig/backups/config-backups/config.json.1767979904135 +0 -47
  234. package/test/hbConfig/backups/config-backups/config.json.1767979906606 +0 -47
  235. package/test/hbConfig/backups/config-backups/config.json.1767979907108 +0 -47
  236. package/test/hbConfig/backups/config-backups/config.json.1767988702341 +0 -47
  237. package/test/hbConfig/backups/config-backups/config.json.1767988702837 +0 -47
  238. package/test/hbConfig/backups/config-backups/config.json.1767988713159 +0 -47
  239. package/test/hbConfig/backups/config-backups/config.json.1767988713664 +0 -47
  240. package/test/hbConfig/backups/config-backups/config.json.1767988918139 +0 -47
  241. package/test/hbConfig/backups/config-backups/config.json.1767988918639 +0 -47
  242. package/test/hbConfig/backups/config-backups/config.json.1767988921120 +0 -47
  243. package/test/hbConfig/backups/config-backups/config.json.1767988921624 +0 -47
  244. package/test/hbConfig/backups/config-backups/config.json.1767988930307 +0 -47
  245. package/test/hbConfig/backups/config-backups/config.json.1767988935070 +0 -47
  246. package/test/hbConfig/backups/config-backups/config.json.1767988935574 +0 -47
  247. package/test/hbConfig/backups/config-backups/config.json.1767989710262 +0 -47
  248. package/test/hbConfig/backups/config-backups/config.json.1767989710760 +0 -47
  249. package/test/hbConfig/backups/config-backups/config.json.1767989729668 +0 -47
  250. package/test/hbConfig/backups/config-backups/config.json.1767990295225 +0 -47
  251. package/test/hbConfig/backups/config-backups/config.json.1767990479921 +0 -47
  252. package/test/hbConfig/backups/config-backups/config.json.1767990481702 +0 -49
  253. package/test/hbConfig/backups/instance-backups/homebridge-backup-1E4A432551BA.1768010187391.tar.gz +0 -0
  254. package/test/hbConfig/backups/instance-backups/homebridge-backup-1E4A432551BA.1768096587387.tar.gz +0 -0
  255. package/test/hbConfig/backups/instance-backups/homebridge-backup-1E4A432551BA.1768182987404.tar.gz +0 -0
  256. package/test/hbConfig/config.json +0 -47
  257. package/test/hbConfig/daikin-cloud-certs/server.crt +0 -22
  258. package/test/hbConfig/daikin-cloud-certs/server.key +0 -28
  259. package/test/hbConfig/persist/AccessoryInfo.1E4A432551BA.json +0 -1
  260. package/test/hbConfig/persist/IdentifierCache.1E4A432551BA.json +0 -1
  261. package/test/hbConfig/tmp/daikin_request/api.onecta.daikineurope.com_01-09-2026-23-29-52/request_14758 +0 -1
  262. package/test/hbConfig/tmp/daikin_request/api.onecta.daikineurope.com_01-09-2026-23-29-52/request_14759 +0 -1
  263. package/test/hbConfig/tmp/daikin_request/api.onecta.daikineurope.com_01-09-2026-23-29-52/request_14760 +0 -1
  264. package/test/hbConfig/tmp/daikin_request/api.onecta.daikineurope.com_01-09-2026-23-29-52/request_14761 +0 -1
  265. package/test/hbConfig/tmp/daikin_request/api.onecta.daikineurope.com_01-09-2026-23-29-52/request_14762 +0 -1
  266. package/test/hbConfig/tmp/daikin_request/api.onecta.daikineurope.com_01-09-2026-23-29-52/request_14764 +0 -1
  267. package/test/helpers/test-isolation.ts +0 -228
  268. package/test/integration/air-conditioning.test.ts +0 -396
  269. package/test/integration/altherma.test.ts +0 -279
  270. package/test/integration/platform.test.ts +0 -118
  271. package/test/mobile-tokens.json +0 -8
  272. package/test/mocks/index.ts +0 -27
  273. package/test/test-gigya-auth.js +0 -443
  274. package/test/test-mobile-oauth.js +0 -175
  275. package/test/test-websocket-mobile.js +0 -123
  276. package/test/test-websocket.js +0 -116
  277. package/test/unit/api/__snapshots__/daikinCloud.test.ts.snap +0 -1320
  278. package/test/unit/api/daikin-api.test.ts +0 -442
  279. package/test/unit/api/daikin-cloud-repository.test.ts +0 -107
  280. package/test/unit/api/daikin-oauth.test.ts +0 -214
  281. package/test/unit/api/daikinCloud.test.ts +0 -12
  282. package/test/unit/api/token-storage.test.ts +0 -90
  283. package/test/unit/config/config-manager.test.ts +0 -271
  284. package/test/unit/device/daikin-device.test.ts +0 -73
  285. package/test/unit/services/hot-water-tank.service.test.ts +0 -123
  286. package/test/unit/utils/log-context.test.ts +0 -271
  287. package/test/unit/utils/update-mapper.test.ts +0 -404
@@ -24,69 +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
- },
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
32
 
33
- /**
33
+ /**
34
34
  * Validate that a hostname is safe for use in shell commands.
35
35
  * Only allows alphanumeric, dots, hyphens, and colons (for IPv6).
36
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
- },
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
+ },
48
48
 
49
- generateCert(hostname, certDir) {
50
- this.validateHostname(hostname);
49
+ generateCert(hostname, certDir) {
50
+ this.validateHostname(hostname);
51
51
 
52
- const keyPath = resolve(certDir, 'server.key');
53
- const certPath = resolve(certDir, 'server.crt');
52
+ const keyPath = resolve(certDir, 'server.key');
53
+ const certPath = resolve(certDir, 'server.crt');
54
54
 
55
- if (!fs.existsSync(certDir)) {
56
- fs.mkdirSync(certDir, { recursive: true });
57
- }
55
+ if (!fs.existsSync(certDir)) {
56
+ fs.mkdirSync(certDir, { recursive: true });
57
+ }
58
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
- }
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
+ }
69
69
 
70
- try {
71
- execSync(`openssl genrsa -out "${keyPath}" 2048`, { stdio: 'pipe' });
70
+ try {
71
+ execSync(`openssl genrsa -out "${keyPath}" 2048`, { stdio: 'pipe' });
72
72
 
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`;
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`;
76
76
 
77
- execSync(
78
- `openssl req -new -x509 -key "${keyPath}" -out "${certPath}" -days 365 -subj "${subj}" -addext "subjectAltName=${sanValue}"`,
79
- { stdio: 'pipe' }
80
- );
77
+ execSync(
78
+ `openssl req -new -x509 -key "${keyPath}" -out "${certPath}" -days 365 -subj "${subj}" -addext "subjectAltName=${sanValue}"`,
79
+ { stdio: 'pipe' },
80
+ );
81
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
- },
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
+ },
90
90
  };
91
91
 
92
92
  // =============================================================================
@@ -94,48 +94,50 @@ const SSLUtils = {
94
94
  // =============================================================================
95
95
 
96
96
  const TokenManager = {
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') throw error;
117
- }
118
- },
119
-
120
- getStatus(tokenSet) {
121
- if (!tokenSet || !tokenSet.access_token) {
122
- return { authenticated: false, message: 'Not authenticated' };
123
- }
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
+ },
124
121
 
125
- const expiresAt = tokenSet.expires_at ? new Date(tokenSet.expires_at * 1000) : null;
126
- const isExpired = expiresAt ? expiresAt < new Date() : false;
127
- const hasRefreshToken = !!tokenSet.refresh_token;
122
+ getStatus(tokenSet) {
123
+ if (!tokenSet || !tokenSet.access_token) {
124
+ return { authenticated: false, message: 'Not authenticated' };
125
+ }
128
126
 
129
- return {
130
- authenticated: true,
131
- isExpired,
132
- canRefresh: hasRefreshToken,
133
- expiresAt: expiresAt ? expiresAt.toISOString() : null,
134
- message: isExpired
135
- ? (hasRefreshToken ? 'Token expired, will refresh automatically' : 'Token expired, re-authentication required')
136
- : 'Authenticated',
137
- };
138
- },
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
+ },
139
141
  };
140
142
 
141
143
  // =============================================================================
@@ -143,98 +145,122 @@ const TokenManager = {
143
145
  // =============================================================================
144
146
 
145
147
  const DeviceExtractor = {
146
- getManagementPoint(device, embeddedId) {
147
- return device.managementPoints?.find(mp => mp.embeddedId === embeddedId) || null;
148
- },
149
-
150
- getClimateControlPoint(device) {
151
- for (const id of CLIMATE_CONTROL_IDS) {
152
- const mp = this.getManagementPoint(device, id);
153
- if (mp) return mp;
154
- }
155
- return null;
156
- },
157
-
158
- extractName(device) {
159
- const climateControl = this.getClimateControlPoint(device);
160
- return climateControl?.name?.value || device.id || 'Unknown Device';
161
- },
162
-
163
- extractModel(device) {
164
- const gateway = this.getManagementPoint(device, 'gateway');
165
- return gateway?.modelInfo?.value || device.deviceModel || 'Unknown Model';
166
- },
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
+ }
167
176
 
168
- extractType(device) {
169
- 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
+ }
170
219
 
171
- for (const mp of device.managementPoints) {
172
- if (mp.embeddedId === 'climateControl') return 'Climate Control';
173
- 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');
174
224
  }
175
- return device.type || 'Unknown Type';
176
- },
177
-
178
- isOnline(device) {
179
- return device.isCloudConnectionUp?.value ?? false;
180
- },
181
-
182
- extractRoomTemp(device) {
183
- const climateControl = this.getClimateControlPoint(device);
184
- const roomTemp = climateControl?.sensoryData?.value?.roomTemperature;
185
- return roomTemp?.value !== undefined ? `${roomTemp.value}${roomTemp.unit || '°C'}` : null;
186
- },
187
-
188
- extractOutdoorTemp(device) {
189
- const climateControl = this.getClimateControlPoint(device);
190
- const outdoorTemp = climateControl?.sensoryData?.value?.outdoorTemperature;
191
- return outdoorTemp?.value !== undefined ? `${outdoorTemp.value}${outdoorTemp.unit || '°C'}` : null;
192
- },
193
-
194
- extractOperationMode(device) {
195
- const climateControl = this.getClimateControlPoint(device);
196
- return climateControl?.operationMode?.value || null;
197
- },
198
-
199
- extractPowerState(device) {
200
- const climateControl = this.getClimateControlPoint(device);
201
- return climateControl?.onOffMode?.value || null;
202
- },
203
-
204
- extractFeatures(device) {
205
- const features = [];
206
- if (!device.managementPoints) return features;
207
-
208
- for (const mp of device.managementPoints) {
209
- if (CLIMATE_CONTROL_IDS.includes(mp.embeddedId)) {
210
- if (mp.onOffMode) features.push('Power');
211
- if (mp.temperatureControl) features.push('Temperature');
212
- if (mp.operationMode) features.push('Mode');
213
- if (mp.fanControl) features.push('Fan');
214
- if (mp.sensoryData) features.push('Sensors');
215
- }
216
- if (mp.embeddedId === 'domesticHotWaterTank') {
217
- if (mp.onOffMode) features.push('Hot Water');
218
- if (mp.temperatureControl) features.push('Water Temp');
219
- }
225
+ if (mp.temperatureControl) {
226
+ features.push('Temperature');
220
227
  }
221
- return features;
222
- },
223
-
224
- extractAll(device) {
225
- return {
226
- id: device.id,
227
- name: this.extractName(device),
228
- model: this.extractModel(device),
229
- type: this.extractType(device),
230
- online: this.isOnline(device),
231
- features: this.extractFeatures(device),
232
- roomTemp: this.extractRoomTemp(device),
233
- outdoorTemp: this.extractOutdoorTemp(device),
234
- operationMode: this.extractOperationMode(device),
235
- powerState: this.extractPowerState(device),
236
- };
237
- },
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
+ },
238
264
  };
239
265
 
240
266
  // =============================================================================
@@ -242,83 +268,83 @@ const DeviceExtractor = {
242
268
  // =============================================================================
243
269
 
244
270
  class CallbackServer {
245
- constructor() {
246
- this.server = null;
247
- this.port = null;
248
- this.connections = new Set();
249
- }
250
-
251
- async start(port, hostname, certDir, requestHandler) {
252
- await this.stop();
253
-
254
- return new Promise((resolve, reject) => {
255
- const tryStart = (attempt = 1) => {
256
- try {
257
- const { key, cert } = SSLUtils.generateCert(hostname, certDir);
258
-
259
- this.server = https.createServer({ key, cert }, requestHandler);
260
-
261
- this.server.on('connection', (conn) => {
262
- this.connections.add(conn);
263
- conn.on('close', () => this.connections.delete(conn));
264
- });
265
-
266
- this.server.on('error', (err) => {
267
- if (err.code === 'EADDRINUSE' && attempt < 3) {
268
- console.warn(`Port ${port} in use, retrying in 1 second (attempt ${attempt}/3)...`);
269
- setTimeout(() => tryStart(attempt + 1), 1000);
270
- } else {
271
- console.error('Callback server error:', err.message);
272
- reject(err);
273
- }
274
- });
275
-
276
- this.server.listen(port, '0.0.0.0', () => {
277
- console.log(`HTTPS callback server listening on port ${port}`);
278
- this.port = port;
279
- resolve({ success: true, port });
280
- });
281
- } catch (error) {
282
- reject(error);
283
- }
284
- };
285
-
286
- tryStart();
287
- });
288
- }
289
-
290
- async stop() {
291
- return new Promise((resolve) => {
292
- if (!this.server) {
293
- resolve({ success: true });
294
- return;
295
- }
271
+ constructor() {
272
+ this.server = null;
273
+ this.port = null;
274
+ this.connections = new Set();
275
+ }
296
276
 
297
- for (const conn of this.connections) {
298
- conn.destroy();
277
+ async start(port, hostname, certDir, requestHandler) {
278
+ await this.stop();
279
+
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);
299
299
  }
300
- this.connections.clear();
300
+ });
301
301
 
302
- const timeout = setTimeout(() => {
303
- console.warn('HTTPS callback server close timed out, forcing cleanup');
304
- this.server = null;
305
- this.port = null;
306
- resolve({ success: true });
307
- }, 2000);
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
+ };
308
311
 
309
- this.server.close(() => {
310
- clearTimeout(timeout);
311
- console.log('HTTPS callback server stopped');
312
- this.server = null;
313
- this.port = null;
314
- resolve({ success: true });
315
- });
316
- });
317
- }
312
+ tryStart();
313
+ });
314
+ }
318
315
 
319
- get isRunning() {
320
- return this.server !== null;
321
- }
316
+ async stop() {
317
+ return new Promise((resolve) => {
318
+ if (!this.server) {
319
+ resolve({ success: true });
320
+ return;
321
+ }
322
+
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
+ }
322
348
  }
323
349
 
324
350
  // =============================================================================
@@ -326,12 +352,12 @@ class CallbackServer {
326
352
  // =============================================================================
327
353
 
328
354
  const HtmlTemplates = {
329
- callbackResponse(success, message) {
330
- const icon = success
331
- ? '<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>'
332
- : '<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>';
333
359
 
334
- return `<!DOCTYPE html>
360
+ return `<!DOCTYPE html>
335
361
  <html>
336
362
  <head>
337
363
  <meta charset="UTF-8">
@@ -387,7 +413,7 @@ const HtmlTemplates = {
387
413
  </div>
388
414
  </body>
389
415
  </html>`;
390
- },
416
+ },
391
417
  };
392
418
 
393
419
  // =============================================================================
@@ -395,504 +421,522 @@ const HtmlTemplates = {
395
421
  // =============================================================================
396
422
 
397
423
  class DaikinCloudUiServer extends HomebridgePluginUiServer {
398
- constructor() {
399
- super();
400
-
401
- this.pendingAuth = null;
402
- this.authResult = null;
403
- this.callbackServer = new CallbackServer();
404
-
405
- this.registerHandlers();
406
- 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;
407
452
  }
408
-
409
- getTokenFilePath() {
410
- 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
+ }
411
500
  }
412
501
 
413
- getMobileTokenFilePath() {
414
- return resolve(this.homebridgeStoragePath || process.env.UIX_STORAGE_PATH || '', '.daikin-mobile-tokenset');
415
- }
416
-
417
- getCertDir() {
418
- return resolve(this.homebridgeStoragePath || process.env.UIX_STORAGE_PATH || '', 'daikin-cloud-certs');
419
- }
420
-
421
- getActiveTokenSet() {
422
- // Check mobile token first (takes precedence)
423
- const mobileTokenSet = TokenManager.load(this.getMobileTokenFilePath());
424
- if (mobileTokenSet?.access_token) {
425
- return mobileTokenSet;
426
- }
427
- // Fall back to developer portal token
428
- return TokenManager.load(this.getTokenFilePath());
429
- }
430
-
431
- registerHandlers() {
432
- this.onRequest('/auth/status', this.handleGetAuthStatus.bind(this));
433
- this.onRequest('/auth/start', this.handleStartAuth.bind(this));
434
- this.onRequest('/auth/', this.handleCallback.bind(this));
435
- this.onRequest('/auth/revoke', this.handleRevokeAuth.bind(this));
436
- this.onRequest('/auth/test', this.handleTestConnection.bind(this));
437
- this.onRequest('/auth/poll', this.handlePollAuthResult.bind(this));
438
- this.onRequest('/auth/stop-server', this.handleStopServer.bind(this));
439
- this.onRequest('/auth/mobile-test', this.handleMobileAuthTest.bind(this));
440
- this.onRequest('/config/validate', this.handleValidateConfig.bind(this));
441
- this.onRequest('/devices/list', this.handleListDevices.bind(this));
442
- this.onRequest('/api/rate-limit', this.handleGetRateLimit.bind(this));
443
- this.onRequest('/server/info', this.handleGetServerInfo.bind(this));
444
- }
445
-
446
- // -------------------------------------------------------------------------
447
- // Server Info Handler
448
- // -------------------------------------------------------------------------
449
-
450
- async handleGetServerInfo() {
451
- const ipAddresses = this.getServerIpAddresses();
452
- return {
453
- ipAddresses,
454
- primaryIp: ipAddresses[0] || null,
455
- hostname: os.hostname(),
456
- };
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' };
457
530
  }
531
+ }
458
532
 
459
- getServerIpAddresses() {
460
- const interfaces = os.networkInterfaces();
461
- const addresses = [];
533
+ // -------------------------------------------------------------------------
534
+ // Start Auth Handler
535
+ // -------------------------------------------------------------------------
462
536
 
463
- for (const nets of Object.values(interfaces)) {
464
- if (!nets) continue;
465
- for (const net of nets) {
466
- // Skip internal and non-IPv4 addresses
467
- if (net.internal || net.family !== 'IPv4') continue;
468
- addresses.push(net.address);
469
- }
470
- }
537
+ async handleStartAuth(payload) {
538
+ const { clientId, clientSecret, callbackServerExternalAddress, callbackServerPort } = payload;
471
539
 
472
- return addresses;
540
+ if (!clientId || !clientSecret) {
541
+ throw new Error('Client ID and Client Secret are required');
473
542
  }
474
-
475
- // -------------------------------------------------------------------------
476
- // Auth Status Handler
477
- // -------------------------------------------------------------------------
478
-
479
- async handleGetAuthStatus() {
480
- try {
481
- // Check both token files - mobile takes precedence if exists
482
- const mobileTokenSet = TokenManager.load(this.getMobileTokenFilePath());
483
- const devPortalTokenSet = TokenManager.load(this.getTokenFilePath());
484
-
485
- if (mobileTokenSet?.access_token) {
486
- const status = TokenManager.getStatus(mobileTokenSet);
487
- status.authMode = 'mobile_app';
488
- return status;
489
- }
490
-
491
- if (devPortalTokenSet?.access_token) {
492
- const status = TokenManager.getStatus(devPortalTokenSet);
493
- status.authMode = 'developer_portal';
494
- return status;
495
- }
496
-
497
- return { authenticated: false, message: 'Not authenticated' };
498
- } catch (error) {
499
- return { authenticated: false, error: error.message, message: 'Error reading token status' };
500
- }
543
+ if (!callbackServerExternalAddress) {
544
+ throw new Error('Callback Server Address is required');
501
545
  }
502
546
 
503
- // -------------------------------------------------------------------------
504
- // Start Auth Handler
505
- // -------------------------------------------------------------------------
506
-
507
- async handleStartAuth(payload) {
508
- const { clientId, clientSecret, callbackServerExternalAddress, callbackServerPort } = payload;
509
-
510
- if (!clientId || !clientSecret) throw new Error('Client ID and Client Secret are required');
511
- if (!callbackServerExternalAddress) throw new Error('Callback Server Address is required');
512
-
513
- const port = parseInt(callbackServerPort || '8582', 10);
514
- const redirectUri = `https://${callbackServerExternalAddress}:${port}`;
515
- 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
+ }
516
576
 
517
- this.pendingAuth = { state, clientId, clientSecret, redirectUri, port, createdAt: Date.now() };
518
- 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
+ }
519
606
 
520
- // Use static method from compiled src/api
521
- const authUrl = DaikinOAuth.buildAuthUrlStatic(clientId, redirectUri, state);
522
- console.log('[DaikinCloud] Generated auth URL:', authUrl);
523
- 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
+ }
524
612
 
525
- // Try to start callback server for automatic code capture
526
- let callbackServerRunning = false;
527
- 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
+ }
528
618
 
529
- try {
530
- await this.callbackServer.start(
531
- port,
532
- callbackServerExternalAddress,
533
- this.getCertDir(),
534
- this.handleHttpsCallback.bind(this)
535
- );
536
- callbackServerRunning = true;
537
- console.log('[DaikinCloud] Callback server started successfully');
538
- } catch (error) {
539
- callbackServerError = error.message;
540
- console.warn('[DaikinCloud] Failed to start callback server:', error.message);
541
- }
619
+ const { clientId, clientSecret, redirectUri } = this.pendingAuth;
542
620
 
543
- return {
544
- authUrl,
545
- state,
546
- redirectUri,
547
- callbackServerRunning,
548
- callbackServerError,
549
- message: callbackServerRunning
550
- ? 'Callback server is running. Authentication will complete automatically.'
551
- : '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,
552
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
+ }
553
660
  }
554
661
 
555
- // -------------------------------------------------------------------------
556
- // HTTPS Callback Handler
557
- // -------------------------------------------------------------------------
558
-
559
- handleHttpsCallback(req, res) {
560
- const url = new URL(req.url, `https://${req.headers.host}`);
561
-
562
- const code = url.searchParams.get('code');
563
- const state = url.searchParams.get('state');
564
- const error = url.searchParams.get('error');
565
- const errorDescription = url.searchParams.get('error_description');
566
-
567
- if (error) {
568
- this.authResult = { success: false, error: errorDescription || error };
569
- this.sendCallbackResponse(res, false, errorDescription || error);
570
- return;
571
- }
572
-
573
- if (!code || !state) {
574
- this.authResult = { success: false, error: 'Missing code or state parameter' };
575
- this.sendCallbackResponse(res, false, 'Missing authorization code');
576
- return;
577
- }
578
-
579
- if (!this.pendingAuth || state !== this.pendingAuth.state) {
580
- this.authResult = { success: false, error: 'Invalid state parameter' };
581
- this.sendCallbackResponse(res, false, 'Invalid state parameter');
582
- return;
583
- }
584
-
585
- const { clientId, clientSecret, redirectUri } = this.pendingAuth;
586
-
587
- // Use static method from compiled src/api
588
- DaikinOAuth.exchangeCodeStatic(code, clientId, clientSecret, redirectUri)
589
- .then((tokenSet) => {
590
- TokenManager.save(this.getTokenFilePath(), tokenSet);
591
- this.authResult = {
592
- success: true,
593
- message: 'Authentication successful!',
594
- expiresAt: tokenSet.expires_at ? new Date(tokenSet.expires_at * 1000).toISOString() : null,
595
- };
596
- this.pendingAuth = null;
597
- this.sendCallbackResponse(res, true, 'Authentication successful! You can close this window.');
598
- this.callbackServer.stop().catch(() => {});
599
- })
600
- .catch((err) => {
601
- this.authResult = { success: false, error: err.message };
602
- this.sendCallbackResponse(res, false, `Token exchange failed: ${err.message}`);
603
- });
604
- }
605
-
606
- sendCallbackResponse(res, success, message) {
607
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
608
- res.end(HtmlTemplates.callbackResponse(success, message));
609
- }
610
-
611
- // -------------------------------------------------------------------------
612
- // Manual Callback Handler
613
- // -------------------------------------------------------------------------
614
-
615
- async handleCallback(payload) {
616
- let { code, state, callbackUrl } = payload;
617
-
618
- if (callbackUrl) {
619
- try {
620
- const url = new URL(callbackUrl);
621
- code = url.searchParams.get('code');
622
- state = url.searchParams.get('state');
623
- } catch (e) {
624
- throw new Error('Invalid callback URL format');
625
- }
626
- }
627
-
628
- if (!code) throw new Error('Authorization code is required');
629
- if (!this.pendingAuth) throw new Error('No pending authorization. Please start the auth flow again.');
630
- if (state && state !== this.pendingAuth.state) throw new Error('Invalid state parameter. Please try again.');
631
- if (Date.now() - this.pendingAuth.createdAt > 10 * 60 * 1000) {
632
- this.pendingAuth = null;
633
- throw new Error('Authorization request expired. Please try again.');
634
- }
635
-
636
- const { clientId, clientSecret, redirectUri } = this.pendingAuth;
637
-
638
- try {
639
- // Use static method from compiled src/api
640
- const tokenSet = await DaikinOAuth.exchangeCodeStatic(code, clientId, clientSecret, redirectUri);
641
- TokenManager.save(this.getTokenFilePath(), tokenSet);
642
- this.pendingAuth = null;
643
-
644
- await this.callbackServer.stop();
645
-
646
- return {
647
- success: true,
648
- message: 'Authentication successful! Restart Homebridge to apply.',
649
- expiresAt: tokenSet.expires_at ? new Date(tokenSet.expires_at * 1000).toISOString() : null,
650
- };
651
- } catch (error) {
652
- throw new Error(`Token exchange failed: ${error.message}`);
653
- }
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.');
654
674
  }
655
675
 
656
- // -------------------------------------------------------------------------
657
- // Poll Auth Result Handler
658
- // -------------------------------------------------------------------------
676
+ const { clientId, clientSecret, redirectUri } = this.pendingAuth;
659
677
 
660
- async handlePollAuthResult() {
661
- if (this.authResult) {
662
- const result = { ...this.authResult };
663
- if (result.success) {
664
- this.authResult = null;
665
- await this.callbackServer.stop();
666
- }
667
- return result;
668
- }
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;
669
683
 
670
- const tokenSet = TokenManager.load(this.getTokenFilePath());
671
- if (tokenSet && tokenSet.access_token) {
672
- return {
673
- success: true,
674
- message: 'Authentication successful!',
675
- expiresAt: tokenSet.expires_at ? new Date(tokenSet.expires_at * 1000).toISOString() : null,
676
- };
677
- }
678
- 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}`);
679
693
  }
694
+ }
680
695
 
681
- // -------------------------------------------------------------------------
682
- // Stop Server Handler
683
- // -------------------------------------------------------------------------
696
+ // -------------------------------------------------------------------------
697
+ // Poll Auth Result Handler
698
+ // -------------------------------------------------------------------------
684
699
 
685
- async handleStopServer() {
686
- this.pendingAuth = null;
700
+ async handlePollAuthResult() {
701
+ if (this.authResult) {
702
+ const result = { ...this.authResult };
703
+ if (result.success) {
687
704
  this.authResult = null;
688
705
  await this.callbackServer.stop();
689
- return { success: true };
706
+ }
707
+ return result;
690
708
  }
691
709
 
692
- // -------------------------------------------------------------------------
693
- // Mobile Auth Test Handler
694
- // -------------------------------------------------------------------------
695
-
696
- async handleMobileAuthTest(payload) {
697
- const { email, password } = payload;
698
-
699
- if (!email || !password) {
700
- return { success: false, message: 'Email and password are required' };
701
- }
702
-
703
- const tokenFilePath = this.getMobileTokenFilePath();
704
-
705
- try {
706
- // Create a temporary mobile OAuth client
707
- const mobileOAuth = new DaikinMobileOAuth({
708
- email,
709
- password,
710
- tokenFilePath,
711
- });
712
-
713
- // Perform authentication
714
- console.log('[DaikinCloud] Testing mobile app authentication...');
715
- const tokenSet = await mobileOAuth.authenticate();
716
- console.log('[DaikinCloud] Mobile authentication successful');
717
-
718
- // Test API access and get device count
719
- let deviceCount = 0;
720
- let rateLimit = null;
721
-
722
- try {
723
- const result = await DaikinApi.requestStatic('/v1/gateway-devices', tokenSet.access_token);
724
- deviceCount = Array.isArray(result.data) ? result.data.length : 0;
725
- rateLimit = result.rateLimit;
726
- } catch (apiError) {
727
- console.warn('[DaikinCloud] API test failed:', apiError.message);
728
- }
729
-
730
- return {
731
- success: true,
732
- message: 'Authentication successful!',
733
- deviceCount,
734
- rateLimit,
735
- expiresAt: tokenSet.expires_at ? new Date(tokenSet.expires_at * 1000).toISOString() : null,
736
- };
737
- } catch (error) {
738
- console.error('[DaikinCloud] Mobile auth test failed:', error.message);
739
- return {
740
- success: false,
741
- message: error.message || 'Authentication failed',
742
- };
743
- }
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
+ };
744
717
  }
718
+ return { pending: true };
719
+ }
745
720
 
746
- // -------------------------------------------------------------------------
747
- // Revoke Auth Handler
748
- // -------------------------------------------------------------------------
721
+ // -------------------------------------------------------------------------
722
+ // Stop Server Handler
723
+ // -------------------------------------------------------------------------
749
724
 
750
- async handleRevokeAuth(payload) {
751
- const devPortalTokenSet = TokenManager.load(this.getTokenFilePath());
752
- const mobileTokenSet = TokenManager.load(this.getMobileTokenFilePath());
725
+ async handleStopServer() {
726
+ this.pendingAuth = null;
727
+ this.authResult = null;
728
+ await this.callbackServer.stop();
729
+ return { success: true };
730
+ }
753
731
 
754
- if (!devPortalTokenSet && !mobileTokenSet) {
755
- return { success: true, message: 'No tokens to revoke' };
756
- }
732
+ // -------------------------------------------------------------------------
733
+ // Mobile Auth Test Handler
734
+ // -------------------------------------------------------------------------
757
735
 
758
- const { clientId, clientSecret } = payload;
736
+ async handleMobileAuthTest(payload) {
737
+ const { email, password } = payload;
759
738
 
760
- if (devPortalTokenSet?.refresh_token && clientId && clientSecret) {
761
- try {
762
- // Use static method from compiled src/api
763
- await DaikinOAuth.revokeTokenStatic(devPortalTokenSet.refresh_token, clientId, clientSecret);
764
- } catch (error) {
765
- console.warn('Failed to revoke token at server:', error.message);
766
- }
767
- }
739
+ if (!email || !password) {
740
+ return { success: false, message: 'Email and password are required' };
741
+ }
768
742
 
769
- // Delete both token files
770
- TokenManager.delete(this.getTokenFilePath());
771
- TokenManager.delete(this.getMobileTokenFilePath());
772
- 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
+ };
773
783
  }
784
+ }
774
785
 
775
- // -------------------------------------------------------------------------
776
- // Test Connection Handler
777
- // -------------------------------------------------------------------------
786
+ // -------------------------------------------------------------------------
787
+ // Revoke Auth Handler
788
+ // -------------------------------------------------------------------------
778
789
 
779
- async handleTestConnection() {
780
- const tokenSet = this.getActiveTokenSet();
781
- if (!tokenSet?.access_token) {
782
- return { success: false, message: 'Not authenticated. Please authenticate first.' };
783
- }
790
+ async handleRevokeAuth(payload) {
791
+ const devPortalTokenSet = TokenManager.load(this.getTokenFilePath());
792
+ const mobileTokenSet = TokenManager.load(this.getMobileTokenFilePath());
784
793
 
785
- try {
786
- // Use static method from compiled src/api
787
- const result = await DaikinApi.requestStatic('/v1/gateway-devices', tokenSet.access_token);
788
- const devices = result.data;
789
- return {
790
- success: true,
791
- message: `Connection successful! Found ${Array.isArray(devices) ? devices.length : 0} device(s).`,
792
- deviceCount: Array.isArray(devices) ? devices.length : 0,
793
- };
794
- } catch (error) {
795
- return { success: false, message: `Connection failed: ${error.message}`, error: error.message };
796
- }
794
+ if (!devPortalTokenSet && !mobileTokenSet) {
795
+ return { success: true, message: 'No tokens to revoke' };
797
796
  }
798
797
 
799
- // -------------------------------------------------------------------------
800
- // List Devices Handler
801
- // -------------------------------------------------------------------------
798
+ const { clientId, clientSecret } = payload;
802
799
 
803
- async handleListDevices(payload) {
804
- const mode = payload?.mode;
805
-
806
- // Get token based on mode parameter, or fall back to active token
807
- let tokenSet;
808
- if (mode === 'mobile_app') {
809
- tokenSet = TokenManager.load(this.getMobileTokenFilePath());
810
- } else if (mode === 'developer_portal') {
811
- tokenSet = TokenManager.load(this.getTokenFilePath());
812
- } else {
813
- tokenSet = this.getActiveTokenSet();
814
- }
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
+ }
815
808
 
816
- // If the requested mode's token doesn't exist or is invalid, fall back to active token
817
- if (!tokenSet?.access_token) {
818
- tokenSet = this.getActiveTokenSet();
819
- }
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
+ }
820
814
 
821
- if (!tokenSet?.access_token) {
822
- return { success: false, devices: [], message: 'Not authenticated. Please authenticate first.' };
823
- }
815
+ // -------------------------------------------------------------------------
816
+ // Test Connection Handler
817
+ // -------------------------------------------------------------------------
824
818
 
825
- try {
826
- // Use static method from compiled src/api
827
- const result = await DaikinApi.requestStatic('/v1/gateway-devices', tokenSet.access_token);
828
- const gatewayDevices = result.data;
829
- const devices = Array.isArray(gatewayDevices)
830
- ? gatewayDevices.map(device => DeviceExtractor.extractAll(device))
831
- : [];
832
-
833
- return { success: true, devices, message: `Found ${devices.length} device(s).` };
834
- } catch (error) {
835
- return { success: false, devices: [], message: `Failed to fetch devices: ${error.message}`, error: error.message };
836
- }
819
+ async handleTestConnection() {
820
+ const tokenSet = this.getActiveTokenSet();
821
+ if (!tokenSet?.access_token) {
822
+ return { success: false, message: 'Not authenticated. Please authenticate first.' };
837
823
  }
838
824
 
839
- // -------------------------------------------------------------------------
840
- // Get Rate Limit Handler
841
- // -------------------------------------------------------------------------
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
+ }
842
855
 
843
- async handleGetRateLimit(payload) {
844
- 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
+ }
845
860
 
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
- }
861
+ if (!tokenSet?.access_token) {
862
+ return { success: false, devices: [], message: 'Not authenticated. Please authenticate first.' };
863
+ }
855
864
 
856
- if (!tokenSet?.access_token) {
857
- return { success: false, message: 'Not authenticated' };
858
- }
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
+ }
859
895
 
860
- try {
861
- // Use static method from compiled src/api
862
- const result = await DaikinApi.requestStatic('/v1/gateway-devices', tokenSet.access_token);
863
- return { success: true, rateLimit: result.rateLimit };
864
- } catch (error) {
865
- return { success: false, message: error.message };
866
- }
896
+ if (!tokenSet?.access_token) {
897
+ return { success: false, message: 'Not authenticated' };
867
898
  }
868
899
 
869
- // -------------------------------------------------------------------------
870
- // Validate Config Handler
871
- // -------------------------------------------------------------------------
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
+ }
872
908
 
873
- async handleValidateConfig(payload) {
874
- const errors = [];
875
- const warnings = [];
876
- const { clientId, clientSecret, callbackServerExternalAddress, callbackServerPort } = payload;
909
+ // -------------------------------------------------------------------------
910
+ // Validate Config Handler
911
+ // -------------------------------------------------------------------------
877
912
 
878
- if (!clientId) errors.push('Client ID is required. Get it from the Daikin Developer Portal.');
879
- 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;
880
917
 
881
- if (!callbackServerExternalAddress) {
882
- errors.push('Callback Server External Address is required.');
883
- } else if (callbackServerExternalAddress === 'localhost' || callbackServerExternalAddress === '127.0.0.1') {
884
- errors.push('Callback address cannot be localhost. Use your external IP or domain.');
885
- }
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
+ }
886
924
 
887
- const port = parseInt(callbackServerPort || '8582', 10);
888
- if (isNaN(port) || port < 1 || port > 65535) {
889
- errors.push('Invalid port number. Must be between 1 and 65535.');
890
- } else if (port < 1024) {
891
- warnings.push('Using a privileged port (< 1024) may require root permissions.');
892
- }
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
+ }
893
930
 
894
- 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.');
895
936
  }
937
+
938
+ return { valid: errors.length === 0, errors, warnings };
939
+ }
896
940
  }
897
941
 
898
942
  // =============================================================================