@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,443 +0,0 @@
1
- /**
2
- * Daikin Mobile App Authentication Flow
3
- *
4
- * Replicates the Gigya (SAP CDC) PKCE authentication flow used by the Daikin Onecta mobile app.
5
- *
6
- * Flow:
7
- * 1. Call /authorize with PKCE to get context JWT
8
- * 2. Call accounts.login with email/password to get login_token
9
- * 3. Call /authorize/continue with context + login_token to get authorization code
10
- * 4. Exchange code for tokens at /token endpoint with PKCE verifier
11
- */
12
-
13
- const https = require('https');
14
- const crypto = require('crypto');
15
- const readline = require('readline');
16
-
17
- // Configuration from mobile app
18
- const CONFIG = {
19
- apiKey: '3_xRB3jaQ62bVjqXU1omaEsPDVYC0Twi1zfq1zHPu_5HFT0zWkDvZJS97Yw1loJnTm',
20
- clientId: 'FjS6T5oZHvzpZENIDybFRdtK',
21
- clientSecret: '_yWGLBGUnQFrN-u7uIOAZhSBsJOfcnBs0IS87wTgUvUmnLnEOs4NQmaKagqZBpQpG0XYl07KeCx8XHHKxAn24w',
22
- redirectUri: 'daikinunified://cdc/',
23
- baseUrl: 'https://cdc.daikin.eu',
24
- idpTokenEndpoint: 'https://idp.onecta.daikineurope.com/v1/oidc/token',
25
- scope: 'openid onecta:onecta.application offline_access',
26
- };
27
-
28
- const OIDC_BASE = `${CONFIG.baseUrl}/oidc/op/v1.0/${CONFIG.apiKey}`;
29
-
30
- // Generate PKCE challenge pair
31
- function generatePKCE() {
32
- const verifier = crypto.randomBytes(32).toString('base64url');
33
- const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
34
- return { verifier, challenge };
35
- }
36
-
37
- // Helper to make HTTPS requests
38
- function httpsRequest(url, options = {}, postData = null) {
39
- return new Promise((resolve, reject) => {
40
- const urlObj = new URL(url);
41
- const reqOptions = {
42
- hostname: urlObj.hostname,
43
- port: 443,
44
- path: urlObj.pathname + urlObj.search,
45
- method: options.method || 'GET',
46
- headers: options.headers || {},
47
- };
48
-
49
- const req = https.request(reqOptions, (res) => {
50
- let data = '';
51
- res.on('data', chunk => data += chunk);
52
- res.on('end', () => {
53
- resolve({
54
- statusCode: res.statusCode,
55
- headers: res.headers,
56
- body: data,
57
- });
58
- });
59
- });
60
-
61
- req.on('error', reject);
62
-
63
- if (postData) {
64
- req.write(postData);
65
- }
66
- req.end();
67
- });
68
- }
69
-
70
- // Step 1: Get OIDC context via authorize endpoint with PKCE
71
- async function getOidcContext(pkce) {
72
- console.log('\n[1/4] Getting OIDC context with PKCE...');
73
-
74
- const params = new URLSearchParams({
75
- client_id: CONFIG.clientId,
76
- redirect_uri: CONFIG.redirectUri,
77
- response_type: 'code',
78
- scope: CONFIG.scope,
79
- code_challenge: pkce.challenge,
80
- code_challenge_method: 'S256',
81
- state: crypto.randomBytes(16).toString('hex'),
82
- });
83
-
84
- const url = `${OIDC_BASE}/authorize?${params}`;
85
- const response = await httpsRequest(url, { method: 'GET' });
86
-
87
- if (response.statusCode === 302) {
88
- const location = response.headers.location;
89
-
90
- // Extract context from redirect URL
91
- const contextMatch = location.match(/context=([^&]+)/);
92
- if (contextMatch) {
93
- const context = decodeURIComponent(contextMatch[1]);
94
- console.log(' ✓ Context obtained');
95
- return context;
96
- }
97
- }
98
-
99
- throw new Error('Failed to get OIDC context');
100
- }
101
-
102
- // Generate riskContext fingerprint (simplified)
103
- function generateRiskContext() {
104
- const now = new Date();
105
- const timeStr = `${now.getHours()}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
106
- return JSON.stringify({
107
- b0: 14063,
108
- b1: [0, 2, 2, 0],
109
- b2: 4,
110
- b3: [],
111
- b4: 2,
112
- b5: 1,
113
- b6: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko)',
114
- b7: [],
115
- b8: timeStr,
116
- b9: 0,
117
- b10: { state: 'denied' },
118
- b11: false,
119
- b12: null,
120
- b13: [5, '402|874|24', false, true],
121
- });
122
- }
123
-
124
- // Step 1.5: Initialize Gigya SDK and get required cookies
125
- async function initGigyaSdk(context) {
126
- console.log('\n[1.5/4] Initializing Gigya SDK...');
127
-
128
- // Build the pageURL like the mobile app does (with context from authorize)
129
- const proxyUrl = `https://id.daikin.eu/cdc/onecta/oidc/proxy.html?context=${encodeURIComponent(context)}&client_id=${CONFIG.clientId}&mode=login&scope=${encodeURIComponent(CONFIG.scope)}&gig_skipConsent=true`;
130
-
131
- const params = new URLSearchParams({
132
- apiKey: CONFIG.apiKey,
133
- pageURL: proxyUrl,
134
- sdk: 'js_latest',
135
- sdkBuild: '18305',
136
- format: 'json',
137
- });
138
-
139
- const response = await httpsRequest(
140
- `${CONFIG.baseUrl}/accounts.webSdkBootstrap?${params}`,
141
- {
142
- method: 'GET',
143
- headers: {
144
- 'Accept': '*/*',
145
- 'Origin': 'https://id.daikin.eu',
146
- 'Referer': 'https://id.daikin.eu/',
147
- },
148
- }
149
- );
150
-
151
- // Extract cookies from response headers (gmid, ucid, hasGmid)
152
- const cookies = [];
153
- const setCookies = response.headers['set-cookie'];
154
- if (setCookies) {
155
- const cookieArray = Array.isArray(setCookies) ? setCookies : [setCookies];
156
- for (const cookie of cookieArray) {
157
- const match = cookie.match(/^([^=]+=[^;]+)/);
158
- if (match) {
159
- cookies.push(match[1]);
160
- }
161
- }
162
- }
163
-
164
- // Add bootstrap cookie
165
- cookies.push(`gig_bootstrap_${CONFIG.apiKey}=cdc_ver4`);
166
-
167
- const cookieStr = cookies.join('; ');
168
- console.log(' ✓ SDK initialized (got', cookies.length, 'cookies)');
169
- return cookieStr;
170
- }
171
-
172
- // Step 2: Login with Gigya
173
- async function gigyaLogin(email, password, cookies) {
174
- console.log('\n[2/4] Authenticating with Gigya...');
175
-
176
- const params = new URLSearchParams({
177
- loginID: email,
178
- password: password,
179
- sessionExpiration: '31536000',
180
- targetEnv: 'jssdk',
181
- include: 'profile,data,emails,subscriptions,preferences,',
182
- includeUserInfo: 'true',
183
- loginMode: 'standard',
184
- lang: 'en',
185
- riskContext: generateRiskContext(),
186
- APIKey: CONFIG.apiKey,
187
- source: 'showScreenSet',
188
- sdk: 'js_latest',
189
- authMode: 'cookie',
190
- pageURL: `https://id.daikin.eu/cdc/onecta/oidc/registration-login.html?gig_client_id=${CONFIG.clientId}`,
191
- sdkBuild: '18305',
192
- format: 'json',
193
- });
194
-
195
- const response = await httpsRequest(
196
- `${CONFIG.baseUrl}/accounts.login`,
197
- {
198
- method: 'POST',
199
- headers: {
200
- 'Content-Type': 'application/x-www-form-urlencoded',
201
- 'Content-Length': Buffer.byteLength(params.toString()),
202
- 'Origin': 'https://id.daikin.eu',
203
- 'Referer': 'https://id.daikin.eu/',
204
- 'Cookie': cookies,
205
- },
206
- },
207
- params.toString()
208
- );
209
-
210
- const result = JSON.parse(response.body);
211
-
212
- if (result.errorCode !== 0) {
213
- console.log(' Debug - Full error response:', JSON.stringify(result, null, 2));
214
- throw new Error(`Login failed (code ${result.errorCode}): ${result.errorMessage || result.errorDetails}`);
215
- }
216
-
217
- console.log(' ✓ Login successful');
218
-
219
- // Get login_token from sessionInfo
220
- if (result.sessionInfo && result.sessionInfo.login_token) {
221
- return result.sessionInfo.login_token;
222
- }
223
-
224
- throw new Error('No login_token in response');
225
- }
226
-
227
- // Step 3: Continue authorization with login token
228
- async function authorizeWithToken(context, loginToken, cookies) {
229
- console.log('\n[3/4] Exchanging login token for authorization code...');
230
-
231
- const params = new URLSearchParams({
232
- context: context,
233
- login_token: loginToken,
234
- });
235
-
236
- // Add login token cookie (glt_{apiKey}) - required for authorize/continue
237
- const cookieStr = cookies + `; glt_${CONFIG.apiKey}=${loginToken}`;
238
-
239
- const url = `${OIDC_BASE}/authorize/continue?${params}`;
240
- const response = await httpsRequest(url, {
241
- method: 'GET',
242
- headers: {
243
- 'Cookie': cookieStr,
244
- 'Referer': 'https://id.daikin.eu/',
245
- },
246
- });
247
-
248
- // Debug output
249
- console.log(' Debug - Status:', response.statusCode);
250
- if (response.headers.location) {
251
- console.log(' Debug - Location:', response.headers.location.substring(0, 150) + '...');
252
- } else {
253
- console.log(' Debug - Body:', response.body.substring(0, 300));
254
- }
255
-
256
- if (response.statusCode === 302) {
257
- const location = response.headers.location;
258
-
259
- // Extract code from redirect
260
- const codeMatch = location.match(/code=([^&]+)/);
261
- if (codeMatch) {
262
- console.log(' ✓ Authorization code obtained');
263
- return codeMatch[1];
264
- }
265
-
266
- // Check for error
267
- const errorMatch = location.match(/error=([^&]+)/);
268
- if (errorMatch) {
269
- const errorDesc = location.match(/error_description=([^&]+)/);
270
- throw new Error(`Authorization error: ${decodeURIComponent(errorDesc ? errorDesc[1] : errorMatch[1])}`);
271
- }
272
- }
273
-
274
- throw new Error('Failed to get authorization code');
275
- }
276
-
277
- // Step 4: Exchange authorization code for tokens at IDP endpoint
278
- async function exchangeCodeForTokens(code, pkce) {
279
- console.log('\n[4/4] Exchanging authorization code for tokens at IDP...');
280
-
281
- // Build Basic Auth header with client_id:client_secret
282
- const basicAuth = Buffer.from(`${CONFIG.clientId}:${CONFIG.clientSecret}`).toString('base64');
283
-
284
- const params = new URLSearchParams({
285
- grant_type: 'authorization_code',
286
- code: code,
287
- redirect_uri: CONFIG.redirectUri,
288
- code_verifier: pkce.verifier,
289
- });
290
-
291
- const response = await httpsRequest(
292
- CONFIG.idpTokenEndpoint,
293
- {
294
- method: 'POST',
295
- headers: {
296
- 'Content-Type': 'application/x-www-form-urlencoded',
297
- 'Authorization': `Basic ${basicAuth}`,
298
- 'Content-Length': Buffer.byteLength(params.toString()),
299
- },
300
- },
301
- params.toString()
302
- );
303
-
304
- console.log(' Debug - Status:', response.statusCode);
305
-
306
- const result = JSON.parse(response.body);
307
-
308
- if (result.error) {
309
- console.log(' Debug - Error response:', JSON.stringify(result, null, 2));
310
- throw new Error(`Token exchange failed: ${result.error_description || result.error}`);
311
- }
312
-
313
- console.log(' ✓ Tokens obtained from IDP');
314
- return result;
315
- }
316
-
317
- // Test API access
318
- async function testApiAccess(accessToken) {
319
- console.log('\n[Test] Testing API access...');
320
-
321
- const response = await httpsRequest(
322
- 'https://api.onecta.daikineurope.com/v1/gateway-devices',
323
- {
324
- method: 'GET',
325
- headers: {
326
- 'Authorization': `Bearer ${accessToken}`,
327
- 'Accept': 'application/json',
328
- },
329
- }
330
- );
331
-
332
- if (response.statusCode === 200) {
333
- const devices = JSON.parse(response.body);
334
- console.log(' ✓ API access successful! Found', devices.length, 'device(s)');
335
-
336
- // Show rate limit
337
- const limitDay = response.headers['x-ratelimit-limit-day'];
338
- const remainingDay = response.headers['x-ratelimit-remaining-day'];
339
- console.log(' Rate limit:', remainingDay + '/' + limitDay, 'requests/day');
340
-
341
- return devices;
342
- } else {
343
- console.log(' ✗ API access failed:', response.statusCode);
344
- return null;
345
- }
346
- }
347
-
348
- // Test WebSocket access
349
- async function testWebSocket(accessToken) {
350
- console.log('\n[Test] Testing WebSocket access...');
351
-
352
- // We just check if the token has WebSocket in audience
353
- const payload = JSON.parse(Buffer.from(accessToken.split('.')[1], 'base64').toString());
354
-
355
- if (payload.aud && payload.aud.includes('wss://wsapi.onecta.daikineurope.com')) {
356
- console.log(' ✓ WebSocket access: ENABLED');
357
- return true;
358
- } else {
359
- console.log(' ✗ WebSocket access: NOT in token audience');
360
- return false;
361
- }
362
- }
363
-
364
- // Main flow
365
- async function main() {
366
- console.log('╔════════════════════════════════════════════════════════════╗');
367
- console.log('║ Daikin Mobile App Authentication (Gigya + PKCE) ║');
368
- console.log('╚════════════════════════════════════════════════════════════╝\n');
369
-
370
- console.log('This uses the official Daikin Onecta mobile app OAuth flow.');
371
- console.log('Benefits: WebSocket access, 5000 API calls/day (vs 200 for Developer Portal)\n');
372
-
373
- // Get credentials from env vars or prompt
374
- let email = process.env.DAIKIN_EMAIL;
375
- let password = process.env.DAIKIN_PASSWORD;
376
- let rl;
377
-
378
- if (!email || !password) {
379
- rl = readline.createInterface({
380
- input: process.stdin,
381
- output: process.stdout,
382
- });
383
-
384
- const question = (q) => new Promise(resolve => rl.question(q, resolve));
385
-
386
- if (!email) email = await question('Daikin account email: ');
387
- if (!password) password = await question('Daikin account password: ');
388
- rl.close();
389
- } else {
390
- console.log('Using credentials from environment variables\n');
391
- }
392
-
393
- try {
394
-
395
- // Generate PKCE
396
- const pkce = generatePKCE();
397
- console.log('\nPKCE generated');
398
-
399
- // Execute the authentication flow
400
- const context = await getOidcContext(pkce);
401
- const cookies = await initGigyaSdk(context);
402
- const loginToken = await gigyaLogin(email, password, cookies);
403
- const code = await authorizeWithToken(context, loginToken, cookies);
404
- const tokens = await exchangeCodeForTokens(code, pkce);
405
-
406
- // Success!
407
- console.log('\n╔════════════════════════════════════════════════════════════╗');
408
- console.log('║ AUTHENTICATION SUCCESS ║');
409
- console.log('╚════════════════════════════════════════════════════════════╝\n');
410
-
411
- console.log('Token type:', tokens.token_type);
412
- console.log('Expires in:', tokens.expires_in, 'seconds');
413
- console.log('Access token:', tokens.access_token.substring(0, 50) + '...');
414
-
415
- if (tokens.refresh_token) {
416
- console.log('Refresh token:', tokens.refresh_token.substring(0, 30) + '...');
417
- }
418
-
419
- // Decode and show scope
420
- const payload = JSON.parse(Buffer.from(tokens.access_token.split('.')[1], 'base64').toString());
421
- console.log('\nToken scope:', payload.scope);
422
-
423
- // Test API and WebSocket
424
- await testApiAccess(tokens.access_token);
425
- await testWebSocket(tokens.access_token);
426
-
427
- // Save tokens
428
- const fs = require('fs');
429
- const tokenData = {
430
- ...tokens,
431
- expires_at: Math.floor(Date.now() / 1000) + tokens.expires_in,
432
- };
433
- fs.writeFileSync('mobile-tokens.json', JSON.stringify(tokenData, null, 2));
434
- console.log('\n✓ Tokens saved to mobile-tokens.json');
435
-
436
- } catch (error) {
437
- console.error('\n✗ Error:', error.message);
438
- rl.close();
439
- process.exit(1);
440
- }
441
- }
442
-
443
- main();
@@ -1,175 +0,0 @@
1
- /**
2
- * Mobile App OAuth Flow Test
3
- *
4
- * Tests if we can authenticate using the mobile app's OAuth flow.
5
- */
6
-
7
- const crypto = require('crypto');
8
- const https = require('https');
9
- const http = require('http');
10
- const { URL } = require('url');
11
-
12
- // Mobile app OAuth config (extracted from JWT)
13
- const GIGYA_API_KEY = '3_xRB3jaQ62bVjqXU1omaEsPDVYC0Twi1zfq1zHPu_5HFT0zWkDvZJS97Yw1loJnTm';
14
- const MOBILE_CLIENT_ID = 'FjS6T5oZHvzpZENIDybFRdtK';
15
- const BASE_URL = `https://cdc.daikin.eu/oidc/op/v1.0/${GIGYA_API_KEY}`;
16
-
17
- // Scopes needed for WebSocket access
18
- const SCOPES = 'openid onecta:onecta.application offline_access';
19
-
20
- // Generate PKCE challenge
21
- function generatePKCE() {
22
- const verifier = crypto.randomBytes(32).toString('base64url');
23
- const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
24
- return { verifier, challenge };
25
- }
26
-
27
- // Generate state
28
- function generateState() {
29
- return crypto.randomBytes(16).toString('hex');
30
- }
31
-
32
- console.log('=== Daikin Mobile App OAuth Flow Test ===\n');
33
- console.log('Gigya API Key:', GIGYA_API_KEY.substring(0, 20) + '...');
34
- console.log('Mobile Client ID:', MOBILE_CLIENT_ID);
35
- console.log('Requested Scopes:', SCOPES);
36
- console.log('');
37
-
38
- // Generate PKCE values
39
- const pkce = generatePKCE();
40
- const state = generateState();
41
-
42
- console.log('PKCE Verifier:', pkce.verifier.substring(0, 20) + '...');
43
- console.log('PKCE Challenge:', pkce.challenge.substring(0, 20) + '...');
44
- console.log('State:', state);
45
- console.log('');
46
-
47
- // We need a redirect URI - let's try a few options
48
- const redirectUris = [
49
- 'daikin://auth', // Mobile app custom scheme
50
- 'https://my.daikin.eu/oauth/callback', // Possible web callback
51
- 'http://localhost:8888/callback', // Local callback for testing
52
- ];
53
-
54
- // Build authorization URL
55
- const authUrl = new URL(`${BASE_URL}/authorize`);
56
- authUrl.searchParams.set('client_id', MOBILE_CLIENT_ID);
57
- authUrl.searchParams.set('response_type', 'code');
58
- authUrl.searchParams.set('scope', SCOPES);
59
- authUrl.searchParams.set('redirect_uri', redirectUris[2]); // localhost for testing
60
- authUrl.searchParams.set('state', state);
61
- authUrl.searchParams.set('code_challenge', pkce.challenge);
62
- authUrl.searchParams.set('code_challenge_method', 'S256');
63
-
64
- console.log('Authorization URL:');
65
- console.log(authUrl.toString());
66
- console.log('');
67
-
68
- // Start local callback server
69
- const server = http.createServer(async (req, res) => {
70
- const reqUrl = new URL(req.url, 'http://localhost:8888');
71
-
72
- if (reqUrl.pathname === '/callback') {
73
- const code = reqUrl.searchParams.get('code');
74
- const returnedState = reqUrl.searchParams.get('state');
75
- const error = reqUrl.searchParams.get('error');
76
-
77
- console.log('\n--- Callback received ---');
78
-
79
- if (error) {
80
- console.log('Error:', error);
81
- console.log('Description:', reqUrl.searchParams.get('error_description'));
82
- res.writeHead(200);
83
- res.end('Error: ' + error);
84
- server.close();
85
- return;
86
- }
87
-
88
- if (returnedState !== state) {
89
- console.log('State mismatch!');
90
- res.writeHead(400);
91
- res.end('State mismatch');
92
- server.close();
93
- return;
94
- }
95
-
96
- console.log('Authorization code received:', code.substring(0, 20) + '...');
97
- console.log('');
98
-
99
- // Exchange code for tokens
100
- console.log('Exchanging code for tokens...');
101
-
102
- const tokenUrl = `${BASE_URL}/token`;
103
- const tokenParams = new URLSearchParams({
104
- grant_type: 'authorization_code',
105
- client_id: MOBILE_CLIENT_ID,
106
- code: code,
107
- redirect_uri: redirectUris[2],
108
- code_verifier: pkce.verifier,
109
- });
110
-
111
- const tokenReq = https.request(tokenUrl, {
112
- method: 'POST',
113
- headers: {
114
- 'Content-Type': 'application/x-www-form-urlencoded',
115
- 'Content-Length': Buffer.byteLength(tokenParams.toString()),
116
- },
117
- }, (tokenRes) => {
118
- let data = '';
119
- tokenRes.on('data', chunk => data += chunk);
120
- tokenRes.on('end', () => {
121
- console.log('Token response status:', tokenRes.statusCode);
122
- console.log('Token response:');
123
- try {
124
- const tokens = JSON.parse(data);
125
- console.log(JSON.stringify(tokens, null, 2));
126
-
127
- if (tokens.access_token) {
128
- console.log('\n✅ SUCCESS! Got access token');
129
- console.log('Token expires in:', tokens.expires_in, 'seconds');
130
-
131
- // Decode token to verify scope
132
- const payload = JSON.parse(Buffer.from(tokens.access_token.split('.')[1], 'base64').toString());
133
- console.log('Token scope:', payload.scope);
134
- console.log('WebSocket access:', payload.aud?.includes('wss://wsapi.onecta.daikineurope.com') ? 'YES' : 'NO');
135
- }
136
- } catch (e) {
137
- console.log('Raw response:', data);
138
- }
139
-
140
- res.writeHead(200);
141
- res.end('Authentication complete! Check terminal for results.');
142
- server.close();
143
- });
144
- });
145
-
146
- tokenReq.on('error', (e) => {
147
- console.log('Token request error:', e.message);
148
- res.writeHead(500);
149
- res.end('Token request failed');
150
- server.close();
151
- });
152
-
153
- tokenReq.write(tokenParams.toString());
154
- tokenReq.end();
155
- } else {
156
- res.writeHead(404);
157
- res.end('Not found');
158
- }
159
- });
160
-
161
- server.listen(8888, () => {
162
- console.log('Callback server listening on http://localhost:8888');
163
- console.log('');
164
- console.log('Open this URL in your browser to authenticate:');
165
- console.log(authUrl.toString());
166
- console.log('');
167
- console.log('Waiting for callback... (Ctrl+C to cancel)');
168
- });
169
-
170
- // Timeout after 5 minutes
171
- setTimeout(() => {
172
- console.log('\nTimeout - no callback received');
173
- server.close();
174
- process.exit(1);
175
- }, 300000);