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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/LICENSE +39 -1
  2. package/README.md +5 -3
  3. package/dist/src/accessories/air-conditioning-accessory.d.ts +2 -2
  4. package/dist/src/accessories/air-conditioning-accessory.d.ts.map +1 -1
  5. package/dist/src/accessories/air-conditioning-accessory.js.map +1 -1
  6. package/dist/src/accessories/altherma-accessory.d.ts +2 -2
  7. package/dist/src/accessories/altherma-accessory.d.ts.map +1 -1
  8. package/dist/src/accessories/altherma-accessory.js.map +1 -1
  9. package/dist/src/accessories/base-accessory.d.ts +6 -6
  10. package/dist/src/accessories/base-accessory.d.ts.map +1 -1
  11. package/dist/src/accessories/base-accessory.js +15 -15
  12. package/dist/src/accessories/base-accessory.js.map +1 -1
  13. package/dist/src/api/daikin-api.d.ts +26 -26
  14. package/dist/src/api/daikin-api.d.ts.map +1 -1
  15. package/dist/src/api/daikin-api.js +68 -42
  16. package/dist/src/api/daikin-api.js.map +1 -1
  17. package/dist/src/api/daikin-cloud.repository.d.ts.map +1 -1
  18. package/dist/src/api/daikin-cloud.repository.js +22 -14
  19. package/dist/src/api/daikin-cloud.repository.js.map +1 -1
  20. package/dist/src/api/daikin-controller.d.ts +41 -47
  21. package/dist/src/api/daikin-controller.d.ts.map +1 -1
  22. package/dist/src/api/daikin-controller.js +40 -64
  23. package/dist/src/api/daikin-controller.js.map +1 -1
  24. package/dist/src/api/daikin-device.d.ts +36 -31
  25. package/dist/src/api/daikin-device.d.ts.map +1 -1
  26. package/dist/src/api/daikin-device.js +45 -31
  27. package/dist/src/api/daikin-device.js.map +1 -1
  28. package/dist/src/api/daikin-mobile-oauth.d.ts +20 -20
  29. package/dist/src/api/daikin-mobile-oauth.d.ts.map +1 -1
  30. package/dist/src/api/daikin-mobile-oauth.js +49 -44
  31. package/dist/src/api/daikin-mobile-oauth.js.map +1 -1
  32. package/dist/src/api/daikin-oauth.d.ts +32 -32
  33. package/dist/src/api/daikin-oauth.d.ts.map +1 -1
  34. package/dist/src/api/daikin-oauth.js +64 -56
  35. package/dist/src/api/daikin-oauth.js.map +1 -1
  36. package/dist/src/api/daikin-schemas.d.ts +476 -351
  37. package/dist/src/api/daikin-schemas.d.ts.map +1 -1
  38. package/dist/src/api/daikin-schemas.js +11 -42
  39. package/dist/src/api/daikin-schemas.js.map +1 -1
  40. package/dist/src/api/daikin-types.d.ts +5 -1
  41. package/dist/src/api/daikin-types.d.ts.map +1 -1
  42. package/dist/src/api/daikin-types.js.map +1 -1
  43. package/dist/src/api/daikin-websocket.d.ts +31 -32
  44. package/dist/src/api/daikin-websocket.d.ts.map +1 -1
  45. package/dist/src/api/daikin-websocket.js +55 -35
  46. package/dist/src/api/daikin-websocket.js.map +1 -1
  47. package/dist/src/api/index.d.ts +2 -1
  48. package/dist/src/api/index.d.ts.map +1 -1
  49. package/dist/src/api/index.js +3 -1
  50. package/dist/src/api/index.js.map +1 -1
  51. package/dist/src/api/token-storage.d.ts +21 -0
  52. package/dist/src/api/token-storage.d.ts.map +1 -0
  53. package/dist/src/api/token-storage.js +90 -0
  54. package/dist/src/api/token-storage.js.map +1 -0
  55. package/dist/src/config/config-manager.d.ts +33 -33
  56. package/dist/src/config/config-manager.d.ts.map +1 -1
  57. package/dist/src/config/config-manager.js +33 -33
  58. package/dist/src/config/config-manager.js.map +1 -1
  59. package/dist/src/constants/api.constants.d.ts +4 -0
  60. package/dist/src/constants/api.constants.d.ts.map +1 -1
  61. package/dist/src/constants/api.constants.js +5 -1
  62. package/dist/src/constants/api.constants.js.map +1 -1
  63. package/dist/src/constants/device.constants.d.ts +4 -0
  64. package/dist/src/constants/device.constants.d.ts.map +1 -1
  65. package/dist/src/constants/device.constants.js +5 -1
  66. package/dist/src/constants/device.constants.js.map +1 -1
  67. package/dist/src/device/accessory-factory.d.ts +10 -10
  68. package/dist/src/device/accessory-factory.d.ts.map +1 -1
  69. package/dist/src/device/accessory-factory.js +7 -7
  70. package/dist/src/device/accessory-factory.js.map +1 -1
  71. package/dist/src/device/capability-detector.d.ts +8 -8
  72. package/dist/src/device/capability-detector.d.ts.map +1 -1
  73. package/dist/src/device/capability-detector.js +6 -6
  74. package/dist/src/device/capability-detector.js.map +1 -1
  75. package/dist/src/device/capability-docs.d.ts +1 -9
  76. package/dist/src/device/capability-docs.d.ts.map +1 -1
  77. package/dist/src/device/capability-docs.js +19 -73
  78. package/dist/src/device/capability-docs.js.map +1 -1
  79. package/dist/src/device/profiles/device-profile.d.ts +1 -1
  80. package/dist/src/device/profiles/device-profile.d.ts.map +1 -1
  81. package/dist/src/device/profiles/device-profile.js +4 -4
  82. package/dist/src/device/profiles/device-profile.js.map +1 -1
  83. package/dist/src/features/base-feature.d.ts +2 -2
  84. package/dist/src/features/base-feature.d.ts.map +1 -1
  85. package/dist/src/features/base-feature.js +2 -3
  86. package/dist/src/features/base-feature.js.map +1 -1
  87. package/dist/src/features/feature-manager.d.ts +8 -16
  88. package/dist/src/features/feature-manager.d.ts.map +1 -1
  89. package/dist/src/features/feature-manager.js +5 -17
  90. package/dist/src/features/feature-manager.js.map +1 -1
  91. package/dist/src/features/modes/dry-operation-mode.feature.d.ts +1 -1
  92. package/dist/src/features/modes/dry-operation-mode.feature.d.ts.map +1 -1
  93. package/dist/src/features/modes/dry-operation-mode.feature.js.map +1 -1
  94. package/dist/src/features/modes/econo-mode.feature.d.ts +1 -1
  95. package/dist/src/features/modes/econo-mode.feature.d.ts.map +1 -1
  96. package/dist/src/features/modes/econo-mode.feature.js.map +1 -1
  97. package/dist/src/features/modes/fan-only-operation-mode.feature.d.ts +1 -1
  98. package/dist/src/features/modes/fan-only-operation-mode.feature.d.ts.map +1 -1
  99. package/dist/src/features/modes/fan-only-operation-mode.feature.js.map +1 -1
  100. package/dist/src/features/modes/indoor-silent-mode.feature.d.ts +1 -1
  101. package/dist/src/features/modes/indoor-silent-mode.feature.d.ts.map +1 -1
  102. package/dist/src/features/modes/indoor-silent-mode.feature.js.map +1 -1
  103. package/dist/src/features/modes/outdoor-silent-mode.feature.d.ts +1 -1
  104. package/dist/src/features/modes/outdoor-silent-mode.feature.d.ts.map +1 -1
  105. package/dist/src/features/modes/outdoor-silent-mode.feature.js.map +1 -1
  106. package/dist/src/features/modes/powerful-mode.feature.d.ts +1 -1
  107. package/dist/src/features/modes/powerful-mode.feature.d.ts.map +1 -1
  108. package/dist/src/features/modes/powerful-mode.feature.js.map +1 -1
  109. package/dist/src/features/modes/streamer-mode.feature.d.ts +1 -1
  110. package/dist/src/features/modes/streamer-mode.feature.d.ts.map +1 -1
  111. package/dist/src/features/modes/streamer-mode.feature.js.map +1 -1
  112. package/dist/src/index.d.ts +1 -1
  113. package/dist/src/index.d.ts.map +1 -1
  114. package/dist/src/index.js.map +1 -1
  115. package/dist/src/platform.d.ts +11 -8
  116. package/dist/src/platform.d.ts.map +1 -1
  117. package/dist/src/platform.js +64 -15
  118. package/dist/src/platform.js.map +1 -1
  119. package/dist/src/services/climate-control.service.d.ts +8 -2
  120. package/dist/src/services/climate-control.service.d.ts.map +1 -1
  121. package/dist/src/services/climate-control.service.js +59 -58
  122. package/dist/src/services/climate-control.service.js.map +1 -1
  123. package/dist/src/services/hot-water-tank.service.d.ts +6 -2
  124. package/dist/src/services/hot-water-tank.service.d.ts.map +1 -1
  125. package/dist/src/services/hot-water-tank.service.js +33 -31
  126. package/dist/src/services/hot-water-tank.service.js.map +1 -1
  127. package/dist/src/types/daikin-enums.js +12 -12
  128. package/dist/src/types/daikin-enums.js.map +1 -1
  129. package/dist/src/types/device-capabilities.d.ts +1 -1
  130. package/dist/src/types/device-capabilities.d.ts.map +1 -1
  131. package/dist/src/utils/log-context.d.ts +23 -23
  132. package/dist/src/utils/log-context.d.ts.map +1 -1
  133. package/dist/src/utils/log-context.js +28 -28
  134. package/dist/src/utils/log-context.js.map +1 -1
  135. package/dist/src/utils/strings.d.ts.map +1 -1
  136. package/dist/src/utils/strings.js.map +1 -1
  137. package/dist/src/utils/update-mapper.d.ts +16 -16
  138. package/dist/src/utils/update-mapper.d.ts.map +1 -1
  139. package/dist/src/utils/update-mapper.js +14 -14
  140. package/dist/src/utils/update-mapper.js.map +1 -1
  141. package/homebridge-ui/public/index.html +2 -2
  142. package/homebridge-ui/public/script.js +957 -898
  143. package/homebridge-ui/server.js +746 -678
  144. package/package.json +29 -27
  145. package/.claude/settings.json +0 -3
  146. package/.claude/settings.local.json +0 -29
  147. package/CHANGELOG.md +0 -103
  148. package/CLAUDE.md +0 -269
  149. package/config.md +0 -2
  150. package/dist/src/api/daikin-device-tracker.d.ts +0 -97
  151. package/dist/src/api/daikin-device-tracker.d.ts.map +0 -1
  152. package/dist/src/api/daikin-device-tracker.js +0 -136
  153. package/dist/src/api/daikin-device-tracker.js.map +0 -1
  154. package/dist/src/api/http-interceptor.d.ts +0 -99
  155. package/dist/src/api/http-interceptor.d.ts.map +0 -1
  156. package/dist/src/api/http-interceptor.js +0 -177
  157. package/dist/src/api/http-interceptor.js.map +0 -1
  158. package/dist/src/di/service-container.d.ts +0 -92
  159. package/dist/src/di/service-container.d.ts.map +0 -1
  160. package/dist/src/di/service-container.js +0 -156
  161. package/dist/src/di/service-container.js.map +0 -1
  162. package/dist/src/features/feature-registry.d.ts +0 -100
  163. package/dist/src/features/feature-registry.d.ts.map +0 -1
  164. package/dist/src/features/feature-registry.js +0 -142
  165. package/dist/src/features/feature-registry.js.map +0 -1
  166. package/dist/src/services/service-factory.d.ts +0 -46
  167. package/dist/src/services/service-factory.d.ts.map +0 -1
  168. package/dist/src/services/service-factory.js +0 -72
  169. package/dist/src/services/service-factory.js.map +0 -1
  170. package/dist/src/utils/error-handler.d.ts +0 -101
  171. package/dist/src/utils/error-handler.d.ts.map +0 -1
  172. package/dist/src/utils/error-handler.js +0 -251
  173. package/dist/src/utils/error-handler.js.map +0 -1
  174. package/dist/src/utils/retry.d.ts +0 -42
  175. package/dist/src/utils/retry.d.ts.map +0 -1
  176. package/dist/src/utils/retry.js +0 -70
  177. package/dist/src/utils/retry.js.map +0 -1
  178. package/docs/ARCHITECTURE.md +0 -645
  179. package/docs/IMPLEMENTATION_GUIDE.md +0 -899
  180. package/docs/IMPROVEMENTS_SUMMARY.md +0 -415
  181. package/docs/NEXT_STEPS.md +0 -368
  182. package/docs/Screenshot 2024-07-04 at 18.41.28.png +0 -0
  183. package/docs/TROUBLESHOOTING.md +0 -475
  184. package/docs/api-response-for-BRP069A8x.json +0 -520
  185. package/docs/api-response-for-BRP069C4x-2.json +0 -881
  186. package/docs/api-response-for-BRP069C4x.json +0 -916
  187. package/docs/api-response-for-altherma.json +0 -759
  188. package/docs/api-response-for-altherma2.json +0 -2735
  189. package/docs/api-response-with-multiple-devices-incl-heatpump.json +0 -2544
  190. package/docs/cr-insance-altherma-id-0.json +0 -834
  191. package/docs/mock-air-to-air-dx23.json +0 -759
  192. package/docs/mock-air-to-air-dx4.json +0 -1134
  193. package/docs/mock-airpurifier-with-humidifier.json +0 -732
  194. package/docs/mock-airpurifier.json +0 -450
  195. package/docs/mock-altherma-air-to-water-lan.json +0 -845
  196. package/docs/mock-altherma-air-to-water-wlan.json +0 -845
  197. package/docs/mock-d2cnd-gas-boiler.json +0 -649
  198. package/docs/setpointmode-vs-controlmode-vs-setpoints-vs-sensorydata.txt +0 -6
  199. package/images/fan-speed.jpeg +0 -0
  200. package/images/homekit-controls.jpeg +0 -0
  201. package/images/homekit-settings.jpeg +0 -0
  202. package/images/swing-mode.png +0 -0
  203. package/jest.config.ts +0 -13
  204. package/test/fixtures/altherma-crSense-2.ts +0 -834
  205. package/test/fixtures/altherma-fraction.ts +0 -718
  206. package/test/fixtures/altherma-heat-pump-2.ts +0 -479
  207. package/test/fixtures/altherma-heat-pump.ts +0 -757
  208. package/test/fixtures/altherma-miladcerkic-off.ts +0 -524
  209. package/test/fixtures/altherma-miladcerkic.ts +0 -524
  210. package/test/fixtures/altherma-v1ckoeln.ts +0 -644
  211. package/test/fixtures/altherma-with-embedded-id-zero.ts +0 -834
  212. package/test/fixtures/dx23-airco-2.ts +0 -343
  213. package/test/fixtures/dx23-airco.ts +0 -518
  214. package/test/fixtures/dx4-airco.ts +0 -914
  215. package/test/fixtures/unknown-jan.ts +0 -488
  216. package/test/fixtures/unknown-kitchen-guests.ts +0 -488
  217. package/test/helpers/test-isolation.ts +0 -228
  218. package/test/integration/air-conditioning.test.ts +0 -410
  219. package/test/integration/altherma.test.ts +0 -289
  220. package/test/integration/platform.test.ts +0 -118
  221. package/test/mocks/index.ts +0 -27
  222. package/test/test-gigya-auth.js +0 -443
  223. package/test/test-mobile-oauth.js +0 -175
  224. package/test/test-websocket-mobile.js +0 -123
  225. package/test/test-websocket.js +0 -116
  226. package/test/unit/api/__snapshots__/daikinCloud.test.ts.snap +0 -1320
  227. package/test/unit/api/daikin-api.test.ts +0 -384
  228. package/test/unit/api/daikin-oauth.test.ts +0 -214
  229. package/test/unit/api/daikinCloud.test.ts +0 -12
  230. package/test/unit/config/config-manager.test.ts +0 -271
  231. package/test/unit/device/daikin-device.test.ts +0 -79
  232. package/test/unit/services/hot-water-tank.service.test.ts +0 -123
  233. package/test/unit/utils/error-handler.test.ts +0 -274
  234. package/test/unit/utils/log-context.test.ts +0 -271
