@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.
Files changed (211) hide show
  1. package/README.md +40 -27
  2. package/config.schema.json +66 -12
  3. package/dist/src/accessories/air-conditioning-accessory.js +1 -1
  4. package/dist/src/accessories/air-conditioning-accessory.js.map +1 -1
  5. package/dist/src/accessories/altherma-accessory.js +1 -1
  6. package/dist/src/accessories/altherma-accessory.js.map +1 -1
  7. package/dist/src/accessories/base-accessory.d.ts +5 -4
  8. package/dist/src/accessories/base-accessory.d.ts.map +1 -1
  9. package/dist/src/accessories/base-accessory.js +9 -10
  10. package/dist/src/accessories/base-accessory.js.map +1 -1
  11. package/dist/src/api/daikin-api.d.ts +22 -6
  12. package/dist/src/api/daikin-api.d.ts.map +1 -1
  13. package/dist/src/api/daikin-api.js +49 -23
  14. package/dist/src/api/daikin-api.js.map +1 -1
  15. package/dist/src/api/daikin-controller.d.ts +4 -48
  16. package/dist/src/api/daikin-controller.d.ts.map +1 -1
  17. package/dist/src/api/daikin-controller.js +14 -84
  18. package/dist/src/api/daikin-controller.js.map +1 -1
  19. package/dist/src/api/daikin-mobile-oauth.d.ts +11 -40
  20. package/dist/src/api/daikin-mobile-oauth.d.ts.map +1 -1
  21. package/dist/src/api/daikin-mobile-oauth.js +36 -160
  22. package/dist/src/api/daikin-mobile-oauth.js.map +1 -1
  23. package/dist/src/api/daikin-oauth.d.ts +10 -37
  24. package/dist/src/api/daikin-oauth.d.ts.map +1 -1
  25. package/dist/src/api/daikin-oauth.js +36 -166
  26. package/dist/src/api/daikin-oauth.js.map +1 -1
  27. package/dist/src/api/daikin-schemas.d.ts +20 -22
  28. package/dist/src/api/daikin-schemas.d.ts.map +1 -1
  29. package/dist/src/api/daikin-schemas.js +17 -49
  30. package/dist/src/api/daikin-schemas.js.map +1 -1
  31. package/dist/src/api/daikin-types.d.ts +11 -23
  32. package/dist/src/api/daikin-types.d.ts.map +1 -1
  33. package/dist/src/api/daikin-types.js.map +1 -1
  34. package/dist/src/api/daikin-websocket.d.ts +4 -5
  35. package/dist/src/api/daikin-websocket.d.ts.map +1 -1
  36. package/dist/src/api/daikin-websocket.js +62 -34
  37. package/dist/src/api/daikin-websocket.js.map +1 -1
  38. package/dist/src/api/http-defaults.d.ts +3 -2
  39. package/dist/src/api/http-defaults.d.ts.map +1 -1
  40. package/dist/src/api/http-defaults.js.map +1 -1
  41. package/dist/src/api/http-transport.d.ts +14 -2
  42. package/dist/src/api/http-transport.d.ts.map +1 -1
  43. package/dist/src/api/http-transport.js +32 -9
  44. package/dist/src/api/http-transport.js.map +1 -1
  45. package/dist/src/api/token-client.d.ts +82 -0
  46. package/dist/src/api/token-client.d.ts.map +1 -0
  47. package/dist/src/api/token-client.js +188 -0
  48. package/dist/src/api/token-client.js.map +1 -0
  49. package/dist/src/api/token-storage.d.ts +6 -0
  50. package/dist/src/api/token-storage.d.ts.map +1 -1
  51. package/dist/src/api/token-storage.js +27 -1
  52. package/dist/src/api/token-storage.js.map +1 -1
  53. package/dist/src/config/config-manager.d.ts +69 -125
  54. package/dist/src/config/config-manager.d.ts.map +1 -1
  55. package/dist/src/config/config-manager.js +288 -225
  56. package/dist/src/config/config-manager.js.map +1 -1
  57. package/dist/src/constants/api.constants.d.ts +6 -0
  58. package/dist/src/constants/api.constants.d.ts.map +1 -1
  59. package/dist/src/constants/api.constants.js +7 -1
  60. package/dist/src/constants/api.constants.js.map +1 -1
  61. package/dist/src/constants/auth.constants.d.ts +11 -0
  62. package/dist/src/constants/auth.constants.d.ts.map +1 -1
  63. package/dist/src/constants/auth.constants.js +12 -1
  64. package/dist/src/constants/auth.constants.js.map +1 -1
  65. package/dist/src/constants/device.constants.d.ts +1 -7
  66. package/dist/src/constants/device.constants.d.ts.map +1 -1
  67. package/dist/src/constants/device.constants.js +2 -11
  68. package/dist/src/constants/device.constants.js.map +1 -1
  69. package/dist/src/constants/time.constants.d.ts +0 -2
  70. package/dist/src/constants/time.constants.d.ts.map +1 -1
  71. package/dist/src/constants/time.constants.js +1 -3
  72. package/dist/src/constants/time.constants.js.map +1 -1
  73. package/dist/src/device/accessory-factory.d.ts +0 -5
  74. package/dist/src/device/accessory-factory.d.ts.map +1 -1
  75. package/dist/src/device/accessory-factory.js +0 -7
  76. package/dist/src/device/accessory-factory.js.map +1 -1
  77. package/dist/src/device/capability-detector.d.ts +9 -1
  78. package/dist/src/device/capability-detector.d.ts.map +1 -1
  79. package/dist/src/device/capability-detector.js +32 -28
  80. package/dist/src/device/capability-detector.js.map +1 -1
  81. package/dist/src/device/capability-docs.d.ts.map +1 -1
  82. package/dist/src/device/capability-docs.js +3 -0
  83. package/dist/src/device/capability-docs.js.map +1 -1
  84. package/dist/src/device/data-paths.d.ts +18 -0
  85. package/dist/src/device/data-paths.d.ts.map +1 -0
  86. package/dist/src/device/data-paths.js +20 -0
  87. package/dist/src/device/data-paths.js.map +1 -0
  88. package/dist/src/device/profiles/device-profile.d.ts +0 -5
  89. package/dist/src/device/profiles/device-profile.d.ts.map +1 -1
  90. package/dist/src/device/profiles/device-profile.js +0 -21
  91. package/dist/src/device/profiles/device-profile.js.map +1 -1
  92. package/dist/src/features/base-feature.d.ts +53 -9
  93. package/dist/src/features/base-feature.d.ts.map +1 -1
  94. package/dist/src/features/base-feature.js +93 -40
  95. package/dist/src/features/base-feature.js.map +1 -1
  96. package/dist/src/features/feature-manager.d.ts +27 -15
  97. package/dist/src/features/feature-manager.d.ts.map +1 -1
  98. package/dist/src/features/feature-manager.js +27 -24
  99. package/dist/src/features/feature-manager.js.map +1 -1
  100. package/dist/src/features/index.d.ts +1 -0
  101. package/dist/src/features/index.d.ts.map +1 -1
  102. package/dist/src/features/index.js +1 -0
  103. package/dist/src/features/index.js.map +1 -1
  104. package/dist/src/features/modes/auto-fan-mode.feature.d.ts +16 -10
  105. package/dist/src/features/modes/auto-fan-mode.feature.d.ts.map +1 -1
  106. package/dist/src/features/modes/auto-fan-mode.feature.js +16 -40
  107. package/dist/src/features/modes/auto-fan-mode.feature.js.map +1 -1
  108. package/dist/src/features/modes/dry-operation-mode.feature.d.ts +12 -9
  109. package/dist/src/features/modes/dry-operation-mode.feature.d.ts.map +1 -1
  110. package/dist/src/features/modes/dry-operation-mode.feature.js +11 -30
  111. package/dist/src/features/modes/dry-operation-mode.feature.js.map +1 -1
  112. package/dist/src/features/modes/econo-mode.feature.d.ts +12 -9
  113. package/dist/src/features/modes/econo-mode.feature.d.ts.map +1 -1
  114. package/dist/src/features/modes/econo-mode.feature.js +11 -31
  115. package/dist/src/features/modes/econo-mode.feature.js.map +1 -1
  116. package/dist/src/features/modes/fan-only-operation-mode.feature.d.ts +12 -9
  117. package/dist/src/features/modes/fan-only-operation-mode.feature.d.ts.map +1 -1
  118. package/dist/src/features/modes/fan-only-operation-mode.feature.js +11 -30
  119. package/dist/src/features/modes/fan-only-operation-mode.feature.js.map +1 -1
  120. package/dist/src/features/modes/firmware-update.feature.d.ts +7 -6
  121. package/dist/src/features/modes/firmware-update.feature.d.ts.map +1 -1
  122. package/dist/src/features/modes/firmware-update.feature.js +15 -22
  123. package/dist/src/features/modes/firmware-update.feature.js.map +1 -1
  124. package/dist/src/features/modes/holiday-mode.feature.d.ts +2 -1
  125. package/dist/src/features/modes/holiday-mode.feature.d.ts.map +1 -1
  126. package/dist/src/features/modes/holiday-mode.feature.js +5 -15
  127. package/dist/src/features/modes/holiday-mode.feature.js.map +1 -1
  128. package/dist/src/features/modes/indoor-silent-mode.feature.d.ts +15 -12
  129. package/dist/src/features/modes/indoor-silent-mode.feature.d.ts.map +1 -1
  130. package/dist/src/features/modes/indoor-silent-mode.feature.js +15 -39
  131. package/dist/src/features/modes/indoor-silent-mode.feature.js.map +1 -1
  132. package/dist/src/features/modes/oscillation-mode.feature.d.ts +5 -6
  133. package/dist/src/features/modes/oscillation-mode.feature.d.ts.map +1 -1
  134. package/dist/src/features/modes/oscillation-mode.feature.js +14 -36
  135. package/dist/src/features/modes/oscillation-mode.feature.js.map +1 -1
  136. package/dist/src/features/modes/outdoor-silent-mode.feature.d.ts +12 -9
  137. package/dist/src/features/modes/outdoor-silent-mode.feature.d.ts.map +1 -1
  138. package/dist/src/features/modes/outdoor-silent-mode.feature.js +11 -31
  139. package/dist/src/features/modes/outdoor-silent-mode.feature.js.map +1 -1
  140. package/dist/src/features/modes/powerful-mode.feature.d.ts +14 -10
  141. package/dist/src/features/modes/powerful-mode.feature.d.ts.map +1 -1
  142. package/dist/src/features/modes/powerful-mode.feature.js +13 -32
  143. package/dist/src/features/modes/powerful-mode.feature.js.map +1 -1
  144. package/dist/src/features/modes/streamer-mode.feature.d.ts +12 -9
  145. package/dist/src/features/modes/streamer-mode.feature.d.ts.map +1 -1
  146. package/dist/src/features/modes/streamer-mode.feature.js +11 -31
  147. package/dist/src/features/modes/streamer-mode.feature.js.map +1 -1
  148. package/dist/src/features/on-off-data-point-feature.d.ts +43 -0
  149. package/dist/src/features/on-off-data-point-feature.d.ts.map +1 -0
  150. package/dist/src/features/on-off-data-point-feature.js +49 -0
  151. package/dist/src/features/on-off-data-point-feature.js.map +1 -0
  152. package/dist/src/platform.d.ts +56 -9
  153. package/dist/src/platform.d.ts.map +1 -1
  154. package/dist/src/platform.js +248 -128
  155. package/dist/src/platform.js.map +1 -1
  156. package/dist/src/services/climate-control/fan-speed.d.ts +45 -0
  157. package/dist/src/services/climate-control/fan-speed.d.ts.map +1 -0
  158. package/dist/src/services/climate-control/fan-speed.js +104 -0
  159. package/dist/src/services/climate-control/fan-speed.js.map +1 -0
  160. package/dist/src/services/climate-control/separate-fan.service.d.ts +45 -0
  161. package/dist/src/services/climate-control/separate-fan.service.d.ts.map +1 -0
  162. package/dist/src/services/climate-control/separate-fan.service.js +82 -0
  163. package/dist/src/services/climate-control/separate-fan.service.js.map +1 -0
  164. package/dist/src/services/climate-control/setpoint-resolver.d.ts +68 -0
  165. package/dist/src/services/climate-control/setpoint-resolver.d.ts.map +1 -0
  166. package/dist/src/services/climate-control/setpoint-resolver.js +170 -0
  167. package/dist/src/services/climate-control/setpoint-resolver.js.map +1 -0
  168. package/dist/src/services/climate-control/swing-controller.d.ts +28 -0
  169. package/dist/src/services/climate-control/swing-controller.d.ts.map +1 -0
  170. package/dist/src/services/climate-control/swing-controller.js +65 -0
  171. package/dist/src/services/climate-control/swing-controller.js.map +1 -0
  172. package/dist/src/services/climate-control.service.d.ts +39 -48
  173. package/dist/src/services/climate-control.service.d.ts.map +1 -1
  174. package/dist/src/services/climate-control.service.js +223 -554
  175. package/dist/src/services/climate-control.service.js.map +1 -1
  176. package/dist/src/services/hot-water-tank.service.d.ts +17 -8
  177. package/dist/src/services/hot-water-tank.service.d.ts.map +1 -1
  178. package/dist/src/services/hot-water-tank.service.js +102 -157
  179. package/dist/src/services/hot-water-tank.service.js.map +1 -1
  180. package/dist/src/types/daikin-enums.d.ts +0 -5
  181. package/dist/src/types/daikin-enums.d.ts.map +1 -1
  182. package/dist/src/types/daikin-enums.js +1 -7
  183. package/dist/src/types/daikin-enums.js.map +1 -1
  184. package/dist/src/types/device-capabilities.d.ts +1 -0
  185. package/dist/src/types/device-capabilities.d.ts.map +1 -1
  186. package/dist/src/utils/errors.d.ts +9 -0
  187. package/dist/src/utils/errors.d.ts.map +1 -0
  188. package/dist/src/utils/errors.js +25 -0
  189. package/dist/src/utils/errors.js.map +1 -0
  190. package/dist/src/utils/hap-write.d.ts +22 -0
  191. package/dist/src/utils/hap-write.d.ts.map +1 -0
  192. package/dist/src/utils/hap-write.js +33 -0
  193. package/dist/src/utils/hap-write.js.map +1 -0
  194. package/dist/src/utils/sleep.d.ts +5 -0
  195. package/dist/src/utils/sleep.d.ts.map +1 -0
  196. package/dist/src/utils/sleep.js +10 -0
  197. package/dist/src/utils/sleep.js.map +1 -0
  198. package/homebridge-ui/README.md +25 -2
  199. package/homebridge-ui/public/index.html +19 -17
  200. package/homebridge-ui/public/script.js +95 -32
  201. package/homebridge-ui/public/styles.css +109 -0
  202. package/homebridge-ui/server.js +321 -125
  203. package/package.json +2 -3
  204. package/dist/src/utils/log-context.d.ts +0 -105
  205. package/dist/src/utils/log-context.d.ts.map +0 -1
  206. package/dist/src/utils/log-context.js +0 -206
  207. package/dist/src/utils/log-context.js.map +0 -1
  208. package/dist/src/utils/update-mapper.d.ts +0 -59
  209. package/dist/src/utils/update-mapper.d.ts.map +0 -1
  210. package/dist/src/utils/update-mapper.js +0 -192
  211. package/dist/src/utils/update-mapper.js.map +0 -1
