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