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,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry for managing plugin devices (ThinQ now, webOS in a later phase).
|
|
3
|
+
* Provides centralized storage and lookup for registered device handles, keyed by device id.
|
|
4
|
+
* Empty skeleton for Phase 0 — device shapes land alongside their family's discovery/configurator (Phase 1/3).
|
|
5
|
+
*/
|
|
6
|
+
export class DeviceRegistry {
|
|
7
|
+
private readonly devices = new Map<string, unknown>();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Register a device by id.
|
|
11
|
+
*/
|
|
12
|
+
public register(deviceId: string, device: unknown): void {
|
|
13
|
+
if (!deviceId) return;
|
|
14
|
+
this.devices.set(deviceId, device);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Unregister a device by id.
|
|
19
|
+
*/
|
|
20
|
+
public unregister(deviceId: string): void {
|
|
21
|
+
this.devices.delete(deviceId);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get a device by id.
|
|
26
|
+
*/
|
|
27
|
+
public getDevice(deviceId: string): unknown {
|
|
28
|
+
return this.devices.get(deviceId);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get all registered devices.
|
|
33
|
+
*/
|
|
34
|
+
public getAllDevices(): unknown[] {
|
|
35
|
+
return Array.from(this.devices.values());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get the number of registered devices.
|
|
40
|
+
*/
|
|
41
|
+
public get size(): number {
|
|
42
|
+
return this.devices.size;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if there are any registered devices.
|
|
47
|
+
*/
|
|
48
|
+
public hasDevices(): boolean {
|
|
49
|
+
return this.devices.size > 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Clear all registered devices.
|
|
54
|
+
*/
|
|
55
|
+
public clear(): void {
|
|
56
|
+
this.devices.clear();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform configuration manager.
|
|
3
|
+
* Provides validation and defaults over the raw plugin config.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { AnsiLogger } from 'matterbridge/logger';
|
|
7
|
+
|
|
8
|
+
import type { AirConditionerCapabilities } from '../core/domain/value-objects/AirConditionerCapabilities.js';
|
|
9
|
+
import { resolveAirConditionerCapabilities } from '../core/domain/value-objects/AirConditionerCapabilities.js';
|
|
10
|
+
import {
|
|
11
|
+
createDefaultAdvancedFeature,
|
|
12
|
+
createDefaultThinqConfig,
|
|
13
|
+
LgThinqPluginPlatformConfig,
|
|
14
|
+
type MatterOverrideSettings,
|
|
15
|
+
type ThinqAcFilterControlConfig,
|
|
16
|
+
type ThinqWasherControlConfig,
|
|
17
|
+
} from '../model/LgThinqPluginPlatformConfig.js';
|
|
18
|
+
|
|
19
|
+
const DEFAULT_THINQ_REFRESH_INTERVAL_SECONDS = 60;
|
|
20
|
+
const DEFAULT_FILTER_MONITORING_INTERVAL_SECONDS = 3600;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Manages platform configuration with validation and defaults.
|
|
24
|
+
*/
|
|
25
|
+
export class PlatformConfigManager {
|
|
26
|
+
private constructor(
|
|
27
|
+
private readonly config: LgThinqPluginPlatformConfig,
|
|
28
|
+
private readonly log: AnsiLogger,
|
|
29
|
+
) {
|
|
30
|
+
this.config.thinq ??= createDefaultThinqConfig();
|
|
31
|
+
this.config.webos ??= { devices: [] };
|
|
32
|
+
this.config.advancedFeature ??= createDefaultAdvancedFeature();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create a PlatformConfigManager with defaults applied.
|
|
37
|
+
*/
|
|
38
|
+
public static create(config: LgThinqPluginPlatformConfig, log: AnsiLogger): PlatformConfigManager {
|
|
39
|
+
return new PlatformConfigManager(config, log);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public get rawConfig(): LgThinqPluginPlatformConfig {
|
|
43
|
+
return this.config;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public get debug(): boolean {
|
|
47
|
+
return this.config.advancedFeature.settings.debug;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public get isClearStorageOnStartupEnabled(): boolean {
|
|
51
|
+
return this.config.advancedFeature.settings.clearStorageOnStartup;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public get isForceAuthenticationEnabled(): boolean {
|
|
55
|
+
return this.config.advancedFeature.settings.forceAuthentication;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public get unregisterOnShutdown(): boolean {
|
|
59
|
+
return this.config.advancedFeature.settings.unregisterOnShutdown;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public get thinqLoginType(): 'account' | 'token' {
|
|
63
|
+
return this.config.thinq.loginType;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public get thinqUsername(): string | undefined {
|
|
67
|
+
return this.config.thinq.username;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public get thinqPassword(): string | undefined {
|
|
71
|
+
return this.config.thinq.password;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public get thinqRefreshToken(): string | undefined {
|
|
75
|
+
return this.config.thinq.refreshToken;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public get country(): string {
|
|
79
|
+
return this.config.thinq.country;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public get language(): string {
|
|
83
|
+
return this.config.thinq.language;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public get thinqRefreshIntervalSeconds(): number {
|
|
87
|
+
return this.config.thinq.refreshIntervalSeconds ?? DEFAULT_THINQ_REFRESH_INTERVAL_SECONDS;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public get thinqFilterMonitoringIntervalSeconds(): number {
|
|
91
|
+
return this.config.thinq.filterMonitoringIntervalSeconds ?? DEFAULT_FILTER_MONITORING_INTERVAL_SECONDS;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public getDeviceCapabilities(deviceId: string): AirConditionerCapabilities {
|
|
95
|
+
return resolveAirConditionerCapabilities(this.config.thinq.devices, deviceId);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
public getWasherControlConfig(deviceId: string): ThinqWasherControlConfig {
|
|
99
|
+
return this.config.thinq.devices?.find((d) => d.deviceId === deviceId)?.washerControl ?? {};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public getAcFilterControlConfig(deviceId: string): ThinqAcFilterControlConfig {
|
|
103
|
+
return this.config.thinq.devices?.find((d) => d.deviceId === deviceId)?.acFilterControl ?? {};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
public get overrideMatterConfiguration(): boolean {
|
|
107
|
+
return this.config.advancedFeature.settings.overrideMatterConfiguration;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public get matterOverrideSettings(): MatterOverrideSettings {
|
|
111
|
+
return this.config.advancedFeature.settings.matterOverrideSettings;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
public getProductNameForDevice(deviceId: string): string | undefined {
|
|
115
|
+
if (!this.overrideMatterConfiguration) return undefined;
|
|
116
|
+
return this.config.thinq.devices?.find((d) => d.deviceId === deviceId)?.productName;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
public getProductIdForDevice(deviceId: string): number | undefined {
|
|
120
|
+
if (!this.overrideMatterConfiguration) return undefined;
|
|
121
|
+
return this.config.thinq.devices?.find((d) => d.deviceId === deviceId)?.productId;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
public validateConfig(): boolean {
|
|
125
|
+
this.log.debug('Validating platform config');
|
|
126
|
+
|
|
127
|
+
if (this.thinqLoginType === 'token') {
|
|
128
|
+
return Boolean(this.thinqRefreshToken);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return Boolean(this.thinqUsername && this.thinqPassword);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform state container.
|
|
3
|
+
* Holds ephemeral runtime state for the platform (startup completion, etc.).
|
|
4
|
+
* Uses strict types (no `any`) and provides simple accessor/mutator API.
|
|
5
|
+
*/
|
|
6
|
+
export class PlatformState {
|
|
7
|
+
private startupCompleted = false;
|
|
8
|
+
|
|
9
|
+
public get isStartupCompleted(): boolean {
|
|
10
|
+
return this.startupCompleted;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
public setStartupCompleted(completed: boolean): void {
|
|
14
|
+
this.startupCompleted = completed;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { MatterbridgeEndpoint } from 'matterbridge';
|
|
2
|
+
import { AnsiLogger } from 'matterbridge/logger';
|
|
3
|
+
import { FanControl, Thermostat } from 'matterbridge/matter/clusters';
|
|
4
|
+
import { isValidNumber } from 'matterbridge/utils';
|
|
5
|
+
|
|
6
|
+
import type { ThinqAirConditionerDevice } from '../../core/domain/entities/ThinqDevice.js';
|
|
7
|
+
import type { AirConditionerCapabilities } from '../../core/domain/value-objects/AirConditionerCapabilities.js';
|
|
8
|
+
import { ThinqApiClient } from '../../services/thinq/thinqApiClient.js';
|
|
9
|
+
|
|
10
|
+
/** LG `airState.windStrength` values (`homebridge-lg-thinq/src/devices/AirConditioner.ts:15,19-25`). */
|
|
11
|
+
export const THINQ_FAN_SPEED_AUTO = 8;
|
|
12
|
+
export const THINQ_FAN_SPEED_LOW = 2;
|
|
13
|
+
export const THINQ_FAN_SPEED_MEDIUM = 4;
|
|
14
|
+
export const THINQ_FAN_SPEED_HIGH = 6;
|
|
15
|
+
|
|
16
|
+
const THINQ_OPERATION_ON = 1;
|
|
17
|
+
const THINQ_OPERATION_OFF = 0;
|
|
18
|
+
|
|
19
|
+
/** LG ACs commonly step Celsius setpoints by half a degree; snapping to this avoids ThinQ 400s on off-step values. */
|
|
20
|
+
const THINQ_TEMPERATURE_STEP_CELSIUS = 0.5;
|
|
21
|
+
|
|
22
|
+
function withErrorHandling(commandName: string, logger: AnsiLogger, handler: () => Promise<void>): () => Promise<void> {
|
|
23
|
+
return async () => {
|
|
24
|
+
try {
|
|
25
|
+
await handler();
|
|
26
|
+
} catch (error) {
|
|
27
|
+
logger.error(
|
|
28
|
+
`ThinQ AirConditioner command '${commandName}' failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
29
|
+
);
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Rounds a Celsius value to the nearest multiple of `step` (e.g. 25.3 -> 25.5 for step 0.5). */
|
|
36
|
+
function roundToStep(value: number, step: number): number {
|
|
37
|
+
return Math.round(value / step) * step;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Reverts a Thermostat setpoint attribute back to `oldValue` after a failed ThinQ command, so Apple Home
|
|
42
|
+
* self-corrects immediately instead of showing a value ThinQ never accepted. Best-effort: `updateAttribute`
|
|
43
|
+
* failures are swallowed since the original command failure is already logged by `withErrorHandling`.
|
|
44
|
+
*/
|
|
45
|
+
function revertSetpointOnFailure(
|
|
46
|
+
airConditioner: MatterbridgeEndpoint,
|
|
47
|
+
attribute: 'occupiedCoolingSetpoint' | 'occupiedHeatingSetpoint',
|
|
48
|
+
oldValue: number,
|
|
49
|
+
): () => void {
|
|
50
|
+
return () => {
|
|
51
|
+
airConditioner.updateAttribute(Thermostat.id, attribute, oldValue, airConditioner.log).catch(() => {
|
|
52
|
+
// Best-effort revert; original failure already logged inside withErrorHandling.
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function mapPercentToWindStrength(percent: number): number | undefined {
|
|
58
|
+
if (percent === 0) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
if (percent <= 33) {
|
|
62
|
+
return THINQ_FAN_SPEED_LOW;
|
|
63
|
+
}
|
|
64
|
+
if (percent <= 66) {
|
|
65
|
+
return THINQ_FAN_SPEED_MEDIUM;
|
|
66
|
+
}
|
|
67
|
+
return THINQ_FAN_SPEED_HIGH;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function mapFanModeToWindStrength(fanMode: FanControl.FanMode): number | undefined {
|
|
71
|
+
switch (fanMode) {
|
|
72
|
+
case FanControl.FanMode.Auto:
|
|
73
|
+
return THINQ_FAN_SPEED_AUTO;
|
|
74
|
+
case FanControl.FanMode.Low:
|
|
75
|
+
return THINQ_FAN_SPEED_LOW;
|
|
76
|
+
case FanControl.FanMode.Medium:
|
|
77
|
+
return THINQ_FAN_SPEED_MEDIUM;
|
|
78
|
+
case FanControl.FanMode.High:
|
|
79
|
+
return THINQ_FAN_SPEED_HIGH;
|
|
80
|
+
default:
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Registers Apple Home → ThinQ command handlers on an already-constructed `AirConditioner` endpoint.
|
|
87
|
+
* `OnOff.on`/`off` and `subscribeAttribute` for setpoints/fan speed all forward to `ThinqApiClient.sendCommand`.
|
|
88
|
+
*/
|
|
89
|
+
export function registerAirConditionerCommandHandlers(
|
|
90
|
+
airConditioner: MatterbridgeEndpoint,
|
|
91
|
+
device: ThinqAirConditionerDevice,
|
|
92
|
+
apiClient: ThinqApiClient,
|
|
93
|
+
logger: AnsiLogger,
|
|
94
|
+
capabilities: AirConditionerCapabilities,
|
|
95
|
+
): void {
|
|
96
|
+
airConditioner.addCommandHandler(
|
|
97
|
+
'on',
|
|
98
|
+
withErrorHandling('on', logger, async () => {
|
|
99
|
+
logger.debug(
|
|
100
|
+
`ThinQ AirConditioner ${device.id}: 'on' command handler invoked, capabilities=${JSON.stringify(capabilities)}`,
|
|
101
|
+
);
|
|
102
|
+
await apiClient.sendCommand(device.id, {
|
|
103
|
+
command: 'Operation',
|
|
104
|
+
dataKey: 'airState.operation',
|
|
105
|
+
dataValue: THINQ_OPERATION_ON,
|
|
106
|
+
});
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
airConditioner.addCommandHandler(
|
|
111
|
+
'off',
|
|
112
|
+
withErrorHandling('off', logger, async () => {
|
|
113
|
+
logger.debug(
|
|
114
|
+
`ThinQ AirConditioner ${device.id}: 'off' command handler invoked, capabilities=${JSON.stringify(capabilities)}`,
|
|
115
|
+
);
|
|
116
|
+
await apiClient.sendCommand(device.id, {
|
|
117
|
+
command: 'Operation',
|
|
118
|
+
dataKey: 'airState.operation',
|
|
119
|
+
dataValue: THINQ_OPERATION_OFF,
|
|
120
|
+
});
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
airConditioner.subscribeAttribute(
|
|
125
|
+
Thermostat.id,
|
|
126
|
+
'occupiedCoolingSetpoint',
|
|
127
|
+
(newValue: number, oldValue: number, context) => {
|
|
128
|
+
if (context.fabric === undefined) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
logger.debug(
|
|
132
|
+
`ThinQ AirConditioner ${device.id}: 'occupiedCoolingSetpoint' command handler invoked, newValue=${newValue}, capabilities=${JSON.stringify(capabilities)}`,
|
|
133
|
+
);
|
|
134
|
+
void withErrorHandling('occupiedCoolingSetpoint', logger, async () => {
|
|
135
|
+
await apiClient.sendCommand(device.id, {
|
|
136
|
+
dataKey: 'airState.tempState.target',
|
|
137
|
+
dataValue: roundToStep(newValue / 100, THINQ_TEMPERATURE_STEP_CELSIUS),
|
|
138
|
+
});
|
|
139
|
+
})().catch(revertSetpointOnFailure(airConditioner, 'occupiedCoolingSetpoint', oldValue));
|
|
140
|
+
},
|
|
141
|
+
airConditioner.log,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
if (capabilities.supportsHeat) {
|
|
145
|
+
airConditioner.subscribeAttribute(
|
|
146
|
+
Thermostat.id,
|
|
147
|
+
'occupiedHeatingSetpoint',
|
|
148
|
+
(newValue: number, oldValue: number, context) => {
|
|
149
|
+
if (context.fabric === undefined) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
logger.debug(
|
|
153
|
+
`ThinQ AirConditioner ${device.id}: 'occupiedHeatingSetpoint' command handler invoked, newValue=${newValue}, capabilities=${JSON.stringify(capabilities)}`,
|
|
154
|
+
);
|
|
155
|
+
void withErrorHandling('occupiedHeatingSetpoint', logger, async () => {
|
|
156
|
+
await apiClient.sendCommand(device.id, {
|
|
157
|
+
dataKey: 'airState.tempState.target',
|
|
158
|
+
dataValue: roundToStep(newValue / 100, THINQ_TEMPERATURE_STEP_CELSIUS),
|
|
159
|
+
});
|
|
160
|
+
})().catch(revertSetpointOnFailure(airConditioner, 'occupiedHeatingSetpoint', oldValue));
|
|
161
|
+
},
|
|
162
|
+
airConditioner.log,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
airConditioner.subscribeAttribute(
|
|
167
|
+
FanControl,
|
|
168
|
+
'percentSetting',
|
|
169
|
+
(newValue, _oldValue, context) => {
|
|
170
|
+
if (context.fabric === undefined) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
logger.debug(
|
|
174
|
+
`ThinQ AirConditioner ${device.id}: 'percentSetting' command handler invoked, newValue=${newValue}, capabilities=${JSON.stringify(capabilities)}`,
|
|
175
|
+
);
|
|
176
|
+
if (!capabilities.supportsFanSpeedControl) {
|
|
177
|
+
logger.debug(
|
|
178
|
+
`ThinQ AirConditioner ${device.id}: ignoring percentSetting change — fan speed control not supported per configured capabilities.`,
|
|
179
|
+
);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (newValue === null || !isValidNumber(newValue, 0, 100)) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const windStrength = mapPercentToWindStrength(newValue);
|
|
186
|
+
if (windStrength === undefined) {
|
|
187
|
+
logger.debug(`ThinQ AirConditioner ${device.id}: ignoring percentSetting=0 (ambiguous, no ThinQ equivalent).`);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
void withErrorHandling('percentSetting', logger, async () => {
|
|
191
|
+
await apiClient.sendCommand(device.id, {
|
|
192
|
+
dataKey: 'airState.windStrength',
|
|
193
|
+
dataValue: windStrength,
|
|
194
|
+
});
|
|
195
|
+
})();
|
|
196
|
+
},
|
|
197
|
+
airConditioner.log,
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
airConditioner.subscribeAttribute(
|
|
201
|
+
FanControl,
|
|
202
|
+
'fanMode',
|
|
203
|
+
(newValue: FanControl.FanMode, _oldValue: FanControl.FanMode, context) => {
|
|
204
|
+
if (context.fabric === undefined) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
logger.debug(
|
|
208
|
+
`ThinQ AirConditioner ${device.id}: 'fanMode' command handler invoked, newValue=${FanControl.FanMode[newValue]}, capabilities=${JSON.stringify(capabilities)}`,
|
|
209
|
+
);
|
|
210
|
+
if (!capabilities.supportsFanSpeedControl) {
|
|
211
|
+
logger.debug(
|
|
212
|
+
`ThinQ AirConditioner ${device.id}: ignoring fanMode change — fan speed control not supported per configured capabilities.`,
|
|
213
|
+
);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const windStrength = mapFanModeToWindStrength(newValue);
|
|
217
|
+
if (windStrength === undefined) {
|
|
218
|
+
logger.debug(
|
|
219
|
+
`ThinQ AirConditioner ${device.id}: ignoring fanMode=${FanControl.FanMode[newValue]} (no ThinQ equivalent).`,
|
|
220
|
+
);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
void withErrorHandling('fanMode', logger, async () => {
|
|
224
|
+
await apiClient.sendCommand(device.id, {
|
|
225
|
+
dataKey: 'airState.windStrength',
|
|
226
|
+
dataValue: windStrength,
|
|
227
|
+
});
|
|
228
|
+
})();
|
|
229
|
+
},
|
|
230
|
+
airConditioner.log,
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
if (capabilities.supportsSwingMode) {
|
|
234
|
+
airConditioner.subscribeAttribute(
|
|
235
|
+
FanControl,
|
|
236
|
+
'rockSetting',
|
|
237
|
+
(newValue: unknown, oldValue: unknown, context) => {
|
|
238
|
+
if (context.fabric === undefined) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const newRock = newValue as Record<string, boolean> | undefined;
|
|
243
|
+
const oldRock = oldValue as Record<string, boolean> | undefined;
|
|
244
|
+
|
|
245
|
+
logger.debug(
|
|
246
|
+
`ThinQ AirConditioner ${device.id}: 'rockSetting' command handler invoked, newValue=${JSON.stringify(newRock)}, capabilities=${JSON.stringify(capabilities)}`,
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
if (!newRock || !oldRock) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const verticalChanged = newRock.rockUpDown !== oldRock.rockUpDown;
|
|
254
|
+
const horizontalChanged = newRock.rockLeftRight !== oldRock.rockLeftRight;
|
|
255
|
+
|
|
256
|
+
if (!verticalChanged && !horizontalChanged) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
void withErrorHandling('rockSetting', logger, async () => {
|
|
261
|
+
if (verticalChanged && horizontalChanged && newRock.rockUpDown && newRock.rockLeftRight) {
|
|
262
|
+
await apiClient.sendCommand(device.id, {
|
|
263
|
+
dataKey: null,
|
|
264
|
+
dataValue: null,
|
|
265
|
+
command: 'Set',
|
|
266
|
+
ctrlKey: 'favoriteCtrl',
|
|
267
|
+
dataSetList: {
|
|
268
|
+
'airState.wDir.vStep': '100',
|
|
269
|
+
'airState.wDir.hStep': '100',
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
} else {
|
|
273
|
+
if (verticalChanged) {
|
|
274
|
+
await apiClient.sendCommand(device.id, {
|
|
275
|
+
dataKey: 'airState.wDir.vStep',
|
|
276
|
+
dataValue: newRock.rockUpDown ? '100' : '0',
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
if (horizontalChanged) {
|
|
280
|
+
await apiClient.sendCommand(device.id, {
|
|
281
|
+
dataKey: 'airState.wDir.hStep',
|
|
282
|
+
dataValue: newRock.rockLeftRight ? '100' : '0',
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
})();
|
|
287
|
+
},
|
|
288
|
+
airConditioner.log,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import {
|
|
2
|
+
airQualitySensor,
|
|
3
|
+
electricalSensor,
|
|
4
|
+
humiditySensor,
|
|
5
|
+
MatterbridgeEndpoint,
|
|
6
|
+
powerSource,
|
|
7
|
+
roomAirConditioner,
|
|
8
|
+
} from 'matterbridge';
|
|
9
|
+
import { AirQuality, FanControl, PowerTopology } from 'matterbridge/matter/clusters';
|
|
10
|
+
|
|
11
|
+
import type { ThinqAirConditionerDevice } from '../../core/domain/entities/ThinqDevice.js';
|
|
12
|
+
import type { AirConditionerCapabilities } from '../../core/domain/value-objects/AirConditionerCapabilities.js';
|
|
13
|
+
|
|
14
|
+
export interface AirConditionerEndpointSetpoints {
|
|
15
|
+
currentTemperature: number;
|
|
16
|
+
targetTemperature: number;
|
|
17
|
+
minHeatSetpointLimitCelsius: number;
|
|
18
|
+
maxHeatSetpointLimitCelsius: number;
|
|
19
|
+
minCoolSetpointLimitCelsius: number;
|
|
20
|
+
maxCoolSetpointLimitCelsius: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Hand-composes an `AirConditioner`-shaped `MatterbridgeEndpoint`, mirroring
|
|
25
|
+
* `@matterbridge/core`'s own `AirConditioner` constructor, but branching the
|
|
26
|
+
* Thermostat/FanControl cluster construction on the device's configured capabilities
|
|
27
|
+
* (`Behaviors.require()` throws on a 2nd call for the same cluster id, so the feature
|
|
28
|
+
* set must be chosen up-front — see `.claude/memory.md`).
|
|
29
|
+
*/
|
|
30
|
+
export interface BuildAirConditionerEndpointOptions {
|
|
31
|
+
vendorId?: number;
|
|
32
|
+
vendorName?: string;
|
|
33
|
+
productId?: number;
|
|
34
|
+
productName?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function buildAirConditionerEndpoint(
|
|
38
|
+
device: ThinqAirConditionerDevice,
|
|
39
|
+
capabilities: AirConditionerCapabilities,
|
|
40
|
+
setpoints: AirConditionerEndpointSetpoints,
|
|
41
|
+
initialFanMode: FanControl.FanMode,
|
|
42
|
+
options?: BuildAirConditionerEndpointOptions,
|
|
43
|
+
): MatterbridgeEndpoint {
|
|
44
|
+
const {
|
|
45
|
+
currentTemperature,
|
|
46
|
+
targetTemperature,
|
|
47
|
+
minHeatSetpointLimitCelsius,
|
|
48
|
+
maxHeatSetpointLimitCelsius,
|
|
49
|
+
minCoolSetpointLimitCelsius,
|
|
50
|
+
maxCoolSetpointLimitCelsius,
|
|
51
|
+
} = setpoints;
|
|
52
|
+
|
|
53
|
+
const endpoint = new MatterbridgeEndpoint(
|
|
54
|
+
capabilities.supportsEnergyMonitoring
|
|
55
|
+
? [roomAirConditioner, powerSource, electricalSensor]
|
|
56
|
+
: [roomAirConditioner, powerSource],
|
|
57
|
+
{
|
|
58
|
+
id: `${device.name.replaceAll(' ', '')}-${device.id.replaceAll(' ', '')}`,
|
|
59
|
+
},
|
|
60
|
+
)
|
|
61
|
+
.createDefaultIdentifyClusterServer()
|
|
62
|
+
.createDefaultBasicInformationClusterServer(
|
|
63
|
+
device.name,
|
|
64
|
+
device.id,
|
|
65
|
+
options?.vendorId ?? 0xfff1,
|
|
66
|
+
options?.vendorName ?? 'Matterbridge',
|
|
67
|
+
options?.productId ?? 0x8000,
|
|
68
|
+
options?.productName ?? 'Matterbridge Air Conditioner',
|
|
69
|
+
)
|
|
70
|
+
.createDefaultPowerSourceWiredClusterServer()
|
|
71
|
+
.createDeadFrontOnOffClusterServer(true);
|
|
72
|
+
|
|
73
|
+
if (capabilities.supportsHeat) {
|
|
74
|
+
endpoint.createDefaultThermostatClusterServer(
|
|
75
|
+
currentTemperature,
|
|
76
|
+
targetTemperature,
|
|
77
|
+
targetTemperature,
|
|
78
|
+
1,
|
|
79
|
+
minHeatSetpointLimitCelsius,
|
|
80
|
+
maxHeatSetpointLimitCelsius,
|
|
81
|
+
minCoolSetpointLimitCelsius,
|
|
82
|
+
maxCoolSetpointLimitCelsius,
|
|
83
|
+
);
|
|
84
|
+
} else {
|
|
85
|
+
endpoint.createDefaultCoolingThermostatClusterServer(
|
|
86
|
+
currentTemperature,
|
|
87
|
+
targetTemperature,
|
|
88
|
+
minCoolSetpointLimitCelsius,
|
|
89
|
+
maxCoolSetpointLimitCelsius,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
endpoint.createDefaultThermostatUserInterfaceConfigurationClusterServer();
|
|
93
|
+
|
|
94
|
+
if (capabilities.supportsFanSpeedControl && capabilities.supportsSwingMode) {
|
|
95
|
+
endpoint.createCompleteFanControlClusterServer(
|
|
96
|
+
initialFanMode,
|
|
97
|
+
FanControl.FanModeSequence.OffLowMedHighAuto,
|
|
98
|
+
0,
|
|
99
|
+
0,
|
|
100
|
+
undefined,
|
|
101
|
+
undefined,
|
|
102
|
+
undefined,
|
|
103
|
+
{ rockLeftRight: true, rockUpDown: true, rockRound: true },
|
|
104
|
+
{ rockLeftRight: false, rockUpDown: false, rockRound: false },
|
|
105
|
+
);
|
|
106
|
+
} else if (capabilities.supportsFanSpeedControl) {
|
|
107
|
+
endpoint.createDefaultFanControlClusterServer(initialFanMode, FanControl.FanModeSequence.OffLowMedHighAuto, 0, 0);
|
|
108
|
+
} else {
|
|
109
|
+
endpoint.createOnOffFanControlClusterServer(initialFanMode);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (capabilities.supportsHumiditySensor) {
|
|
113
|
+
// RelativeHumidityMeasurement is spec-legal directly on roomAirConditioner, but Apple Home only builds a
|
|
114
|
+
// Humidity service from an endpoint actually typed humiditySensor (confirmed by regression: moving this
|
|
115
|
+
// onto the AC's own endpoint made the reading disappear from Apple Home entirely). Keep as a child.
|
|
116
|
+
endpoint
|
|
117
|
+
.addChildDeviceType('HumiditySensor', [humiditySensor])
|
|
118
|
+
.createDefaultIdentifyClusterServer()
|
|
119
|
+
.createDefaultRelativeHumidityMeasurementClusterServer(0);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (capabilities.supportsAirQualitySensor) {
|
|
123
|
+
endpoint
|
|
124
|
+
.addChildDeviceType('AirQualitySensor', [airQualitySensor])
|
|
125
|
+
.createDefaultIdentifyClusterServer()
|
|
126
|
+
.createDefaultAirQualityClusterServer(AirQuality.AirQualityEnum.Unknown)
|
|
127
|
+
.createDefaultPm25ConcentrationMeasurementClusterServer()
|
|
128
|
+
.createDefaultPm10ConcentrationMeasurementClusterServer();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (capabilities.supportsEnergyMonitoring) {
|
|
132
|
+
// Electrical measurement lives on the AC endpoint itself (electricalSensor device type). PowerTopology is
|
|
133
|
+
// mandatory for electricalSensor; NodeTopology because the AC is now its own standalone Matter node (server
|
|
134
|
+
// mode), not shared with a bridge. activePower starts at 0 (not null) because Apple may hide null.
|
|
135
|
+
endpoint
|
|
136
|
+
.createDefaultPowerTopologyClusterServer(PowerTopology.Feature.NodeTopology)
|
|
137
|
+
.createDefaultElectricalPowerMeasurementClusterServer(null, null, 0, null);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (capabilities.supportsFilterMonitoring) {
|
|
141
|
+
endpoint.createDefaultHepaFilterMonitoringClusterServer();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return endpoint;
|
|
145
|
+
}
|