matterbridge-lg-thinq-plugin 0.1.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 (210) hide show
  1. package/.claude/CLAUDE.md +43 -0
  2. package/.claude/agents/compiler.md +91 -0
  3. package/.claude/agents/direct-executor.md +98 -0
  4. package/.claude/agents/documenter.md +78 -0
  5. package/.claude/agents/finalizer.md +233 -0
  6. package/.claude/agents/implementer.md +112 -0
  7. package/.claude/agents/investigator.md +100 -0
  8. package/.claude/agents/release-manager.md +203 -0
  9. package/.claude/agents/reviewer.md +131 -0
  10. package/.claude/agents/technical-architect.md +403 -0
  11. package/.claude/agents/test-writer.md +159 -0
  12. package/.claude/agents/wiki-manager.md +147 -0
  13. package/.claude/instructions/agent-prompts.md +90 -0
  14. package/.claude/instructions/shared-rules.md +11 -0
  15. package/.claude/instructions/team-orchestrator-policy.md +165 -0
  16. package/.claude/memory.md +186 -0
  17. package/.claude/output-styles/engineer-manager.md +38 -0
  18. package/.claude/settings.json +40 -0
  19. package/.claude/settings.local.json +30 -0
  20. package/.claude/skills/load-policy/SKILL.md +13 -0
  21. package/.claude/skills/ref-idea/SKILL.md +44 -0
  22. package/.claude/skills/status-of/SKILL.md +38 -0
  23. package/.claude/skills/workspace-console/SKILL.md +49 -0
  24. package/.claude/statusline-command.sh +130 -0
  25. package/.claude/statusline.md +53 -0
  26. package/.claude/templates/approval-packet.template.html +171 -0
  27. package/.claude/templates/reference-workspaces.md +16 -0
  28. package/.claude/tools/cli-runner-server.cjs +220 -0
  29. package/.claude/tools/discord-send-server.cjs +188 -0
  30. package/.claude/tools/github-release-server.cjs +181 -0
  31. package/.claude/tools/glob-grep-server.cjs +267 -0
  32. package/.claude/tools/read-log-server.cjs +175 -0
  33. package/.github/codeql/codeql-config.yml +14 -0
  34. package/.github/workflows/build.yml +54 -0
  35. package/.github/workflows/codeql.yml +59 -0
  36. package/.github/workflows/coverage.yml +70 -0
  37. package/.github/workflows/publish.yml +103 -0
  38. package/.mcp.json +34 -0
  39. package/CHANGELOG.md +195 -0
  40. package/LICENSE +21 -0
  41. package/dist/cli/commands/devices.js +52 -0
  42. package/dist/cli/commands/energy.js +122 -0
  43. package/dist/cli/commands/login.js +54 -0
  44. package/dist/cli/help.js +50 -0
  45. package/dist/cli/main.js +56 -0
  46. package/dist/cli/session.js +13 -0
  47. package/dist/cli/types.js +1 -0
  48. package/dist/cli/utils.js +29 -0
  49. package/dist/cli.js +2 -0
  50. package/dist/constants/index.js +1 -0
  51. package/dist/core/domain/entities/ThinqDevice.js +43 -0
  52. package/dist/core/domain/value-objects/AirConditionerCapabilities.js +22 -0
  53. package/dist/core/domain/value-objects/ThinqFilterState.js +35 -0
  54. package/dist/core/domain/value-objects/ThinqSnapshot.js +125 -0
  55. package/dist/errors/AuthenticationError.js +26 -0
  56. package/dist/errors/BaseError.js +23 -0
  57. package/dist/errors/index.js +2 -0
  58. package/dist/model/LgThinkqPluginPlatformConfig.js +23 -0
  59. package/dist/model/LgThinqPluginPlatformConfig.js +23 -0
  60. package/dist/module.js +212 -0
  61. package/dist/platform/deviceRegistry.js +26 -0
  62. package/dist/platform/platformConfigManager.js +89 -0
  63. package/dist/platform/platformState.js +9 -0
  64. package/dist/platform/thinq/thinqAirConditionerCommandHandlers.js +190 -0
  65. package/dist/platform/thinq/thinqAirConditionerEndpointFactory.js +53 -0
  66. package/dist/platform/thinq/thinqAirConditionerFilterResetCommandHandler.js +54 -0
  67. package/dist/platform/thinq/thinqAirConditionerFilterStateSync.js +19 -0
  68. package/dist/platform/thinq/thinqAirConditionerStateSync.js +205 -0
  69. package/dist/platform/thinq/thinqDeviceConfigReconciler.js +26 -0
  70. package/dist/platform/thinq/thinqDeviceConfigurator.js +124 -0
  71. package/dist/platform/thinq/thinqDeviceDiscovery.js +33 -0
  72. package/dist/platform/thinq/thinqWasherCommandHandlers.js +54 -0
  73. package/dist/platform/thinq/thinqWasherEndpointFactory.js +6 -0
  74. package/dist/platform/thinq/thinqWasherStateSync.js +31 -0
  75. package/dist/platform/thinq/thinqWasherStopCommandResolver.js +38 -0
  76. package/dist/services/authentication/AccountAuthStrategy.js +135 -0
  77. package/dist/services/authentication/AuthContext.js +1 -0
  78. package/dist/services/authentication/AuthenticationCoordinator.js +29 -0
  79. package/dist/services/authentication/IAuthStrategy.js +1 -0
  80. package/dist/services/authentication/TokenAuthStrategy.js +27 -0
  81. package/dist/services/authentication/UserDataRepository.js +32 -0
  82. package/dist/services/thinq/gateway.js +61 -0
  83. package/dist/services/thinq/mqtt/mqttCertificate.js +63 -0
  84. package/dist/services/thinq/mqtt/mqttConnectionEvents.js +37 -0
  85. package/dist/services/thinq/mqtt/mqttKeyRepository.js +33 -0
  86. package/dist/services/thinq/mqtt/mqttRetry.js +25 -0
  87. package/dist/services/thinq/mqtt/thinqMqttListener.js +97 -0
  88. package/dist/services/thinq/serviceContainer.js +73 -0
  89. package/dist/services/thinq/session.js +48 -0
  90. package/dist/services/thinq/thinqApiClient.js +258 -0
  91. package/dist/services/thinq/thinqDeviceService.js +75 -0
  92. package/dist/services/thinq/thinqFilterMonitoringService.js +40 -0
  93. package/dist/settings.js +9 -0
  94. package/dist/tests/helpers/testUtils.js +87 -0
  95. package/dist/utils/mask.js +39 -0
  96. package/docs/README_CLI.md +166 -0
  97. package/eslint.config.js +194 -0
  98. package/matterbridge-lg-thinq-plugin.config.json +27 -0
  99. package/matterbridge-lg-thinq-plugin.schema.json +520 -0
  100. package/package.json +107 -0
  101. package/prettier.config.js +49 -0
  102. package/scripts/parse-junit-failures.mjs +45 -0
  103. package/scripts/parse-tsc-failures.mjs +48 -0
  104. package/scripts/run-build-local-summary.mjs +29 -0
  105. package/scripts/run-format-summary.mjs +36 -0
  106. package/scripts/run-lint-fix-summary.mjs +38 -0
  107. package/scripts/run-precommit-summary.mjs +94 -0
  108. package/src/cli/commands/devices.ts +67 -0
  109. package/src/cli/commands/energy.ts +160 -0
  110. package/src/cli/commands/login.ts +74 -0
  111. package/src/cli/help.ts +53 -0
  112. package/src/cli/main.ts +66 -0
  113. package/src/cli/session.ts +15 -0
  114. package/src/cli/types.ts +10 -0
  115. package/src/cli/utils.ts +30 -0
  116. package/src/cli.ts +3 -0
  117. package/src/constants/index.ts +8 -0
  118. package/src/core/domain/entities/ThinqDevice.test.ts +224 -0
  119. package/src/core/domain/entities/ThinqDevice.ts +74 -0
  120. package/src/core/domain/value-objects/AirConditionerCapabilities.ts +50 -0
  121. package/src/core/domain/value-objects/ThinqFilterState.ts +50 -0
  122. package/src/core/domain/value-objects/ThinqSnapshot.ts +162 -0
  123. package/src/errors/AuthenticationError.ts +37 -0
  124. package/src/errors/BaseError.ts +38 -0
  125. package/src/errors/index.ts +24 -0
  126. package/src/model/LgThinqPluginPlatformConfig.ts +94 -0
  127. package/src/module.ts +298 -0
  128. package/src/platform/deviceRegistry.ts +58 -0
  129. package/src/platform/platformConfigManager.ts +133 -0
  130. package/src/platform/platformState.ts +16 -0
  131. package/src/platform/thinq/thinqAirConditionerCommandHandlers.ts +291 -0
  132. package/src/platform/thinq/thinqAirConditionerEndpointFactory.ts +145 -0
  133. package/src/platform/thinq/thinqAirConditionerFilterResetCommandHandler.ts +96 -0
  134. package/src/platform/thinq/thinqAirConditionerFilterStateSync.ts +42 -0
  135. package/src/platform/thinq/thinqAirConditionerStateSync.ts +343 -0
  136. package/src/platform/thinq/thinqDeviceConfigReconciler.ts +42 -0
  137. package/src/platform/thinq/thinqDeviceConfigurator.test.ts +90 -0
  138. package/src/platform/thinq/thinqDeviceConfigurator.ts +191 -0
  139. package/src/platform/thinq/thinqDeviceDiscovery.test.ts +173 -0
  140. package/src/platform/thinq/thinqDeviceDiscovery.ts +51 -0
  141. package/src/platform/thinq/thinqWasherCommandHandlers.ts +120 -0
  142. package/src/platform/thinq/thinqWasherEndpointFactory.ts +37 -0
  143. package/src/platform/thinq/thinqWasherStateSync.ts +60 -0
  144. package/src/platform/thinq/thinqWasherStopCommandResolver.ts +62 -0
  145. package/src/services/authentication/AccountAuthStrategy.test.ts +358 -0
  146. package/src/services/authentication/AccountAuthStrategy.ts +209 -0
  147. package/src/services/authentication/AuthContext.ts +19 -0
  148. package/src/services/authentication/AuthenticationCoordinator.test.ts +217 -0
  149. package/src/services/authentication/AuthenticationCoordinator.ts +59 -0
  150. package/src/services/authentication/IAuthStrategy.ts +10 -0
  151. package/src/services/authentication/TokenAuthStrategy.test.ts +167 -0
  152. package/src/services/authentication/TokenAuthStrategy.ts +40 -0
  153. package/src/services/authentication/UserDataRepository.test.ts +149 -0
  154. package/src/services/authentication/UserDataRepository.ts +46 -0
  155. package/src/services/thinq/gateway.ts +86 -0
  156. package/src/services/thinq/mqtt/mqttCertificate.test.ts +344 -0
  157. package/src/services/thinq/mqtt/mqttCertificate.ts +102 -0
  158. package/src/services/thinq/mqtt/mqttConnectionEvents.test.ts +276 -0
  159. package/src/services/thinq/mqtt/mqttConnectionEvents.ts +76 -0
  160. package/src/services/thinq/mqtt/mqttKeyRepository.test.ts +164 -0
  161. package/src/services/thinq/mqtt/mqttKeyRepository.ts +43 -0
  162. package/src/services/thinq/mqtt/mqttRetry.test.ts +239 -0
  163. package/src/services/thinq/mqtt/mqttRetry.ts +43 -0
  164. package/src/services/thinq/mqtt/thinqMqttListener.test.ts +349 -0
  165. package/src/services/thinq/mqtt/thinqMqttListener.ts +120 -0
  166. package/src/services/thinq/serviceContainer.test.ts +203 -0
  167. package/src/services/thinq/serviceContainer.ts +105 -0
  168. package/src/services/thinq/session.test.ts +287 -0
  169. package/src/services/thinq/session.ts +69 -0
  170. package/src/services/thinq/thinqApiClient.test.ts +702 -0
  171. package/src/services/thinq/thinqApiClient.ts +418 -0
  172. package/src/services/thinq/thinqDeviceService.test.ts +415 -0
  173. package/src/services/thinq/thinqDeviceService.ts +101 -0
  174. package/src/services/thinq/thinqFilterMonitoringService.test.ts +264 -0
  175. package/src/services/thinq/thinqFilterMonitoringService.ts +53 -0
  176. package/src/settings.ts +18 -0
  177. package/src/tests/cli/commands/devices.test.ts +293 -0
  178. package/src/tests/cli/commands/login.test.ts +240 -0
  179. package/src/tests/cli/help.test.ts +46 -0
  180. package/src/tests/cli/main.test.ts +214 -0
  181. package/src/tests/cli/session.test.ts +198 -0
  182. package/src/tests/cli/utils.test.ts +194 -0
  183. package/src/tests/core/domain/value-objects/AirConditionerCapabilities.test.ts +685 -0
  184. package/src/tests/core/domain/value-objects/ThinqFilterState.test.ts +210 -0
  185. package/src/tests/core/domain/value-objects/ThinqSnapshot.test.ts +1035 -0
  186. package/src/tests/helpers/testUtils.ts +118 -0
  187. package/src/tests/model/LgThinqPluginPlatformConfig.test.ts +81 -0
  188. package/src/tests/platform/deviceRegistry.test.ts +177 -0
  189. package/src/tests/platform/platformConfigManager.test.ts +790 -0
  190. package/src/tests/platform/platformState.test.ts +41 -0
  191. package/src/tests/platform/thinq/thinqAirConditionerCommandHandlers.test.ts +879 -0
  192. package/src/tests/platform/thinq/thinqAirConditionerEndpointFactory.test.ts +993 -0
  193. package/src/tests/platform/thinq/thinqAirConditionerFilterResetCommandHandler.test.ts +508 -0
  194. package/src/tests/platform/thinq/thinqAirConditionerFilterStateSync.test.ts +212 -0
  195. package/src/tests/platform/thinq/thinqAirConditionerStateSync.test.ts +1586 -0
  196. package/src/tests/platform/thinq/thinqDeviceConfigReconciler.test.ts +269 -0
  197. package/src/tests/platform/thinq/thinqDeviceConfigurator.test.ts +492 -0
  198. package/src/tests/platform/thinq/thinqWasherCommandHandlers.test.ts +594 -0
  199. package/src/tests/platform/thinq/thinqWasherEndpointFactory.test.ts +84 -0
  200. package/src/tests/platform/thinq/thinqWasherStateSync.test.ts +373 -0
  201. package/src/tests/platform/thinq/thinqWasherStopCommandResolver.test.ts +392 -0
  202. package/src/tests/settings.test.ts +33 -0
  203. package/src/utils/mask.ts +50 -0
  204. package/tsconfig.base.json +25 -0
  205. package/tsconfig.json +9 -0
  206. package/tsconfig.production.json +13 -0
  207. package/tsconfig.vitest.json +5 -0
  208. package/vite.config.ts +43 -0
  209. package/wiki/memory-archive.md +12 -0
  210. package/wiki/reference-workspaces.md +23 -0