@@ -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 updateInterval;
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;AASjI,OAAO,KAAK,EAAE,iBAAiB,EAA0B,MAAM,OAAO,CAAC;AACvE,OAAO,EAAmB,qBAAqB,EAAE,MAAM,OAAO,CAAC;AAa/D,MAAM,MAAM,2BAA2B,GAAG;IACtC,MAAM,EAAE,iBAAiB,CAAC;CAC7B,CAAC;AAEF,qBAAa,mBAAoB,YAAW,qBAAqB;aAkBzC,GAAG,EAAE,MAAM;aACX,MAAM,EAAE,cAAc;aACtB,GAAG,EAAE,GAAG;IAnB9B,SAAgB,OAAO,EAAE,OAAO,OAAO,CAAC;IACxC,SAAgB,cAAc,EAAE,OAAO,cAAc,CAAC;IAEtD,SAAgB,WAAW,EAAE,iBAAiB,CAAC,2BAA2B,CAAC,EAAE,CAAM;IAEnF,SAAgB,WAAW,EAAE,MAAM,CAAM;IAClC,UAAU,EAAE,qBAAqB,GAAG,SAAS,CAAC;IAErD,SAAgB,mBAAmB,EAAE,MAAM,CAAC;IAC5C,OAAO,CAAC,cAAc,CAA6B;IACnD,OAAO,CAAC,kBAAkB,CAA6B;IACvD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAmB;IACpD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoC;IAC7D,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAiC;gBAG3C,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,GAAG;IAmLvB,kBAAkB,CAAC,SAAS,EAAE,iBAAiB;YAKxC,eAAe;IAgB7B,OAAO,CAAC,aAAa;IAsDrB,OAAO,CAAC,oBAAoB;IAQ5B,sBAAsB,CAAC,SAAS,EAAE,iBAAiB,CAAC,2BAA2B,CAAC,EAAE,QAAQ,EAAE,MAAM,IAAI;YAIxF,aAAa;IAW3B,kBAAkB,CAAC,KAAK,GAAE,MAAmF;IA0B7G,OAAO,CAAC,0BAA0B;YAWpB,eAAe;IAc7B,OAAO,CAAC,gBAAgB;IAIxB,OAAO,CAAC,wBAAwB;IAchC,OAAO,CAAC,mBAAmB;IAU3B;;SAEK;IACL,OAAO,CAAC,2BAA2B;CAuBpC"}
1
+ {"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../../src/platform.ts"],"names":[],"mappings":"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"}
@@ -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
- updateInterval;
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:', this.config.name);
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 = constants_1.ONE_MINUTE_MS * (this.config.updateIntervalInMinutes || constants_1.DEFAULT_UPDATE_INTERVAL_MINUTES);
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 = this.config.authMode === 'mobile_app' ? 'mobile_app' : 'developer_portal';
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)(this.config.httpTransport);
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 (!this.config.clientId || !this.config.clientSecret) {
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 tokenFileName = this.authMode === 'mobile_app'
81
- ? '.daikin-mobile-tokenset'
82
- : '.daikin-controller-cloud-tokenset';
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.message}`);
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.config.enableWebSocket !== false) {
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
- clearInterval(this.updateInterval);
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, JSON.stringify(api_1.DaikinCloudRepo.maskSensitiveCloudDeviceData(device.desc), null, 4));
221
- if (this.isExcludedDevice(this.config.excludedDevicesByDeviceId, deviceId)) {
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:', JSON.stringify(api_1.DaikinCloudRepo.maskSensitiveCloudDeviceData(device.desc), null, 2));
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
- async updateDevices() {
269
- if (!this.controller) {
270
- return;
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
- try {
273
- await this.controller.updateAllDeviceData();
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
- catch (error) {
276
- this.log.error(`[API Syncing] Failed to update devices data: ${JSON.stringify(error)}`);
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 = Math.max(0, this.config.forceUpdateDelay || constants_1.DEFAULT_FORCE_UPDATE_DELAY_MS)) {
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
- clearInterval(this.updateInterval);
363
+ this.stopPolling();
293
364
  }
294
- this.forceUpdateTimeout = setTimeout(async () => {
365
+ this.forceUpdateTimeout = setTimeout(() => {
295
366
  this.forceUpdateTimeout = undefined;
296
- try {
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.log.debug(`[API Syncing] (Re)starting update devices interval every ${this.updateIntervalDelay / constants_1.ONE_MINUTE_MS} minutes`);
307
- this.updateInterval = setInterval(async () => {
308
- try {
309
- await this.updateDevices();
310
- }
311
- catch (error) {
312
- this.log.error(`[API Syncing] Periodic update failed: ${error.message}`);
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
- }, this.updateIntervalDelay);
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.message}`);
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