@mp-consulting/homebridge-daikin-cloud 1.3.6 → 1.3.8

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 (291) 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/index.html +24 -24
  131. package/homebridge-ui/public/lib/kit.css +253 -0
  132. package/homebridge-ui/public/lib/kit.js +133 -0
  133. package/homebridge-ui/public/script.js +957 -898
  134. package/homebridge-ui/public/styles.css +0 -1
  135. package/homebridge-ui/server.js +739 -695
  136. package/package.json +30 -25
  137. package/.claude/settings.json +0 -3
  138. package/.claude/settings.local.json +0 -24
  139. package/CHANGELOG.md +0 -114
  140. package/CLAUDE.md +0 -269
  141. package/config.md +0 -2
  142. package/docs/ARCHITECTURE.md +0 -645
  143. package/docs/IMPLEMENTATION_GUIDE.md +0 -899
  144. package/docs/IMPROVEMENTS_SUMMARY.md +0 -415
  145. package/docs/NEXT_STEPS.md +0 -368
  146. package/docs/Screenshot 2024-07-04 at 18.41.28.png +0 -0
  147. package/docs/TROUBLESHOOTING.md +0 -475
  148. package/docs/api-response-for-BRP069A8x.json +0 -520
  149. package/docs/api-response-for-BRP069C4x-2.json +0 -881
  150. package/docs/api-response-for-BRP069C4x.json +0 -916
  151. package/docs/api-response-for-altherma.json +0 -759
  152. package/docs/api-response-for-altherma2.json +0 -2735
  153. package/docs/api-response-with-multiple-devices-incl-heatpump.json +0 -2544
  154. package/docs/cr-insance-altherma-id-0.json +0 -834
  155. package/docs/mock-air-to-air-dx23.json +0 -759
  156. package/docs/mock-air-to-air-dx4.json +0 -1134
  157. package/docs/mock-airpurifier-with-humidifier.json +0 -732
  158. package/docs/mock-airpurifier.json +0 -450
  159. package/docs/mock-altherma-air-to-water-lan.json +0 -845
  160. package/docs/mock-altherma-air-to-water-wlan.json +0 -845
  161. package/docs/mock-d2cnd-gas-boiler.json +0 -649
  162. package/docs/setpointmode-vs-controlmode-vs-setpoints-vs-sensorydata.txt +0 -6
  163. package/images/fan-speed.jpeg +0 -0
  164. package/images/homekit-controls.jpeg +0 -0
  165. package/images/homekit-settings.jpeg +0 -0
  166. package/images/swing-mode.png +0 -0
  167. package/jest.config.ts +0 -21
  168. package/test/fixtures/altherma-crSense-2.ts +0 -834
  169. package/test/fixtures/altherma-fraction.ts +0 -718
  170. package/test/fixtures/altherma-heat-pump-2.ts +0 -479
  171. package/test/fixtures/altherma-heat-pump.ts +0 -757
  172. package/test/fixtures/altherma-miladcerkic-off.ts +0 -524
  173. package/test/fixtures/altherma-miladcerkic.ts +0 -524
  174. package/test/fixtures/altherma-v1ckoeln.ts +0 -644
  175. package/test/fixtures/altherma-with-embedded-id-zero.ts +0 -834
  176. package/test/fixtures/dx23-airco-2.ts +0 -343
  177. package/test/fixtures/dx23-airco.ts +0 -518
  178. package/test/fixtures/dx4-airco.ts +0 -914
  179. package/test/fixtures/unknown-jan.ts +0 -488
  180. package/test/fixtures/unknown-kitchen-guests.ts +0 -488
  181. package/test/hbConfig/.daikin-mobile-tokenset +0 -8
  182. package/test/hbConfig/.uix-dashboard.json +0 -1
  183. package/test/hbConfig/.uix-secrets +0 -1
  184. package/test/hbConfig/accessories/.cachedAccessories.bak +0 -1
  185. package/test/hbConfig/accessories/cachedAccessories +0 -1
  186. package/test/hbConfig/accessories/uiAccessoriesLayout.json +0 -1
  187. package/test/hbConfig/auth.json +0 -10
  188. package/test/hbConfig/backups/config-backups/config.json.1767953686461 +0 -25
  189. package/test/hbConfig/backups/config-backups/config.json.1767953695236 +0 -29
  190. package/test/hbConfig/backups/config-backups/config.json.1767953814763 +0 -29
  191. package/test/hbConfig/backups/config-backups/config.json.1767953823101 +0 -29
  192. package/test/hbConfig/backups/config-backups/config.json.1767954822835 +0 -29
  193. package/test/hbConfig/backups/config-backups/config.json.1767954859218 +0 -29
  194. package/test/hbConfig/backups/config-backups/config.json.1767960145503 +0 -33
  195. package/test/hbConfig/backups/config-backups/config.json.1767960168068 +0 -44
  196. package/test/hbConfig/backups/config-backups/config.json.1767960170333 +0 -46
  197. package/test/hbConfig/backups/config-backups/config.json.1767960172731 +0 -44
  198. package/test/hbConfig/backups/config-backups/config.json.1767960179323 +0 -44
  199. package/test/hbConfig/backups/config-backups/config.json.1767960182114 +0 -44
  200. package/test/hbConfig/backups/config-backups/config.json.1767960189302 +0 -44
  201. package/test/hbConfig/backups/config-backups/config.json.1767960195194 +0 -44
  202. package/test/hbConfig/backups/config-backups/config.json.1767960197301 +0 -44
  203. package/test/hbConfig/backups/config-backups/config.json.1767960199151 +0 -44
  204. package/test/hbConfig/backups/config-backups/config.json.1767960199667 +0 -44
  205. package/test/hbConfig/backups/config-backups/config.json.1767960329839 +0 -44
  206. package/test/hbConfig/backups/config-backups/config.json.1767960334503 +0 -44
  207. package/test/hbConfig/backups/config-backups/config.json.1767960336208 +0 -44
  208. package/test/hbConfig/backups/config-backups/config.json.1767960338537 +0 -44
  209. package/test/hbConfig/backups/config-backups/config.json.1767963223953 +0 -44
  210. package/test/hbConfig/backups/config-backups/config.json.1767963241753 +0 -44
  211. package/test/hbConfig/backups/config-backups/config.json.1767963252785 +0 -44
  212. package/test/hbConfig/backups/config-backups/config.json.1767963463944 +0 -44
  213. package/test/hbConfig/backups/config-backups/config.json.1767963834475 +0 -44
  214. package/test/hbConfig/backups/config-backups/config.json.1767963838474 +0 -44
  215. package/test/hbConfig/backups/config-backups/config.json.1767963843066 +0 -44
  216. package/test/hbConfig/backups/config-backups/config.json.1767965217715 +0 -44
  217. package/test/hbConfig/backups/config-backups/config.json.1767965419624 +0 -25
  218. package/test/hbConfig/backups/config-backups/config.json.1767965870934 +0 -32
  219. package/test/hbConfig/backups/config-backups/config.json.1767977675045 +0 -32
  220. package/test/hbConfig/backups/config-backups/config.json.1767977677222 +0 -33
  221. package/test/hbConfig/backups/config-backups/config.json.1767977710226 +0 -33
  222. package/test/hbConfig/backups/config-backups/config.json.1767977741397 +0 -33
  223. package/test/hbConfig/backups/config-backups/config.json.1767977977093 +0 -35
  224. package/test/hbConfig/backups/config-backups/config.json.1767977981773 +0 -35
  225. package/test/hbConfig/backups/config-backups/config.json.1767977986514 +0 -35
  226. package/test/hbConfig/backups/config-backups/config.json.1767977991174 +0 -35
  227. package/test/hbConfig/backups/config-backups/config.json.1767979424487 +0 -35
  228. package/test/hbConfig/backups/config-backups/config.json.1767979424987 +0 -35
  229. package/test/hbConfig/backups/config-backups/config.json.1767979432646 +0 -47
  230. package/test/hbConfig/backups/config-backups/config.json.1767979433150 +0 -47
  231. package/test/hbConfig/backups/config-backups/config.json.1767979436933 +0 -47
  232. package/test/hbConfig/backups/config-backups/config.json.1767979437438 +0 -47
  233. package/test/hbConfig/backups/config-backups/config.json.1767979441676 +0 -47
  234. package/test/hbConfig/backups/config-backups/config.json.1767979442180 +0 -47
  235. package/test/hbConfig/backups/config-backups/config.json.1767979466735 +0 -47
  236. package/test/hbConfig/backups/config-backups/config.json.1767979903636 +0 -47
  237. package/test/hbConfig/backups/config-backups/config.json.1767979904135 +0 -47
  238. package/test/hbConfig/backups/config-backups/config.json.1767979906606 +0 -47
  239. package/test/hbConfig/backups/config-backups/config.json.1767979907108 +0 -47
  240. package/test/hbConfig/backups/config-backups/config.json.1767988702341 +0 -47
  241. package/test/hbConfig/backups/config-backups/config.json.1767988702837 +0 -47
  242. package/test/hbConfig/backups/config-backups/config.json.1767988713159 +0 -47
  243. package/test/hbConfig/backups/config-backups/config.json.1767988713664 +0 -47
  244. package/test/hbConfig/backups/config-backups/config.json.1767988918139 +0 -47
  245. package/test/hbConfig/backups/config-backups/config.json.1767988918639 +0 -47
  246. package/test/hbConfig/backups/config-backups/config.json.1767988921120 +0 -47
  247. package/test/hbConfig/backups/config-backups/config.json.1767988921624 +0 -47
  248. package/test/hbConfig/backups/config-backups/config.json.1767988930307 +0 -47
  249. package/test/hbConfig/backups/config-backups/config.json.1767988935070 +0 -47
  250. package/test/hbConfig/backups/config-backups/config.json.1767988935574 +0 -47
  251. package/test/hbConfig/backups/config-backups/config.json.1767989710262 +0 -47
  252. package/test/hbConfig/backups/config-backups/config.json.1767989710760 +0 -47
  253. package/test/hbConfig/backups/config-backups/config.json.1767989729668 +0 -47
  254. package/test/hbConfig/backups/config-backups/config.json.1767990295225 +0 -47
  255. package/test/hbConfig/backups/config-backups/config.json.1767990479921 +0 -47
  256. package/test/hbConfig/backups/config-backups/config.json.1767990481702 +0 -49
  257. package/test/hbConfig/backups/instance-backups/homebridge-backup-1E4A432551BA.1768010187391.tar.gz +0 -0
  258. package/test/hbConfig/backups/instance-backups/homebridge-backup-1E4A432551BA.1768096587387.tar.gz +0 -0
  259. package/test/hbConfig/backups/instance-backups/homebridge-backup-1E4A432551BA.1768182987404.tar.gz +0 -0
  260. package/test/hbConfig/config.json +0 -47
  261. package/test/hbConfig/daikin-cloud-certs/server.crt +0 -22
  262. package/test/hbConfig/daikin-cloud-certs/server.key +0 -28
  263. package/test/hbConfig/persist/AccessoryInfo.1E4A432551BA.json +0 -1
  264. package/test/hbConfig/persist/IdentifierCache.1E4A432551BA.json +0 -1
  265. package/test/hbConfig/tmp/daikin_request/api.onecta.daikineurope.com_01-09-2026-23-29-52/request_14758 +0 -1
  266. package/test/hbConfig/tmp/daikin_request/api.onecta.daikineurope.com_01-09-2026-23-29-52/request_14759 +0 -1
  267. package/test/hbConfig/tmp/daikin_request/api.onecta.daikineurope.com_01-09-2026-23-29-52/request_14760 +0 -1
  268. package/test/hbConfig/tmp/daikin_request/api.onecta.daikineurope.com_01-09-2026-23-29-52/request_14761 +0 -1
  269. package/test/hbConfig/tmp/daikin_request/api.onecta.daikineurope.com_01-09-2026-23-29-52/request_14762 +0 -1
  270. package/test/hbConfig/tmp/daikin_request/api.onecta.daikineurope.com_01-09-2026-23-29-52/request_14764 +0 -1
  271. package/test/helpers/test-isolation.ts +0 -228
  272. package/test/integration/air-conditioning.test.ts +0 -396
  273. package/test/integration/altherma.test.ts +0 -279
  274. package/test/integration/platform.test.ts +0 -118
  275. package/test/mobile-tokens.json +0 -8
  276. package/test/mocks/index.ts +0 -27
  277. package/test/test-gigya-auth.js +0 -443
  278. package/test/test-mobile-oauth.js +0 -175
  279. package/test/test-websocket-mobile.js +0 -123
  280. package/test/test-websocket.js +0 -116
  281. package/test/unit/api/__snapshots__/daikinCloud.test.ts.snap +0 -1320
  282. package/test/unit/api/daikin-api.test.ts +0 -442
  283. package/test/unit/api/daikin-cloud-repository.test.ts +0 -107
  284. package/test/unit/api/daikin-oauth.test.ts +0 -214
  285. package/test/unit/api/daikinCloud.test.ts +0 -12
  286. package/test/unit/api/token-storage.test.ts +0 -90
  287. package/test/unit/config/config-manager.test.ts +0 -271
  288. package/test/unit/device/daikin-device.test.ts +0 -73
  289. package/test/unit/services/hot-water-tank.service.test.ts +0 -123
  290. package/test/unit/utils/log-context.test.ts +0 -271
  291. 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
- });