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,993 @@
|
|
|
1
|
+
import { FanControl, PowerTopology } from 'matterbridge/matter/clusters';
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import type { ThinqAirConditionerDevice } from '../../../core/domain/entities/ThinqDevice.js';
|
|
5
|
+
import type { AirConditionerCapabilities } from '../../../core/domain/value-objects/AirConditionerCapabilities.js';
|
|
6
|
+
import { DEFAULT_AIR_CONDITIONER_CAPABILITIES } from '../../../core/domain/value-objects/AirConditionerCapabilities.js';
|
|
7
|
+
import { buildAirConditionerEndpoint } from '../../../platform/thinq/thinqAirConditionerEndpointFactory.js';
|
|
8
|
+
import { asPartial } from '../../helpers/testUtils.js';
|
|
9
|
+
|
|
10
|
+
// Use hoisted to set up mocks before vi.mock is processed
|
|
11
|
+
const { MatterbridgeEndpointMockFn } = vi.hoisted(() => {
|
|
12
|
+
let mockEndpointInstance: any;
|
|
13
|
+
|
|
14
|
+
const MatterbridgeEndpointMockFn = vi.fn(function () {
|
|
15
|
+
return mockEndpointInstance;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Store the setter on the mock function itself
|
|
19
|
+
(MatterbridgeEndpointMockFn as any).setEndpoint = (endpoint: any) => {
|
|
20
|
+
mockEndpointInstance = endpoint;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return { MatterbridgeEndpointMockFn };
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
vi.mock('matterbridge', () => ({
|
|
27
|
+
MatterbridgeEndpoint: MatterbridgeEndpointMockFn,
|
|
28
|
+
roomAirConditioner: { id: 'roomAirConditioner' },
|
|
29
|
+
powerSource: { id: 'powerSource' },
|
|
30
|
+
humiditySensor: { id: 'humiditySensor' },
|
|
31
|
+
airQualitySensor: { id: 'airQualitySensor' },
|
|
32
|
+
electricalSensor: { id: 'electricalSensor' },
|
|
33
|
+
genericSwitch: { id: 'genericSwitch' },
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
vi.mock('../../../platform/thinq/thinqAirConditionerAuxiliaryToggles.js', () => ({
|
|
37
|
+
addAuxiliaryToggleEndpoints: vi.fn(),
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
function createChainableMock() {
|
|
41
|
+
return {
|
|
42
|
+
createDefaultIdentifyClusterServer: vi.fn().mockReturnThis(),
|
|
43
|
+
createDefaultBasicInformationClusterServer: vi.fn().mockReturnThis(),
|
|
44
|
+
createDefaultPowerSourceWiredClusterServer: vi.fn().mockReturnThis(),
|
|
45
|
+
createDeadFrontOnOffClusterServer: vi.fn().mockReturnThis(),
|
|
46
|
+
createDefaultThermostatClusterServer: vi.fn().mockReturnThis(),
|
|
47
|
+
createDefaultCoolingThermostatClusterServer: vi.fn().mockReturnThis(),
|
|
48
|
+
createDefaultThermostatUserInterfaceConfigurationClusterServer: vi.fn().mockReturnThis(),
|
|
49
|
+
createDefaultFanControlClusterServer: vi.fn().mockReturnThis(),
|
|
50
|
+
createCompleteFanControlClusterServer: vi.fn().mockReturnThis(),
|
|
51
|
+
createOnOffFanControlClusterServer: vi.fn().mockReturnThis(),
|
|
52
|
+
addChildDeviceType: vi.fn().mockReturnThis(),
|
|
53
|
+
createDefaultRelativeHumidityMeasurementClusterServer: vi.fn().mockReturnThis(),
|
|
54
|
+
createDefaultAirQualityClusterServer: vi.fn().mockReturnThis(),
|
|
55
|
+
createDefaultPm25ConcentrationMeasurementClusterServer: vi.fn().mockReturnThis(),
|
|
56
|
+
createDefaultPm10ConcentrationMeasurementClusterServer: vi.fn().mockReturnThis(),
|
|
57
|
+
createDefaultPowerTopologyClusterServer: vi.fn().mockReturnThis(),
|
|
58
|
+
createDefaultElectricalPowerMeasurementClusterServer: vi.fn().mockReturnThis(),
|
|
59
|
+
createDefaultHepaFilterMonitoringClusterServer: vi.fn().mockReturnThis(),
|
|
60
|
+
createDefaultMomentarySwitchClusterServer: vi.fn().mockReturnThis(),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe('buildAirConditionerEndpoint', () => {
|
|
65
|
+
let mockDevice: ThinqAirConditionerDevice;
|
|
66
|
+
let setpoints: {
|
|
67
|
+
currentTemperature: number;
|
|
68
|
+
targetTemperature: number;
|
|
69
|
+
minHeatSetpointLimitCelsius: number;
|
|
70
|
+
maxHeatSetpointLimitCelsius: number;
|
|
71
|
+
minCoolSetpointLimitCelsius: number;
|
|
72
|
+
maxCoolSetpointLimitCelsius: number;
|
|
73
|
+
};
|
|
74
|
+
let mockEndpoint: any;
|
|
75
|
+
|
|
76
|
+
beforeEach(() => {
|
|
77
|
+
vi.clearAllMocks();
|
|
78
|
+
|
|
79
|
+
// Create a fresh chainable mock for each test
|
|
80
|
+
mockEndpoint = createChainableMock();
|
|
81
|
+
|
|
82
|
+
// Set the endpoint for the mock constructor
|
|
83
|
+
(MatterbridgeEndpointMockFn as any).setEndpoint(mockEndpoint);
|
|
84
|
+
|
|
85
|
+
mockDevice = asPartial<ThinqAirConditionerDevice>({
|
|
86
|
+
name: 'Living Room AC',
|
|
87
|
+
id: '12345678-1234-1234-1234-123456789012',
|
|
88
|
+
type: 'AC',
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
setpoints = {
|
|
92
|
+
currentTemperature: 24,
|
|
93
|
+
targetTemperature: 22,
|
|
94
|
+
minHeatSetpointLimitCelsius: 16,
|
|
95
|
+
maxHeatSetpointLimitCelsius: 30,
|
|
96
|
+
minCoolSetpointLimitCelsius: 16,
|
|
97
|
+
maxCoolSetpointLimitCelsius: 30,
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('endpoint construction', () => {
|
|
102
|
+
it('should construct MatterbridgeEndpoint with roomAirConditioner and powerSource devices', () => {
|
|
103
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
104
|
+
supportsHeat: true,
|
|
105
|
+
supportsFanSpeedControl: true,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
109
|
+
|
|
110
|
+
expect(MatterbridgeEndpointMockFn).toHaveBeenCalledWith(
|
|
111
|
+
expect.any(Array),
|
|
112
|
+
expect.objectContaining({
|
|
113
|
+
id: expect.any(String),
|
|
114
|
+
}),
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should remove spaces from device name and id in endpoint id', () => {
|
|
119
|
+
mockDevice = asPartial<ThinqAirConditionerDevice>({
|
|
120
|
+
name: 'Living Room AC With Spaces',
|
|
121
|
+
id: 'device id with spaces',
|
|
122
|
+
type: 'AC',
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
126
|
+
supportsHeat: true,
|
|
127
|
+
supportsFanSpeedControl: true,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
131
|
+
|
|
132
|
+
expect(MatterbridgeEndpointMockFn).toHaveBeenCalledWith(
|
|
133
|
+
expect.any(Array),
|
|
134
|
+
expect.objectContaining({
|
|
135
|
+
id: 'LivingRoomACWithSpaces-deviceidwithspaces',
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should call createDefaultIdentifyClusterServer', () => {
|
|
141
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
142
|
+
supportsHeat: true,
|
|
143
|
+
supportsFanSpeedControl: true,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
147
|
+
|
|
148
|
+
expect(mockEndpoint.createDefaultIdentifyClusterServer).toHaveBeenCalledTimes(1);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should call createDefaultBasicInformationClusterServer with correct args', () => {
|
|
152
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
153
|
+
supportsHeat: true,
|
|
154
|
+
supportsFanSpeedControl: true,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
158
|
+
|
|
159
|
+
expect(mockEndpoint.createDefaultBasicInformationClusterServer).toHaveBeenCalledWith(
|
|
160
|
+
mockDevice.name,
|
|
161
|
+
mockDevice.id,
|
|
162
|
+
0xfff1,
|
|
163
|
+
'Matterbridge',
|
|
164
|
+
0x8000,
|
|
165
|
+
'Matterbridge Air Conditioner',
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should call createDefaultPowerSourceWiredClusterServer', () => {
|
|
170
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
171
|
+
supportsHeat: true,
|
|
172
|
+
supportsFanSpeedControl: true,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
176
|
+
|
|
177
|
+
expect(mockEndpoint.createDefaultPowerSourceWiredClusterServer).toHaveBeenCalledTimes(1);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should call createDeadFrontOnOffClusterServer with true', () => {
|
|
181
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
182
|
+
supportsHeat: true,
|
|
183
|
+
supportsFanSpeedControl: true,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
187
|
+
|
|
188
|
+
expect(mockEndpoint.createDeadFrontOnOffClusterServer).toHaveBeenCalledWith(true);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should call createDefaultThermostatUserInterfaceConfigurationClusterServer', () => {
|
|
192
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
193
|
+
supportsHeat: true,
|
|
194
|
+
supportsFanSpeedControl: true,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
198
|
+
|
|
199
|
+
expect(mockEndpoint.createDefaultThermostatUserInterfaceConfigurationClusterServer).toHaveBeenCalledTimes(1);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('thermostat cluster selection when supportsHeat=true', () => {
|
|
204
|
+
it('should call createDefaultThermostatClusterServer with correct args', () => {
|
|
205
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
206
|
+
supportsHeat: true,
|
|
207
|
+
supportsFanSpeedControl: true,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
211
|
+
|
|
212
|
+
expect(mockEndpoint.createDefaultThermostatClusterServer).toHaveBeenCalledWith(
|
|
213
|
+
setpoints.currentTemperature,
|
|
214
|
+
setpoints.targetTemperature,
|
|
215
|
+
setpoints.targetTemperature,
|
|
216
|
+
1,
|
|
217
|
+
setpoints.minHeatSetpointLimitCelsius,
|
|
218
|
+
setpoints.maxHeatSetpointLimitCelsius,
|
|
219
|
+
setpoints.minCoolSetpointLimitCelsius,
|
|
220
|
+
setpoints.maxCoolSetpointLimitCelsius,
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should not call createDefaultCoolingThermostatClusterServer', () => {
|
|
225
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
226
|
+
supportsHeat: true,
|
|
227
|
+
supportsFanSpeedControl: true,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
231
|
+
|
|
232
|
+
expect(mockEndpoint.createDefaultCoolingThermostatClusterServer).not.toHaveBeenCalled();
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe('thermostat cluster selection when supportsHeat=false', () => {
|
|
237
|
+
it('should call createDefaultCoolingThermostatClusterServer with correct args', () => {
|
|
238
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
239
|
+
supportsHeat: false,
|
|
240
|
+
supportsFanSpeedControl: true,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
244
|
+
|
|
245
|
+
expect(mockEndpoint.createDefaultCoolingThermostatClusterServer).toHaveBeenCalledWith(
|
|
246
|
+
setpoints.currentTemperature,
|
|
247
|
+
setpoints.targetTemperature,
|
|
248
|
+
setpoints.minCoolSetpointLimitCelsius,
|
|
249
|
+
setpoints.maxCoolSetpointLimitCelsius,
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should not call createDefaultThermostatClusterServer', () => {
|
|
254
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
255
|
+
supportsHeat: false,
|
|
256
|
+
supportsFanSpeedControl: true,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
260
|
+
|
|
261
|
+
expect(mockEndpoint.createDefaultThermostatClusterServer).not.toHaveBeenCalled();
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe('fan control cluster selection when supportsFanSpeedControl=true', () => {
|
|
266
|
+
it('should call createDefaultFanControlClusterServer with correct args', () => {
|
|
267
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
268
|
+
supportsHeat: true,
|
|
269
|
+
supportsFanSpeedControl: true,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const initialFanMode = FanControl.FanMode.Low;
|
|
273
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, initialFanMode);
|
|
274
|
+
|
|
275
|
+
expect(mockEndpoint.createDefaultFanControlClusterServer).toHaveBeenCalledWith(
|
|
276
|
+
initialFanMode,
|
|
277
|
+
FanControl.FanModeSequence.OffLowMedHighAuto,
|
|
278
|
+
0,
|
|
279
|
+
0,
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('should not call createOnOffFanControlClusterServer', () => {
|
|
284
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
285
|
+
supportsHeat: true,
|
|
286
|
+
supportsFanSpeedControl: true,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const initialFanMode = FanControl.FanMode.Low;
|
|
290
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, initialFanMode);
|
|
291
|
+
|
|
292
|
+
expect(mockEndpoint.createOnOffFanControlClusterServer).not.toHaveBeenCalled();
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
describe('fan control cluster selection when supportsFanSpeedControl=false', () => {
|
|
297
|
+
it('should call createOnOffFanControlClusterServer with correct args', () => {
|
|
298
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
299
|
+
supportsHeat: true,
|
|
300
|
+
supportsFanSpeedControl: false,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const initialFanMode = FanControl.FanMode.Low;
|
|
304
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, initialFanMode);
|
|
305
|
+
|
|
306
|
+
expect(mockEndpoint.createOnOffFanControlClusterServer).toHaveBeenCalledWith(initialFanMode);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('should not call createDefaultFanControlClusterServer', () => {
|
|
310
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
311
|
+
supportsHeat: true,
|
|
312
|
+
supportsFanSpeedControl: false,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const initialFanMode = FanControl.FanMode.Low;
|
|
316
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, initialFanMode);
|
|
317
|
+
|
|
318
|
+
expect(mockEndpoint.createDefaultFanControlClusterServer).not.toHaveBeenCalled();
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe('return value', () => {
|
|
323
|
+
it('should return the chainable endpoint', () => {
|
|
324
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
325
|
+
supportsHeat: true,
|
|
326
|
+
supportsFanSpeedControl: true,
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const result = buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
330
|
+
|
|
331
|
+
expect(result).toBe(mockEndpoint);
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
describe('swing mode (Phase B)', () => {
|
|
336
|
+
it('should call createCompleteFanControlClusterServer when supportsFanSpeedControl and supportsSwingMode are both true', () => {
|
|
337
|
+
// Arrange
|
|
338
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
339
|
+
supportsHeat: true,
|
|
340
|
+
supportsFanSpeedControl: true,
|
|
341
|
+
supportsSwingMode: true,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const initialFanMode = FanControl.FanMode.Low;
|
|
345
|
+
|
|
346
|
+
// Act
|
|
347
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, initialFanMode);
|
|
348
|
+
|
|
349
|
+
// Assert
|
|
350
|
+
expect(mockEndpoint.createCompleteFanControlClusterServer).toHaveBeenCalledWith(
|
|
351
|
+
initialFanMode,
|
|
352
|
+
FanControl.FanModeSequence.OffLowMedHighAuto,
|
|
353
|
+
0,
|
|
354
|
+
0,
|
|
355
|
+
undefined,
|
|
356
|
+
undefined,
|
|
357
|
+
undefined,
|
|
358
|
+
{ rockLeftRight: true, rockUpDown: true, rockRound: true },
|
|
359
|
+
{ rockLeftRight: false, rockUpDown: false, rockRound: false },
|
|
360
|
+
);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('should not call createDefaultFanControlClusterServer when supportsSwingMode is true', () => {
|
|
364
|
+
// Arrange
|
|
365
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
366
|
+
supportsHeat: true,
|
|
367
|
+
supportsFanSpeedControl: true,
|
|
368
|
+
supportsSwingMode: true,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// Act
|
|
372
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
373
|
+
|
|
374
|
+
// Assert
|
|
375
|
+
expect(mockEndpoint.createDefaultFanControlClusterServer).not.toHaveBeenCalled();
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('should call createDefaultFanControlClusterServer when supportsFanSpeedControl is true but supportsSwingMode is false', () => {
|
|
379
|
+
// Arrange
|
|
380
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
381
|
+
supportsHeat: true,
|
|
382
|
+
supportsFanSpeedControl: true,
|
|
383
|
+
supportsSwingMode: false,
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
const initialFanMode = FanControl.FanMode.Low;
|
|
387
|
+
|
|
388
|
+
// Act
|
|
389
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, initialFanMode);
|
|
390
|
+
|
|
391
|
+
// Assert
|
|
392
|
+
expect(mockEndpoint.createDefaultFanControlClusterServer).toHaveBeenCalledWith(
|
|
393
|
+
initialFanMode,
|
|
394
|
+
FanControl.FanModeSequence.OffLowMedHighAuto,
|
|
395
|
+
0,
|
|
396
|
+
0,
|
|
397
|
+
);
|
|
398
|
+
expect(mockEndpoint.createCompleteFanControlClusterServer).not.toHaveBeenCalled();
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('should call createOnOffFanControlClusterServer when supportsFanSpeedControl is false, even if supportsSwingMode is true', () => {
|
|
402
|
+
// Arrange
|
|
403
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
404
|
+
supportsHeat: true,
|
|
405
|
+
supportsFanSpeedControl: false,
|
|
406
|
+
supportsSwingMode: true,
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
const initialFanMode = FanControl.FanMode.Low;
|
|
410
|
+
|
|
411
|
+
// Act
|
|
412
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, initialFanMode);
|
|
413
|
+
|
|
414
|
+
// Assert
|
|
415
|
+
expect(mockEndpoint.createOnOffFanControlClusterServer).toHaveBeenCalledWith(initialFanMode);
|
|
416
|
+
expect(mockEndpoint.createCompleteFanControlClusterServer).not.toHaveBeenCalled();
|
|
417
|
+
expect(mockEndpoint.createDefaultFanControlClusterServer).not.toHaveBeenCalled();
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
describe('humidity sensor child endpoint (Phase C)', () => {
|
|
422
|
+
it('should create HumiditySensor child endpoint when supportsHumiditySensor is true', () => {
|
|
423
|
+
// Arrange
|
|
424
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
425
|
+
supportsHeat: true,
|
|
426
|
+
supportsFanSpeedControl: true,
|
|
427
|
+
supportsHumiditySensor: true,
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// Act
|
|
431
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
432
|
+
|
|
433
|
+
// Assert
|
|
434
|
+
expect(mockEndpoint.addChildDeviceType).toHaveBeenCalledWith('HumiditySensor', expect.any(Array));
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('should create RelativeHumidityMeasurementClusterServer on HumiditySensor child', () => {
|
|
438
|
+
// Arrange
|
|
439
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
440
|
+
supportsHeat: true,
|
|
441
|
+
supportsFanSpeedControl: true,
|
|
442
|
+
supportsHumiditySensor: true,
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// Act
|
|
446
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
447
|
+
|
|
448
|
+
// Assert
|
|
449
|
+
expect(mockEndpoint.createDefaultRelativeHumidityMeasurementClusterServer).toHaveBeenCalledWith(0);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('should not create HumiditySensor child endpoint when supportsHumiditySensor is false', () => {
|
|
453
|
+
// Arrange
|
|
454
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
455
|
+
supportsHeat: true,
|
|
456
|
+
supportsFanSpeedControl: true,
|
|
457
|
+
supportsHumiditySensor: false,
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// Act
|
|
461
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
462
|
+
|
|
463
|
+
// Assert
|
|
464
|
+
const calls = (mockEndpoint.addChildDeviceType as any).mock.calls;
|
|
465
|
+
const humidityCallExists = calls.some((call: any[]) => call[0] === 'HumiditySensor');
|
|
466
|
+
expect(humidityCallExists).toBe(false);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('should not call createDefaultRelativeHumidityMeasurementClusterServer when supportsHumiditySensor is false', () => {
|
|
470
|
+
// Arrange
|
|
471
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
472
|
+
supportsHeat: true,
|
|
473
|
+
supportsFanSpeedControl: true,
|
|
474
|
+
supportsHumiditySensor: false,
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// Act
|
|
478
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
479
|
+
|
|
480
|
+
// Assert
|
|
481
|
+
expect(mockEndpoint.createDefaultRelativeHumidityMeasurementClusterServer).not.toHaveBeenCalled();
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
describe('air quality sensor child endpoint (Phase C)', () => {
|
|
486
|
+
it('should create AirQualitySensor child endpoint when supportsAirQualitySensor is true', () => {
|
|
487
|
+
// Arrange
|
|
488
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
489
|
+
supportsHeat: true,
|
|
490
|
+
supportsFanSpeedControl: true,
|
|
491
|
+
supportsAirQualitySensor: true,
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// Act
|
|
495
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
496
|
+
|
|
497
|
+
// Assert
|
|
498
|
+
expect(mockEndpoint.addChildDeviceType).toHaveBeenCalledWith('AirQualitySensor', expect.any(Array));
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('should create AirQualityClusterServer on AirQualitySensor child', () => {
|
|
502
|
+
// Arrange
|
|
503
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
504
|
+
supportsHeat: true,
|
|
505
|
+
supportsFanSpeedControl: true,
|
|
506
|
+
supportsAirQualitySensor: true,
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// Act
|
|
510
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
511
|
+
|
|
512
|
+
// Assert
|
|
513
|
+
expect(mockEndpoint.createDefaultAirQualityClusterServer).toHaveBeenCalled();
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it('should create PM2.5 ConcentrationMeasurementClusterServer on AirQualitySensor child', () => {
|
|
517
|
+
// Arrange
|
|
518
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
519
|
+
supportsHeat: true,
|
|
520
|
+
supportsFanSpeedControl: true,
|
|
521
|
+
supportsAirQualitySensor: true,
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// Act
|
|
525
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
526
|
+
|
|
527
|
+
// Assert
|
|
528
|
+
expect(mockEndpoint.createDefaultPm25ConcentrationMeasurementClusterServer).toHaveBeenCalled();
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('should create PM10 ConcentrationMeasurementClusterServer on AirQualitySensor child', () => {
|
|
532
|
+
// Arrange
|
|
533
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
534
|
+
supportsHeat: true,
|
|
535
|
+
supportsFanSpeedControl: true,
|
|
536
|
+
supportsAirQualitySensor: true,
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
// Act
|
|
540
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
541
|
+
|
|
542
|
+
// Assert
|
|
543
|
+
expect(mockEndpoint.createDefaultPm10ConcentrationMeasurementClusterServer).toHaveBeenCalled();
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it('should not create AirQualitySensor child endpoint when supportsAirQualitySensor is false', () => {
|
|
547
|
+
// Arrange
|
|
548
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
549
|
+
supportsHeat: true,
|
|
550
|
+
supportsFanSpeedControl: true,
|
|
551
|
+
supportsAirQualitySensor: false,
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
// Act
|
|
555
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
556
|
+
|
|
557
|
+
// Assert
|
|
558
|
+
const calls = (mockEndpoint.addChildDeviceType as any).mock.calls;
|
|
559
|
+
const airQualityCallExists = calls.some((call: any[]) => call[0] === 'AirQualitySensor');
|
|
560
|
+
expect(airQualityCallExists).toBe(false);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it('should not call air quality cluster servers when supportsAirQualitySensor is false', () => {
|
|
564
|
+
// Arrange
|
|
565
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
566
|
+
supportsHeat: true,
|
|
567
|
+
supportsFanSpeedControl: true,
|
|
568
|
+
supportsAirQualitySensor: false,
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
// Act
|
|
572
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
573
|
+
|
|
574
|
+
// Assert
|
|
575
|
+
expect(mockEndpoint.createDefaultAirQualityClusterServer).not.toHaveBeenCalled();
|
|
576
|
+
expect(mockEndpoint.createDefaultPm25ConcentrationMeasurementClusterServer).not.toHaveBeenCalled();
|
|
577
|
+
expect(mockEndpoint.createDefaultPm10ConcentrationMeasurementClusterServer).not.toHaveBeenCalled();
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it('should create both humidity and air quality sensors when both capabilities are enabled', () => {
|
|
581
|
+
// Arrange
|
|
582
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
583
|
+
supportsHeat: true,
|
|
584
|
+
supportsFanSpeedControl: true,
|
|
585
|
+
supportsHumiditySensor: true,
|
|
586
|
+
supportsAirQualitySensor: true,
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
// Act
|
|
590
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
591
|
+
|
|
592
|
+
// Assert
|
|
593
|
+
const calls = (mockEndpoint.addChildDeviceType as any).mock.calls;
|
|
594
|
+
expect(calls.some((call: any[]) => call[0] === 'HumiditySensor')).toBe(true);
|
|
595
|
+
expect(calls.some((call: any[]) => call[0] === 'AirQualitySensor')).toBe(true);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it('should maintain regression parity when both sensor capabilities are false (default)', () => {
|
|
599
|
+
// Arrange
|
|
600
|
+
const capabilities = { ...DEFAULT_AIR_CONDITIONER_CAPABILITIES };
|
|
601
|
+
|
|
602
|
+
// Act
|
|
603
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
604
|
+
|
|
605
|
+
// Assert
|
|
606
|
+
const calls = (mockEndpoint.addChildDeviceType as any).mock.calls;
|
|
607
|
+
const hasHumiditySensor = calls.some((call: any[]) => call[0] === 'HumiditySensor');
|
|
608
|
+
const hasAirQualitySensor = calls.some((call: any[]) => call[0] === 'AirQualitySensor');
|
|
609
|
+
expect(hasHumiditySensor).toBe(false);
|
|
610
|
+
expect(hasAirQualitySensor).toBe(false);
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
describe('energy monitoring on AC endpoint (Phase D)', () => {
|
|
615
|
+
it('should add electricalSensor device type when supportsEnergyMonitoring is true', () => {
|
|
616
|
+
// Arrange
|
|
617
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
618
|
+
supportsHeat: true,
|
|
619
|
+
supportsFanSpeedControl: true,
|
|
620
|
+
supportsEnergyMonitoring: true,
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// Act
|
|
624
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
625
|
+
|
|
626
|
+
// Assert
|
|
627
|
+
const firstArg = (MatterbridgeEndpointMockFn as any).mock.calls[0][0];
|
|
628
|
+
expect(firstArg).toContainEqual({ id: 'roomAirConditioner' });
|
|
629
|
+
expect(firstArg).toContainEqual({ id: 'powerSource' });
|
|
630
|
+
expect(firstArg).toContainEqual({ id: 'electricalSensor' });
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it('should have roomAirConditioner as first device type', () => {
|
|
634
|
+
// Arrange
|
|
635
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
636
|
+
supportsHeat: true,
|
|
637
|
+
supportsFanSpeedControl: true,
|
|
638
|
+
supportsEnergyMonitoring: true,
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
// Act
|
|
642
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
643
|
+
|
|
644
|
+
// Assert
|
|
645
|
+
const firstArg = (MatterbridgeEndpointMockFn as any).mock.calls[0][0];
|
|
646
|
+
expect(firstArg[0]).toEqual({ id: 'roomAirConditioner' });
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
it('should create PowerTopology and ElectricalPowerMeasurement on AC endpoint when supportsEnergyMonitoring is true', () => {
|
|
650
|
+
// Arrange
|
|
651
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
652
|
+
supportsHeat: true,
|
|
653
|
+
supportsFanSpeedControl: true,
|
|
654
|
+
supportsEnergyMonitoring: true,
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
// Act
|
|
658
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
659
|
+
|
|
660
|
+
// Assert
|
|
661
|
+
expect(mockEndpoint.createDefaultPowerTopologyClusterServer).toHaveBeenCalledWith(
|
|
662
|
+
PowerTopology.Feature.NodeTopology,
|
|
663
|
+
);
|
|
664
|
+
expect(mockEndpoint.createDefaultElectricalPowerMeasurementClusterServer).toHaveBeenCalledWith(
|
|
665
|
+
null,
|
|
666
|
+
null,
|
|
667
|
+
0,
|
|
668
|
+
null,
|
|
669
|
+
);
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it('should not create EnergyMonitor child endpoint', () => {
|
|
673
|
+
// Arrange
|
|
674
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
675
|
+
supportsHeat: true,
|
|
676
|
+
supportsFanSpeedControl: true,
|
|
677
|
+
supportsEnergyMonitoring: true,
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
// Act
|
|
681
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
682
|
+
|
|
683
|
+
// Assert
|
|
684
|
+
const calls = (mockEndpoint.addChildDeviceType as any).mock.calls;
|
|
685
|
+
const hasEnergyMonitor = calls.some((call: any[]) => call[0] === 'EnergyMonitor');
|
|
686
|
+
expect(hasEnergyMonitor).toBe(false);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it('should not add electricalSensor or create energy clusters when supportsEnergyMonitoring is false', () => {
|
|
690
|
+
// Arrange
|
|
691
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
692
|
+
supportsHeat: true,
|
|
693
|
+
supportsFanSpeedControl: true,
|
|
694
|
+
supportsEnergyMonitoring: false,
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
// Act
|
|
698
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
699
|
+
|
|
700
|
+
// Assert
|
|
701
|
+
const firstArg = (MatterbridgeEndpointMockFn as any).mock.calls[0][0];
|
|
702
|
+
const hasSensor = firstArg.some((dt: any) => dt.id === 'electricalSensor');
|
|
703
|
+
expect(hasSensor).toBe(false);
|
|
704
|
+
expect(mockEndpoint.createDefaultPowerTopologyClusterServer).not.toHaveBeenCalled();
|
|
705
|
+
expect(mockEndpoint.createDefaultElectricalPowerMeasurementClusterServer).not.toHaveBeenCalled();
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
it('should maintain regression parity when energy monitoring capability is false (default)', () => {
|
|
709
|
+
// Arrange
|
|
710
|
+
const capabilities = { ...DEFAULT_AIR_CONDITIONER_CAPABILITIES };
|
|
711
|
+
|
|
712
|
+
// Act
|
|
713
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
714
|
+
|
|
715
|
+
// Assert
|
|
716
|
+
const firstArg = (MatterbridgeEndpointMockFn as any).mock.calls[0][0];
|
|
717
|
+
const hasSensor = firstArg.some((dt: any) => dt.id === 'electricalSensor');
|
|
718
|
+
expect(hasSensor).toBe(false);
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
it('should not add electricalSensor when supportsEnergyMonitoring is false', () => {
|
|
722
|
+
// Arrange
|
|
723
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
724
|
+
supportsHeat: true,
|
|
725
|
+
supportsFanSpeedControl: true,
|
|
726
|
+
supportsEnergyMonitoring: false,
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
// Act
|
|
730
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
731
|
+
|
|
732
|
+
// Assert
|
|
733
|
+
const firstArg = (MatterbridgeEndpointMockFn as any).mock.calls[0][0];
|
|
734
|
+
expect(firstArg).toContainEqual({ id: 'roomAirConditioner' });
|
|
735
|
+
expect(firstArg).toContainEqual({ id: 'powerSource' });
|
|
736
|
+
expect(firstArg).not.toContainEqual({ id: 'electricalSensor' });
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
it('should coexist with humidity sensor and air quality sensor when energy is on endpoint', () => {
|
|
740
|
+
// Arrange
|
|
741
|
+
const childHumidityMock = createChainableMock();
|
|
742
|
+
const childAirQualityMock = createChainableMock();
|
|
743
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
744
|
+
supportsHeat: true,
|
|
745
|
+
supportsFanSpeedControl: true,
|
|
746
|
+
supportsEnergyMonitoring: true,
|
|
747
|
+
supportsHumiditySensor: true,
|
|
748
|
+
supportsAirQualitySensor: true,
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
let callCount = 0;
|
|
752
|
+
mockEndpoint.addChildDeviceType = vi.fn((name: string) => {
|
|
753
|
+
callCount++;
|
|
754
|
+
if (name === 'HumiditySensor') return childHumidityMock;
|
|
755
|
+
if (name === 'AirQualitySensor') return childAirQualityMock;
|
|
756
|
+
return createChainableMock();
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
// Act
|
|
760
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
761
|
+
|
|
762
|
+
// Assert
|
|
763
|
+
const calls = (mockEndpoint.addChildDeviceType as any).mock.calls;
|
|
764
|
+
const childNames = calls.map((call: any[]) => call[0]);
|
|
765
|
+
expect(childNames).toContain('HumiditySensor');
|
|
766
|
+
expect(childNames).toContain('AirQualitySensor');
|
|
767
|
+
expect(childNames).not.toContain('EnergyMonitor');
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
it('should not include onOffPlugInUnit in device types for endpoint mode', () => {
|
|
771
|
+
// Arrange
|
|
772
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
773
|
+
supportsHeat: true,
|
|
774
|
+
supportsFanSpeedControl: true,
|
|
775
|
+
supportsEnergyMonitoring: true,
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
// Act
|
|
779
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
780
|
+
|
|
781
|
+
// Assert
|
|
782
|
+
const firstArg = (MatterbridgeEndpointMockFn as any).mock.calls[0][0];
|
|
783
|
+
const deviceTypeIds = firstArg.map((dt: any) => dt.id);
|
|
784
|
+
expect(deviceTypeIds).not.toContain('onOffPlugInUnit');
|
|
785
|
+
});
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
describe('filter monitoring cluster on AC endpoint', () => {
|
|
789
|
+
it('should call createDefaultHepaFilterMonitoringClusterServer when supportsFilterMonitoring is true', () => {
|
|
790
|
+
// Arrange
|
|
791
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
792
|
+
supportsHeat: true,
|
|
793
|
+
supportsFanSpeedControl: true,
|
|
794
|
+
supportsFilterMonitoring: true,
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
// Act
|
|
798
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
799
|
+
|
|
800
|
+
// Assert
|
|
801
|
+
expect(mockEndpoint.createDefaultHepaFilterMonitoringClusterServer).toHaveBeenCalledTimes(1);
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
it('should not call createDefaultHepaFilterMonitoringClusterServer when supportsFilterMonitoring is false', () => {
|
|
805
|
+
// Arrange
|
|
806
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
807
|
+
supportsHeat: true,
|
|
808
|
+
supportsFanSpeedControl: true,
|
|
809
|
+
supportsFilterMonitoring: false,
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
// Act
|
|
813
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
814
|
+
|
|
815
|
+
// Assert
|
|
816
|
+
expect(mockEndpoint.createDefaultHepaFilterMonitoringClusterServer).not.toHaveBeenCalled();
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
it('should maintain regression parity when filter monitoring capability is false (default)', () => {
|
|
820
|
+
// Arrange
|
|
821
|
+
const capabilities = { ...DEFAULT_AIR_CONDITIONER_CAPABILITIES };
|
|
822
|
+
|
|
823
|
+
// Act
|
|
824
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
825
|
+
|
|
826
|
+
// Assert
|
|
827
|
+
expect(mockEndpoint.createDefaultHepaFilterMonitoringClusterServer).not.toHaveBeenCalled();
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
it('should coexist with energy monitoring when both capabilities are enabled', () => {
|
|
831
|
+
// Arrange
|
|
832
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
833
|
+
supportsHeat: true,
|
|
834
|
+
supportsFanSpeedControl: true,
|
|
835
|
+
supportsEnergyMonitoring: true,
|
|
836
|
+
supportsFilterMonitoring: true,
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
// Act
|
|
840
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
841
|
+
|
|
842
|
+
// Assert
|
|
843
|
+
expect(mockEndpoint.createDefaultPowerTopologyClusterServer).toHaveBeenCalled();
|
|
844
|
+
expect(mockEndpoint.createDefaultElectricalPowerMeasurementClusterServer).toHaveBeenCalled();
|
|
845
|
+
expect(mockEndpoint.createDefaultHepaFilterMonitoringClusterServer).toHaveBeenCalled();
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
it('should coexist with humidity and air quality sensors when all capabilities are enabled', () => {
|
|
849
|
+
// Arrange
|
|
850
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
851
|
+
supportsHeat: true,
|
|
852
|
+
supportsFanSpeedControl: true,
|
|
853
|
+
supportsHumiditySensor: true,
|
|
854
|
+
supportsAirQualitySensor: true,
|
|
855
|
+
supportsFilterMonitoring: true,
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
// Act
|
|
859
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
860
|
+
|
|
861
|
+
// Assert
|
|
862
|
+
const calls = (mockEndpoint.addChildDeviceType as any).mock.calls;
|
|
863
|
+
const childNames = calls.map((call: any[]) => call[0]);
|
|
864
|
+
expect(childNames).toContain('HumiditySensor');
|
|
865
|
+
expect(childNames).toContain('AirQualitySensor');
|
|
866
|
+
// Filter monitoring is on the AC endpoint itself, not a child
|
|
867
|
+
expect(childNames).not.toContain('FilterMonitor');
|
|
868
|
+
expect(mockEndpoint.createDefaultHepaFilterMonitoringClusterServer).toHaveBeenCalled();
|
|
869
|
+
});
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
describe('Matter override configuration (Phase G)', () => {
|
|
873
|
+
it('should use default values when no options passed', () => {
|
|
874
|
+
// Arrange
|
|
875
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
876
|
+
supportsHeat: true,
|
|
877
|
+
supportsFanSpeedControl: true,
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
// Act
|
|
881
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low);
|
|
882
|
+
|
|
883
|
+
// Assert
|
|
884
|
+
expect(mockEndpoint.createDefaultBasicInformationClusterServer).toHaveBeenCalledWith(
|
|
885
|
+
mockDevice.name,
|
|
886
|
+
mockDevice.id,
|
|
887
|
+
0xfff1, // Default vendor ID
|
|
888
|
+
'Matterbridge', // Default vendor name
|
|
889
|
+
0x8000, // Default product ID
|
|
890
|
+
'Matterbridge Air Conditioner', // Default product name
|
|
891
|
+
);
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
it('should use override values when options with all 4 fields are passed', () => {
|
|
895
|
+
// Arrange
|
|
896
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
897
|
+
supportsHeat: true,
|
|
898
|
+
supportsFanSpeedControl: true,
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
// Act
|
|
902
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low, {
|
|
903
|
+
vendorId: 4996,
|
|
904
|
+
vendorName: 'LG',
|
|
905
|
+
productId: 1,
|
|
906
|
+
productName: 'My AC',
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
// Assert
|
|
910
|
+
expect(mockEndpoint.createDefaultBasicInformationClusterServer).toHaveBeenCalledWith(
|
|
911
|
+
mockDevice.name,
|
|
912
|
+
mockDevice.id,
|
|
913
|
+
4996, // Custom vendor ID
|
|
914
|
+
'LG', // Custom vendor name
|
|
915
|
+
1, // Custom product ID
|
|
916
|
+
'My AC', // Custom product name
|
|
917
|
+
);
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
it('should fall back to defaults when only some override fields are provided', () => {
|
|
921
|
+
// Arrange
|
|
922
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
923
|
+
supportsHeat: true,
|
|
924
|
+
supportsFanSpeedControl: true,
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
// Act
|
|
928
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low, {
|
|
929
|
+
productName: 'Premium AC',
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
// Assert
|
|
933
|
+
expect(mockEndpoint.createDefaultBasicInformationClusterServer).toHaveBeenCalledWith(
|
|
934
|
+
mockDevice.name,
|
|
935
|
+
mockDevice.id,
|
|
936
|
+
0xfff1, // Falls back to default
|
|
937
|
+
'Matterbridge', // Falls back to default
|
|
938
|
+
0x8000, // Falls back to default
|
|
939
|
+
'Premium AC', // Custom value
|
|
940
|
+
);
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
it('should use undefined values when options are explicitly passed as undefined', () => {
|
|
944
|
+
// Arrange
|
|
945
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
946
|
+
supportsHeat: true,
|
|
947
|
+
supportsFanSpeedControl: true,
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
// Act
|
|
951
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low, {
|
|
952
|
+
vendorId: undefined,
|
|
953
|
+
vendorName: undefined,
|
|
954
|
+
productId: undefined,
|
|
955
|
+
productName: undefined,
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
// Assert
|
|
959
|
+
expect(mockEndpoint.createDefaultBasicInformationClusterServer).toHaveBeenCalledWith(
|
|
960
|
+
mockDevice.name,
|
|
961
|
+
mockDevice.id,
|
|
962
|
+
0xfff1, // Falls back to default when undefined
|
|
963
|
+
'Matterbridge', // Falls back to default when undefined
|
|
964
|
+
0x8000, // Falls back to default when undefined
|
|
965
|
+
'Matterbridge Air Conditioner', // Falls back to default when undefined
|
|
966
|
+
);
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
it('should support partial override with multiple fields', () => {
|
|
970
|
+
// Arrange
|
|
971
|
+
const capabilities = asPartial<AirConditionerCapabilities>({
|
|
972
|
+
supportsHeat: true,
|
|
973
|
+
supportsFanSpeedControl: true,
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
// Act
|
|
977
|
+
buildAirConditionerEndpoint(mockDevice, capabilities, setpoints, FanControl.FanMode.Low, {
|
|
978
|
+
vendorId: 0xabcd,
|
|
979
|
+
productName: 'Custom Premium AC',
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
// Assert
|
|
983
|
+
expect(mockEndpoint.createDefaultBasicInformationClusterServer).toHaveBeenCalledWith(
|
|
984
|
+
mockDevice.name,
|
|
985
|
+
mockDevice.id,
|
|
986
|
+
0xabcd, // Custom vendor ID
|
|
987
|
+
'Matterbridge', // Falls back to default
|
|
988
|
+
0x8000, // Falls back to default
|
|
989
|
+
'Custom Premium AC', // Custom product name
|
|
990
|
+
);
|
|
991
|
+
});
|
|
992
|
+
});
|
|
993
|
+
});
|