@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/dist/src/platform.d.ts
CHANGED
|
@@ -1,41 +1,88 @@
|
|
|
1
1
|
import type { API, Characteristic, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig, Service } from 'homebridge';
|
|
2
2
|
import type { DaikinCloudDevice } from './api';
|
|
3
3
|
import { DaikinCloudController } from './api';
|
|
4
|
+
import { ConfigManager } from './config/config-manager';
|
|
5
|
+
/** Safety-net poll interval while the WebSocket is delivering push updates */
|
|
6
|
+
export declare const WEBSOCKET_SAFETY_POLL_INTERVAL_MS: number;
|
|
7
|
+
/** Minimum poll interval once the daily quota is nearly exhausted */
|
|
8
|
+
export declare const LOW_QUOTA_POLL_INTERVAL_MS: number;
|
|
9
|
+
/** Upper bound of the failure backoff, as a multiple of the base interval */
|
|
10
|
+
export declare const MAX_POLL_BACKOFF_MULTIPLIER = 4;
|
|
11
|
+
export interface PollIntervalInput {
|
|
12
|
+
configuredMs: number;
|
|
13
|
+
webSocketConnected: boolean;
|
|
14
|
+
consecutiveFailures: number;
|
|
15
|
+
remainingDay?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Compute the delay until the next periodic poll:
|
|
19
|
+
* - WebSocket connected: push updates arrive anyway, poll only as a safety net
|
|
20
|
+
* - consecutive failures: exponential backoff, capped at MAX_POLL_BACKOFF_MULTIPLIER
|
|
21
|
+
* - daily quota nearly exhausted: stretch to LOW_QUOTA_POLL_INTERVAL_MS
|
|
22
|
+
*/
|
|
23
|
+
export declare function computePollInterval(input: PollIntervalInput): number;
|
|
4
24
|
export type DaikinCloudAccessoryContext = {
|
|
5
25
|
device: DaikinCloudDevice;
|
|
6
26
|
};
|
|
7
27
|
export declare class DaikinCloudPlatform implements DynamicPlatformPlugin {
|
|
8
28
|
readonly log: Logger;
|
|
9
|
-
readonly config: PlatformConfig;
|
|
10
29
|
readonly api: API;
|
|
11
30
|
readonly Service: typeof Service;
|
|
12
31
|
readonly Characteristic: typeof Characteristic;
|
|
13
32
|
readonly accessories: PlatformAccessory<DaikinCloudAccessoryContext>[];
|
|
14
33
|
readonly storagePath: string;
|
|
34
|
+
/** Single source of truth for the plugin configuration: read config only through this */
|
|
35
|
+
readonly configManager: ConfigManager;
|
|
15
36
|
controller: DaikinCloudController | undefined;
|
|
16
37
|
readonly updateIntervalDelay: number;
|
|
17
|
-
private
|
|
38
|
+
private pollTimer;
|
|
18
39
|
private forceUpdateTimeout;
|
|
40
|
+
private inFlightUpdate;
|
|
41
|
+
private pollingStarted;
|
|
42
|
+
private isShuttingDown;
|
|
43
|
+
private consecutivePollFailures;
|
|
44
|
+
private remainingDay;
|
|
45
|
+
private webSocketHasConnected;
|
|
46
|
+
private pendingRateLimitStatus;
|
|
47
|
+
private rateLimitWriteTimer;
|
|
48
|
+
private lastRateLimitWriteAt;
|
|
19
49
|
private readonly accessoryFactory;
|
|
20
|
-
private readonly updateMapper;
|
|
21
50
|
private readonly authMode;
|
|
22
51
|
private readonly deviceListeners;
|
|
23
52
|
constructor(log: Logger, config: PlatformConfig, api: API);
|
|
53
|
+
private registerControllerListeners;
|
|
24
54
|
configureAccessory(accessory: PlatformAccessory): void;
|
|
25
55
|
private discoverDevices;
|
|
26
56
|
private createDevices;
|
|
27
57
|
private removeDeviceListener;
|
|
28
58
|
registerDeviceListener(accessory: PlatformAccessory<DaikinCloudAccessoryContext>, listener: () => void): void;
|
|
59
|
+
/**
|
|
60
|
+
* Fetch fresh device data. Overlapping callers (periodic poll, forced update,
|
|
61
|
+
* WebSocket reconnect catch-up) share a single in-flight request.
|
|
62
|
+
*/
|
|
29
63
|
private updateDevices;
|
|
64
|
+
/**
|
|
65
|
+
* Poll now, then schedule the next periodic poll.
|
|
66
|
+
*/
|
|
67
|
+
private pollNow;
|
|
30
68
|
forceUpdateDevices(delay?: number): void;
|
|
69
|
+
/**
|
|
70
|
+
* (Re)schedule the next periodic poll. Always replaces any previously
|
|
71
|
+
* scheduled poll, so there is never more than one pending.
|
|
72
|
+
*/
|
|
31
73
|
private startUpdateDevicesInterval;
|
|
74
|
+
private stopPolling;
|
|
75
|
+
private getPollInterval;
|
|
76
|
+
private isWebSocketConnected;
|
|
77
|
+
private onWebSocketConnected;
|
|
78
|
+
private onRateLimitStatus;
|
|
79
|
+
/**
|
|
80
|
+
* Persist the latest rate-limit headers for the settings UI (throttled,
|
|
81
|
+
* best effort: failures are logged at debug level and never thrown).
|
|
82
|
+
*/
|
|
83
|
+
private saveRateLimitStatus;
|
|
84
|
+
private flushRateLimitStatus;
|
|
32
85
|
private enableWebSocket;
|
|
33
|
-
private isExcludedDevice;
|
|
34
|
-
private getPrivacyFriendlyConfig;
|
|
35
86
|
private onInvalidGrantError;
|
|
36
|
-
/**
|
|
37
|
-
* Handle WebSocket device updates by pushing updated values to HomeKit
|
|
38
|
-
*/
|
|
39
|
-
private handleWebSocketDeviceUpdate;
|
|
40
87
|
}
|
|
41
88
|
//# sourceMappingURL=platform.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../../src/platform.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,EAAE,iBAAiB,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../../src/platform.ts"],"names":[],"mappings":"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"}
|
package/dist/src/platform.js
CHANGED
|
@@ -3,56 +3,100 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.DaikinCloudPlatform = void 0;
|
|
6
|
+
exports.DaikinCloudPlatform = exports.MAX_POLL_BACKOFF_MULTIPLIER = exports.LOW_QUOTA_POLL_INTERVAL_MS = exports.WEBSOCKET_SAFETY_POLL_INTERVAL_MS = void 0;
|
|
7
|
+
exports.computePollInterval = computePollInterval;
|
|
7
8
|
const settings_1 = require("./settings");
|
|
8
9
|
const device_1 = require("./device");
|
|
9
10
|
const node_path_1 = require("node:path");
|
|
11
|
+
const node_util_1 = require("node:util");
|
|
10
12
|
const strings_1 = require("./utils/strings");
|
|
13
|
+
const errors_1 = require("./utils/errors");
|
|
11
14
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
15
|
const api_1 = require("./api");
|
|
13
16
|
const http_transport_1 = require("./api/http-transport");
|
|
14
|
-
const update_mapper_1 = require("./utils/update-mapper");
|
|
15
17
|
const config_manager_1 = require("./config/config-manager");
|
|
16
18
|
const constants_1 = require("./constants");
|
|
19
|
+
/** Safety-net poll interval while the WebSocket is delivering push updates */
|
|
20
|
+
exports.WEBSOCKET_SAFETY_POLL_INTERVAL_MS = 60 * constants_1.ONE_MINUTE_MS;
|
|
21
|
+
/** Minimum poll interval once the daily quota is nearly exhausted */
|
|
22
|
+
exports.LOW_QUOTA_POLL_INTERVAL_MS = 60 * constants_1.ONE_MINUTE_MS;
|
|
23
|
+
/** Upper bound of the failure backoff, as a multiple of the base interval */
|
|
24
|
+
exports.MAX_POLL_BACKOFF_MULTIPLIER = 4;
|
|
25
|
+
/** Minimum time between two writes of the rate-limit status file */
|
|
26
|
+
const RATE_LIMIT_FILE_MIN_WRITE_INTERVAL_MS = 5 * constants_1.ONE_SECOND_MS;
|
|
27
|
+
/**
|
|
28
|
+
* Compute the delay until the next periodic poll:
|
|
29
|
+
* - WebSocket connected: push updates arrive anyway, poll only as a safety net
|
|
30
|
+
* - consecutive failures: exponential backoff, capped at MAX_POLL_BACKOFF_MULTIPLIER
|
|
31
|
+
* - daily quota nearly exhausted: stretch to LOW_QUOTA_POLL_INTERVAL_MS
|
|
32
|
+
*/
|
|
33
|
+
function computePollInterval(input) {
|
|
34
|
+
let interval = input.configuredMs;
|
|
35
|
+
if (input.webSocketConnected) {
|
|
36
|
+
interval = Math.max(interval, exports.WEBSOCKET_SAFETY_POLL_INTERVAL_MS);
|
|
37
|
+
}
|
|
38
|
+
if (input.consecutiveFailures > 0) {
|
|
39
|
+
interval *= Math.min(2 ** input.consecutiveFailures, exports.MAX_POLL_BACKOFF_MULTIPLIER);
|
|
40
|
+
}
|
|
41
|
+
if (input.remainingDay !== undefined && input.remainingDay <= constants_1.RATE_LIMIT_WARNING_THRESHOLD) {
|
|
42
|
+
interval = Math.max(interval, exports.LOW_QUOTA_POLL_INTERVAL_MS);
|
|
43
|
+
}
|
|
44
|
+
return interval;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Wrap an expensive serialisation so it only runs if the logger actually
|
|
48
|
+
* formats the parameter (Homebridge skips formatting when debug is off).
|
|
49
|
+
*/
|
|
50
|
+
function lazyJson(producer, space) {
|
|
51
|
+
return { [node_util_1.inspect.custom]: () => JSON.stringify(producer(), null, space) };
|
|
52
|
+
}
|
|
17
53
|
class DaikinCloudPlatform {
|
|
18
54
|
log;
|
|
19
|
-
config;
|
|
20
55
|
api;
|
|
21
56
|
Service;
|
|
22
57
|
Characteristic;
|
|
23
58
|
accessories = [];
|
|
24
59
|
storagePath = '';
|
|
60
|
+
/** Single source of truth for the plugin configuration: read config only through this */
|
|
61
|
+
configManager;
|
|
25
62
|
controller;
|
|
26
63
|
updateIntervalDelay;
|
|
27
|
-
|
|
64
|
+
pollTimer;
|
|
28
65
|
forceUpdateTimeout;
|
|
66
|
+
inFlightUpdate;
|
|
67
|
+
pollingStarted = false;
|
|
68
|
+
isShuttingDown = false;
|
|
69
|
+
consecutivePollFailures = 0;
|
|
70
|
+
remainingDay;
|
|
71
|
+
webSocketHasConnected = false;
|
|
72
|
+
pendingRateLimitStatus;
|
|
73
|
+
rateLimitWriteTimer;
|
|
74
|
+
lastRateLimitWriteAt = 0;
|
|
29
75
|
accessoryFactory;
|
|
30
|
-
updateMapper;
|
|
31
76
|
authMode;
|
|
32
77
|
deviceListeners = new Map();
|
|
33
78
|
constructor(log, config, api) {
|
|
34
79
|
this.log = log;
|
|
35
|
-
this.config = config;
|
|
36
80
|
this.api = api;
|
|
81
|
+
this.configManager = new config_manager_1.ConfigManager(config);
|
|
82
|
+
const configManager = this.configManager;
|
|
37
83
|
this.log.info('--- Daikin info for debugging reasons (enable Debug Mode for more logs) ---');
|
|
38
|
-
this.log.debug('[Platform] Initializing platform:',
|
|
84
|
+
this.log.debug('[Platform] Initializing platform:', configManager.getName());
|
|
39
85
|
this.Service = this.api.hap.Service;
|
|
40
86
|
this.Characteristic = this.api.hap.Characteristic;
|
|
41
87
|
this.storagePath = api.user.storagePath();
|
|
42
|
-
this.updateIntervalDelay =
|
|
88
|
+
this.updateIntervalDelay = configManager.getUpdateIntervalMs();
|
|
43
89
|
this.accessoryFactory = new device_1.AccessoryFactory(this);
|
|
44
|
-
this.updateMapper = new update_mapper_1.UpdateMapper(this.log, this.Service, this.Characteristic);
|
|
45
90
|
// Determine authentication mode
|
|
46
|
-
this.authMode =
|
|
91
|
+
this.authMode = configManager.getAuthMode();
|
|
47
92
|
this.log.info(`[Config] Authentication mode: ${this.authMode}`);
|
|
48
93
|
// Select the HTTP transport (env var DAIKIN_HTTP_TRANSPORT wins over config)
|
|
49
|
-
(0, http_transport_1.configureHttpTransport)(
|
|
94
|
+
(0, http_transport_1.configureHttpTransport)(configManager.getHttpTransport());
|
|
50
95
|
if ((0, http_transport_1.getHttpTransportMode)() === 'curl') {
|
|
51
96
|
this.log.info('[Config] HTTP transport: curl subprocess (WAF fingerprint workaround). '
|
|
52
97
|
+ 'Note: WebSocket connections still use Node TLS — disable WebSocket if it cannot connect.');
|
|
53
98
|
}
|
|
54
99
|
// Validate configuration
|
|
55
|
-
const configManager = new config_manager_1.ConfigManager(this.config);
|
|
56
100
|
const validation = configManager.validate();
|
|
57
101
|
for (const warning of validation.warnings) {
|
|
58
102
|
this.log.warn(`[Config] ${warning}`);
|
|
@@ -69,32 +113,22 @@ class DaikinCloudPlatform {
|
|
|
69
113
|
}
|
|
70
114
|
}
|
|
71
115
|
else {
|
|
72
|
-
if (!
|
|
116
|
+
if (!configManager.hasDeveloperCredentials()) {
|
|
73
117
|
this.log.warn('[Config] Client ID and/or Client Secret not configured.');
|
|
74
118
|
this.log.warn('[Config] Please configure the plugin using the Homebridge UI.');
|
|
75
119
|
this.log.info('--------------- End Daikin info for debugging reasons --------------------');
|
|
76
120
|
return;
|
|
77
121
|
}
|
|
78
122
|
}
|
|
123
|
+
// Credentials are present: the remaining errors (e.g. a localhost callback
|
|
124
|
+
// address or an invalid port) do not stop startup, but must be visible.
|
|
125
|
+
for (const error of validation.errors) {
|
|
126
|
+
this.log.error(`[Config] ${error}`);
|
|
127
|
+
}
|
|
79
128
|
// Use different token file for mobile auth to avoid conflicts
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const tokenFilePath = (0, node_path_1.resolve)(this.storagePath, tokenFileName);
|
|
84
|
-
const daikinCloudControllerConfig = {
|
|
85
|
-
authMode: this.authMode,
|
|
86
|
-
tokenFilePath,
|
|
87
|
-
// Developer Portal fields
|
|
88
|
-
clientId: this.config.clientId,
|
|
89
|
-
clientSecret: this.config.clientSecret,
|
|
90
|
-
callbackServerExternalAddress: this.config.callbackServerExternalAddress,
|
|
91
|
-
callbackServerPort: this.config.callbackServerPort || 8582,
|
|
92
|
-
oidcCallbackServerBindAddr: this.config.oidcCallbackServerBindAddr,
|
|
93
|
-
// Mobile App fields
|
|
94
|
-
email: configManager.getMobileEmail(),
|
|
95
|
-
password: configManager.getMobilePassword(),
|
|
96
|
-
};
|
|
97
|
-
this.log.debug('[Config] Homebridge config', this.getPrivacyFriendlyConfig(this.config));
|
|
129
|
+
const tokenFilePath = (0, node_path_1.resolve)(this.storagePath, constants_1.TOKEN_FILES[this.authMode]);
|
|
130
|
+
const daikinCloudControllerConfig = configManager.getControllerConfig(tokenFilePath);
|
|
131
|
+
this.log.debug('[Config] Homebridge config', configManager.getRedactedConfig());
|
|
98
132
|
node_fs_1.default.stat(tokenFilePath, (err, stats) => {
|
|
99
133
|
if (err) {
|
|
100
134
|
this.log.debug('[Config] Token file does NOT exist.');
|
|
@@ -111,6 +145,9 @@ class DaikinCloudPlatform {
|
|
|
111
145
|
if (!this.controller) {
|
|
112
146
|
return;
|
|
113
147
|
}
|
|
148
|
+
// Register listeners before authenticating: an 'error' emitted without a
|
|
149
|
+
// listener would throw.
|
|
150
|
+
this.registerControllerListeners(this.controller);
|
|
114
151
|
// Handle authentication based on mode
|
|
115
152
|
if (!this.controller.isAuthenticated()) {
|
|
116
153
|
if (this.authMode === 'mobile_app') {
|
|
@@ -121,7 +158,7 @@ class DaikinCloudPlatform {
|
|
|
121
158
|
this.log.info('[Auth] Authentication successful!');
|
|
122
159
|
}
|
|
123
160
|
catch (error) {
|
|
124
|
-
this.log.error(`[Auth] Authentication failed: ${error
|
|
161
|
+
this.log.error(`[Auth] Authentication failed: ${(0, errors_1.toMessage)(error)}`);
|
|
125
162
|
this.log.info('--------------- End Daikin info for debugging reasons --------------------');
|
|
126
163
|
return;
|
|
127
164
|
}
|
|
@@ -132,45 +169,14 @@ class DaikinCloudPlatform {
|
|
|
132
169
|
return;
|
|
133
170
|
}
|
|
134
171
|
}
|
|
135
|
-
this.controller.on('rate_limit_status', (rateLimitStatus) => {
|
|
136
|
-
if (rateLimitStatus.remainingDay && rateLimitStatus.remainingDay <= constants_1.RATE_LIMIT_WARNING_THRESHOLD) {
|
|
137
|
-
this.log.warn(`[Rate Limit] Rate limit almost reached, you only have ${rateLimitStatus.remainingDay} calls left today`);
|
|
138
|
-
}
|
|
139
|
-
// Only show minute limits if available (Developer Portal mode)
|
|
140
|
-
const minuteInfo = rateLimitStatus.limitMinute !== undefined
|
|
141
|
-
? ` -- this minute: ${rateLimitStatus.remainingMinute}/${rateLimitStatus.limitMinute}`
|
|
142
|
-
: '';
|
|
143
|
-
this.log.debug(`[Rate Limit] Remaining calls today: ${rateLimitStatus.remainingDay}/${rateLimitStatus.limitDay}${minuteInfo}`);
|
|
144
|
-
});
|
|
145
|
-
this.controller.on('error', (error) => {
|
|
146
|
-
this.log.error(`[Error] ${error}`);
|
|
147
|
-
});
|
|
148
|
-
this.controller.on('log', (message) => {
|
|
149
|
-
this.log.info(message);
|
|
150
|
-
});
|
|
151
|
-
// WebSocket event handlers
|
|
152
|
-
this.controller.on('websocket_connected', () => {
|
|
153
|
-
this.log.info('[WebSocket] Connected - receiving real-time updates');
|
|
154
|
-
});
|
|
155
|
-
this.controller.on('websocket_disconnected', (info) => {
|
|
156
|
-
if (info?.reconnecting) {
|
|
157
|
-
this.log.debug('[WebSocket] Disconnected, attempting to reconnect...');
|
|
158
|
-
}
|
|
159
|
-
else {
|
|
160
|
-
this.log.info('[WebSocket] Disconnected');
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
this.controller.on('websocket_device_update', (update) => {
|
|
164
|
-
this.log.debug(`[WebSocket] Device update: ${update.deviceId} - ${update.characteristicName}`, JSON.stringify(update.data));
|
|
165
|
-
this.handleWebSocketDeviceUpdate(update);
|
|
166
|
-
});
|
|
167
172
|
const onInvalidGrantError = () => this.onInvalidGrantError(tokenFilePath);
|
|
168
173
|
const devices = await this.discoverDevices(this.controller, onInvalidGrantError);
|
|
169
|
-
if (devices.length > 0) {
|
|
174
|
+
if (devices.length > 0 && !this.isShuttingDown) {
|
|
170
175
|
this.createDevices(devices);
|
|
176
|
+
this.pollingStarted = true;
|
|
171
177
|
this.startUpdateDevicesInterval();
|
|
172
178
|
// Enable WebSocket for real-time updates (unless explicitly disabled)
|
|
173
|
-
if (this.
|
|
179
|
+
if (this.configManager.isWebSocketEnabled()) {
|
|
174
180
|
await this.enableWebSocket();
|
|
175
181
|
}
|
|
176
182
|
}
|
|
@@ -179,8 +185,13 @@ class DaikinCloudPlatform {
|
|
|
179
185
|
// Shutdown handler: clean up timers, WebSocket, and device listeners on platform shutdown
|
|
180
186
|
this.api.on('shutdown', () => {
|
|
181
187
|
this.log.debug('[Platform] Shutting down, cleaning up resources...');
|
|
182
|
-
|
|
188
|
+
this.isShuttingDown = true;
|
|
189
|
+
this.stopPolling();
|
|
183
190
|
clearTimeout(this.forceUpdateTimeout);
|
|
191
|
+
this.forceUpdateTimeout = undefined;
|
|
192
|
+
clearTimeout(this.rateLimitWriteTimer);
|
|
193
|
+
this.rateLimitWriteTimer = undefined;
|
|
194
|
+
this.flushRateLimitStatus();
|
|
184
195
|
this.controller?.disableWebSocket();
|
|
185
196
|
for (const [uuid, listener] of this.deviceListeners) {
|
|
186
197
|
const accessory = this.accessories.find(a => a.UUID === uuid);
|
|
@@ -191,6 +202,33 @@ class DaikinCloudPlatform {
|
|
|
191
202
|
this.deviceListeners.clear();
|
|
192
203
|
});
|
|
193
204
|
}
|
|
205
|
+
registerControllerListeners(controller) {
|
|
206
|
+
controller.on('rate_limit_status', (rateLimitStatus) => this.onRateLimitStatus(rateLimitStatus));
|
|
207
|
+
controller.on('error', (error) => {
|
|
208
|
+
this.log.error(`[Error] ${(0, errors_1.toMessage)(error)}`);
|
|
209
|
+
});
|
|
210
|
+
controller.on('log', (message) => {
|
|
211
|
+
this.log.info(message);
|
|
212
|
+
});
|
|
213
|
+
controller.on('websocket_connected', () => this.onWebSocketConnected());
|
|
214
|
+
controller.on('websocket_disconnected', (info) => {
|
|
215
|
+
if (info?.reconnecting) {
|
|
216
|
+
this.log.debug('[WebSocket] Disconnected, attempting to reconnect...');
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
this.log.info('[WebSocket] Disconnected');
|
|
220
|
+
}
|
|
221
|
+
// Push updates are gone: fall back to the configured polling interval
|
|
222
|
+
if (this.pollingStarted) {
|
|
223
|
+
this.startUpdateDevicesInterval();
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
// The controller already applied the update to the device, which emits
|
|
227
|
+
// 'updated' and makes the accessory refresh its characteristics.
|
|
228
|
+
controller.on('websocket_device_update', (update) => {
|
|
229
|
+
this.log.debug(`[WebSocket] Device update: ${update.deviceId} - ${update.characteristicName}`, lazyJson(() => update.data));
|
|
230
|
+
});
|
|
231
|
+
}
|
|
194
232
|
configureAccessory(accessory) {
|
|
195
233
|
this.log.info('[Platform] Loading accessory from cache:', accessory.displayName);
|
|
196
234
|
this.accessories.push(accessory);
|
|
@@ -217,8 +255,8 @@ class DaikinCloudPlatform {
|
|
|
217
255
|
const uuid = this.api.hap.uuid.generate(deviceId);
|
|
218
256
|
const deviceModel = device.getDescription().deviceModel;
|
|
219
257
|
const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid);
|
|
220
|
-
this.log.debug('Create Device', deviceModel,
|
|
221
|
-
if (this.
|
|
258
|
+
this.log.debug('Create Device', deviceModel, lazyJson(() => api_1.DaikinCloudRepo.maskSensitiveCloudDeviceData(device.desc), 4));
|
|
259
|
+
if (this.configManager.isDeviceExcluded(deviceId)) {
|
|
222
260
|
this.log.info(`[Platform] Device ${deviceModel} (id: ${deviceId}) is excluded, don't add accessory`);
|
|
223
261
|
if (existingAccessory) {
|
|
224
262
|
this.api.unregisterPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [existingAccessory]);
|
|
@@ -250,7 +288,7 @@ class DaikinCloudPlatform {
|
|
|
250
288
|
if (error instanceof Error) {
|
|
251
289
|
this.log.error(`[Platform] Failed to create accessory: ${error.message}`);
|
|
252
290
|
this.log.debug('[Platform] Error details:', error.stack);
|
|
253
|
-
this.log.debug('[Platform] Device JSON:',
|
|
291
|
+
this.log.debug('[Platform] Device JSON:', lazyJson(() => api_1.DaikinCloudRepo.maskSensitiveCloudDeviceData(device.desc), 2));
|
|
254
292
|
}
|
|
255
293
|
}
|
|
256
294
|
}
|
|
@@ -265,18 +303,51 @@ class DaikinCloudPlatform {
|
|
|
265
303
|
registerDeviceListener(accessory, listener) {
|
|
266
304
|
this.deviceListeners.set(accessory.UUID, listener);
|
|
267
305
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
306
|
+
/**
|
|
307
|
+
* Fetch fresh device data. Overlapping callers (periodic poll, forced update,
|
|
308
|
+
* WebSocket reconnect catch-up) share a single in-flight request.
|
|
309
|
+
*/
|
|
310
|
+
updateDevices() {
|
|
311
|
+
const controller = this.controller;
|
|
312
|
+
if (!controller) {
|
|
313
|
+
return Promise.resolve();
|
|
271
314
|
}
|
|
272
|
-
|
|
273
|
-
|
|
315
|
+
if (!this.inFlightUpdate) {
|
|
316
|
+
this.inFlightUpdate = (async () => {
|
|
317
|
+
try {
|
|
318
|
+
await controller.updateAllDeviceData();
|
|
319
|
+
this.consecutivePollFailures = 0;
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
this.consecutivePollFailures++;
|
|
323
|
+
this.log.error(`[API Syncing] Failed to update devices data: ${(0, errors_1.toMessage)(error)}`);
|
|
324
|
+
}
|
|
325
|
+
})().finally(() => {
|
|
326
|
+
this.inFlightUpdate = undefined;
|
|
327
|
+
});
|
|
274
328
|
}
|
|
275
|
-
|
|
276
|
-
|
|
329
|
+
return this.inFlightUpdate;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Poll now, then schedule the next periodic poll.
|
|
333
|
+
*/
|
|
334
|
+
async pollNow() {
|
|
335
|
+
if (this.isShuttingDown) {
|
|
336
|
+
return;
|
|
277
337
|
}
|
|
338
|
+
this.stopPolling();
|
|
339
|
+
await this.updateDevices();
|
|
340
|
+
this.startUpdateDevicesInterval();
|
|
278
341
|
}
|
|
279
|
-
forceUpdateDevices(delay =
|
|
342
|
+
forceUpdateDevices(delay = this.configManager.getForceUpdateDelayMs()) {
|
|
343
|
+
if (this.isShuttingDown) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
// With a healthy WebSocket the change is pushed to us; a follow-up GET would only cost quota.
|
|
347
|
+
if (this.isWebSocketConnected()) {
|
|
348
|
+
this.log.debug('[API Syncing] WebSocket connected, skipping forced update (waiting for push update)');
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
280
351
|
// Trailing debounce: reset the timer on every change so a burst of rapid
|
|
281
352
|
// SETs (e.g. toggling power, fan speed and mode in quick succession)
|
|
282
353
|
// collapses into a single poll fired `delay` ms after the *last* change,
|
|
@@ -289,29 +360,110 @@ class DaikinCloudPlatform {
|
|
|
289
360
|
this.log.debug(`[API Syncing] Force update devices data (delayed by ${delay}ms)`);
|
|
290
361
|
// Pause periodic polling while we wait for the change to settle; it is
|
|
291
362
|
// restarted once the debounced update fires.
|
|
292
|
-
|
|
363
|
+
this.stopPolling();
|
|
293
364
|
}
|
|
294
|
-
this.forceUpdateTimeout = setTimeout(
|
|
365
|
+
this.forceUpdateTimeout = setTimeout(() => {
|
|
295
366
|
this.forceUpdateTimeout = undefined;
|
|
296
|
-
|
|
297
|
-
await this.updateDevices();
|
|
298
|
-
}
|
|
299
|
-
catch (error) {
|
|
300
|
-
this.log.error(`[API Syncing] Force update failed: ${error.message}`);
|
|
301
|
-
}
|
|
302
|
-
this.startUpdateDevicesInterval();
|
|
367
|
+
void this.pollNow();
|
|
303
368
|
}, delay);
|
|
304
369
|
}
|
|
370
|
+
/**
|
|
371
|
+
* (Re)schedule the next periodic poll. Always replaces any previously
|
|
372
|
+
* scheduled poll, so there is never more than one pending.
|
|
373
|
+
*/
|
|
305
374
|
startUpdateDevicesInterval() {
|
|
306
|
-
this.
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
375
|
+
this.stopPolling();
|
|
376
|
+
// Shutting down, or a pending forced update will restart polling when it fires
|
|
377
|
+
if (this.isShuttingDown || this.forceUpdateTimeout) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
const delay = this.getPollInterval();
|
|
381
|
+
this.log.debug(`[API Syncing] Next update of devices data in ${Math.round(delay / constants_1.ONE_MINUTE_MS)} minutes`);
|
|
382
|
+
this.pollTimer = setTimeout(() => {
|
|
383
|
+
this.pollTimer = undefined;
|
|
384
|
+
void this.pollNow();
|
|
385
|
+
}, delay);
|
|
386
|
+
}
|
|
387
|
+
stopPolling() {
|
|
388
|
+
clearTimeout(this.pollTimer);
|
|
389
|
+
this.pollTimer = undefined;
|
|
390
|
+
}
|
|
391
|
+
getPollInterval() {
|
|
392
|
+
return computePollInterval({
|
|
393
|
+
configuredMs: this.updateIntervalDelay,
|
|
394
|
+
webSocketConnected: this.isWebSocketConnected(),
|
|
395
|
+
consecutiveFailures: this.consecutivePollFailures,
|
|
396
|
+
remainingDay: this.remainingDay,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
isWebSocketConnected() {
|
|
400
|
+
return this.controller?.isWebSocketConnected() === true;
|
|
401
|
+
}
|
|
402
|
+
onWebSocketConnected() {
|
|
403
|
+
this.log.info('[WebSocket] Connected - receiving real-time updates');
|
|
404
|
+
const isReconnect = this.webSocketHasConnected;
|
|
405
|
+
this.webSocketHasConnected = true;
|
|
406
|
+
if (!this.pollingStarted || this.isShuttingDown) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (isReconnect) {
|
|
410
|
+
// Catch up on anything that changed while the socket was down
|
|
411
|
+
void this.pollNow();
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
// Push updates are flowing: stretch polling to the safety interval
|
|
415
|
+
this.startUpdateDevicesInterval();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
onRateLimitStatus(rateLimitStatus) {
|
|
419
|
+
const { remainingDay, limitDay } = rateLimitStatus;
|
|
420
|
+
if (remainingDay !== undefined) {
|
|
421
|
+
this.remainingDay = remainingDay;
|
|
422
|
+
if (remainingDay <= constants_1.RATE_LIMIT_WARNING_THRESHOLD) {
|
|
423
|
+
this.log.warn(`[Rate Limit] Rate limit almost reached, you only have ${remainingDay} calls left today`);
|
|
313
424
|
}
|
|
314
|
-
}
|
|
425
|
+
}
|
|
426
|
+
// Only show minute limits if available (Developer Portal mode)
|
|
427
|
+
const minuteInfo = rateLimitStatus.limitMinute !== undefined
|
|
428
|
+
? ` -- this minute: ${rateLimitStatus.remainingMinute}/${rateLimitStatus.limitMinute}`
|
|
429
|
+
: '';
|
|
430
|
+
this.log.debug(`[Rate Limit] Remaining calls today: ${remainingDay}/${limitDay}${minuteInfo}`);
|
|
431
|
+
this.saveRateLimitStatus(rateLimitStatus);
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Persist the latest rate-limit headers for the settings UI (throttled,
|
|
435
|
+
* best effort: failures are logged at debug level and never thrown).
|
|
436
|
+
*/
|
|
437
|
+
saveRateLimitStatus(rateLimitStatus) {
|
|
438
|
+
this.pendingRateLimitStatus = {
|
|
439
|
+
...rateLimitStatus,
|
|
440
|
+
mode: this.authMode,
|
|
441
|
+
updatedAt: new Date().toISOString(),
|
|
442
|
+
};
|
|
443
|
+
if (this.rateLimitWriteTimer || this.isShuttingDown) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const wait = Math.max(0, this.lastRateLimitWriteAt + RATE_LIMIT_FILE_MIN_WRITE_INTERVAL_MS - Date.now());
|
|
447
|
+
this.rateLimitWriteTimer = setTimeout(() => {
|
|
448
|
+
this.rateLimitWriteTimer = undefined;
|
|
449
|
+
this.flushRateLimitStatus();
|
|
450
|
+
}, wait);
|
|
451
|
+
}
|
|
452
|
+
flushRateLimitStatus() {
|
|
453
|
+
const status = this.pendingRateLimitStatus;
|
|
454
|
+
if (!status) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
this.pendingRateLimitStatus = undefined;
|
|
458
|
+
this.lastRateLimitWriteAt = Date.now();
|
|
459
|
+
const filePath = (0, node_path_1.resolve)(this.storagePath, constants_1.RATE_LIMIT_STATUS_FILE);
|
|
460
|
+
const tmpPath = `${filePath}.tmp`;
|
|
461
|
+
// Write-then-rename so the UI never reads a half-written file
|
|
462
|
+
node_fs_1.default.promises.writeFile(tmpPath, JSON.stringify(status, null, 2))
|
|
463
|
+
.then(() => node_fs_1.default.promises.rename(tmpPath, filePath))
|
|
464
|
+
.catch((error) => {
|
|
465
|
+
this.log.debug(`[Rate Limit] Could not write ${constants_1.RATE_LIMIT_STATUS_FILE}: ${(0, errors_1.toMessage)(error)}`);
|
|
466
|
+
});
|
|
315
467
|
}
|
|
316
468
|
async enableWebSocket() {
|
|
317
469
|
if (!this.controller) {
|
|
@@ -322,26 +474,10 @@ class DaikinCloudPlatform {
|
|
|
322
474
|
await this.controller.enableWebSocket();
|
|
323
475
|
}
|
|
324
476
|
catch (error) {
|
|
325
|
-
this.log.warn(`[WebSocket] Failed to enable: ${error
|
|
477
|
+
this.log.warn(`[WebSocket] Failed to enable: ${(0, errors_1.toMessage)(error)}`);
|
|
326
478
|
this.log.warn('[WebSocket] Falling back to polling-only mode');
|
|
327
479
|
}
|
|
328
480
|
}
|
|
329
|
-
isExcludedDevice(excludedDevicesByDeviceId, deviceId) {
|
|
330
|
-
return Array.isArray(excludedDevicesByDeviceId) && excludedDevicesByDeviceId.includes(deviceId);
|
|
331
|
-
}
|
|
332
|
-
getPrivacyFriendlyConfig(config) {
|
|
333
|
-
return {
|
|
334
|
-
...config,
|
|
335
|
-
clientId: strings_1.StringUtils.mask(config.clientId),
|
|
336
|
-
clientSecret: strings_1.StringUtils.mask(config.clientSecret),
|
|
337
|
-
daikinEmail: strings_1.StringUtils.mask(config.daikinEmail),
|
|
338
|
-
daikinPassword: config.daikinPassword ? '***' : undefined,
|
|
339
|
-
// Legacy aliases, masked too so they never leak into debug logs
|
|
340
|
-
email: strings_1.StringUtils.mask(config.email),
|
|
341
|
-
password: config.password ? '***' : undefined,
|
|
342
|
-
excludedDevicesByDeviceId: config.excludedDevicesByDeviceId ? config.excludedDevicesByDeviceId.map((deviceId) => strings_1.StringUtils.mask(deviceId)) : [],
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
481
|
onInvalidGrantError(tokenFilePath) {
|
|
346
482
|
this.log.warn('[API Syncing] TokenSet is invalid, removing TokenSet file');
|
|
347
483
|
try {
|
|
@@ -352,22 +488,6 @@ class DaikinCloudPlatform {
|
|
|
352
488
|
this.log.error('[API Syncing] TokenSet file could not be removed. Location:', tokenFilePath, e);
|
|
353
489
|
}
|
|
354
490
|
}
|
|
355
|
-
/**
|
|
356
|
-
* Handle WebSocket device updates by pushing updated values to HomeKit
|
|
357
|
-
*/
|
|
358
|
-
handleWebSocketDeviceUpdate(update) {
|
|
359
|
-
// Find the accessory for this device
|
|
360
|
-
const accessory = this.accessories.find(a => a.context.device.getId() === update.deviceId);
|
|
361
|
-
if (!accessory) {
|
|
362
|
-
this.log.debug(`[WebSocket] No accessory found for device ${update.deviceId}`);
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
// Use the UpdateMapper to apply the update
|
|
366
|
-
const result = this.updateMapper.applyUpdate(accessory, update);
|
|
367
|
-
if (result.success) {
|
|
368
|
-
this.log.debug(`[WebSocket] Updated ${result.updated.join(', ')}`);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
491
|
}
|
|
372
492
|
exports.DaikinCloudPlatform = DaikinCloudPlatform;
|
|
373
493
|
//# sourceMappingURL=platform.js.map
|