@@ -1,384 +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
-
172
- describe('rate limiting', () => {
173
- it('should throw RateLimitedError on 429', async () => {
174
- mockHttpsRequest(429, 'Too Many Requests', {'retry-after': '60'});
175
-
176
- const api = new DaikinApi(mockOAuth);
177
-
178
- await expect(api.getDevices()).rejects.toThrow(RateLimitedError);
179
- });
180
-
181
- it('should block subsequent requests after rate limit', async () => {
182
- mockHttpsRequest(429, 'Too Many Requests', {'retry-after': '60'});
183
-
184
- const api = new DaikinApi(mockOAuth);
185
-
186
- await expect(api.getDevices()).rejects.toThrow(RateLimitedError);
187
- expect(api.isRateLimited()).toBe(true);
188
-
189
- // Reset mock to return 200, but should still be blocked
190
- mockHttpsRequest(200, '[]');
191
-
192
- await expect(api.getDevices()).rejects.toThrow(
193
- 'API request blocked due to rate limit',
194
- );
195
- });
196
- });
197
-
198
- describe('gateway errors', () => {
199
- it('should retry on 504 Gateway Timeout and succeed', async () => {
200
- const devices = [{id: 'device-1', managementPoints: []}];
201
- let callCount = 0;
202
-
203
- const mockRequest = {
204
- on: jest.fn().mockReturnThis(),
205
- write: jest.fn(),
206
- end: jest.fn(),
207
- };
208
-
209
- (https.request as jest.Mock).mockImplementation((options, callback) => {
210
- callCount++;
211
- // First call returns 504, second returns 200
212
- const statusCode = callCount === 1 ? 504 : 200;
213
- const body = callCount === 1 ? 'Gateway Timeout' : JSON.stringify(devices);
214
-
215
- const mockResponse: any = {
216
- statusCode,
217
- headers: {},
218
- on: jest.fn((event, cb) => {
219
- if (event === 'data') {
220
- cb(body);
221
- }
222
- if (event === 'end') {
223
- cb();
224
- }
225
- return mockResponse;
226
- }),
227
- };
228
- callback(mockResponse);
229
- return mockRequest;
230
- });
231
-
232
- const api = new DaikinApi(mockOAuth);
233
- const result = await runWithTimers(api.getDevices());
234
-
235
- expect(result).toEqual(devices);
236
- expect(https.request).toHaveBeenCalledTimes(2);
237
- });
238
-
239
- it('should retry on 502 Bad Gateway and succeed', async () => {
240
- const devices = [{id: 'device-1', managementPoints: []}];
241
- let callCount = 0;
242
-
243
- const mockRequest = {
244
- on: jest.fn().mockReturnThis(),
245
- write: jest.fn(),
246
- end: jest.fn(),
247
- };
248
-
249
- (https.request as jest.Mock).mockImplementation((options, callback) => {
250
- callCount++;
251
- const statusCode = callCount === 1 ? 502 : 200;
252
- const body = callCount === 1 ? 'Bad Gateway' : JSON.stringify(devices);
253
-
254
- const mockResponse: any = {
255
- statusCode,
256
- headers: {},
257
- on: jest.fn((event, cb) => {
258
- if (event === 'data') {
259
- cb(body);
260
- }
261
- if (event === 'end') {
262
- cb();
263
- }
264
- return mockResponse;
265
- }),
266
- };
267
- callback(mockResponse);
268
- return mockRequest;
269
- });
270
-
271
- const api = new DaikinApi(mockOAuth);
272
- const result = await runWithTimers(api.getDevices());
273
-
274
- expect(result).toEqual(devices);
275
- expect(https.request).toHaveBeenCalledTimes(2);
276
- });
277
-
278
- it('should retry on 503 Service Unavailable and succeed', async () => {
279
- const devices = [{id: 'device-1', managementPoints: []}];
280
- let callCount = 0;
281
-
282
- const mockRequest = {
283
- on: jest.fn().mockReturnThis(),
284
- write: jest.fn(),
285
- end: jest.fn(),
286
- };
287
-
288
- (https.request as jest.Mock).mockImplementation((options, callback) => {
289
- callCount++;
290
- const statusCode = callCount === 1 ? 503 : 200;
291
- const body = callCount === 1 ? 'Service Unavailable' : JSON.stringify(devices);
292
-
293
- const mockResponse: any = {
294
- statusCode,
295
- headers: {},
296
- on: jest.fn((event, cb) => {
297
- if (event === 'data') {
298
- cb(body);
299
- }
300
- if (event === 'end') {
301
- cb();
302
- }
303
- return mockResponse;
304
- }),
305
- };
306
- callback(mockResponse);
307
- return mockRequest;
308
- });
309
-
310
- const api = new DaikinApi(mockOAuth);
311
- const result = await runWithTimers(api.getDevices());
312
-
313
- expect(result).toEqual(devices);
314
- expect(https.request).toHaveBeenCalledTimes(2);
315
- });
316
-
317
- it('should throw ApiTimeoutError after exhausting retries on 504', async () => {
318
- // Always return 504
319
- mockHttpsRequest(504, 'Gateway Timeout');
320
-
321
- const api = new DaikinApi(mockOAuth);
322
-
323
- // Temporarily override sleep to be instant for testing
324
- (api as unknown as { sleep: (ms: number) => Promise<void> }).sleep = () => Promise.resolve();
325
-
326
- await expect(api.getDevices()).rejects.toThrow(ApiTimeoutError);
327
- // Initial request + MAX_RETRY_ATTEMPTS retries
328
- expect(https.request).toHaveBeenCalledTimes(MAX_RETRY_ATTEMPTS + 1);
329
- });
330
-
331
- it('should include status code and attempts in ApiTimeoutError', async () => {
332
- mockHttpsRequest(504, 'Gateway Timeout');
333
-
334
- const api = new DaikinApi(mockOAuth);
335
- (api as unknown as { sleep: (ms: number) => Promise<void> }).sleep = () => Promise.resolve();
336
-
337
- try {
338
- await api.getDevices();
339
- fail('Expected ApiTimeoutError to be thrown');
340
- } catch (error) {
341
- expect(error).toBeInstanceOf(ApiTimeoutError);
342
- const timeoutError = error as ApiTimeoutError;
343
- expect(timeoutError.statusCode).toBe(504);
344
- expect(timeoutError.attemptsMade).toBe(MAX_RETRY_ATTEMPTS + 1);
345
- expect(timeoutError.message).toContain('Gateway Timeout');
346
- expect(timeoutError.message).toContain('504');
347
- }
348
- });
349
-
350
- it('should throw ApiTimeoutError with correct name for 502', async () => {
351
- mockHttpsRequest(502, 'Bad Gateway');
352
-
353
- const api = new DaikinApi(mockOAuth);
354
- (api as unknown as { sleep: (ms: number) => Promise<void> }).sleep = () => Promise.resolve();
355
-
356
- try {
357
- await api.getDevices();
358
- fail('Expected ApiTimeoutError to be thrown');
359
- } catch (error) {
360
- expect(error).toBeInstanceOf(ApiTimeoutError);
361
- const timeoutError = error as ApiTimeoutError;
362
- expect(timeoutError.statusCode).toBe(502);
363
- expect(timeoutError.message).toContain('Bad Gateway');
364
- }
365
- });
366
-
367
- it('should throw ApiTimeoutError with correct name for 503', async () => {
368
- mockHttpsRequest(503, 'Service Unavailable');
369
-
370
- const api = new DaikinApi(mockOAuth);
371
- (api as unknown as { sleep: (ms: number) => Promise<void> }).sleep = () => Promise.resolve();
372
-
373
- try {
374
- await api.getDevices();
375
- fail('Expected ApiTimeoutError to be thrown');
376
- } catch (error) {
377
- expect(error).toBeInstanceOf(ApiTimeoutError);
378
- const timeoutError = error as ApiTimeoutError;
379
- expect(timeoutError.statusCode).toBe(503);
380
- expect(timeoutError.message).toContain('Service Unavailable');
381
- }
382
- });
383
- });
384
- });
@@ -1,214 +0,0 @@
1
- import {DaikinOAuth} from '../../../src/api/daikin-oauth';
2
- import * as fs from 'node:fs';
3
- import * as https from 'node:https';
4
-
5
- jest.mock('node:fs');
6
- jest.mock('node:https');
7
-
8
- describe('DaikinOAuth', () => {
9
- const mockConfig = {
10
- clientId: 'test-client-id',
11
- clientSecret: 'test-client-secret',
12
- callbackServerExternalAddress: '192.168.1.1',
13
- callbackServerPort: 8582,
14
- tokenFilePath: '/tmp/test-token.json',
15
- };
16
-
17
- beforeEach(() => {
18
- jest.clearAllMocks();
19
- (fs.existsSync as jest.Mock).mockReturnValue(false);
20
- });
21
-
22
- describe('getAccessToken', () => {
23
- it('should return the access token if not expired', async () => {
24
- const futureExpiry = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now
25
- const tokenSet = {
26
- access_token: 'valid-token',
27
- refresh_token: 'refresh-token',
28
- token_type: 'Bearer',
29
- expires_at: futureExpiry,
30
- };
31
-
32
- (fs.existsSync as jest.Mock).mockReturnValue(true);
33
- (fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify(tokenSet));
34
-
35
- const oauth = new DaikinOAuth(mockConfig);
36
- const token = await oauth.getAccessToken();
37
-
38
- expect(token).toBe('valid-token');
39
- });
40
-
41
- it('should refresh the token if expired', async () => {
42
- const pastExpiry = Math.floor(Date.now() / 1000) - 100; // 100 seconds ago
43
- const tokenSet = {
44
- access_token: 'expired-token',
45
- refresh_token: 'refresh-token',
46
- token_type: 'Bearer',
47
- expires_at: pastExpiry,
48
- };
49
-
50
- const newTokenSet = {
51
- access_token: 'new-token',
52
- refresh_token: 'new-refresh-token',
53
- token_type: 'Bearer',
54
- expires_in: 3600,
55
- };
56
-
57
- (fs.existsSync as jest.Mock).mockReturnValue(true);
58
- (fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify(tokenSet));
59
-
60
- // Mock the HTTPS request for token refresh
61
- const mockResponse = {
62
- on: jest.fn((event, callback) => {
63
- if (event === 'data') {
64
- callback(JSON.stringify(newTokenSet));
65
- }
66
- if (event === 'end') {
67
- callback();
68
- }
69
- }),
70
- };
71
- const mockRequest = {
72
- on: jest.fn(),
73
- write: jest.fn(),
74
- end: jest.fn(),
75
- };
76
- (https.request as jest.Mock).mockImplementation((options, callback) => {
77
- callback(mockResponse);
78
- return mockRequest;
79
- });
80
-
81
- const oauth = new DaikinOAuth(mockConfig);
82
- const token = await oauth.getAccessToken();
83
-
84
- expect(token).toBe('new-token');
85
- expect(https.request).toHaveBeenCalled();
86
- });
87
-
88
- it('should refresh the token if about to expire (within 10 seconds)', async () => {
89
- const soonExpiry = Math.floor(Date.now() / 1000) + 5; // 5 seconds from now
90
- const tokenSet = {
91
- access_token: 'soon-expiring-token',
92
- refresh_token: 'refresh-token',
93
- token_type: 'Bearer',
94
- expires_at: soonExpiry,
95
- };
96
-
97
- const newTokenSet = {
98
- access_token: 'refreshed-token',
99
- refresh_token: 'new-refresh-token',
100
- token_type: 'Bearer',
101
- expires_in: 3600,
102
- };
103
-
104
- (fs.existsSync as jest.Mock).mockReturnValue(true);
105
- (fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify(tokenSet));
106
-
107
- const mockResponse = {
108
- on: jest.fn((event, callback) => {
109
- if (event === 'data') {
110
- callback(JSON.stringify(newTokenSet));
111
- }
112
- if (event === 'end') {
113
- callback();
114
- }
115
- }),
116
- };
117
- const mockRequest = {
118
- on: jest.fn(),
119
- write: jest.fn(),
120
- end: jest.fn(),
121
- };
122
- (https.request as jest.Mock).mockImplementation((options, callback) => {
123
- callback(mockResponse);
124
- return mockRequest;
125
- });
126
-
127
- const oauth = new DaikinOAuth(mockConfig);
128
- const token = await oauth.getAccessToken();
129
-
130
- expect(token).toBe('refreshed-token');
131
- expect(https.request).toHaveBeenCalled();
132
- });
133
-
134
- it('should throw error if token expired and no refresh token', async () => {
135
- const pastExpiry = Math.floor(Date.now() / 1000) - 100;
136
- const tokenSet = {
137
- access_token: 'expired-token',
138
- token_type: 'Bearer',
139
- expires_at: pastExpiry,
140
- // No refresh_token
141
- };
142
-
143
- (fs.existsSync as jest.Mock).mockReturnValue(true);
144
- (fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify(tokenSet));
145
-
146
- const oauth = new DaikinOAuth(mockConfig);
147
-
148
- await expect(oauth.getAccessToken()).rejects.toThrow(
149
- 'Token expired and no refresh token available. Please re-authenticate.',
150
- );
151
- });
152
-
153
- it('should throw error if not authenticated', async () => {
154
- const oauth = new DaikinOAuth(mockConfig);
155
-
156
- await expect(oauth.getAccessToken()).rejects.toThrow(
157
- 'Not authenticated. Please authenticate first.',
158
- );
159
- });
160
- });
161
-
162
- describe('isAuthenticated', () => {
163
- it('should return true if token set exists', () => {
164
- const tokenSet = {
165
- access_token: 'token',
166
- token_type: 'Bearer',
167
- };
168
-
169
- (fs.existsSync as jest.Mock).mockReturnValue(true);
170
- (fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify(tokenSet));
171
-
172
- const oauth = new DaikinOAuth(mockConfig);
173
- expect(oauth.isAuthenticated()).toBe(true);
174
- });
175
-
176
- it('should return false if no token set', () => {
177
- const oauth = new DaikinOAuth(mockConfig);
178
- expect(oauth.isAuthenticated()).toBe(false);
179
- });
180
- });
181
-
182
- describe('getTokenExpiration', () => {
183
- it('should return expiration date', () => {
184
- const expiresAt = Math.floor(Date.now() / 1000) + 3600;
185
- const tokenSet = {
186
- access_token: 'token',
187
- token_type: 'Bearer',
188
- expires_at: expiresAt,
189
- };
190
-
191
- (fs.existsSync as jest.Mock).mockReturnValue(true);
192
- (fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify(tokenSet));
193
-
194
- const oauth = new DaikinOAuth(mockConfig);
195
- const expiration = oauth.getTokenExpiration();
196
-
197
- expect(expiration).toBeInstanceOf(Date);
198
- expect(expiration?.getTime()).toBe(expiresAt * 1000);
199
- });
200
-
201
- it('should return null if no expiration', () => {
202
- const tokenSet = {
203
- access_token: 'token',
204
- token_type: 'Bearer',
205
- };
206
-
207
- (fs.existsSync as jest.Mock).mockReturnValue(true);
208
- (fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify(tokenSet));
209
-
210
- const oauth = new DaikinOAuth(mockConfig);
211
- expect(oauth.getTokenExpiration()).toBeNull();
212
- });
213
- });
214
- });
@@ -1,12 +0,0 @@
1
- import {DaikinCloudRepo} from '../../../src/api/daikin-cloud.repository';
2
- import {dx4Airco} from '../../fixtures/dx4-airco';
3
- import {dx23Airco} from '../../fixtures/dx23-airco';
4
- import {althermaHeatPump} from '../../fixtures/altherma-heat-pump';
5
-
6
- test.each<Array<string | any>>([
7
- ['dx4', dx4Airco],
8
- ['dx23', dx23Airco],
9
- ['altherma', althermaHeatPump],
10
- ])('Clean cloud device data for %s device', (name, deviceJson) => {
11
- expect(DaikinCloudRepo.maskSensitiveCloudDeviceData(deviceJson)).toMatchSnapshot();
12
- });