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.
- package/.claude/CLAUDE.md +43 -0
- package/.claude/agents/compiler.md +91 -0
- package/.claude/agents/direct-executor.md +98 -0
- package/.claude/agents/documenter.md +78 -0
- package/.claude/agents/finalizer.md +233 -0
- package/.claude/agents/implementer.md +112 -0
- package/.claude/agents/investigator.md +100 -0
- package/.claude/agents/release-manager.md +203 -0
- package/.claude/agents/reviewer.md +131 -0
- package/.claude/agents/technical-architect.md +403 -0
- package/.claude/agents/test-writer.md +159 -0
- package/.claude/agents/wiki-manager.md +147 -0
- package/.claude/instructions/agent-prompts.md +90 -0
- package/.claude/instructions/shared-rules.md +11 -0
- package/.claude/instructions/team-orchestrator-policy.md +165 -0
- package/.claude/memory.md +186 -0
- package/.claude/output-styles/engineer-manager.md +38 -0
- package/.claude/settings.json +40 -0
- package/.claude/settings.local.json +30 -0
- package/.claude/skills/load-policy/SKILL.md +13 -0
- package/.claude/skills/ref-idea/SKILL.md +44 -0
- package/.claude/skills/status-of/SKILL.md +38 -0
- package/.claude/skills/workspace-console/SKILL.md +49 -0
- package/.claude/statusline-command.sh +130 -0
- package/.claude/statusline.md +53 -0
- package/.claude/templates/approval-packet.template.html +171 -0
- package/.claude/templates/reference-workspaces.md +16 -0
- package/.claude/tools/cli-runner-server.cjs +220 -0
- package/.claude/tools/discord-send-server.cjs +188 -0
- package/.claude/tools/github-release-server.cjs +181 -0
- package/.claude/tools/glob-grep-server.cjs +267 -0
- package/.claude/tools/read-log-server.cjs +175 -0
- package/.github/codeql/codeql-config.yml +14 -0
- package/.github/workflows/build.yml +54 -0
- package/.github/workflows/codeql.yml +59 -0
- package/.github/workflows/coverage.yml +70 -0
- package/.github/workflows/publish.yml +103 -0
- package/.mcp.json +34 -0
- package/CHANGELOG.md +195 -0
- package/LICENSE +21 -0
- package/dist/cli/commands/devices.js +52 -0
- package/dist/cli/commands/energy.js +122 -0
- package/dist/cli/commands/login.js +54 -0
- package/dist/cli/help.js +50 -0
- package/dist/cli/main.js +56 -0
- package/dist/cli/session.js +13 -0
- package/dist/cli/types.js +1 -0
- package/dist/cli/utils.js +29 -0
- package/dist/cli.js +2 -0
- package/dist/constants/index.js +1 -0
- package/dist/core/domain/entities/ThinqDevice.js +43 -0
- package/dist/core/domain/value-objects/AirConditionerCapabilities.js +22 -0
- package/dist/core/domain/value-objects/ThinqFilterState.js +35 -0
- package/dist/core/domain/value-objects/ThinqSnapshot.js +125 -0
- package/dist/errors/AuthenticationError.js +26 -0
- package/dist/errors/BaseError.js +23 -0
- package/dist/errors/index.js +2 -0
- package/dist/model/LgThinkqPluginPlatformConfig.js +23 -0
- package/dist/model/LgThinqPluginPlatformConfig.js +23 -0
- package/dist/module.js +212 -0
- package/dist/platform/deviceRegistry.js +26 -0
- package/dist/platform/platformConfigManager.js +89 -0
- package/dist/platform/platformState.js +9 -0
- package/dist/platform/thinq/thinqAirConditionerCommandHandlers.js +190 -0
- package/dist/platform/thinq/thinqAirConditionerEndpointFactory.js +53 -0
- package/dist/platform/thinq/thinqAirConditionerFilterResetCommandHandler.js +54 -0
- package/dist/platform/thinq/thinqAirConditionerFilterStateSync.js +19 -0
- package/dist/platform/thinq/thinqAirConditionerStateSync.js +205 -0
- package/dist/platform/thinq/thinqDeviceConfigReconciler.js +26 -0
- package/dist/platform/thinq/thinqDeviceConfigurator.js +124 -0
- package/dist/platform/thinq/thinqDeviceDiscovery.js +33 -0
- package/dist/platform/thinq/thinqWasherCommandHandlers.js +54 -0
- package/dist/platform/thinq/thinqWasherEndpointFactory.js +6 -0
- package/dist/platform/thinq/thinqWasherStateSync.js +31 -0
- package/dist/platform/thinq/thinqWasherStopCommandResolver.js +38 -0
- package/dist/services/authentication/AccountAuthStrategy.js +135 -0
- package/dist/services/authentication/AuthContext.js +1 -0
- package/dist/services/authentication/AuthenticationCoordinator.js +29 -0
- package/dist/services/authentication/IAuthStrategy.js +1 -0
- package/dist/services/authentication/TokenAuthStrategy.js +27 -0
- package/dist/services/authentication/UserDataRepository.js +32 -0
- package/dist/services/thinq/gateway.js +61 -0
- package/dist/services/thinq/mqtt/mqttCertificate.js +63 -0
- package/dist/services/thinq/mqtt/mqttConnectionEvents.js +37 -0
- package/dist/services/thinq/mqtt/mqttKeyRepository.js +33 -0
- package/dist/services/thinq/mqtt/mqttRetry.js +25 -0
- package/dist/services/thinq/mqtt/thinqMqttListener.js +97 -0
- package/dist/services/thinq/serviceContainer.js +73 -0
- package/dist/services/thinq/session.js +48 -0
- package/dist/services/thinq/thinqApiClient.js +258 -0
- package/dist/services/thinq/thinqDeviceService.js +75 -0
- package/dist/services/thinq/thinqFilterMonitoringService.js +40 -0
- package/dist/settings.js +9 -0
- package/dist/tests/helpers/testUtils.js +87 -0
- package/dist/utils/mask.js +39 -0
- package/docs/README_CLI.md +166 -0
- package/eslint.config.js +194 -0
- package/matterbridge-lg-thinq-plugin.config.json +27 -0
- package/matterbridge-lg-thinq-plugin.schema.json +520 -0
- package/package.json +107 -0
- package/prettier.config.js +49 -0
- package/scripts/parse-junit-failures.mjs +45 -0
- package/scripts/parse-tsc-failures.mjs +48 -0
- package/scripts/run-build-local-summary.mjs +29 -0
- package/scripts/run-format-summary.mjs +36 -0
- package/scripts/run-lint-fix-summary.mjs +38 -0
- package/scripts/run-precommit-summary.mjs +94 -0
- package/src/cli/commands/devices.ts +67 -0
- package/src/cli/commands/energy.ts +160 -0
- package/src/cli/commands/login.ts +74 -0
- package/src/cli/help.ts +53 -0
- package/src/cli/main.ts +66 -0
- package/src/cli/session.ts +15 -0
- package/src/cli/types.ts +10 -0
- package/src/cli/utils.ts +30 -0
- package/src/cli.ts +3 -0
- package/src/constants/index.ts +8 -0
- package/src/core/domain/entities/ThinqDevice.test.ts +224 -0
- package/src/core/domain/entities/ThinqDevice.ts +74 -0
- package/src/core/domain/value-objects/AirConditionerCapabilities.ts +50 -0
- package/src/core/domain/value-objects/ThinqFilterState.ts +50 -0
- package/src/core/domain/value-objects/ThinqSnapshot.ts +162 -0
- package/src/errors/AuthenticationError.ts +37 -0
- package/src/errors/BaseError.ts +38 -0
- package/src/errors/index.ts +24 -0
- package/src/model/LgThinqPluginPlatformConfig.ts +94 -0
- package/src/module.ts +298 -0
- package/src/platform/deviceRegistry.ts +58 -0
- package/src/platform/platformConfigManager.ts +133 -0
- package/src/platform/platformState.ts +16 -0
- package/src/platform/thinq/thinqAirConditionerCommandHandlers.ts +291 -0
- package/src/platform/thinq/thinqAirConditionerEndpointFactory.ts +145 -0
- package/src/platform/thinq/thinqAirConditionerFilterResetCommandHandler.ts +96 -0
- package/src/platform/thinq/thinqAirConditionerFilterStateSync.ts +42 -0
- package/src/platform/thinq/thinqAirConditionerStateSync.ts +343 -0
- package/src/platform/thinq/thinqDeviceConfigReconciler.ts +42 -0
- package/src/platform/thinq/thinqDeviceConfigurator.test.ts +90 -0
- package/src/platform/thinq/thinqDeviceConfigurator.ts +191 -0
- package/src/platform/thinq/thinqDeviceDiscovery.test.ts +173 -0
- package/src/platform/thinq/thinqDeviceDiscovery.ts +51 -0
- package/src/platform/thinq/thinqWasherCommandHandlers.ts +120 -0
- package/src/platform/thinq/thinqWasherEndpointFactory.ts +37 -0
- package/src/platform/thinq/thinqWasherStateSync.ts +60 -0
- package/src/platform/thinq/thinqWasherStopCommandResolver.ts +62 -0
- package/src/services/authentication/AccountAuthStrategy.test.ts +358 -0
- package/src/services/authentication/AccountAuthStrategy.ts +209 -0
- package/src/services/authentication/AuthContext.ts +19 -0
- package/src/services/authentication/AuthenticationCoordinator.test.ts +217 -0
- package/src/services/authentication/AuthenticationCoordinator.ts +59 -0
- package/src/services/authentication/IAuthStrategy.ts +10 -0
- package/src/services/authentication/TokenAuthStrategy.test.ts +167 -0
- package/src/services/authentication/TokenAuthStrategy.ts +40 -0
- package/src/services/authentication/UserDataRepository.test.ts +149 -0
- package/src/services/authentication/UserDataRepository.ts +46 -0
- package/src/services/thinq/gateway.ts +86 -0
- package/src/services/thinq/mqtt/mqttCertificate.test.ts +344 -0
- package/src/services/thinq/mqtt/mqttCertificate.ts +102 -0
- package/src/services/thinq/mqtt/mqttConnectionEvents.test.ts +276 -0
- package/src/services/thinq/mqtt/mqttConnectionEvents.ts +76 -0
- package/src/services/thinq/mqtt/mqttKeyRepository.test.ts +164 -0
- package/src/services/thinq/mqtt/mqttKeyRepository.ts +43 -0
- package/src/services/thinq/mqtt/mqttRetry.test.ts +239 -0
- package/src/services/thinq/mqtt/mqttRetry.ts +43 -0
- package/src/services/thinq/mqtt/thinqMqttListener.test.ts +349 -0
- package/src/services/thinq/mqtt/thinqMqttListener.ts +120 -0
- package/src/services/thinq/serviceContainer.test.ts +203 -0
- package/src/services/thinq/serviceContainer.ts +105 -0
- package/src/services/thinq/session.test.ts +287 -0
- package/src/services/thinq/session.ts +69 -0
- package/src/services/thinq/thinqApiClient.test.ts +702 -0
- package/src/services/thinq/thinqApiClient.ts +418 -0
- package/src/services/thinq/thinqDeviceService.test.ts +415 -0
- package/src/services/thinq/thinqDeviceService.ts +101 -0
- package/src/services/thinq/thinqFilterMonitoringService.test.ts +264 -0
- package/src/services/thinq/thinqFilterMonitoringService.ts +53 -0
- package/src/settings.ts +18 -0
- package/src/tests/cli/commands/devices.test.ts +293 -0
- package/src/tests/cli/commands/login.test.ts +240 -0
- package/src/tests/cli/help.test.ts +46 -0
- package/src/tests/cli/main.test.ts +214 -0
- package/src/tests/cli/session.test.ts +198 -0
- package/src/tests/cli/utils.test.ts +194 -0
- package/src/tests/core/domain/value-objects/AirConditionerCapabilities.test.ts +685 -0
- package/src/tests/core/domain/value-objects/ThinqFilterState.test.ts +210 -0
- package/src/tests/core/domain/value-objects/ThinqSnapshot.test.ts +1035 -0
- package/src/tests/helpers/testUtils.ts +118 -0
- package/src/tests/model/LgThinqPluginPlatformConfig.test.ts +81 -0
- package/src/tests/platform/deviceRegistry.test.ts +177 -0
- package/src/tests/platform/platformConfigManager.test.ts +790 -0
- package/src/tests/platform/platformState.test.ts +41 -0
- package/src/tests/platform/thinq/thinqAirConditionerCommandHandlers.test.ts +879 -0
- package/src/tests/platform/thinq/thinqAirConditionerEndpointFactory.test.ts +993 -0
- package/src/tests/platform/thinq/thinqAirConditionerFilterResetCommandHandler.test.ts +508 -0
- package/src/tests/platform/thinq/thinqAirConditionerFilterStateSync.test.ts +212 -0
- package/src/tests/platform/thinq/thinqAirConditionerStateSync.test.ts +1586 -0
- package/src/tests/platform/thinq/thinqDeviceConfigReconciler.test.ts +269 -0
- package/src/tests/platform/thinq/thinqDeviceConfigurator.test.ts +492 -0
- package/src/tests/platform/thinq/thinqWasherCommandHandlers.test.ts +594 -0
- package/src/tests/platform/thinq/thinqWasherEndpointFactory.test.ts +84 -0
- package/src/tests/platform/thinq/thinqWasherStateSync.test.ts +373 -0
- package/src/tests/platform/thinq/thinqWasherStopCommandResolver.test.ts +392 -0
- package/src/tests/settings.test.ts +33 -0
- package/src/utils/mask.ts +50 -0
- package/tsconfig.base.json +25 -0
- package/tsconfig.json +9 -0
- package/tsconfig.production.json +13 -0
- package/tsconfig.vitest.json +5 -0
- package/vite.config.ts +43 -0
- package/wiki/memory-archive.md +12 -0
- package/wiki/reference-workspaces.md +23 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { device as awsIotDevice } from 'aws-iot-device-sdk';
|
|
2
|
+
import { ThinqSnapshot } from '../../../core/domain/value-objects/ThinqSnapshot.js';
|
|
3
|
+
import { certificateRequestBody, downloadRootCa, writeMqttCertificateFiles } from './mqttCertificate.js';
|
|
4
|
+
import { wireMqttDeviceEvents } from './mqttConnectionEvents.js';
|
|
5
|
+
import { retryMqttRegistration } from './mqttRetry.js';
|
|
6
|
+
export class ThinqMqttListener {
|
|
7
|
+
apiClient;
|
|
8
|
+
keyRepository;
|
|
9
|
+
mqttDir;
|
|
10
|
+
logger;
|
|
11
|
+
stopped = false;
|
|
12
|
+
device;
|
|
13
|
+
reconnectTimer;
|
|
14
|
+
constructor(apiClient, keyRepository, mqttDir, logger) {
|
|
15
|
+
this.apiClient = apiClient;
|
|
16
|
+
this.keyRepository = keyRepository;
|
|
17
|
+
this.mqttDir = mqttDir;
|
|
18
|
+
this.logger = logger;
|
|
19
|
+
}
|
|
20
|
+
async start(onUpdate) {
|
|
21
|
+
this.stop();
|
|
22
|
+
this.stopped = false;
|
|
23
|
+
const started = await retryMqttRegistration({
|
|
24
|
+
register: () => this.connect(onUpdate),
|
|
25
|
+
logger: this.logger,
|
|
26
|
+
});
|
|
27
|
+
if (!started) {
|
|
28
|
+
this.logger.error('ThinQ MQTT: unable to start after retries, continuing with polling only.');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
stop() {
|
|
32
|
+
this.stopped = true;
|
|
33
|
+
if (this.reconnectTimer) {
|
|
34
|
+
clearTimeout(this.reconnectTimer);
|
|
35
|
+
this.reconnectTimer = undefined;
|
|
36
|
+
}
|
|
37
|
+
this.device?.end();
|
|
38
|
+
this.device = undefined;
|
|
39
|
+
}
|
|
40
|
+
async connect(onUpdate) {
|
|
41
|
+
const route = await this.apiClient.getMqttRouteInfo();
|
|
42
|
+
const hostname = new URL(route.mqttServer).hostname;
|
|
43
|
+
const rootCA = await downloadRootCa(hostname);
|
|
44
|
+
const keys = await this.keyRepository.getOrCreateKeyPair();
|
|
45
|
+
const csr = await this.keyRepository.getOrCreateCsr(keys);
|
|
46
|
+
await this.apiClient.registerMqttClient();
|
|
47
|
+
const certificate = await this.apiClient.requestMqttCertificate(certificateRequestBody(csr));
|
|
48
|
+
const paths = await writeMqttCertificateFiles({
|
|
49
|
+
mqttDir: this.mqttDir,
|
|
50
|
+
rootCA,
|
|
51
|
+
privateKey: keys.privateKey,
|
|
52
|
+
certificatePem: certificate.certificatePem,
|
|
53
|
+
});
|
|
54
|
+
this.logger.debug(`ThinQ MQTT: connecting to ${route.mqttServer}`);
|
|
55
|
+
const device = new awsIotDevice({
|
|
56
|
+
...paths,
|
|
57
|
+
clientId: this.apiClient.getClientId(),
|
|
58
|
+
host: hostname,
|
|
59
|
+
});
|
|
60
|
+
this.device = device;
|
|
61
|
+
const reconnect = async () => {
|
|
62
|
+
if (this.stopped) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
await this.connect(onUpdate);
|
|
66
|
+
};
|
|
67
|
+
const scheduleReconnect = (handler, delayMs) => {
|
|
68
|
+
if (this.stopped) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
this.reconnectTimer = setTimeout(handler, delayMs);
|
|
72
|
+
return this.reconnectTimer;
|
|
73
|
+
};
|
|
74
|
+
wireMqttDeviceEvents({
|
|
75
|
+
device,
|
|
76
|
+
logger: this.logger,
|
|
77
|
+
mqttServer: route.mqttServer,
|
|
78
|
+
subscriptions: certificate.subscriptions,
|
|
79
|
+
onMessage: (payload) => this.handleMessage(payload, onUpdate),
|
|
80
|
+
reconnect,
|
|
81
|
+
scheduleReconnect,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
handleMessage(payload, onUpdate) {
|
|
85
|
+
if (typeof payload !== 'object' || payload === null || !('deviceId' in payload) || !('data' in payload)) {
|
|
86
|
+
this.logger.debug('ThinQ MQTT: received malformed message, skipping.');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const message = payload;
|
|
90
|
+
const reported = message.data?.state?.reported;
|
|
91
|
+
if (typeof message.deviceId !== 'string' || typeof reported !== 'object' || reported === null) {
|
|
92
|
+
this.logger.debug('ThinQ MQTT: received message with missing deviceId/reported state, skipping.');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
onUpdate(message.deviceId, new ThinqSnapshot(reported));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { ThinqDeviceConfigurator } from '../../platform/thinq/thinqDeviceConfigurator.js';
|
|
2
|
+
import { ThinqDeviceDiscovery } from '../../platform/thinq/thinqDeviceDiscovery.js';
|
|
3
|
+
import { AccountAuthStrategy } from '../authentication/AccountAuthStrategy.js';
|
|
4
|
+
import { AuthenticationCoordinator } from '../authentication/AuthenticationCoordinator.js';
|
|
5
|
+
import { TokenAuthStrategy } from '../authentication/TokenAuthStrategy.js';
|
|
6
|
+
import { UserDataRepository } from '../authentication/UserDataRepository.js';
|
|
7
|
+
import { MqttKeyRepository } from './mqtt/mqttKeyRepository.js';
|
|
8
|
+
import { ThinqMqttListener } from './mqtt/thinqMqttListener.js';
|
|
9
|
+
import { ThinqSession } from './session.js';
|
|
10
|
+
import { ThinqApiClient } from './thinqApiClient.js';
|
|
11
|
+
import { ThinqDeviceService } from './thinqDeviceService.js';
|
|
12
|
+
import { ThinqFilterMonitoringService } from './thinqFilterMonitoringService.js';
|
|
13
|
+
export class ThinqServiceContainer {
|
|
14
|
+
logger;
|
|
15
|
+
persist;
|
|
16
|
+
configManager;
|
|
17
|
+
mqttCertDir;
|
|
18
|
+
apiClientInstance;
|
|
19
|
+
authenticationCoordinatorInstance;
|
|
20
|
+
deviceServiceInstance;
|
|
21
|
+
filterMonitoringServiceInstance;
|
|
22
|
+
deviceDiscoveryInstance;
|
|
23
|
+
deviceConfiguratorInstance;
|
|
24
|
+
userDataRepositoryInstance;
|
|
25
|
+
mqttKeyRepositoryInstance;
|
|
26
|
+
mqttListenerInstance;
|
|
27
|
+
constructor(logger, persist, configManager, mqttCertDir) {
|
|
28
|
+
this.logger = logger;
|
|
29
|
+
this.persist = persist;
|
|
30
|
+
this.configManager = configManager;
|
|
31
|
+
this.mqttCertDir = mqttCertDir;
|
|
32
|
+
this.apiClientInstance = new ThinqApiClient(new ThinqSession('', '', 0), this.configManager.country, this.configManager.language, this.logger);
|
|
33
|
+
}
|
|
34
|
+
get apiClient() {
|
|
35
|
+
return this.apiClientInstance;
|
|
36
|
+
}
|
|
37
|
+
getUserDataRepository() {
|
|
38
|
+
this.userDataRepositoryInstance ??= new UserDataRepository(this.persist, this.configManager, this.logger);
|
|
39
|
+
return this.userDataRepositoryInstance;
|
|
40
|
+
}
|
|
41
|
+
getAuthenticationCoordinator() {
|
|
42
|
+
if (!this.authenticationCoordinatorInstance) {
|
|
43
|
+
const accountStrategy = new AccountAuthStrategy(this.apiClientInstance, this.logger);
|
|
44
|
+
const tokenStrategy = new TokenAuthStrategy(this.apiClientInstance, this.logger);
|
|
45
|
+
this.authenticationCoordinatorInstance = new AuthenticationCoordinator(accountStrategy, tokenStrategy, this.apiClientInstance, this.logger);
|
|
46
|
+
}
|
|
47
|
+
return this.authenticationCoordinatorInstance;
|
|
48
|
+
}
|
|
49
|
+
getDeviceService() {
|
|
50
|
+
this.deviceServiceInstance ??= new ThinqDeviceService(this.apiClientInstance, this.logger);
|
|
51
|
+
return this.deviceServiceInstance;
|
|
52
|
+
}
|
|
53
|
+
getFilterMonitoringService() {
|
|
54
|
+
this.filterMonitoringServiceInstance ??= new ThinqFilterMonitoringService(this.apiClientInstance, this.logger);
|
|
55
|
+
return this.filterMonitoringServiceInstance;
|
|
56
|
+
}
|
|
57
|
+
getDeviceDiscovery() {
|
|
58
|
+
this.deviceDiscoveryInstance ??= new ThinqDeviceDiscovery(this.getDeviceService(), this.logger);
|
|
59
|
+
return this.deviceDiscoveryInstance;
|
|
60
|
+
}
|
|
61
|
+
getDeviceConfigurator() {
|
|
62
|
+
this.deviceConfiguratorInstance ??= new ThinqDeviceConfigurator(this.logger, this.apiClientInstance, this.configManager);
|
|
63
|
+
return this.deviceConfiguratorInstance;
|
|
64
|
+
}
|
|
65
|
+
getMqttListener() {
|
|
66
|
+
this.mqttListenerInstance ??= new ThinqMqttListener(this.apiClientInstance, this.getMqttKeyRepository(), this.mqttCertDir, this.logger);
|
|
67
|
+
return this.mqttListenerInstance;
|
|
68
|
+
}
|
|
69
|
+
getMqttKeyRepository() {
|
|
70
|
+
this.mqttKeyRepositoryInstance ??= new MqttKeyRepository(this.persist, this.logger);
|
|
71
|
+
return this.mqttKeyRepositoryInstance;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export class ThinqSession {
|
|
2
|
+
accessTokenValue;
|
|
3
|
+
refreshTokenValue;
|
|
4
|
+
expiresAtEpochSecondsValue;
|
|
5
|
+
constructor(accessToken, refreshToken, expiresAtEpochSeconds) {
|
|
6
|
+
this.accessTokenValue = accessToken;
|
|
7
|
+
this.refreshTokenValue = refreshToken;
|
|
8
|
+
this.expiresAtEpochSecondsValue = expiresAtEpochSeconds;
|
|
9
|
+
}
|
|
10
|
+
get accessToken() {
|
|
11
|
+
return this.accessTokenValue;
|
|
12
|
+
}
|
|
13
|
+
get refreshToken() {
|
|
14
|
+
return this.refreshTokenValue;
|
|
15
|
+
}
|
|
16
|
+
get expiresAtEpochSeconds() {
|
|
17
|
+
return this.expiresAtEpochSecondsValue;
|
|
18
|
+
}
|
|
19
|
+
hasToken() {
|
|
20
|
+
return this.accessTokenValue !== '';
|
|
21
|
+
}
|
|
22
|
+
isExpired() {
|
|
23
|
+
return this.expiresAtEpochSecondsValue < ThinqSession.currentEpochSeconds();
|
|
24
|
+
}
|
|
25
|
+
hasValidToken() {
|
|
26
|
+
return this.hasToken() && !this.isExpired();
|
|
27
|
+
}
|
|
28
|
+
updateAccessToken(accessToken, expiresAtEpochSeconds) {
|
|
29
|
+
this.accessTokenValue = accessToken;
|
|
30
|
+
this.expiresAtEpochSecondsValue = expiresAtEpochSeconds;
|
|
31
|
+
}
|
|
32
|
+
static expiryFromExpiresIn(expiresInSeconds) {
|
|
33
|
+
return ThinqSession.currentEpochSeconds() + expiresInSeconds;
|
|
34
|
+
}
|
|
35
|
+
toData() {
|
|
36
|
+
return {
|
|
37
|
+
accessToken: this.accessTokenValue,
|
|
38
|
+
refreshToken: this.refreshTokenValue,
|
|
39
|
+
expiresAtEpochSeconds: this.expiresAtEpochSecondsValue,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
static fromData(data) {
|
|
43
|
+
return new ThinqSession(data.accessToken, data.refreshToken, data.expiresAtEpochSeconds);
|
|
44
|
+
}
|
|
45
|
+
static currentEpochSeconds() {
|
|
46
|
+
return Math.round(Date.now() / 1000);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import { AuthenticationError, TokenExpiredError } from '../../errors/index.js';
|
|
4
|
+
import { maskSensitiveFields } from '../../utils/mask.js';
|
|
5
|
+
import { formatRfc2822Utc, Gateway, signThinqMessage, THINQ_APPLICATION_KEY, THINQ_CLIENT_ID, THINQ_OAUTH_SECRET_KEY, } from './gateway.js';
|
|
6
|
+
import { ThinqSession } from './session.js';
|
|
7
|
+
const GATEWAY_URL = 'https://route.lgthinq.com:46030/v1/service/application/gateway-uri';
|
|
8
|
+
export const MQTT_ROUTE_URL = 'https://common.lgthinq.com/route';
|
|
9
|
+
const API_KEY = 'VGhpblEyLjAgU0VSVklDRQ==';
|
|
10
|
+
const API_CLIENT_ID = 'c713ea8e50f657534ff8b9d373dfebfc2ed70b88285c26b8ade49868c0b164d9';
|
|
11
|
+
const THINQ_TOKEN_EXPIRED_RESULT_CODE = '0102';
|
|
12
|
+
function randomMessageId(length = 22) {
|
|
13
|
+
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
14
|
+
let result = '';
|
|
15
|
+
for (let i = 0; i < length; i++) {
|
|
16
|
+
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
export class ThinqApiClient {
|
|
21
|
+
session;
|
|
22
|
+
country;
|
|
23
|
+
language;
|
|
24
|
+
logger;
|
|
25
|
+
gateway;
|
|
26
|
+
homesCache;
|
|
27
|
+
userNumber;
|
|
28
|
+
clientId;
|
|
29
|
+
constructor(session, country, language, logger) {
|
|
30
|
+
this.session = session;
|
|
31
|
+
this.country = country;
|
|
32
|
+
this.language = language;
|
|
33
|
+
this.logger = logger;
|
|
34
|
+
}
|
|
35
|
+
setSession(session) {
|
|
36
|
+
this.session = session;
|
|
37
|
+
}
|
|
38
|
+
setUserNumber(userNumber) {
|
|
39
|
+
this.userNumber = userNumber;
|
|
40
|
+
this.clientId = crypto.createHash('sha256').update(`${userNumber}${Date.now()}`).digest('hex');
|
|
41
|
+
}
|
|
42
|
+
async getGateway() {
|
|
43
|
+
if (!this.gateway) {
|
|
44
|
+
const headers = this.defaultHeaders;
|
|
45
|
+
this.logger.debug(`ThinQ getGateway request -> GET ${GATEWAY_URL} headers=${JSON.stringify(maskSensitiveFields(headers))}`);
|
|
46
|
+
const response = await axios.get(GATEWAY_URL, { headers });
|
|
47
|
+
this.gateway = new Gateway(response.data.result);
|
|
48
|
+
}
|
|
49
|
+
return this.gateway.data;
|
|
50
|
+
}
|
|
51
|
+
async getDeviceModel(modelJsonUri) {
|
|
52
|
+
this.logger.debug(`ThinQ getDeviceModel request -> GET ${modelJsonUri}`);
|
|
53
|
+
const response = await axios.get(modelJsonUri);
|
|
54
|
+
return response.data;
|
|
55
|
+
}
|
|
56
|
+
async getListHomes() {
|
|
57
|
+
if (!this.homesCache) {
|
|
58
|
+
const data = await this.request('get', 'service/homes');
|
|
59
|
+
this.homesCache = data.result?.item ?? [];
|
|
60
|
+
}
|
|
61
|
+
return this.homesCache;
|
|
62
|
+
}
|
|
63
|
+
async getListDevices() {
|
|
64
|
+
const homes = await this.getListHomes();
|
|
65
|
+
const devices = [];
|
|
66
|
+
for (const home of homes) {
|
|
67
|
+
const data = await this.request('get', `service/homes/${home.homeId}`);
|
|
68
|
+
devices.push(...(data.result?.devices ?? []));
|
|
69
|
+
}
|
|
70
|
+
return devices;
|
|
71
|
+
}
|
|
72
|
+
async sendCommand(deviceId, payload) {
|
|
73
|
+
if (!deviceId.trim()) {
|
|
74
|
+
throw new Error('Invalid deviceId: must be a non-empty string.');
|
|
75
|
+
}
|
|
76
|
+
await this.request('post', `service/devices/${deviceId}/control-sync`, {
|
|
77
|
+
ctrlKey: 'basicCtrl',
|
|
78
|
+
command: 'Set',
|
|
79
|
+
...payload,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
async sendCommandAndGetResponse(deviceId, payload) {
|
|
83
|
+
if (!deviceId.trim()) {
|
|
84
|
+
throw new Error('Invalid deviceId: must be a non-empty string.');
|
|
85
|
+
}
|
|
86
|
+
return this.request('post', `service/devices/${deviceId}/control-sync`, {
|
|
87
|
+
ctrlKey: 'basicCtrl',
|
|
88
|
+
command: 'Set',
|
|
89
|
+
...payload,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async getFilterState(deviceId) {
|
|
93
|
+
if (!deviceId.trim()) {
|
|
94
|
+
throw new Error('Invalid deviceId: must be a non-empty string.');
|
|
95
|
+
}
|
|
96
|
+
const response = await this.sendCommandAndGetResponse(deviceId, {
|
|
97
|
+
ctrlKey: 'filterMngStateCtrl',
|
|
98
|
+
command: 'Get',
|
|
99
|
+
dataGetList: [
|
|
100
|
+
'airState.filterMngState.useTime',
|
|
101
|
+
'airState.filterMngState.remainTime',
|
|
102
|
+
'airState.filterMngState.maxTime',
|
|
103
|
+
'airState.filterMngState.changeDate',
|
|
104
|
+
'airState.filterMngState.type',
|
|
105
|
+
],
|
|
106
|
+
});
|
|
107
|
+
return response.result?.data;
|
|
108
|
+
}
|
|
109
|
+
async sendKeepAlive(deviceId) {
|
|
110
|
+
if (!deviceId.trim()) {
|
|
111
|
+
throw new Error('Invalid deviceId: must be a non-empty string.');
|
|
112
|
+
}
|
|
113
|
+
await this.request('post', `service/devices/${deviceId}/control`, {
|
|
114
|
+
ctrlKey: 'allEventEnable',
|
|
115
|
+
command: 'Set',
|
|
116
|
+
dataKey: 'airState.mon.timeout',
|
|
117
|
+
dataValue: '70',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
async refreshToken(session) {
|
|
121
|
+
const tokenUrl = `https://${this.country.toLowerCase()}.lgeapi.com/oauth/1.0/oauth2/token`;
|
|
122
|
+
const data = new URLSearchParams({ grant_type: 'refresh_token', refresh_token: session.refreshToken });
|
|
123
|
+
const timestamp = formatRfc2822Utc(new Date());
|
|
124
|
+
const requestUrl = `/oauth/1.0/oauth2/token?${data.toString()}`;
|
|
125
|
+
const signature = signThinqMessage(`${requestUrl}\n${timestamp}`, THINQ_OAUTH_SECRET_KEY);
|
|
126
|
+
const headers = {
|
|
127
|
+
'x-lge-app-os': 'ADR',
|
|
128
|
+
'x-lge-appkey': THINQ_CLIENT_ID,
|
|
129
|
+
'x-lge-oauth-signature': signature,
|
|
130
|
+
'x-lge-oauth-date': timestamp,
|
|
131
|
+
Accept: 'application/json',
|
|
132
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
133
|
+
};
|
|
134
|
+
this.logger.debug(`ThinQ refreshToken request -> POST ${tokenUrl} headers=${JSON.stringify(maskSensitiveFields(headers))} body=${JSON.stringify(maskSensitiveFields(Object.fromEntries(data.entries())))}`);
|
|
135
|
+
try {
|
|
136
|
+
const response = await axios.post(tokenUrl, data.toString(), {
|
|
137
|
+
headers,
|
|
138
|
+
});
|
|
139
|
+
session.updateAccessToken(response.data.access_token, ThinqSession.expiryFromExpiresIn(Number.parseInt(response.data.expires_in, 10)));
|
|
140
|
+
this.session = session;
|
|
141
|
+
return session;
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
this.logger.error(`ThinQ token refresh failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
145
|
+
throw new TokenExpiredError();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async getUserNumber(accessToken) {
|
|
149
|
+
const profileUrl = `https://${this.country.toLowerCase()}.lgeapi.com/users/profile`;
|
|
150
|
+
const timestamp = formatRfc2822Utc(new Date());
|
|
151
|
+
const signature = signThinqMessage(`/users/profile\n${timestamp}`, THINQ_OAUTH_SECRET_KEY);
|
|
152
|
+
const headers = {
|
|
153
|
+
Accept: 'application/json',
|
|
154
|
+
Authorization: `Bearer ${accessToken}`,
|
|
155
|
+
'X-Lge-Svccode': 'SVC202',
|
|
156
|
+
'X-Application-Key': THINQ_APPLICATION_KEY,
|
|
157
|
+
'lgemp-x-app-key': THINQ_CLIENT_ID,
|
|
158
|
+
'X-Device-Type': 'M01',
|
|
159
|
+
'X-Device-Platform': 'ADR',
|
|
160
|
+
'x-lge-oauth-date': timestamp,
|
|
161
|
+
'x-lge-oauth-signature': signature,
|
|
162
|
+
};
|
|
163
|
+
this.logger.debug(`ThinQ getUserNumber request -> GET ${profileUrl} headers=${JSON.stringify(maskSensitiveFields(headers))}`);
|
|
164
|
+
try {
|
|
165
|
+
const response = await axios.get(profileUrl, {
|
|
166
|
+
headers,
|
|
167
|
+
});
|
|
168
|
+
if (response.data.status === 2) {
|
|
169
|
+
throw new AuthenticationError(response.data.message ?? 'LG user profile lookup failed.');
|
|
170
|
+
}
|
|
171
|
+
return response.data.account.userNo;
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
this.logger.error(`ThinQ getUserNumber failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async getMqttRouteInfo() {
|
|
179
|
+
const data = await this.request('get', MQTT_ROUTE_URL);
|
|
180
|
+
return data.result;
|
|
181
|
+
}
|
|
182
|
+
async registerMqttClient() {
|
|
183
|
+
await this.request('post', 'service/users/client', {});
|
|
184
|
+
}
|
|
185
|
+
async requestMqttCertificate(csrBody) {
|
|
186
|
+
const data = await this.request('post', 'service/users/client/certificate', { csr: csrBody });
|
|
187
|
+
return data.result;
|
|
188
|
+
}
|
|
189
|
+
getClientId() {
|
|
190
|
+
return this.clientId ?? API_CLIENT_ID;
|
|
191
|
+
}
|
|
192
|
+
get defaultHeaders() {
|
|
193
|
+
const authHeaders = {};
|
|
194
|
+
if (this.session.accessToken) {
|
|
195
|
+
authHeaders['x-emp-token'] = this.session.accessToken;
|
|
196
|
+
}
|
|
197
|
+
if (this.userNumber) {
|
|
198
|
+
authHeaders['x-user-no'] = this.userNumber;
|
|
199
|
+
}
|
|
200
|
+
authHeaders['x-client-id'] = this.clientId ?? API_CLIENT_ID;
|
|
201
|
+
return {
|
|
202
|
+
'x-api-key': API_KEY,
|
|
203
|
+
'x-thinq-app-ver': '3.6.1200',
|
|
204
|
+
'x-thinq-app-type': 'NUTS',
|
|
205
|
+
'x-thinq-app-level': 'PRD',
|
|
206
|
+
'x-thinq-app-os': 'ANDROID',
|
|
207
|
+
'x-thinq-app-logintype': 'LGE',
|
|
208
|
+
'x-service-code': 'SVC202',
|
|
209
|
+
'x-country-code': this.country,
|
|
210
|
+
'x-language-code': this.language,
|
|
211
|
+
'x-service-phase': 'OP',
|
|
212
|
+
'x-origin': 'app-native',
|
|
213
|
+
'x-model-name': 'samsung/SM-G930L',
|
|
214
|
+
'x-os-version': 'AOS/7.1.2',
|
|
215
|
+
'x-app-version': 'LG ThinQ/3.6.12110',
|
|
216
|
+
'x-message-id': randomMessageId(),
|
|
217
|
+
'user-agent': 'okhttp/3.14.9',
|
|
218
|
+
...authHeaders,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
isTokenExpiredError(error) {
|
|
222
|
+
if (!axios.isAxiosError(error)) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
if (error.response?.status === 401) {
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
const data = error.response?.data;
|
|
229
|
+
return typeof data?.resultCode === 'string' && data.resultCode === THINQ_TOKEN_EXPIRED_RESULT_CODE;
|
|
230
|
+
}
|
|
231
|
+
async request(method, uri, data, retry = false) {
|
|
232
|
+
await this.getGateway();
|
|
233
|
+
const gateway = this.gateway;
|
|
234
|
+
if (!gateway) {
|
|
235
|
+
throw new Error('ThinQ gateway is unavailable.');
|
|
236
|
+
}
|
|
237
|
+
const url = new URL(uri, gateway.thinq2Url).href;
|
|
238
|
+
const headers = this.defaultHeaders;
|
|
239
|
+
this.logger.debug(`ThinQ request -> ${method.toUpperCase()} ${url} headers=${JSON.stringify(maskSensitiveFields(headers))} data=${JSON.stringify(maskSensitiveFields(data))}`);
|
|
240
|
+
try {
|
|
241
|
+
const response = await axios.request({ method, url, data, headers });
|
|
242
|
+
return response.data;
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
if (this.isTokenExpiredError(error)) {
|
|
246
|
+
if (retry) {
|
|
247
|
+
throw new TokenExpiredError();
|
|
248
|
+
}
|
|
249
|
+
await this.refreshToken(this.session);
|
|
250
|
+
return this.request(method, uri, data, true);
|
|
251
|
+
}
|
|
252
|
+
if (axios.isAxiosError(error)) {
|
|
253
|
+
this.logger.debug(`ThinQ request failed <- ${method.toUpperCase()} ${url} status=${error.response?.status} data=${JSON.stringify(error.response?.data)}`);
|
|
254
|
+
}
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { isAirConditionerDevice, isValidThinqDeviceId, toThinqDevice, } from '../../core/domain/entities/ThinqDevice.js';
|
|
2
|
+
export const THINQ_KEEP_ALIVE_INTERVAL_MS = 60000;
|
|
3
|
+
export class ThinqDeviceService {
|
|
4
|
+
apiClient;
|
|
5
|
+
logger;
|
|
6
|
+
pollTimer;
|
|
7
|
+
pollTickCount = 0;
|
|
8
|
+
keepAliveTimer;
|
|
9
|
+
airConditionerOnlineStatus = new Map();
|
|
10
|
+
constructor(apiClient, logger) {
|
|
11
|
+
this.apiClient = apiClient;
|
|
12
|
+
this.logger = logger;
|
|
13
|
+
}
|
|
14
|
+
async discoverDevices() {
|
|
15
|
+
const rawDevices = await this.apiClient.getListDevices();
|
|
16
|
+
return rawDevices.filter((device) => isValidThinqDeviceId(device.deviceId)).map((device) => toThinqDevice(device));
|
|
17
|
+
}
|
|
18
|
+
startPolling(intervalMs, onUpdate) {
|
|
19
|
+
this.stopPolling();
|
|
20
|
+
this.pollTickCount = 0;
|
|
21
|
+
this.logger.debug(`ThinQ polling: starting with interval=${intervalMs}ms`);
|
|
22
|
+
this.pollTimer = setInterval(() => {
|
|
23
|
+
void this.pollOnce(onUpdate);
|
|
24
|
+
}, intervalMs);
|
|
25
|
+
}
|
|
26
|
+
stopPolling() {
|
|
27
|
+
if (this.pollTimer) {
|
|
28
|
+
clearInterval(this.pollTimer);
|
|
29
|
+
this.pollTimer = undefined;
|
|
30
|
+
this.logger.debug(`ThinQ polling: stopped after ${this.pollTickCount} tick(s)`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
startKeepAlive(intervalMs = THINQ_KEEP_ALIVE_INTERVAL_MS) {
|
|
34
|
+
this.stopKeepAlive();
|
|
35
|
+
this.logger.debug(`ThinQ keep-alive: starting with interval=${intervalMs}ms`);
|
|
36
|
+
this.keepAliveTimer = setInterval(() => {
|
|
37
|
+
void this.keepAliveOnce();
|
|
38
|
+
}, intervalMs);
|
|
39
|
+
}
|
|
40
|
+
stopKeepAlive() {
|
|
41
|
+
if (this.keepAliveTimer) {
|
|
42
|
+
clearInterval(this.keepAliveTimer);
|
|
43
|
+
this.keepAliveTimer = undefined;
|
|
44
|
+
this.logger.debug(`ThinQ keep-alive: stopped`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async keepAliveOnce() {
|
|
48
|
+
this.logger.debug(`ThinQ keep-alive: tick at ${new Date().toISOString()}`);
|
|
49
|
+
for (const [deviceId, online] of this.airConditionerOnlineStatus.entries()) {
|
|
50
|
+
if (!online) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
await this.apiClient.sendKeepAlive(deviceId);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
this.logger.debug(`ThinQ keep-alive failed for ${deviceId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async pollOnce(onUpdate) {
|
|
62
|
+
this.pollTickCount += 1;
|
|
63
|
+
this.logger.debug(`ThinQ polling: tick #${this.pollTickCount} at ${new Date().toISOString()}`);
|
|
64
|
+
try {
|
|
65
|
+
const devices = await this.discoverDevices();
|
|
66
|
+
this.airConditionerOnlineStatus = new Map(devices.filter(isAirConditionerDevice).map((d) => [d.id, d.online]));
|
|
67
|
+
for (const device of devices) {
|
|
68
|
+
onUpdate(device.id, device.snapshot);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
this.logger.error(`ThinQ polling tick failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ThinqFilterState } from '../../core/domain/value-objects/ThinqFilterState.js';
|
|
2
|
+
export class ThinqFilterMonitoringService {
|
|
3
|
+
apiClient;
|
|
4
|
+
logger;
|
|
5
|
+
pollTimer;
|
|
6
|
+
pollTickCount = 0;
|
|
7
|
+
constructor(apiClient, logger) {
|
|
8
|
+
this.apiClient = apiClient;
|
|
9
|
+
this.logger = logger;
|
|
10
|
+
}
|
|
11
|
+
startPolling(intervalMs, deviceIds, onUpdate) {
|
|
12
|
+
this.stopPolling();
|
|
13
|
+
this.pollTickCount = 0;
|
|
14
|
+
this.logger.debug(`ThinQ filter monitoring: starting with interval=${intervalMs}ms, devices=${deviceIds.length}`);
|
|
15
|
+
this.pollTimer = setInterval(() => {
|
|
16
|
+
void this.pollOnce(deviceIds, onUpdate);
|
|
17
|
+
}, intervalMs);
|
|
18
|
+
}
|
|
19
|
+
stopPolling() {
|
|
20
|
+
if (this.pollTimer) {
|
|
21
|
+
clearInterval(this.pollTimer);
|
|
22
|
+
this.pollTimer = undefined;
|
|
23
|
+
this.logger.debug(`ThinQ filter monitoring: stopped after ${this.pollTickCount} tick(s)`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async pollOnce(deviceIds, onUpdate) {
|
|
27
|
+
this.pollTickCount += 1;
|
|
28
|
+
this.logger.debug(`ThinQ filter monitoring: tick #${this.pollTickCount} at ${new Date().toISOString()}`);
|
|
29
|
+
for (const deviceId of deviceIds) {
|
|
30
|
+
try {
|
|
31
|
+
const data = await this.apiClient.getFilterState(deviceId);
|
|
32
|
+
onUpdate(deviceId, ThinqFilterState.fromRaw(data));
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
this.logger.debug(`ThinQ filter monitoring failed for ${deviceId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
36
|
+
onUpdate(deviceId, undefined);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
package/dist/settings.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
const PACKAGE_JSON = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
|
|
5
|
+
const PACKAGE = JSON.parse(readFileSync(PACKAGE_JSON, 'utf-8'));
|
|
6
|
+
export const ENGINES = PACKAGE.engines;
|
|
7
|
+
export const PLUGIN_NAME = PACKAGE.name;
|
|
8
|
+
export const PLUGIN_VERSION = PACKAGE.version;
|
|
9
|
+
export const PLUGIN_URL = PACKAGE.homepage;
|