@mp-consulting/homebridge-daikin-cloud 1.5.2 → 1.6.0
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/README.md +40 -27
- package/config.schema.json +66 -12
- package/dist/src/accessories/air-conditioning-accessory.js +1 -1
- package/dist/src/accessories/air-conditioning-accessory.js.map +1 -1
- package/dist/src/accessories/altherma-accessory.js +1 -1
- package/dist/src/accessories/altherma-accessory.js.map +1 -1
- package/dist/src/accessories/base-accessory.d.ts +5 -4
- package/dist/src/accessories/base-accessory.d.ts.map +1 -1
- package/dist/src/accessories/base-accessory.js +9 -10
- package/dist/src/accessories/base-accessory.js.map +1 -1
- package/dist/src/api/daikin-api.d.ts +22 -6
- package/dist/src/api/daikin-api.d.ts.map +1 -1
- package/dist/src/api/daikin-api.js +49 -23
- package/dist/src/api/daikin-api.js.map +1 -1
- package/dist/src/api/daikin-controller.d.ts +4 -48
- package/dist/src/api/daikin-controller.d.ts.map +1 -1
- package/dist/src/api/daikin-controller.js +14 -84
- package/dist/src/api/daikin-controller.js.map +1 -1
- package/dist/src/api/daikin-mobile-oauth.d.ts +11 -40
- package/dist/src/api/daikin-mobile-oauth.d.ts.map +1 -1
- package/dist/src/api/daikin-mobile-oauth.js +36 -160
- package/dist/src/api/daikin-mobile-oauth.js.map +1 -1
- package/dist/src/api/daikin-oauth.d.ts +10 -37
- package/dist/src/api/daikin-oauth.d.ts.map +1 -1
- package/dist/src/api/daikin-oauth.js +36 -166
- package/dist/src/api/daikin-oauth.js.map +1 -1
- package/dist/src/api/daikin-schemas.d.ts +20 -22
- package/dist/src/api/daikin-schemas.d.ts.map +1 -1
- package/dist/src/api/daikin-schemas.js +17 -49
- package/dist/src/api/daikin-schemas.js.map +1 -1
- package/dist/src/api/daikin-types.d.ts +11 -23
- 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 +4 -5
- package/dist/src/api/daikin-websocket.d.ts.map +1 -1
- package/dist/src/api/daikin-websocket.js +62 -34
- package/dist/src/api/daikin-websocket.js.map +1 -1
- package/dist/src/api/http-defaults.d.ts +3 -2
- package/dist/src/api/http-defaults.d.ts.map +1 -1
- package/dist/src/api/http-defaults.js.map +1 -1
- package/dist/src/api/http-transport.d.ts +14 -2
- package/dist/src/api/http-transport.d.ts.map +1 -1
- package/dist/src/api/http-transport.js +32 -9
- package/dist/src/api/http-transport.js.map +1 -1
- package/dist/src/api/token-client.d.ts +82 -0
- package/dist/src/api/token-client.d.ts.map +1 -0
- package/dist/src/api/token-client.js +188 -0
- package/dist/src/api/token-client.js.map +1 -0
- package/dist/src/api/token-storage.d.ts +6 -0
- package/dist/src/api/token-storage.d.ts.map +1 -1
- package/dist/src/api/token-storage.js +27 -1
- package/dist/src/api/token-storage.js.map +1 -1
- package/dist/src/config/config-manager.d.ts +69 -125
- package/dist/src/config/config-manager.d.ts.map +1 -1
- package/dist/src/config/config-manager.js +288 -225
- package/dist/src/config/config-manager.js.map +1 -1
- package/dist/src/constants/api.constants.d.ts +6 -0
- package/dist/src/constants/api.constants.d.ts.map +1 -1
- package/dist/src/constants/api.constants.js +7 -1
- package/dist/src/constants/api.constants.js.map +1 -1
- package/dist/src/constants/auth.constants.d.ts +11 -0
- package/dist/src/constants/auth.constants.d.ts.map +1 -1
- package/dist/src/constants/auth.constants.js +12 -1
- package/dist/src/constants/auth.constants.js.map +1 -1
- package/dist/src/constants/device.constants.d.ts +1 -7
- package/dist/src/constants/device.constants.d.ts.map +1 -1
- package/dist/src/constants/device.constants.js +2 -11
- package/dist/src/constants/device.constants.js.map +1 -1
- package/dist/src/constants/time.constants.d.ts +0 -2
- package/dist/src/constants/time.constants.d.ts.map +1 -1
- package/dist/src/constants/time.constants.js +1 -3
- package/dist/src/constants/time.constants.js.map +1 -1
- package/dist/src/device/accessory-factory.d.ts +0 -5
- package/dist/src/device/accessory-factory.d.ts.map +1 -1
- package/dist/src/device/accessory-factory.js +0 -7
- package/dist/src/device/accessory-factory.js.map +1 -1
- package/dist/src/device/capability-detector.d.ts +9 -1
- package/dist/src/device/capability-detector.d.ts.map +1 -1
- package/dist/src/device/capability-detector.js +32 -28
- package/dist/src/device/capability-detector.js.map +1 -1
- package/dist/src/device/capability-docs.d.ts.map +1 -1
- package/dist/src/device/capability-docs.js +3 -0
- package/dist/src/device/capability-docs.js.map +1 -1
- package/dist/src/device/data-paths.d.ts +18 -0
- package/dist/src/device/data-paths.d.ts.map +1 -0
- package/dist/src/device/data-paths.js +20 -0
- package/dist/src/device/data-paths.js.map +1 -0
- package/dist/src/device/profiles/device-profile.d.ts +0 -5
- package/dist/src/device/profiles/device-profile.d.ts.map +1 -1
- package/dist/src/device/profiles/device-profile.js +0 -21
- package/dist/src/device/profiles/device-profile.js.map +1 -1
- package/dist/src/features/base-feature.d.ts +53 -9
- package/dist/src/features/base-feature.d.ts.map +1 -1
- package/dist/src/features/base-feature.js +93 -40
- package/dist/src/features/base-feature.js.map +1 -1
- package/dist/src/features/feature-manager.d.ts +27 -15
- package/dist/src/features/feature-manager.d.ts.map +1 -1
- package/dist/src/features/feature-manager.js +27 -24
- package/dist/src/features/feature-manager.js.map +1 -1
- package/dist/src/features/index.d.ts +1 -0
- package/dist/src/features/index.d.ts.map +1 -1
- package/dist/src/features/index.js +1 -0
- package/dist/src/features/index.js.map +1 -1
- package/dist/src/features/modes/auto-fan-mode.feature.d.ts +16 -10
- package/dist/src/features/modes/auto-fan-mode.feature.d.ts.map +1 -1
- package/dist/src/features/modes/auto-fan-mode.feature.js +16 -40
- package/dist/src/features/modes/auto-fan-mode.feature.js.map +1 -1
- package/dist/src/features/modes/dry-operation-mode.feature.d.ts +12 -9
- package/dist/src/features/modes/dry-operation-mode.feature.d.ts.map +1 -1
- package/dist/src/features/modes/dry-operation-mode.feature.js +11 -30
- package/dist/src/features/modes/dry-operation-mode.feature.js.map +1 -1
- package/dist/src/features/modes/econo-mode.feature.d.ts +12 -9
- package/dist/src/features/modes/econo-mode.feature.d.ts.map +1 -1
- package/dist/src/features/modes/econo-mode.feature.js +11 -31
- package/dist/src/features/modes/econo-mode.feature.js.map +1 -1
- package/dist/src/features/modes/fan-only-operation-mode.feature.d.ts +12 -9
- 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 +11 -30
- package/dist/src/features/modes/fan-only-operation-mode.feature.js.map +1 -1
- package/dist/src/features/modes/firmware-update.feature.d.ts +7 -6
- package/dist/src/features/modes/firmware-update.feature.d.ts.map +1 -1
- package/dist/src/features/modes/firmware-update.feature.js +15 -22
- package/dist/src/features/modes/firmware-update.feature.js.map +1 -1
- package/dist/src/features/modes/holiday-mode.feature.d.ts +2 -1
- package/dist/src/features/modes/holiday-mode.feature.d.ts.map +1 -1
- package/dist/src/features/modes/holiday-mode.feature.js +5 -15
- package/dist/src/features/modes/holiday-mode.feature.js.map +1 -1
- package/dist/src/features/modes/indoor-silent-mode.feature.d.ts +15 -12
- package/dist/src/features/modes/indoor-silent-mode.feature.d.ts.map +1 -1
- package/dist/src/features/modes/indoor-silent-mode.feature.js +15 -39
- package/dist/src/features/modes/indoor-silent-mode.feature.js.map +1 -1
- package/dist/src/features/modes/oscillation-mode.feature.d.ts +5 -6
- package/dist/src/features/modes/oscillation-mode.feature.d.ts.map +1 -1
- package/dist/src/features/modes/oscillation-mode.feature.js +14 -36
- package/dist/src/features/modes/oscillation-mode.feature.js.map +1 -1
- package/dist/src/features/modes/outdoor-silent-mode.feature.d.ts +12 -9
- package/dist/src/features/modes/outdoor-silent-mode.feature.d.ts.map +1 -1
- package/dist/src/features/modes/outdoor-silent-mode.feature.js +11 -31
- package/dist/src/features/modes/outdoor-silent-mode.feature.js.map +1 -1
- package/dist/src/features/modes/powerful-mode.feature.d.ts +14 -10
- package/dist/src/features/modes/powerful-mode.feature.d.ts.map +1 -1
- package/dist/src/features/modes/powerful-mode.feature.js +13 -32
- package/dist/src/features/modes/powerful-mode.feature.js.map +1 -1
- package/dist/src/features/modes/streamer-mode.feature.d.ts +12 -9
- package/dist/src/features/modes/streamer-mode.feature.d.ts.map +1 -1
- package/dist/src/features/modes/streamer-mode.feature.js +11 -31
- package/dist/src/features/modes/streamer-mode.feature.js.map +1 -1
- package/dist/src/features/on-off-data-point-feature.d.ts +43 -0
- package/dist/src/features/on-off-data-point-feature.d.ts.map +1 -0
- package/dist/src/features/on-off-data-point-feature.js +49 -0
- package/dist/src/features/on-off-data-point-feature.js.map +1 -0
- package/dist/src/platform.d.ts +56 -9
- package/dist/src/platform.d.ts.map +1 -1
- package/dist/src/platform.js +248 -128
- package/dist/src/platform.js.map +1 -1
- package/dist/src/services/climate-control/fan-speed.d.ts +45 -0
- package/dist/src/services/climate-control/fan-speed.d.ts.map +1 -0
- package/dist/src/services/climate-control/fan-speed.js +104 -0
- package/dist/src/services/climate-control/fan-speed.js.map +1 -0
- package/dist/src/services/climate-control/separate-fan.service.d.ts +45 -0
- package/dist/src/services/climate-control/separate-fan.service.d.ts.map +1 -0
- package/dist/src/services/climate-control/separate-fan.service.js +82 -0
- package/dist/src/services/climate-control/separate-fan.service.js.map +1 -0
- package/dist/src/services/climate-control/setpoint-resolver.d.ts +68 -0
- package/dist/src/services/climate-control/setpoint-resolver.d.ts.map +1 -0
- package/dist/src/services/climate-control/setpoint-resolver.js +170 -0
- package/dist/src/services/climate-control/setpoint-resolver.js.map +1 -0
- package/dist/src/services/climate-control/swing-controller.d.ts +28 -0
- package/dist/src/services/climate-control/swing-controller.d.ts.map +1 -0
- package/dist/src/services/climate-control/swing-controller.js +65 -0
- package/dist/src/services/climate-control/swing-controller.js.map +1 -0
- package/dist/src/services/climate-control.service.d.ts +39 -48
- package/dist/src/services/climate-control.service.d.ts.map +1 -1
- package/dist/src/services/climate-control.service.js +223 -554
- package/dist/src/services/climate-control.service.js.map +1 -1
- package/dist/src/services/hot-water-tank.service.d.ts +17 -8
- package/dist/src/services/hot-water-tank.service.d.ts.map +1 -1
- package/dist/src/services/hot-water-tank.service.js +102 -157
- package/dist/src/services/hot-water-tank.service.js.map +1 -1
- package/dist/src/types/daikin-enums.d.ts +0 -5
- package/dist/src/types/daikin-enums.d.ts.map +1 -1
- package/dist/src/types/daikin-enums.js +1 -7
- package/dist/src/types/daikin-enums.js.map +1 -1
- package/dist/src/types/device-capabilities.d.ts +1 -0
- package/dist/src/types/device-capabilities.d.ts.map +1 -1
- package/dist/src/utils/errors.d.ts +9 -0
- package/dist/src/utils/errors.d.ts.map +1 -0
- package/dist/src/utils/errors.js +25 -0
- package/dist/src/utils/errors.js.map +1 -0
- package/dist/src/utils/hap-write.d.ts +22 -0
- package/dist/src/utils/hap-write.d.ts.map +1 -0
- package/dist/src/utils/hap-write.js +33 -0
- package/dist/src/utils/hap-write.js.map +1 -0
- package/dist/src/utils/sleep.d.ts +5 -0
- package/dist/src/utils/sleep.d.ts.map +1 -0
- package/dist/src/utils/sleep.js +10 -0
- package/dist/src/utils/sleep.js.map +1 -0
- package/homebridge-ui/README.md +25 -2
- package/homebridge-ui/public/index.html +19 -17
- package/homebridge-ui/public/script.js +95 -32
- package/homebridge-ui/public/styles.css +109 -0
- package/homebridge-ui/server.js +321 -125
- package/package.json +2 -3
- package/dist/src/utils/log-context.d.ts +0 -105
- package/dist/src/utils/log-context.d.ts.map +0 -1
- package/dist/src/utils/log-context.js +0 -206
- package/dist/src/utils/log-context.js.map +0 -1
- package/dist/src/utils/update-mapper.d.ts +0 -59
- package/dist/src/utils/update-mapper.d.ts.map +0 -1
- package/dist/src/utils/update-mapper.js +0 -192
- package/dist/src/utils/update-mapper.js.map +0 -1
package/homebridge-ui/server.js
CHANGED
|
@@ -5,6 +5,7 @@ const https = require('https');
|
|
|
5
5
|
const crypto = require('crypto');
|
|
6
6
|
const { execSync } = require('child_process');
|
|
7
7
|
const os = require('os');
|
|
8
|
+
const net = require('net');
|
|
8
9
|
|
|
9
10
|
// Import from compiled src/api modules (single source of truth)
|
|
10
11
|
// Use path relative to this file, not cwd
|
|
@@ -13,6 +14,15 @@ const { DaikinOAuth } = require(join(distPath, 'daikin-oauth'));
|
|
|
13
14
|
const { DaikinMobileOAuth } = require(join(distPath, 'daikin-mobile-oauth'));
|
|
14
15
|
const { DaikinApi } = require(join(distPath, 'daikin-api'));
|
|
15
16
|
const { configureHttpTransport } = require(join(distPath, 'http-transport'));
|
|
17
|
+
const { loadTokenFromFile, saveTokenToFile, deleteTokenFile } = require(join(distPath, 'token-storage'));
|
|
18
|
+
const {
|
|
19
|
+
TOKEN_FILES,
|
|
20
|
+
DEFAULT_CALLBACK_PORT,
|
|
21
|
+
DEFAULT_CALLBACK_BIND_ADDR,
|
|
22
|
+
PENDING_AUTH_TTL_MS,
|
|
23
|
+
RATE_LIMIT_STATUS_FILE,
|
|
24
|
+
} = require(join(__dirname, '..', 'dist', 'src', 'constants'));
|
|
25
|
+
const { validateConfig } = require(join(__dirname, '..', 'dist', 'src', 'config', 'config-manager'));
|
|
16
26
|
|
|
17
27
|
// =============================================================================
|
|
18
28
|
// Configuration
|
|
@@ -20,6 +30,63 @@ const { configureHttpTransport } = require(join(distPath, 'http-transport'));
|
|
|
20
30
|
|
|
21
31
|
const CLIMATE_CONTROL_IDS = ['climateControl', 'climateControlMainZone', 'climateControlSecondaryZone'];
|
|
22
32
|
|
|
33
|
+
/** How long a /devices/list result is reused before calling the Daikin API again */
|
|
34
|
+
const DEVICES_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
35
|
+
|
|
36
|
+
/** Security headers for the OAuth callback page */
|
|
37
|
+
const CALLBACK_RESPONSE_HEADERS = {
|
|
38
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
39
|
+
'Content-Security-Policy': "default-src 'none'; style-src 'unsafe-inline'",
|
|
40
|
+
'X-Content-Type-Options': 'nosniff',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// =============================================================================
|
|
44
|
+
// Network Helpers
|
|
45
|
+
// =============================================================================
|
|
46
|
+
|
|
47
|
+
const NetUtils = {
|
|
48
|
+
/**
|
|
49
|
+
* Return the bind address if it is a valid IPv4/IPv6 literal, otherwise the default.
|
|
50
|
+
*/
|
|
51
|
+
resolveBindAddress(bindAddr) {
|
|
52
|
+
const value = typeof bindAddr === 'string' ? bindAddr.trim() : '';
|
|
53
|
+
return value && net.isIP(value) !== 0 ? value : DEFAULT_CALLBACK_BIND_ADDR;
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* True for loopback hosts: 127.0.0.0/8, ::1, IPv4-mapped loopback and "localhost".
|
|
58
|
+
*/
|
|
59
|
+
isLoopback(host) {
|
|
60
|
+
if (!host || typeof host !== 'string') {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
const value = host.trim().toLowerCase().replace(/^\[|\]$/g, '');
|
|
64
|
+
if (value === 'localhost' || value.endsWith('.localhost')) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
if (net.isIPv4(value)) {
|
|
68
|
+
return value.startsWith('127.');
|
|
69
|
+
}
|
|
70
|
+
if (net.isIPv6(value)) {
|
|
71
|
+
return value === '::1' || /^::ffff:127\./.test(value);
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// =============================================================================
|
|
78
|
+
// HTML Helpers
|
|
79
|
+
// =============================================================================
|
|
80
|
+
|
|
81
|
+
function escapeHtml(value) {
|
|
82
|
+
return String(value ?? '')
|
|
83
|
+
.replace(/&/g, '&')
|
|
84
|
+
.replace(/</g, '<')
|
|
85
|
+
.replace(/>/g, '>')
|
|
86
|
+
.replace(/"/g, '"')
|
|
87
|
+
.replace(/'/g, ''');
|
|
88
|
+
}
|
|
89
|
+
|
|
23
90
|
// =============================================================================
|
|
24
91
|
// SSL Certificate Utilities
|
|
25
92
|
// =============================================================================
|
|
@@ -95,30 +162,10 @@ const SSLUtils = {
|
|
|
95
162
|
// =============================================================================
|
|
96
163
|
|
|
97
164
|
const TokenManager = {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
} catch (error) {
|
|
104
|
-
console.error('Error loading token set:', error.message);
|
|
105
|
-
}
|
|
106
|
-
return null;
|
|
107
|
-
},
|
|
108
|
-
|
|
109
|
-
save(filePath, tokenSet) {
|
|
110
|
-
fs.writeFileSync(filePath, JSON.stringify(tokenSet, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
111
|
-
},
|
|
112
|
-
|
|
113
|
-
delete(filePath) {
|
|
114
|
-
try {
|
|
115
|
-
fs.unlinkSync(filePath);
|
|
116
|
-
} catch (error) {
|
|
117
|
-
if (error.code !== 'ENOENT') {
|
|
118
|
-
throw error;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
},
|
|
165
|
+
// File access is shared with the plugin (Zod-validated reads, 0600 writes)
|
|
166
|
+
load: (filePath) => loadTokenFromFile(filePath),
|
|
167
|
+
save: (filePath, tokenSet) => saveTokenToFile(filePath, tokenSet),
|
|
168
|
+
delete: (filePath) => deleteTokenFile(filePath),
|
|
122
169
|
|
|
123
170
|
getStatus(tokenSet) {
|
|
124
171
|
if (!tokenSet || !tokenSet.access_token) {
|
|
@@ -273,11 +320,24 @@ class CallbackServer {
|
|
|
273
320
|
this.server = null;
|
|
274
321
|
this.port = null;
|
|
275
322
|
this.connections = new Set();
|
|
323
|
+
this.ttlTimer = null;
|
|
276
324
|
}
|
|
277
325
|
|
|
278
|
-
|
|
326
|
+
/**
|
|
327
|
+
* Start the HTTPS callback server.
|
|
328
|
+
* @param {object} options
|
|
329
|
+
* @param {number} options.port - Port to listen on
|
|
330
|
+
* @param {string} options.hostname - External hostname/IP used for the certificate
|
|
331
|
+
* @param {string} [options.bindAddr] - Interface to bind (validated IP literal)
|
|
332
|
+
* @param {string} options.certDir - Directory for the self-signed certificate
|
|
333
|
+
* @param {Function} options.requestHandler - HTTPS request handler
|
|
334
|
+
* @param {number} [options.ttlMs] - Auto-stop delay (defaults to the pending auth lifetime)
|
|
335
|
+
*/
|
|
336
|
+
async start({ port, hostname, bindAddr, certDir, requestHandler, ttlMs = PENDING_AUTH_TTL_MS }) {
|
|
279
337
|
await this.stop();
|
|
280
338
|
|
|
339
|
+
const host = NetUtils.resolveBindAddress(bindAddr);
|
|
340
|
+
|
|
281
341
|
return new Promise((resolve, reject) => {
|
|
282
342
|
const tryStart = (attempt = 1) => {
|
|
283
343
|
try {
|
|
@@ -296,14 +356,16 @@ class CallbackServer {
|
|
|
296
356
|
setTimeout(() => tryStart(attempt + 1), 1000);
|
|
297
357
|
} else {
|
|
298
358
|
console.error('Callback server error:', err.message);
|
|
359
|
+
this.server = null;
|
|
299
360
|
reject(err);
|
|
300
361
|
}
|
|
301
362
|
});
|
|
302
363
|
|
|
303
|
-
this.server.listen(port,
|
|
304
|
-
console.log(`HTTPS callback server listening on
|
|
364
|
+
this.server.listen(port, host, () => {
|
|
365
|
+
console.log(`HTTPS callback server listening on ${host}:${port}`);
|
|
305
366
|
this.port = port;
|
|
306
|
-
|
|
367
|
+
this.scheduleAutoStop(ttlMs);
|
|
368
|
+
resolve({ success: true, port, host });
|
|
307
369
|
});
|
|
308
370
|
} catch (error) {
|
|
309
371
|
reject(error);
|
|
@@ -314,7 +376,28 @@ class CallbackServer {
|
|
|
314
376
|
});
|
|
315
377
|
}
|
|
316
378
|
|
|
379
|
+
scheduleAutoStop(ttlMs) {
|
|
380
|
+
this.clearAutoStop();
|
|
381
|
+
this.ttlTimer = setTimeout(() => {
|
|
382
|
+
this.ttlTimer = null;
|
|
383
|
+
console.log('[DaikinCloud] Authorization window expired, stopping callback server');
|
|
384
|
+
this.stop().catch(() => {});
|
|
385
|
+
}, ttlMs);
|
|
386
|
+
if (typeof this.ttlTimer.unref === 'function') {
|
|
387
|
+
this.ttlTimer.unref();
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
clearAutoStop() {
|
|
392
|
+
if (this.ttlTimer) {
|
|
393
|
+
clearTimeout(this.ttlTimer);
|
|
394
|
+
this.ttlTimer = null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
317
398
|
async stop() {
|
|
399
|
+
this.clearAutoStop();
|
|
400
|
+
|
|
318
401
|
return new Promise((resolve) => {
|
|
319
402
|
if (!this.server) {
|
|
320
403
|
resolve({ success: true });
|
|
@@ -409,7 +492,7 @@ const HtmlTemplates = {
|
|
|
409
492
|
<div class="container">
|
|
410
493
|
<div class="icon">${icon}</div>
|
|
411
494
|
<h1>${success ? 'Success!' : 'Error'}</h1>
|
|
412
|
-
<p class="message">${message}</p>
|
|
495
|
+
<p class="message">${escapeHtml(message)}</p>
|
|
413
496
|
<p class="hint">You can close this window and return to Homebridge.</p>
|
|
414
497
|
</div>
|
|
415
498
|
</body>
|
|
@@ -428,12 +511,27 @@ class DaikinCloudUiServer extends HomebridgePluginUiServer {
|
|
|
428
511
|
this.pendingAuth = null;
|
|
429
512
|
this.authResult = null;
|
|
430
513
|
this.callbackServer = new CallbackServer();
|
|
514
|
+
this.devicesCache = new Map();
|
|
431
515
|
|
|
432
516
|
this.applyTransportFromConfig();
|
|
433
517
|
this.registerHandlers();
|
|
434
518
|
this.ready();
|
|
435
519
|
}
|
|
436
520
|
|
|
521
|
+
getStoragePath() {
|
|
522
|
+
return this.homebridgeStoragePath || process.env.UIX_STORAGE_PATH || '';
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Read the DaikinCloud platform block from the Homebridge config.json.
|
|
527
|
+
* Throws if the config file cannot be read or parsed.
|
|
528
|
+
*/
|
|
529
|
+
readPlatformConfig() {
|
|
530
|
+
const configPath = this.homebridgeConfigPath || resolve(this.getStoragePath(), 'config.json');
|
|
531
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
532
|
+
return (config.platforms || []).find((p) => p.platform === 'DaikinCloud') || null;
|
|
533
|
+
}
|
|
534
|
+
|
|
437
535
|
/**
|
|
438
536
|
* Honour the plugin's httpTransport setting in the setup wizard too, so a
|
|
439
537
|
* network that blocks Node's TLS fingerprint (issue #6) can still complete
|
|
@@ -441,35 +539,89 @@ class DaikinCloudUiServer extends HomebridgePluginUiServer {
|
|
|
441
539
|
*/
|
|
442
540
|
applyTransportFromConfig() {
|
|
443
541
|
try {
|
|
444
|
-
const
|
|
445
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
446
|
-
const platform = (config.platforms || []).find((p) => p.platform === 'DaikinCloud');
|
|
542
|
+
const platform = this.readPlatformConfig();
|
|
447
543
|
configureHttpTransport(platform && platform.httpTransport);
|
|
448
544
|
} catch (e) {
|
|
449
545
|
console.log('[DaikinCloud] Could not read httpTransport from config, using default:', e.message);
|
|
450
546
|
}
|
|
451
547
|
}
|
|
452
548
|
|
|
549
|
+
/**
|
|
550
|
+
* Resolve the callback server bind address from the request payload, then the
|
|
551
|
+
* saved platform config, falling back to DEFAULT_CALLBACK_BIND_ADDR when the
|
|
552
|
+
* value is missing or not an IPv4/IPv6 literal.
|
|
553
|
+
*/
|
|
554
|
+
getCallbackBindAddress(payload) {
|
|
555
|
+
let configured = payload?.oidcCallbackServerBindAddr;
|
|
556
|
+
if (configured === undefined || configured === null || configured === '') {
|
|
557
|
+
try {
|
|
558
|
+
configured = this.readPlatformConfig()?.oidcCallbackServerBindAddr;
|
|
559
|
+
} catch (e) {
|
|
560
|
+
configured = undefined;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const bindAddr = NetUtils.resolveBindAddress(configured);
|
|
565
|
+
if (configured && bindAddr !== String(configured).trim()) {
|
|
566
|
+
console.warn(`[DaikinCloud] Invalid callback bind address "${configured}", using ${bindAddr}`);
|
|
567
|
+
}
|
|
568
|
+
return bindAddr;
|
|
569
|
+
}
|
|
570
|
+
|
|
453
571
|
getTokenFilePath() {
|
|
454
|
-
return resolve(this.
|
|
572
|
+
return resolve(this.getStoragePath(), TOKEN_FILES.developer_portal);
|
|
455
573
|
}
|
|
456
574
|
|
|
457
575
|
getMobileTokenFilePath() {
|
|
458
|
-
return resolve(this.
|
|
576
|
+
return resolve(this.getStoragePath(), TOKEN_FILES.mobile_app);
|
|
459
577
|
}
|
|
460
578
|
|
|
461
579
|
getCertDir() {
|
|
462
|
-
return resolve(this.
|
|
580
|
+
return resolve(this.getStoragePath(), 'daikin-cloud-certs');
|
|
463
581
|
}
|
|
464
582
|
|
|
465
|
-
|
|
466
|
-
|
|
583
|
+
getRateLimitFilePath() {
|
|
584
|
+
return resolve(this.getStoragePath(), RATE_LIMIT_STATUS_FILE);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Active token set and the auth mode it belongs to (mobile takes precedence).
|
|
589
|
+
*/
|
|
590
|
+
getActiveToken() {
|
|
467
591
|
const mobileTokenSet = TokenManager.load(this.getMobileTokenFilePath());
|
|
468
592
|
if (mobileTokenSet?.access_token) {
|
|
469
|
-
return mobileTokenSet;
|
|
593
|
+
return { tokenSet: mobileTokenSet, mode: 'mobile_app' };
|
|
470
594
|
}
|
|
471
|
-
|
|
472
|
-
|
|
595
|
+
return { tokenSet: TokenManager.load(this.getTokenFilePath()), mode: 'developer_portal' };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
getActiveTokenSet() {
|
|
599
|
+
return this.getActiveToken().tokenSet;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Token set for the requested mode, falling back to the active token set.
|
|
604
|
+
*/
|
|
605
|
+
resolveToken(mode) {
|
|
606
|
+
let tokenSet = null;
|
|
607
|
+
if (mode === 'mobile_app') {
|
|
608
|
+
tokenSet = TokenManager.load(this.getMobileTokenFilePath());
|
|
609
|
+
} else if (mode === 'developer_portal') {
|
|
610
|
+
tokenSet = TokenManager.load(this.getTokenFilePath());
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (tokenSet?.access_token) {
|
|
614
|
+
return { tokenSet, mode };
|
|
615
|
+
}
|
|
616
|
+
return this.getActiveToken();
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
invalidateDevicesCache() {
|
|
620
|
+
this.devicesCache.clear();
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
isPendingAuthExpired() {
|
|
624
|
+
return !!this.pendingAuth && Date.now() - this.pendingAuth.createdAt > PENDING_AUTH_TTL_MS;
|
|
473
625
|
}
|
|
474
626
|
|
|
475
627
|
registerHandlers() {
|
|
@@ -562,16 +714,26 @@ class DaikinCloudUiServer extends HomebridgePluginUiServer {
|
|
|
562
714
|
throw new Error('Callback Server Address is required');
|
|
563
715
|
}
|
|
564
716
|
|
|
565
|
-
const
|
|
717
|
+
const bindAddr = this.getCallbackBindAddress(payload);
|
|
718
|
+
if (NetUtils.isLoopback(bindAddr) && !NetUtils.isLoopback(callbackServerExternalAddress)) {
|
|
719
|
+
// The browser is redirected to the external address, which can never reach a loopback-only server
|
|
720
|
+
throw new Error(
|
|
721
|
+
`The callback server bind address is ${bindAddr} (this machine only), but the callback address is ` +
|
|
722
|
+
`${callbackServerExternalAddress}, so Daikin's redirect could never reach it. Set "Callback Server Bind Address" ` +
|
|
723
|
+
'to 0.0.0.0 (or this host\'s LAN IP) in Settings > Network, then start authentication again.',
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const port = parseInt(callbackServerPort || String(DEFAULT_CALLBACK_PORT), 10);
|
|
566
728
|
const redirectUri = `https://${callbackServerExternalAddress}:${port}`;
|
|
567
729
|
const state = crypto.randomBytes(32).toString('hex');
|
|
568
730
|
|
|
569
731
|
this.pendingAuth = { state, clientId, clientSecret, redirectUri, port, createdAt: Date.now() };
|
|
570
732
|
this.authResult = null;
|
|
733
|
+
this.invalidateDevicesCache();
|
|
571
734
|
|
|
572
735
|
// Use static method from compiled src/api
|
|
573
736
|
const authUrl = DaikinOAuth.buildAuthUrlStatic(clientId, redirectUri, state);
|
|
574
|
-
console.log('[DaikinCloud] Generated auth URL:', authUrl);
|
|
575
737
|
console.log('[DaikinCloud] Redirect URI:', redirectUri);
|
|
576
738
|
|
|
577
739
|
// Try to start callback server for automatic code capture
|
|
@@ -579,12 +741,13 @@ class DaikinCloudUiServer extends HomebridgePluginUiServer {
|
|
|
579
741
|
let callbackServerError = null;
|
|
580
742
|
|
|
581
743
|
try {
|
|
582
|
-
await this.callbackServer.start(
|
|
744
|
+
await this.callbackServer.start({
|
|
583
745
|
port,
|
|
584
|
-
callbackServerExternalAddress,
|
|
585
|
-
|
|
586
|
-
this.
|
|
587
|
-
|
|
746
|
+
hostname: callbackServerExternalAddress,
|
|
747
|
+
bindAddr,
|
|
748
|
+
certDir: this.getCertDir(),
|
|
749
|
+
requestHandler: this.handleHttpsCallback.bind(this),
|
|
750
|
+
});
|
|
588
751
|
callbackServerRunning = true;
|
|
589
752
|
console.log('[DaikinCloud] Callback server started successfully');
|
|
590
753
|
} catch (error) {
|
|
@@ -609,37 +772,53 @@ class DaikinCloudUiServer extends HomebridgePluginUiServer {
|
|
|
609
772
|
// -------------------------------------------------------------------------
|
|
610
773
|
|
|
611
774
|
handleHttpsCallback(req, res) {
|
|
612
|
-
|
|
775
|
+
let url;
|
|
776
|
+
try {
|
|
777
|
+
url = new URL(req.url, 'https://localhost');
|
|
778
|
+
} catch (e) {
|
|
779
|
+
this.sendCallbackResponse(res, false, 'Invalid request', 400);
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
613
782
|
|
|
614
783
|
const code = url.searchParams.get('code');
|
|
615
784
|
const state = url.searchParams.get('state');
|
|
616
785
|
const error = url.searchParams.get('error');
|
|
617
786
|
const errorDescription = url.searchParams.get('error_description');
|
|
618
787
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
788
|
+
// Requests that do not carry the pending state are not ours: reject them
|
|
789
|
+
// without touching authResult so they cannot disrupt a running login.
|
|
790
|
+
if (!this.pendingAuth || !state || state !== this.pendingAuth.state) {
|
|
791
|
+
this.sendCallbackResponse(res, false, 'Invalid or missing state parameter', 400);
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
if (this.isPendingAuthExpired()) {
|
|
796
|
+
this.pendingAuth = null;
|
|
797
|
+
this.authResult = { success: false, error: 'Authorization request expired. Please try again.' };
|
|
798
|
+
this.sendCallbackResponse(res, false, 'Authorization request expired. Please start again from Homebridge.', 400);
|
|
799
|
+
this.callbackServer.stop().catch(() => {});
|
|
622
800
|
return;
|
|
623
801
|
}
|
|
624
802
|
|
|
625
|
-
if (
|
|
626
|
-
this.authResult = { success: false, error:
|
|
627
|
-
this.sendCallbackResponse(res, false,
|
|
803
|
+
if (error) {
|
|
804
|
+
this.authResult = { success: false, error: errorDescription || error };
|
|
805
|
+
this.sendCallbackResponse(res, false, errorDescription || error);
|
|
628
806
|
return;
|
|
629
807
|
}
|
|
630
808
|
|
|
631
|
-
if (!
|
|
632
|
-
this.authResult = { success: false, error: '
|
|
633
|
-
this.sendCallbackResponse(res, false, '
|
|
809
|
+
if (!code) {
|
|
810
|
+
this.authResult = { success: false, error: 'Missing code parameter' };
|
|
811
|
+
this.sendCallbackResponse(res, false, 'Missing authorization code', 400);
|
|
634
812
|
return;
|
|
635
813
|
}
|
|
636
814
|
|
|
637
815
|
const { clientId, clientSecret, redirectUri } = this.pendingAuth;
|
|
638
816
|
|
|
639
817
|
// Use static method from compiled src/api
|
|
640
|
-
DaikinOAuth.exchangeCodeStatic(code, clientId, clientSecret, redirectUri)
|
|
818
|
+
return DaikinOAuth.exchangeCodeStatic(code, clientId, clientSecret, redirectUri)
|
|
641
819
|
.then((tokenSet) => {
|
|
642
820
|
TokenManager.save(this.getTokenFilePath(), tokenSet);
|
|
821
|
+
this.invalidateDevicesCache();
|
|
643
822
|
this.authResult = {
|
|
644
823
|
success: true,
|
|
645
824
|
message: 'Authentication successful!',
|
|
@@ -655,8 +834,8 @@ class DaikinCloudUiServer extends HomebridgePluginUiServer {
|
|
|
655
834
|
});
|
|
656
835
|
}
|
|
657
836
|
|
|
658
|
-
sendCallbackResponse(res, success, message) {
|
|
659
|
-
res.writeHead(
|
|
837
|
+
sendCallbackResponse(res, success, message, statusCode = 200) {
|
|
838
|
+
res.writeHead(statusCode, CALLBACK_RESPONSE_HEADERS);
|
|
660
839
|
res.end(HtmlTemplates.callbackResponse(success, message));
|
|
661
840
|
}
|
|
662
841
|
|
|
@@ -683,10 +862,10 @@ class DaikinCloudUiServer extends HomebridgePluginUiServer {
|
|
|
683
862
|
if (!this.pendingAuth) {
|
|
684
863
|
throw new Error('No pending authorization. Please start the auth flow again.');
|
|
685
864
|
}
|
|
686
|
-
if (state
|
|
687
|
-
throw new Error('Invalid state parameter. Please
|
|
865
|
+
if (!state || state !== this.pendingAuth.state) {
|
|
866
|
+
throw new Error('Invalid or missing state parameter. Please paste the full callback URL or start again.');
|
|
688
867
|
}
|
|
689
|
-
if (
|
|
868
|
+
if (this.isPendingAuthExpired()) {
|
|
690
869
|
this.pendingAuth = null;
|
|
691
870
|
throw new Error('Authorization request expired. Please try again.');
|
|
692
871
|
}
|
|
@@ -697,6 +876,7 @@ class DaikinCloudUiServer extends HomebridgePluginUiServer {
|
|
|
697
876
|
// Use static method from compiled src/api
|
|
698
877
|
const tokenSet = await DaikinOAuth.exchangeCodeStatic(code, clientId, clientSecret, redirectUri);
|
|
699
878
|
TokenManager.save(this.getTokenFilePath(), tokenSet);
|
|
879
|
+
this.invalidateDevicesCache();
|
|
700
880
|
this.pendingAuth = null;
|
|
701
881
|
|
|
702
882
|
await this.callbackServer.stop();
|
|
@@ -759,6 +939,7 @@ class DaikinCloudUiServer extends HomebridgePluginUiServer {
|
|
|
759
939
|
}
|
|
760
940
|
|
|
761
941
|
const tokenFilePath = this.getMobileTokenFilePath();
|
|
942
|
+
this.invalidateDevicesCache();
|
|
762
943
|
|
|
763
944
|
try {
|
|
764
945
|
// Create a temporary mobile OAuth client
|
|
@@ -806,6 +987,8 @@ class DaikinCloudUiServer extends HomebridgePluginUiServer {
|
|
|
806
987
|
// -------------------------------------------------------------------------
|
|
807
988
|
|
|
808
989
|
async handleRevokeAuth(payload) {
|
|
990
|
+
this.invalidateDevicesCache();
|
|
991
|
+
|
|
809
992
|
const devPortalTokenSet = TokenManager.load(this.getTokenFilePath());
|
|
810
993
|
const mobileTokenSet = TokenManager.load(this.getMobileTokenFilePath());
|
|
811
994
|
|
|
@@ -859,25 +1042,20 @@ class DaikinCloudUiServer extends HomebridgePluginUiServer {
|
|
|
859
1042
|
// -------------------------------------------------------------------------
|
|
860
1043
|
|
|
861
1044
|
async handleListDevices(payload) {
|
|
862
|
-
|
|
1045
|
+
// Token for the requested mode, or fall back to the active token
|
|
1046
|
+
const { tokenSet, mode } = this.resolveToken(payload?.mode);
|
|
863
1047
|
|
|
864
|
-
// Get token based on mode parameter, or fall back to active token
|
|
865
|
-
let tokenSet;
|
|
866
|
-
if (mode === 'mobile_app') {
|
|
867
|
-
tokenSet = TokenManager.load(this.getMobileTokenFilePath());
|
|
868
|
-
} else if (mode === 'developer_portal') {
|
|
869
|
-
tokenSet = TokenManager.load(this.getTokenFilePath());
|
|
870
|
-
} else {
|
|
871
|
-
tokenSet = this.getActiveTokenSet();
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
// If the requested mode's token doesn't exist or is invalid, fall back to active token
|
|
875
1048
|
if (!tokenSet?.access_token) {
|
|
876
|
-
|
|
1049
|
+
return { success: false, devices: [], message: 'Not authenticated. Please authenticate first.' };
|
|
877
1050
|
}
|
|
878
1051
|
|
|
879
|
-
|
|
880
|
-
|
|
1052
|
+
// Reuse a recent result to spare the daily API quota (explicit refresh bypasses it)
|
|
1053
|
+
if (payload?.refresh) {
|
|
1054
|
+
this.devicesCache.delete(mode);
|
|
1055
|
+
}
|
|
1056
|
+
const cached = this.devicesCache.get(mode);
|
|
1057
|
+
if (cached && Date.now() - cached.fetchedAt < DEVICES_CACHE_TTL_MS) {
|
|
1058
|
+
return { ...cached.result, cached: true, fetchedAt: new Date(cached.fetchedAt).toISOString() };
|
|
881
1059
|
}
|
|
882
1060
|
|
|
883
1061
|
try {
|
|
@@ -888,7 +1066,10 @@ class DaikinCloudUiServer extends HomebridgePluginUiServer {
|
|
|
888
1066
|
? gatewayDevices.map(device => DeviceExtractor.extractAll(device))
|
|
889
1067
|
: [];
|
|
890
1068
|
|
|
891
|
-
|
|
1069
|
+
const response = { success: true, devices, message: `Found ${devices.length} device(s).` };
|
|
1070
|
+
const fetchedAt = Date.now();
|
|
1071
|
+
this.devicesCache.set(mode, { result: response, fetchedAt });
|
|
1072
|
+
return { ...response, cached: false, fetchedAt: new Date(fetchedAt).toISOString() };
|
|
892
1073
|
} catch (error) {
|
|
893
1074
|
return { success: false, devices: [], message: `Failed to fetch devices: ${error.message}`, error: error.message };
|
|
894
1075
|
}
|
|
@@ -898,62 +1079,61 @@ class DaikinCloudUiServer extends HomebridgePluginUiServer {
|
|
|
898
1079
|
// Get Rate Limit Handler
|
|
899
1080
|
// -------------------------------------------------------------------------
|
|
900
1081
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1082
|
+
/**
|
|
1083
|
+
* Report the rate limit status the running plugin records after each API
|
|
1084
|
+
* response, instead of spending an API call just to read the headers.
|
|
1085
|
+
*/
|
|
1086
|
+
async handleGetRateLimit() {
|
|
1087
|
+
let status;
|
|
1088
|
+
try {
|
|
1089
|
+
status = JSON.parse(fs.readFileSync(this.getRateLimitFilePath(), 'utf8'));
|
|
1090
|
+
} catch (error) {
|
|
1091
|
+
if (error.code === 'ENOENT') {
|
|
1092
|
+
return {
|
|
1093
|
+
success: false,
|
|
1094
|
+
noData: true,
|
|
1095
|
+
message: 'No data yet — the plugin records this after its next API call.',
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
return { success: false, message: `Could not read rate limit status: ${error.message}` };
|
|
912
1099
|
}
|
|
913
1100
|
|
|
914
|
-
if (!
|
|
915
|
-
return { success: false, message: '
|
|
1101
|
+
if (!status || typeof status !== 'object') {
|
|
1102
|
+
return { success: false, message: 'Could not read rate limit status: invalid file contents' };
|
|
916
1103
|
}
|
|
917
1104
|
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
1105
|
+
const { limitMinute, remainingMinute, limitDay, remainingDay, mode, updatedAt } = status;
|
|
1106
|
+
const updatedAtMs = typeof updatedAt === 'number' ? updatedAt : Date.parse(updatedAt);
|
|
1107
|
+
const hasTime = Number.isFinite(updatedAtMs);
|
|
1108
|
+
|
|
1109
|
+
return {
|
|
1110
|
+
success: true,
|
|
1111
|
+
rateLimit: { limitMinute, remainingMinute, limitDay, remainingDay },
|
|
1112
|
+
mode: mode || null,
|
|
1113
|
+
updatedAt: hasTime ? new Date(updatedAtMs).toISOString() : null,
|
|
1114
|
+
ageMs: hasTime ? Math.max(0, Date.now() - updatedAtMs) : null,
|
|
1115
|
+
};
|
|
925
1116
|
}
|
|
926
1117
|
|
|
927
1118
|
// -------------------------------------------------------------------------
|
|
928
1119
|
// Validate Config Handler
|
|
929
1120
|
// -------------------------------------------------------------------------
|
|
930
1121
|
|
|
1122
|
+
/**
|
|
1123
|
+
* Validate the Developer Portal wizard fields with the plugin's own rules
|
|
1124
|
+
* (ConfigManager), so the UI and the plugin can never disagree.
|
|
1125
|
+
* Response: { valid, errors, warnings }.
|
|
1126
|
+
*/
|
|
931
1127
|
async handleValidateConfig(payload) {
|
|
932
|
-
const
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
if (!callbackServerExternalAddress) {
|
|
944
|
-
errors.push('Callback Server External Address is required.');
|
|
945
|
-
} else if (callbackServerExternalAddress === 'localhost' || callbackServerExternalAddress === '127.0.0.1') {
|
|
946
|
-
errors.push('Callback address cannot be localhost. Use your external IP or domain.');
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
const port = parseInt(callbackServerPort || '8582', 10);
|
|
950
|
-
if (isNaN(port) || port < 1 || port > 65535) {
|
|
951
|
-
errors.push('Invalid port number. Must be between 1 and 65535.');
|
|
952
|
-
} else if (port < 1024) {
|
|
953
|
-
warnings.push('Using a privileged port (< 1024) may require root permissions.');
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
return { valid: errors.length === 0, errors, warnings };
|
|
1128
|
+
const { clientId, clientSecret, callbackServerExternalAddress, callbackServerPort } = payload || {};
|
|
1129
|
+
return validateConfig({
|
|
1130
|
+
platform: 'DaikinCloud',
|
|
1131
|
+
authMode: 'developer_portal',
|
|
1132
|
+
clientId,
|
|
1133
|
+
clientSecret,
|
|
1134
|
+
callbackServerExternalAddress,
|
|
1135
|
+
callbackServerPort,
|
|
1136
|
+
});
|
|
957
1137
|
}
|
|
958
1138
|
}
|
|
959
1139
|
|
|
@@ -961,4 +1141,20 @@ class DaikinCloudUiServer extends HomebridgePluginUiServer {
|
|
|
961
1141
|
// Initialize Server
|
|
962
1142
|
// =============================================================================
|
|
963
1143
|
|
|
964
|
-
|
|
1144
|
+
module.exports = {
|
|
1145
|
+
DaikinCloudUiServer,
|
|
1146
|
+
CallbackServer,
|
|
1147
|
+
SSLUtils,
|
|
1148
|
+
NetUtils,
|
|
1149
|
+
TokenManager,
|
|
1150
|
+
DeviceExtractor,
|
|
1151
|
+
HtmlTemplates,
|
|
1152
|
+
escapeHtml,
|
|
1153
|
+
DEVICES_CACHE_TTL_MS,
|
|
1154
|
+
};
|
|
1155
|
+
|
|
1156
|
+
// Homebridge UI forks this file as a child process; only start the server then,
|
|
1157
|
+
// so tests can require it without side effects.
|
|
1158
|
+
if (require.main === module) {
|
|
1159
|
+
(() => new DaikinCloudUiServer())();
|
|
1160
|
+
}
|