@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
@@ -1,442 +0,0 @@
1
- import {DaikinApi, RateLimitedError, ApiTimeoutError} from '../../../src/api/daikin-api';
2
- import {OAuthProvider, TokenSet} from '../../../src/api/daikin-types';
3
- import {MAX_RETRY_ATTEMPTS} from '../../../src/constants';
4
- import * as https from 'node:https';
5
-
6
- jest.mock('node:https');
7
-
8
- describe('DaikinApi', () => {
9
- let mockOAuth: jest.Mocked<OAuthProvider>;
10
-
11
- beforeEach(() => {
12
- jest.clearAllMocks();
13
- jest.useFakeTimers();
14
- mockOAuth = {
15
- getAccessToken: jest.fn().mockResolvedValue('valid-token'),
16
- isAuthenticated: jest.fn().mockReturnValue(true),
17
- refreshToken: jest.fn().mockResolvedValue({
18
- access_token: 'new-token',
19
- refresh_token: 'new-refresh-token',
20
- token_type: 'Bearer',
21
- expires_in: 3600,
22
- } as TokenSet),
23
- };
24
- });
25
-
26
- afterEach(() => {
27
- jest.useRealTimers();
28
- });
29
-
30
- // Helper to run async operations with fake timers
31
- async function runWithTimers<T>(promise: Promise<T>): Promise<T> {
32
- const result = promise;
33
- // Run timers until all pending timers are exhausted
34
- await jest.runAllTimersAsync();
35
- return result;
36
- }
37
-
38
- function mockHttpsRequest(statusCode: number, body: string, headers: Record<string, string> = {}) {
39
- const mockResponse: any = {
40
- statusCode,
41
- headers,
42
- on: jest.fn((event, callback) => {
43
- if (event === 'data') {
44
- callback(body);
45
- }
46
- if (event === 'end') {
47
- callback();
48
- }
49
- return mockResponse;
50
- }),
51
- };
52
- const mockRequest = {
53
- on: jest.fn().mockReturnThis(),
54
- write: jest.fn(),
55
- end: jest.fn(),
56
- };
57
- (https.request as jest.Mock).mockImplementation((options, callback) => {
58
- callback(mockResponse);
59
- return mockRequest;
60
- });
61
- return {mockRequest, mockResponse};
62
- }
63
-
64
- describe('getDevices', () => {
65
- it('should return devices on successful request', async () => {
66
- const devices = [{id: 'device-1', managementPoints: []}];
67
- mockHttpsRequest(200, JSON.stringify(devices));
68
-
69
- const api = new DaikinApi(mockOAuth);
70
- const result = await api.getDevices();
71
-
72
- expect(result).toEqual(devices);
73
- expect(mockOAuth.getAccessToken).toHaveBeenCalled();
74
- });
75
-
76
- it('should refresh token and retry on 401 Unauthorized with exponential backoff', async () => {
77
- const devices = [{id: 'device-1', managementPoints: []}];
78
- let callCount = 0;
79
-
80
- const mockRequest = {
81
- on: jest.fn().mockReturnThis(),
82
- write: jest.fn(),
83
- end: jest.fn(),
84
- };
85
-
86
- (https.request as jest.Mock).mockImplementation((options, callback) => {
87
- callCount++;
88
- const statusCode = callCount === 1 ? 401 : 200;
89
- const body = callCount === 1 ? 'Unauthorized' : JSON.stringify(devices);
90
-
91
- const mockResponse: any = {
92
- statusCode,
93
- headers: {},
94
- on: jest.fn((event, cb) => {
95
- if (event === 'data') {
96
- cb(body);
97
- }
98
- if (event === 'end') {
99
- cb();
100
- }
101
- return mockResponse;
102
- }),
103
- };
104
- callback(mockResponse);
105
- return mockRequest;
106
- });
107
-
108
- // After refresh, return a new token
109
- mockOAuth.getAccessToken
110
- .mockResolvedValueOnce('expired-token')
111
- .mockResolvedValueOnce('new-token');
112
-
113
- const api = new DaikinApi(mockOAuth);
114
- const result = await runWithTimers(api.getDevices());
115
-
116
- expect(result).toEqual(devices);
117
- expect(mockOAuth.refreshToken).toHaveBeenCalledTimes(1);
118
- expect(https.request).toHaveBeenCalledTimes(2);
119
- });
120
-
121
- it('should throw error if refresh fails on 401', async () => {
122
- mockHttpsRequest(401, 'Unauthorized');
123
- mockOAuth.refreshToken.mockRejectedValue(new Error('Refresh failed'));
124
-
125
- const api = new DaikinApi(mockOAuth);
126
-
127
- await expect(api.getDevices()).rejects.toThrow(
128
- 'Unauthorized (401): Token refresh failed. Please re-authenticate.',
129
- );
130
- expect(mockOAuth.refreshToken).toHaveBeenCalledTimes(1);
131
- });
132
-
133
- it('should throw error if retry after refresh still returns 401', async () => {
134
- jest.useRealTimers(); // Use real timers for this test
135
- // Always return 401
136
- mockHttpsRequest(401, 'Unauthorized');
137
-
138
- const api = new DaikinApi(mockOAuth);
139
-
140
- // Temporarily override sleep to be instant for testing
141
- const originalSleep = (api as unknown as { sleep: (ms: number) => Promise<void> }).sleep;
142
- (api as unknown as { sleep: (ms: number) => Promise<void> }).sleep = () => Promise.resolve();
143
-
144
- await expect(api.getDevices()).rejects.toThrow(
145
- 'Unauthorized (401): Token expired or invalid',
146
- );
147
- // Should retry MAX_RETRY_ATTEMPTS times
148
- expect(mockOAuth.refreshToken).toHaveBeenCalledTimes(MAX_RETRY_ATTEMPTS);
149
- // Initial request + MAX_RETRY_ATTEMPTS retries
150
- expect(https.request).toHaveBeenCalledTimes(MAX_RETRY_ATTEMPTS + 1);
151
-
152
- // Restore original sleep
153
- (api as unknown as { sleep: (ms: number) => Promise<void> }).sleep = originalSleep;
154
- });
155
-
156
- it('should not retry more than MAX_RETRY_ATTEMPTS times on 401', async () => {
157
- jest.useRealTimers(); // Use real timers for this test
158
- // Always return 401
159
- mockHttpsRequest(401, 'Unauthorized');
160
-
161
- const api = new DaikinApi(mockOAuth);
162
-
163
- // Temporarily override sleep to be instant for testing
164
- (api as unknown as { sleep: (ms: number) => Promise<void> }).sleep = () => Promise.resolve();
165
-
166
- await expect(api.getDevices()).rejects.toThrow('Unauthorized');
167
- // Should only refresh MAX_RETRY_ATTEMPTS times
168
- expect(mockOAuth.refreshToken).toHaveBeenCalledTimes(MAX_RETRY_ATTEMPTS);
169
- });
170
-
171
- it('should deduplicate concurrent token refresh requests', async () => {
172
- jest.useRealTimers();
173
-
174
- // Create a slow refresh that we can control
175
- let resolveRefresh!: () => void;
176
- const refreshPromise = new Promise<void>((resolve) => {
177
- resolveRefresh = resolve;
178
- });
179
-
180
- let callCount = 0;
181
- mockOAuth.refreshToken.mockImplementation(async () => {
182
- callCount++;
183
- await refreshPromise;
184
- return {
185
- access_token: 'new-token',
186
- refresh_token: 'new-refresh',
187
- token_type: 'Bearer',
188
- expires_in: 3600,
189
- } as TokenSet;
190
- });
191
-
192
- const devices = [{id: 'device-1', managementPoints: []}];
193
- let httpCallCount = 0;
194
- const mockRequest = {
195
- on: jest.fn().mockReturnThis(),
196
- write: jest.fn(),
197
- end: jest.fn(),
198
- setTimeout: jest.fn(),
199
- };
200
-
201
- (https.request as jest.Mock).mockImplementation((options, callback) => {
202
- httpCallCount++;
203
- // First two return 401, then 200
204
- const statusCode = httpCallCount <= 2 ? 401 : 200;
205
- const body = statusCode === 200 ? JSON.stringify(devices) : 'Unauthorized';
206
- const mockResponse: any = {
207
- statusCode,
208
- headers: {},
209
- on: jest.fn((event, cb) => {
210
- if (event === 'data') {
211
- cb(body);
212
- }
213
- if (event === 'end') {
214
- cb();
215
- }
216
- return mockResponse;
217
- }),
218
- };
219
- callback(mockResponse);
220
- return mockRequest;
221
- });
222
-
223
- const api = new DaikinApi(mockOAuth);
224
- (api as unknown as { sleep: (ms: number) => Promise<void> }).sleep = () => Promise.resolve();
225
-
226
- mockOAuth.getAccessToken
227
- .mockResolvedValueOnce('expired-token')
228
- .mockResolvedValueOnce('expired-token')
229
- .mockResolvedValue('new-token');
230
-
231
- // Fire two concurrent requests that will both get 401
232
- const p1 = api.getDevices();
233
- const p2 = api.getDevices();
234
-
235
- // Let the refresh complete
236
- resolveRefresh();
237
-
238
- await Promise.all([p1, p2]);
239
-
240
- // Should only have refreshed once despite two concurrent 401s
241
- expect(callCount).toBe(1);
242
- });
243
- });
244
-
245
- describe('rate limiting', () => {
246
- it('should throw RateLimitedError on 429', async () => {
247
- mockHttpsRequest(429, 'Too Many Requests', {'retry-after': '60'});
248
-
249
- const api = new DaikinApi(mockOAuth);
250
-
251
- await expect(api.getDevices()).rejects.toThrow(RateLimitedError);
252
- });
253
-
254
- it('should block subsequent requests after rate limit', async () => {
255
- mockHttpsRequest(429, 'Too Many Requests', {'retry-after': '60'});
256
-
257
- const api = new DaikinApi(mockOAuth);
258
-
259
- await expect(api.getDevices()).rejects.toThrow(RateLimitedError);
260
- expect(api.isRateLimited()).toBe(true);
261
-
262
- // Reset mock to return 200, but should still be blocked
263
- mockHttpsRequest(200, '[]');
264
-
265
- await expect(api.getDevices()).rejects.toThrow(
266
- 'API request blocked due to rate limit',
267
- );
268
- });
269
- });
270
-
271
- describe('gateway errors', () => {
272
- it('should retry on 504 Gateway Timeout and succeed', async () => {
273
- const devices = [{id: 'device-1', managementPoints: []}];
274
- let callCount = 0;
275
-
276
- const mockRequest = {
277
- on: jest.fn().mockReturnThis(),
278
- write: jest.fn(),
279
- end: jest.fn(),
280
- };
281
-
282
- (https.request as jest.Mock).mockImplementation((options, callback) => {
283
- callCount++;
284
- // First call returns 504, second returns 200
285
- const statusCode = callCount === 1 ? 504 : 200;
286
- const body = callCount === 1 ? 'Gateway Timeout' : JSON.stringify(devices);
287
-
288
- const mockResponse: any = {
289
- statusCode,
290
- headers: {},
291
- on: jest.fn((event, cb) => {
292
- if (event === 'data') {
293
- cb(body);
294
- }
295
- if (event === 'end') {
296
- cb();
297
- }
298
- return mockResponse;
299
- }),
300
- };
301
- callback(mockResponse);
302
- return mockRequest;
303
- });
304
-
305
- const api = new DaikinApi(mockOAuth);
306
- const result = await runWithTimers(api.getDevices());
307
-
308
- expect(result).toEqual(devices);
309
- expect(https.request).toHaveBeenCalledTimes(2);
310
- });
311
-
312
- it('should retry on 502 Bad Gateway and succeed', async () => {
313
- const devices = [{id: 'device-1', managementPoints: []}];
314
- let callCount = 0;
315
-
316
- const mockRequest = {
317
- on: jest.fn().mockReturnThis(),
318
- write: jest.fn(),
319
- end: jest.fn(),
320
- };
321
-
322
- (https.request as jest.Mock).mockImplementation((options, callback) => {
323
- callCount++;
324
- const statusCode = callCount === 1 ? 502 : 200;
325
- const body = callCount === 1 ? 'Bad Gateway' : JSON.stringify(devices);
326
-
327
- const mockResponse: any = {
328
- statusCode,
329
- headers: {},
330
- on: jest.fn((event, cb) => {
331
- if (event === 'data') {
332
- cb(body);
333
- }
334
- if (event === 'end') {
335
- cb();
336
- }
337
- return mockResponse;
338
- }),
339
- };
340
- callback(mockResponse);
341
- return mockRequest;
342
- });
343
-
344
- const api = new DaikinApi(mockOAuth);
345
- const result = await runWithTimers(api.getDevices());
346
-
347
- expect(result).toEqual(devices);
348
- expect(https.request).toHaveBeenCalledTimes(2);
349
- });
350
-
351
- it('should retry on 503 Service Unavailable and succeed', async () => {
352
- const devices = [{id: 'device-1', managementPoints: []}];
353
- let callCount = 0;
354
-
355
- const mockRequest = {
356
- on: jest.fn().mockReturnThis(),
357
- write: jest.fn(),
358
- end: jest.fn(),
359
- };
360
-
361
- (https.request as jest.Mock).mockImplementation((options, callback) => {
362
- callCount++;
363
- const statusCode = callCount === 1 ? 503 : 200;
364
- const body = callCount === 1 ? 'Service Unavailable' : JSON.stringify(devices);
365
-
366
- const mockResponse: any = {
367
- statusCode,
368
- headers: {},
369
- on: jest.fn((event, cb) => {
370
- if (event === 'data') {
371
- cb(body);
372
- }
373
- if (event === 'end') {
374
- cb();
375
- }
376
- return mockResponse;
377
- }),
378
- };
379
- callback(mockResponse);
380
- return mockRequest;
381
- });
382
-
383
- const api = new DaikinApi(mockOAuth);
384
- const result = await runWithTimers(api.getDevices());
385
-
386
- expect(result).toEqual(devices);
387
- expect(https.request).toHaveBeenCalledTimes(2);
388
- });
389
-
390
- it('should throw ApiTimeoutError after exhausting retries on 504', async () => {
391
- // Always return 504
392
- mockHttpsRequest(504, 'Gateway Timeout');
393
-
394
- const api = new DaikinApi(mockOAuth);
395
-
396
- // Temporarily override sleep to be instant for testing
397
- (api as unknown as { sleep: (ms: number) => Promise<void> }).sleep = () => Promise.resolve();
398
-
399
- await expect(api.getDevices()).rejects.toThrow(ApiTimeoutError);
400
- // Initial request + MAX_RETRY_ATTEMPTS retries
401
- expect(https.request).toHaveBeenCalledTimes(MAX_RETRY_ATTEMPTS + 1);
402
- });
403
-
404
- it('should include status code and attempts in ApiTimeoutError', async () => {
405
- mockHttpsRequest(504, 'Gateway Timeout');
406
-
407
- const api = new DaikinApi(mockOAuth);
408
- (api as unknown as { sleep: (ms: number) => Promise<void> }).sleep = () => Promise.resolve();
409
-
410
- const error = await api.getDevices().catch((e) => e);
411
- expect(error).toBeInstanceOf(ApiTimeoutError);
412
- expect(error.statusCode).toBe(504);
413
- expect(error.attemptsMade).toBe(MAX_RETRY_ATTEMPTS + 1);
414
- expect(error.message).toContain('Gateway Timeout');
415
- expect(error.message).toContain('504');
416
- });
417
-
418
- it('should throw ApiTimeoutError with correct name for 502', async () => {
419
- mockHttpsRequest(502, 'Bad Gateway');
420
-
421
- const api = new DaikinApi(mockOAuth);
422
- (api as unknown as { sleep: (ms: number) => Promise<void> }).sleep = () => Promise.resolve();
423
-
424
- const error = await api.getDevices().catch((e) => e);
425
- expect(error).toBeInstanceOf(ApiTimeoutError);
426
- expect(error.statusCode).toBe(502);
427
- expect(error.message).toContain('Bad Gateway');
428
- });
429
-
430
- it('should throw ApiTimeoutError with correct name for 503', async () => {
431
- mockHttpsRequest(503, 'Service Unavailable');
432
-
433
- const api = new DaikinApi(mockOAuth);
434
- (api as unknown as { sleep: (ms: number) => Promise<void> }).sleep = () => Promise.resolve();
435
-
436
- const error = await api.getDevices().catch((e) => e);
437
- expect(error).toBeInstanceOf(ApiTimeoutError);
438
- expect(error.statusCode).toBe(503);
439
- expect(error.message).toContain('Service Unavailable');
440
- });
441
- });
442
- });
@@ -1,107 +0,0 @@
1
- import {DaikinCloudRepo} from '../../../src/api/daikin-cloud.repository';
2
-
3
- describe('DaikinCloudRepo', () => {
4
- describe('maskSensitiveCloudDeviceData', () => {
5
- it('should not mutate the original data', () => {
6
- const original = {
7
- id: 'device-1',
8
- managementPoints: [
9
- {
10
- embeddedId: 'gateway',
11
- ipAddress: {value: '192.168.1.100'},
12
- macAddress: {value: 'AA:BB:CC:DD:EE:FF'},
13
- serialNumber: {value: 'SN12345'},
14
- },
15
- ],
16
- };
17
-
18
- DaikinCloudRepo.maskSensitiveCloudDeviceData(original);
19
-
20
- // Original should NOT be mutated
21
- expect(original.managementPoints[0].ipAddress.value).toBe('192.168.1.100');
22
- expect(original.managementPoints[0].macAddress.value).toBe('AA:BB:CC:DD:EE:FF');
23
- expect(original.managementPoints[0].serialNumber.value).toBe('SN12345');
24
- });
25
-
26
- it('should return masked data in the cloned output', () => {
27
- const original = {
28
- id: 'device-1',
29
- managementPoints: [
30
- {
31
- embeddedId: 'gateway',
32
- ipAddress: {value: '192.168.1.100'},
33
- macAddress: {value: 'AA:BB:CC:DD:EE:FF'},
34
- ssid: {value: 'MyNetwork'},
35
- serialNumber: {value: 'SN12345'},
36
- wifiConnectionSSID: {value: 'MyNetwork'},
37
- consumptionData: {value: 'some data'},
38
- schedule: {value: 'some schedule'},
39
- },
40
- ],
41
- };
42
-
43
- const masked = DaikinCloudRepo.maskSensitiveCloudDeviceData(original);
44
-
45
- expect(masked.managementPoints[0].ipAddress.value).toBe('REDACTED');
46
- expect(masked.managementPoints[0].macAddress.value).toBe('REDACTED');
47
- expect(masked.managementPoints[0].ssid.value).toBe('REDACTED');
48
- expect(masked.managementPoints[0].serialNumber.value).toBe('REDACTED');
49
- expect(masked.managementPoints[0].wifiConnectionSSID.value).toBe('REDACTED');
50
- expect(masked.managementPoints[0].consumptionData).toBe('REDACTED');
51
- expect(masked.managementPoints[0].schedule).toBe('REDACTED');
52
- });
53
-
54
- it('should preserve non-sensitive data', () => {
55
- const original = {
56
- id: 'device-1',
57
- deviceModel: 'DX23',
58
- managementPoints: [
59
- {
60
- embeddedId: 'climateControl',
61
- onOffMode: {value: 'on'},
62
- operationMode: {value: 'cooling'},
63
- },
64
- ],
65
- };
66
-
67
- const masked = DaikinCloudRepo.maskSensitiveCloudDeviceData(original);
68
-
69
- expect(masked.id).toBe('device-1');
70
- expect(masked.deviceModel).toBe('DX23');
71
- expect(masked.managementPoints[0].embeddedId).toBe('climateControl');
72
- expect(masked.managementPoints[0].onOffMode.value).toBe('on');
73
- });
74
-
75
- it('should handle data without managementPoints', () => {
76
- const original = {id: 'device-1', deviceModel: 'Test'};
77
-
78
- const masked = DaikinCloudRepo.maskSensitiveCloudDeviceData(original);
79
-
80
- expect(masked.id).toBe('device-1');
81
- expect(masked.managementPoints).toBeUndefined();
82
- });
83
-
84
- it('should handle empty managementPoints array', () => {
85
- const original = {id: 'device-1', managementPoints: []};
86
-
87
- const masked = DaikinCloudRepo.maskSensitiveCloudDeviceData(original);
88
-
89
- expect(masked.managementPoints).toHaveLength(0);
90
- });
91
-
92
- it('should handle managementPoints without sensitive fields', () => {
93
- const original = {
94
- managementPoints: [
95
- {
96
- embeddedId: 'climateControl',
97
- onOffMode: {value: 'on'},
98
- },
99
- ],
100
- };
101
-
102
- const masked = DaikinCloudRepo.maskSensitiveCloudDeviceData(original);
103
-
104
- expect(masked.managementPoints[0].onOffMode.value).toBe('on');
105
- });
106
- });
107
- });