@@ -0,0 +1,120 @@
1
+ import { device as awsIotDevice } from 'aws-iot-device-sdk';
2
+ import { AnsiLogger } from 'matterbridge/logger';
3
+
4
+ import { ThinqSnapshot } from '../../../core/domain/value-objects/ThinqSnapshot.js';
5
+ import { ThinqApiClient } from '../thinqApiClient.js';
6
+ import { ThinqDeviceUpdateListener } from '../thinqDeviceService.js';
7
+ import { certificateRequestBody, downloadRootCa, writeMqttCertificateFiles } from './mqttCertificate.js';
8
+ import { MqttReconnectScheduler, MqttRuntimeDevice, wireMqttDeviceEvents } from './mqttConnectionEvents.js';
9
+ import { MqttKeyRepository } from './mqttKeyRepository.js';
10
+ import { retryMqttRegistration } from './mqttRetry.js';
11
+
12
+ /**
13
+ * Orchestrates MQTT push updates for ThinQ2 AirConditioner devices, running concurrently alongside
14
+ * `ThinqDeviceService`'s polling loop. Never rejects from `start()` — graceful degradation is structural
15
+ * (bounded retries, then log-and-return), so polling can continue independently on total MQTT failure.
16
+ */
17
+ export class ThinqMqttListener {
18
+ private stopped = false;
19
+ private device: MqttRuntimeDevice | undefined;
20
+ private reconnectTimer: NodeJS.Timeout | undefined;
21
+
22
+ constructor(
23
+ private readonly apiClient: ThinqApiClient,
24
+ private readonly keyRepository: MqttKeyRepository,
25
+ private readonly mqttDir: string,
26
+ private readonly logger: AnsiLogger,
27
+ ) {}
28
+
29
+ public async start(onUpdate: ThinqDeviceUpdateListener): Promise<void> {
30
+ this.stop();
31
+ this.stopped = false;
32
+
33
+ const started = await retryMqttRegistration({
34
+ register: () => this.connect(onUpdate),
35
+ logger: this.logger,
36
+ });
37
+
38
+ if (!started) {
39
+ this.logger.error('ThinQ MQTT: unable to start after retries, continuing with polling only.');
40
+ }
41
+ }
42
+
43
+ public stop(): void {
44
+ this.stopped = true;
45
+ if (this.reconnectTimer) {
46
+ clearTimeout(this.reconnectTimer);
47
+ this.reconnectTimer = undefined;
48
+ }
49
+ this.device?.end();
50
+ this.device = undefined;
51
+ }
52
+
53
+ private async connect(onUpdate: ThinqDeviceUpdateListener): Promise<void> {
54
+ const route = await this.apiClient.getMqttRouteInfo();
55
+ const hostname = new URL(route.mqttServer).hostname;
56
+ const rootCA = await downloadRootCa(hostname);
57
+
58
+ const keys = await this.keyRepository.getOrCreateKeyPair();
59
+ const csr = await this.keyRepository.getOrCreateCsr(keys);
60
+
61
+ await this.apiClient.registerMqttClient();
62
+ const certificate = await this.apiClient.requestMqttCertificate(certificateRequestBody(csr));
63
+
64
+ const paths = await writeMqttCertificateFiles({
65
+ mqttDir: this.mqttDir,
66
+ rootCA,
67
+ privateKey: keys.privateKey,
68
+ certificatePem: certificate.certificatePem,
69
+ });
70
+
71
+ this.logger.debug(`ThinQ MQTT: connecting to ${route.mqttServer}`);
72
+ const device = new awsIotDevice({
73
+ ...paths,
74
+ clientId: this.apiClient.getClientId(),
75
+ host: hostname,
76
+ }) as unknown as MqttRuntimeDevice;
77
+ this.device = device;
78
+
79
+ const reconnect = async (): Promise<void> => {
80
+ if (this.stopped) {
81
+ return;
82
+ }
83
+ await this.connect(onUpdate);
84
+ };
85
+
86
+ const scheduleReconnect: MqttReconnectScheduler = (handler, delayMs) => {
87
+ if (this.stopped) {
88
+ return undefined;
89
+ }
90
+ this.reconnectTimer = setTimeout(handler, delayMs);
91
+ return this.reconnectTimer;
92
+ };
93
+
94
+ wireMqttDeviceEvents({
95
+ device,
96
+ logger: this.logger,
97
+ mqttServer: route.mqttServer,
98
+ subscriptions: certificate.subscriptions,
99
+ onMessage: (payload) => this.handleMessage(payload, onUpdate),
100
+ reconnect,
101
+ scheduleReconnect,
102
+ });
103
+ }
104
+
105
+ private handleMessage(payload: unknown, onUpdate: ThinqDeviceUpdateListener): void {
106
+ if (typeof payload !== 'object' || payload === null || !('deviceId' in payload) || !('data' in payload)) {
107
+ this.logger.debug('ThinQ MQTT: received malformed message, skipping.');
108
+ return;
109
+ }
110
+
111
+ const message = payload as { deviceId: unknown; data?: { state?: { reported?: unknown } } };
112
+ const reported = message.data?.state?.reported;
113
+ if (typeof message.deviceId !== 'string' || typeof reported !== 'object' || reported === null) {
114
+ this.logger.debug('ThinQ MQTT: received message with missing deviceId/reported state, skipping.');
115
+ return;
116
+ }
117
+
118
+ onUpdate(message.deviceId, new ThinqSnapshot(reported as Record<string, unknown>));
119
+ }
120
+ }
@@ -0,0 +1,203 @@
1
+ import type { LocalStorage } from 'node-persist';
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import type { PlatformConfigManager } from '../../platform/platformConfigManager.js';
5
+ import { asPartial, createMockLogger } from '../../tests/helpers/testUtils.js';
6
+ import { ThinqServiceContainer } from './serviceContainer.js';
7
+
8
+ describe('ThinqServiceContainer', () => {
9
+ let mockLogger: ReturnType<typeof createMockLogger>;
10
+ let mockPersist: LocalStorage;
11
+ let mockConfigManager: PlatformConfigManager;
12
+ let container: ThinqServiceContainer;
13
+
14
+ beforeEach(() => {
15
+ vi.clearAllMocks();
16
+ mockLogger = createMockLogger();
17
+ mockPersist = asPartial<LocalStorage>({
18
+ getItem: vi.fn(),
19
+ setItem: vi.fn(),
20
+ });
21
+ mockConfigManager = asPartial<PlatformConfigManager>({
22
+ country: 'US',
23
+ language: 'en',
24
+ });
25
+ container = new ThinqServiceContainer(mockLogger, mockPersist, mockConfigManager, '/tmp/mqtt');
26
+ });
27
+
28
+ afterEach(() => {
29
+ vi.clearAllMocks();
30
+ });
31
+
32
+ describe('apiClient', () => {
33
+ it('should return a ThinqApiClient instance', () => {
34
+ const apiClient = container.apiClient;
35
+
36
+ expect(apiClient).toBeDefined();
37
+ });
38
+
39
+ it('should return the same instance on repeated access', () => {
40
+ const apiClient1 = container.apiClient;
41
+ const apiClient2 = container.apiClient;
42
+
43
+ expect(apiClient1).toBe(apiClient2);
44
+ });
45
+ });
46
+
47
+ describe('getUserDataRepository', () => {
48
+ it('should return a UserDataRepository instance', () => {
49
+ const repo = container.getUserDataRepository();
50
+
51
+ expect(repo).toBeDefined();
52
+ });
53
+
54
+ it('should return the same instance on repeated calls (singleton)', () => {
55
+ const repo1 = container.getUserDataRepository();
56
+ const repo2 = container.getUserDataRepository();
57
+
58
+ expect(repo1).toBe(repo2);
59
+ });
60
+ });
61
+
62
+ describe('getAuthenticationCoordinator', () => {
63
+ it('should return an AuthenticationCoordinator instance', () => {
64
+ const coordinator = container.getAuthenticationCoordinator();
65
+
66
+ expect(coordinator).toBeDefined();
67
+ });
68
+
69
+ it('should return the same instance on repeated calls (singleton)', () => {
70
+ const coordinator1 = container.getAuthenticationCoordinator();
71
+ const coordinator2 = container.getAuthenticationCoordinator();
72
+
73
+ expect(coordinator1).toBe(coordinator2);
74
+ });
75
+
76
+ it('should construct coordinator with apiClient instance', () => {
77
+ const coordinator = container.getAuthenticationCoordinator();
78
+
79
+ expect(coordinator['apiClient']).toBe(container.apiClient);
80
+ });
81
+ });
82
+
83
+ describe('getDeviceService', () => {
84
+ it('should return a ThinqDeviceService instance', () => {
85
+ const service = container.getDeviceService();
86
+
87
+ expect(service).toBeDefined();
88
+ });
89
+
90
+ it('should return the same instance on repeated calls (singleton)', () => {
91
+ const service1 = container.getDeviceService();
92
+ const service2 = container.getDeviceService();
93
+
94
+ expect(service1).toBe(service2);
95
+ });
96
+ });
97
+
98
+ describe('getDeviceDiscovery', () => {
99
+ it('should return a ThinqDeviceDiscovery instance', () => {
100
+ const discovery = container.getDeviceDiscovery();
101
+
102
+ expect(discovery).toBeDefined();
103
+ });
104
+
105
+ it('should return the same instance on repeated calls (singleton)', () => {
106
+ const discovery1 = container.getDeviceDiscovery();
107
+ const discovery2 = container.getDeviceDiscovery();
108
+
109
+ expect(discovery1).toBe(discovery2);
110
+ });
111
+
112
+ it('should reuse the same ThinqDeviceService instance', () => {
113
+ const deviceService = container.getDeviceService();
114
+ const discovery = container.getDeviceDiscovery();
115
+
116
+ expect(discovery['deviceService']).toBe(deviceService);
117
+ });
118
+ });
119
+
120
+ describe('getDeviceConfigurator', () => {
121
+ it('should return a ThinqDeviceConfigurator instance', () => {
122
+ const configurator = container.getDeviceConfigurator();
123
+
124
+ expect(configurator).toBeDefined();
125
+ });
126
+
127
+ it('should return the same instance on repeated calls (singleton)', () => {
128
+ const configurator1 = container.getDeviceConfigurator();
129
+ const configurator2 = container.getDeviceConfigurator();
130
+
131
+ expect(configurator1).toBe(configurator2);
132
+ });
133
+ });
134
+
135
+ describe('getMqttListener', () => {
136
+ it('should return a ThinqMqttListener instance', () => {
137
+ const listener = container.getMqttListener();
138
+
139
+ expect(listener).toBeDefined();
140
+ expect(listener).toHaveProperty('start');
141
+ expect(listener).toHaveProperty('stop');
142
+ });
143
+
144
+ it('should return the same instance on repeated calls (singleton)', () => {
145
+ const listener1 = container.getMqttListener();
146
+ const listener2 = container.getMqttListener();
147
+
148
+ expect(listener1).toBe(listener2);
149
+ });
150
+
151
+ it('should pass the mqttCertDir to the listener instance', () => {
152
+ const mqttDir = '/custom/mqtt/dir';
153
+ const customContainer = new ThinqServiceContainer(mockLogger, mockPersist, mockConfigManager, mqttDir);
154
+
155
+ const listener = customContainer.getMqttListener();
156
+
157
+ expect(listener).toBeDefined();
158
+ // Verify by checking the private mqttDir field (if accessible, or through behavior)
159
+ expect(listener['mqttDir']).toBe(mqttDir);
160
+ });
161
+
162
+ it('should use the apiClient instance for the listener', () => {
163
+ const listener = container.getMqttListener();
164
+
165
+ expect(listener['apiClient']).toBe(container.apiClient);
166
+ });
167
+
168
+ it('should use the correct logger for the listener', () => {
169
+ const listener = container.getMqttListener();
170
+
171
+ expect(listener['logger']).toBe(mockLogger);
172
+ });
173
+ });
174
+
175
+ describe('getFilterMonitoringService', () => {
176
+ it('should return a ThinqFilterMonitoringService instance', () => {
177
+ const service = container.getFilterMonitoringService();
178
+
179
+ expect(service).toBeDefined();
180
+ expect(service).toHaveProperty('startPolling');
181
+ expect(service).toHaveProperty('stopPolling');
182
+ });
183
+
184
+ it('should return the same instance on repeated calls (singleton)', () => {
185
+ const service1 = container.getFilterMonitoringService();
186
+ const service2 = container.getFilterMonitoringService();
187
+
188
+ expect(service1).toBe(service2);
189
+ });
190
+
191
+ it('should use the apiClient instance', () => {
192
+ const service = container.getFilterMonitoringService();
193
+
194
+ expect(service['apiClient']).toBe(container.apiClient);
195
+ });
196
+
197
+ it('should use the correct logger', () => {
198
+ const service = container.getFilterMonitoringService();
199
+
200
+ expect(service['logger']).toBe(mockLogger);
201
+ });
202
+ });
203
+ });
@@ -0,0 +1,105 @@
1
+ import { AnsiLogger } from 'matterbridge/logger';
2
+ import { LocalStorage } from 'node-persist';
3
+
4
+ import { PlatformConfigManager } from '../../platform/platformConfigManager.js';
5
+ import { ThinqDeviceConfigurator } from '../../platform/thinq/thinqDeviceConfigurator.js';
6
+ import { ThinqDeviceDiscovery } from '../../platform/thinq/thinqDeviceDiscovery.js';
7
+ import { AccountAuthStrategy } from '../authentication/AccountAuthStrategy.js';
8
+ import { AuthenticationCoordinator } from '../authentication/AuthenticationCoordinator.js';
9
+ import { TokenAuthStrategy } from '../authentication/TokenAuthStrategy.js';
10
+ import { UserDataRepository } from '../authentication/UserDataRepository.js';
11
+ import { MqttKeyRepository } from './mqtt/mqttKeyRepository.js';
12
+ import { ThinqMqttListener } from './mqtt/thinqMqttListener.js';
13
+ import { ThinqSession } from './session.js';
14
+ import { ThinqApiClient } from './thinqApiClient.js';
15
+ import { ThinqDeviceService } from './thinqDeviceService.js';
16
+ import { ThinqFilterMonitoringService } from './thinqFilterMonitoringService.js';
17
+
18
+ /** DI container wiring together ThinQ authentication, discovery, polling, and MQTT push services (singletons). */
19
+ export class ThinqServiceContainer {
20
+ private readonly apiClientInstance: ThinqApiClient;
21
+ private authenticationCoordinatorInstance: AuthenticationCoordinator | undefined;
22
+ private deviceServiceInstance: ThinqDeviceService | undefined;
23
+ private filterMonitoringServiceInstance: ThinqFilterMonitoringService | undefined;
24
+ private deviceDiscoveryInstance: ThinqDeviceDiscovery | undefined;
25
+ private deviceConfiguratorInstance: ThinqDeviceConfigurator | undefined;
26
+ private userDataRepositoryInstance: UserDataRepository | undefined;
27
+ private mqttKeyRepositoryInstance: MqttKeyRepository | undefined;
28
+ private mqttListenerInstance: ThinqMqttListener | undefined;
29
+
30
+ constructor(
31
+ private readonly logger: AnsiLogger,
32
+ private readonly persist: LocalStorage,
33
+ private readonly configManager: PlatformConfigManager,
34
+ private readonly mqttCertDir: string,
35
+ ) {
36
+ this.apiClientInstance = new ThinqApiClient(
37
+ new ThinqSession('', '', 0),
38
+ this.configManager.country,
39
+ this.configManager.language,
40
+ this.logger,
41
+ );
42
+ }
43
+
44
+ public get apiClient(): ThinqApiClient {
45
+ return this.apiClientInstance;
46
+ }
47
+
48
+ public getUserDataRepository(): UserDataRepository {
49
+ this.userDataRepositoryInstance ??= new UserDataRepository(this.persist, this.configManager, this.logger);
50
+ return this.userDataRepositoryInstance;
51
+ }
52
+
53
+ public getAuthenticationCoordinator(): AuthenticationCoordinator {
54
+ if (!this.authenticationCoordinatorInstance) {
55
+ const accountStrategy = new AccountAuthStrategy(this.apiClientInstance, this.logger);
56
+ const tokenStrategy = new TokenAuthStrategy(this.apiClientInstance, this.logger);
57
+ this.authenticationCoordinatorInstance = new AuthenticationCoordinator(
58
+ accountStrategy,
59
+ tokenStrategy,
60
+ this.apiClientInstance,
61
+ this.logger,
62
+ );
63
+ }
64
+ return this.authenticationCoordinatorInstance;
65
+ }
66
+
67
+ public getDeviceService(): ThinqDeviceService {
68
+ this.deviceServiceInstance ??= new ThinqDeviceService(this.apiClientInstance, this.logger);
69
+ return this.deviceServiceInstance;
70
+ }
71
+
72
+ public getFilterMonitoringService(): ThinqFilterMonitoringService {
73
+ this.filterMonitoringServiceInstance ??= new ThinqFilterMonitoringService(this.apiClientInstance, this.logger);
74
+ return this.filterMonitoringServiceInstance;
75
+ }
76
+
77
+ public getDeviceDiscovery(): ThinqDeviceDiscovery {
78
+ this.deviceDiscoveryInstance ??= new ThinqDeviceDiscovery(this.getDeviceService(), this.logger);
79
+ return this.deviceDiscoveryInstance;
80
+ }
81
+
82
+ public getDeviceConfigurator(): ThinqDeviceConfigurator {
83
+ this.deviceConfiguratorInstance ??= new ThinqDeviceConfigurator(
84
+ this.logger,
85
+ this.apiClientInstance,
86
+ this.configManager,
87
+ );
88
+ return this.deviceConfiguratorInstance;
89
+ }
90
+
91
+ public getMqttListener(): ThinqMqttListener {
92
+ this.mqttListenerInstance ??= new ThinqMqttListener(
93
+ this.apiClientInstance,
94
+ this.getMqttKeyRepository(),
95
+ this.mqttCertDir,
96
+ this.logger,
97
+ );
98
+ return this.mqttListenerInstance;
99
+ }
100
+
101
+ private getMqttKeyRepository(): MqttKeyRepository {
102
+ this.mqttKeyRepositoryInstance ??= new MqttKeyRepository(this.persist, this.logger);
103
+ return this.mqttKeyRepositoryInstance;
104
+ }
105
+ }