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,373 @@
|
|
|
1
|
+
import { OnOff, OperationalState } from 'matterbridge/matter/clusters';
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { ThinqSnapshot } from '../../../core/domain/value-objects/ThinqSnapshot.js';
|
|
5
|
+
import {
|
|
6
|
+
applyThinqSnapshotToWasher,
|
|
7
|
+
mapWasherStateToOperationalState,
|
|
8
|
+
} from '../../../platform/thinq/thinqWasherStateSync.js';
|
|
9
|
+
import { asPartial, createMockLogger } from '../../helpers/testUtils.js';
|
|
10
|
+
|
|
11
|
+
function createMockWasherEndpoint(): any {
|
|
12
|
+
return asPartial<any>({
|
|
13
|
+
serialNumber: 'washer-123',
|
|
14
|
+
uniqueId: undefined,
|
|
15
|
+
log: createMockLogger(),
|
|
16
|
+
updateAttribute: vi.fn().mockResolvedValue(false),
|
|
17
|
+
getAttribute: vi.fn().mockReturnValue(OperationalState.OperationalStateEnum.Stopped),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('thinqWasherStateSync', () => {
|
|
22
|
+
describe('mapWasherStateToOperationalState', () => {
|
|
23
|
+
it('should return Error when isWasherError is true', () => {
|
|
24
|
+
// Arrange
|
|
25
|
+
const snapshot = new ThinqSnapshot({
|
|
26
|
+
washerDryer: { state: 'ERROR' },
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Act
|
|
30
|
+
const result = mapWasherStateToOperationalState(snapshot);
|
|
31
|
+
|
|
32
|
+
// Assert
|
|
33
|
+
expect(result).toBe(OperationalState.OperationalStateEnum.Error);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should return Running when isWasherRunning is true', () => {
|
|
37
|
+
// Arrange
|
|
38
|
+
const snapshot = new ThinqSnapshot({
|
|
39
|
+
washerDryer: { state: 'RUNNING' },
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Act
|
|
43
|
+
const result = mapWasherStateToOperationalState(snapshot);
|
|
44
|
+
|
|
45
|
+
// Assert
|
|
46
|
+
expect(result).toBe(OperationalState.OperationalStateEnum.Running);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should return Paused when washerRawState is PAUSE', () => {
|
|
50
|
+
// Arrange
|
|
51
|
+
const snapshot = new ThinqSnapshot({
|
|
52
|
+
washerDryer: { state: 'PAUSE' },
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Act
|
|
56
|
+
const result = mapWasherStateToOperationalState(snapshot);
|
|
57
|
+
|
|
58
|
+
// Assert
|
|
59
|
+
expect(result).toBe(OperationalState.OperationalStateEnum.Paused);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should return Stopped when powered off (POWEROFF)', () => {
|
|
63
|
+
// Arrange
|
|
64
|
+
const snapshot = new ThinqSnapshot({
|
|
65
|
+
washerDryer: { state: 'POWEROFF' },
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Act
|
|
69
|
+
const result = mapWasherStateToOperationalState(snapshot);
|
|
70
|
+
|
|
71
|
+
// Assert
|
|
72
|
+
expect(result).toBe(OperationalState.OperationalStateEnum.Stopped);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should return Stopped for END state', () => {
|
|
76
|
+
// Arrange
|
|
77
|
+
const snapshot = new ThinqSnapshot({
|
|
78
|
+
washerDryer: { state: 'END' },
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Act
|
|
82
|
+
const result = mapWasherStateToOperationalState(snapshot);
|
|
83
|
+
|
|
84
|
+
// Assert
|
|
85
|
+
expect(result).toBe(OperationalState.OperationalStateEnum.Stopped);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should return Running for unknown state (not in NOT_RUNNING_STATES)', () => {
|
|
89
|
+
// Arrange
|
|
90
|
+
const snapshot = new ThinqSnapshot({
|
|
91
|
+
washerDryer: { state: 'UNKNOWN_STATE' },
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Act
|
|
95
|
+
const result = mapWasherStateToOperationalState(snapshot);
|
|
96
|
+
|
|
97
|
+
// Assert
|
|
98
|
+
// Unknown states not in WASHER_NOT_RUNNING_STATES are treated as running
|
|
99
|
+
expect(result).toBe(OperationalState.OperationalStateEnum.Running);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should return Stopped when washerDryer.state is absent', () => {
|
|
103
|
+
// Arrange
|
|
104
|
+
const snapshot = new ThinqSnapshot({});
|
|
105
|
+
|
|
106
|
+
// Act
|
|
107
|
+
const result = mapWasherStateToOperationalState(snapshot);
|
|
108
|
+
|
|
109
|
+
// Assert
|
|
110
|
+
expect(result).toBe(OperationalState.OperationalStateEnum.Stopped);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should prioritize error over running state', () => {
|
|
114
|
+
// Arrange (ERROR state takes priority over running logic)
|
|
115
|
+
const snapshot = new ThinqSnapshot({
|
|
116
|
+
washerDryer: { state: 'ERROR' },
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Act
|
|
120
|
+
const result = mapWasherStateToOperationalState(snapshot);
|
|
121
|
+
|
|
122
|
+
// Assert
|
|
123
|
+
expect(result).toBe(OperationalState.OperationalStateEnum.Error);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('applyThinqSnapshotToWasher', () => {
|
|
128
|
+
let mockLogger: ReturnType<typeof createMockLogger>;
|
|
129
|
+
let mockWasher: any;
|
|
130
|
+
|
|
131
|
+
beforeEach(() => {
|
|
132
|
+
vi.clearAllMocks();
|
|
133
|
+
mockLogger = createMockLogger();
|
|
134
|
+
mockWasher = createMockWasherEndpoint();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should skip update when washerDryer.state is absent', async () => {
|
|
138
|
+
// Arrange
|
|
139
|
+
const snapshot = new ThinqSnapshot({
|
|
140
|
+
washerDryer: { remainTimeHour: 1 },
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Act
|
|
144
|
+
await applyThinqSnapshotToWasher(mockWasher, snapshot, mockLogger);
|
|
145
|
+
|
|
146
|
+
// Assert
|
|
147
|
+
expect(mockWasher.updateAttribute).not.toHaveBeenCalled();
|
|
148
|
+
expect(mockLogger.debug).toHaveBeenCalledWith(
|
|
149
|
+
expect.stringContaining('skipped (source key absent): washerDryer.state'),
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should update onOff, operationalState, and operationalError when state is present and running', async () => {
|
|
154
|
+
// Arrange
|
|
155
|
+
const snapshot = new ThinqSnapshot({
|
|
156
|
+
washerDryer: { state: 'RUNNING' },
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Act
|
|
160
|
+
await applyThinqSnapshotToWasher(mockWasher, snapshot, mockLogger);
|
|
161
|
+
|
|
162
|
+
// Assert
|
|
163
|
+
expect(mockWasher.updateAttribute).toHaveBeenCalledWith(OnOff.id, 'onOff', true, mockLogger);
|
|
164
|
+
expect(mockWasher.updateAttribute).toHaveBeenCalledWith(
|
|
165
|
+
OperationalState.id,
|
|
166
|
+
'operationalState',
|
|
167
|
+
OperationalState.OperationalStateEnum.Running,
|
|
168
|
+
mockLogger,
|
|
169
|
+
);
|
|
170
|
+
expect(mockWasher.updateAttribute).toHaveBeenCalledWith(
|
|
171
|
+
OperationalState.id,
|
|
172
|
+
'operationalError',
|
|
173
|
+
{ errorStateId: OperationalState.ErrorState.NoError },
|
|
174
|
+
mockLogger,
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should set onOff to false when powered off (POWEROFF)', async () => {
|
|
179
|
+
// Arrange
|
|
180
|
+
const snapshot = new ThinqSnapshot({
|
|
181
|
+
washerDryer: { state: 'POWEROFF' },
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Act
|
|
185
|
+
await applyThinqSnapshotToWasher(mockWasher, snapshot, mockLogger);
|
|
186
|
+
|
|
187
|
+
// Assert
|
|
188
|
+
expect(mockWasher.updateAttribute).toHaveBeenCalledWith(OnOff.id, 'onOff', false, mockLogger);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should set operationalState to Stopped when powered off', async () => {
|
|
192
|
+
// Arrange
|
|
193
|
+
const snapshot = new ThinqSnapshot({
|
|
194
|
+
washerDryer: { state: 'POWEROFF' },
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Act
|
|
198
|
+
await applyThinqSnapshotToWasher(mockWasher, snapshot, mockLogger);
|
|
199
|
+
|
|
200
|
+
// Assert
|
|
201
|
+
expect(mockWasher.updateAttribute).toHaveBeenCalledWith(
|
|
202
|
+
OperationalState.id,
|
|
203
|
+
'operationalState',
|
|
204
|
+
OperationalState.OperationalStateEnum.Stopped,
|
|
205
|
+
mockLogger,
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should set operationalError to UnableToCompleteOperation when ERROR state', async () => {
|
|
210
|
+
// Arrange
|
|
211
|
+
const snapshot = new ThinqSnapshot({
|
|
212
|
+
washerDryer: { state: 'ERROR' },
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Act
|
|
216
|
+
await applyThinqSnapshotToWasher(mockWasher, snapshot, mockLogger);
|
|
217
|
+
|
|
218
|
+
// Assert
|
|
219
|
+
expect(mockWasher.updateAttribute).toHaveBeenCalledWith(
|
|
220
|
+
OperationalState.id,
|
|
221
|
+
'operationalError',
|
|
222
|
+
{ errorStateId: OperationalState.ErrorState.UnableToCompleteOperation },
|
|
223
|
+
mockLogger,
|
|
224
|
+
);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should update countdownTime when both remainTimeHour and remainTimeMinute are present', async () => {
|
|
228
|
+
// Arrange
|
|
229
|
+
const snapshot = new ThinqSnapshot({
|
|
230
|
+
washerDryer: { state: 'RUNNING', remainTimeHour: 1, remainTimeMinute: 30 },
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Act
|
|
234
|
+
await applyThinqSnapshotToWasher(mockWasher, snapshot, mockLogger);
|
|
235
|
+
|
|
236
|
+
// Assert
|
|
237
|
+
const calls = vi.mocked(mockWasher.updateAttribute).mock.calls;
|
|
238
|
+
const countdownCall = calls.find((call: any[]) => call[0] === OperationalState.id && call[1] === 'countdownTime');
|
|
239
|
+
expect(countdownCall).toBeDefined();
|
|
240
|
+
expect(countdownCall?.at(2)).toBe(5400); // 1 hour (3600s) + 30 minutes (1800s)
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should update countdownTime when only remainTimeHour is present', async () => {
|
|
244
|
+
// Arrange
|
|
245
|
+
const snapshot = new ThinqSnapshot({
|
|
246
|
+
washerDryer: { state: 'RUNNING', remainTimeHour: 2 },
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Act
|
|
250
|
+
await applyThinqSnapshotToWasher(mockWasher, snapshot, mockLogger);
|
|
251
|
+
|
|
252
|
+
// Assert
|
|
253
|
+
const calls = vi.mocked(mockWasher.updateAttribute).mock.calls;
|
|
254
|
+
const countdownCall = calls.find((call: any[]) => call[0] === OperationalState.id && call[1] === 'countdownTime');
|
|
255
|
+
expect(countdownCall).toBeDefined();
|
|
256
|
+
expect(countdownCall?.at(2)).toBe(7200); // 2 hours
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('should update countdownTime when only remainTimeMinute is present', async () => {
|
|
260
|
+
// Arrange
|
|
261
|
+
const snapshot = new ThinqSnapshot({
|
|
262
|
+
washerDryer: { state: 'RUNNING', remainTimeMinute: 45 },
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Act
|
|
266
|
+
await applyThinqSnapshotToWasher(mockWasher, snapshot, mockLogger);
|
|
267
|
+
|
|
268
|
+
// Assert
|
|
269
|
+
const calls = vi.mocked(mockWasher.updateAttribute).mock.calls;
|
|
270
|
+
const countdownCall = calls.find((call: any[]) => call[0] === OperationalState.id && call[1] === 'countdownTime');
|
|
271
|
+
expect(countdownCall).toBeDefined();
|
|
272
|
+
expect(countdownCall?.at(2)).toBe(2700); // 45 minutes
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('should NOT update countdownTime when remain time keys are absent', async () => {
|
|
276
|
+
// Arrange
|
|
277
|
+
const snapshot = new ThinqSnapshot({
|
|
278
|
+
washerDryer: { state: 'RUNNING' },
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Act
|
|
282
|
+
await applyThinqSnapshotToWasher(mockWasher, snapshot, mockLogger);
|
|
283
|
+
|
|
284
|
+
// Assert
|
|
285
|
+
const calls = vi.mocked(mockWasher.updateAttribute).mock.calls;
|
|
286
|
+
const countdownCall = calls.find((call: any[]) => call[0] === OperationalState.id && call[1] === 'countdownTime');
|
|
287
|
+
expect(countdownCall).toBeUndefined();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('should set countdownTime to 0 when washer not running but remain time keys present', async () => {
|
|
291
|
+
// Arrange
|
|
292
|
+
const snapshot = new ThinqSnapshot({
|
|
293
|
+
washerDryer: { state: 'END', remainTimeHour: 1 },
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Act
|
|
297
|
+
await applyThinqSnapshotToWasher(mockWasher, snapshot, mockLogger);
|
|
298
|
+
|
|
299
|
+
// Assert
|
|
300
|
+
const calls = vi.mocked(mockWasher.updateAttribute).mock.calls;
|
|
301
|
+
const countdownCall = calls.find((call: any[]) => call[0] === OperationalState.id && call[1] === 'countdownTime');
|
|
302
|
+
expect(countdownCall).toBeDefined();
|
|
303
|
+
expect(countdownCall?.at(2)).toBe(0); // Zeroed when not running
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('should update attribute when updateAttribute succeeds', async () => {
|
|
307
|
+
// Arrange
|
|
308
|
+
const snapshot = new ThinqSnapshot({
|
|
309
|
+
washerDryer: { state: 'RUNNING' },
|
|
310
|
+
});
|
|
311
|
+
mockWasher.updateAttribute.mockResolvedValue(false);
|
|
312
|
+
|
|
313
|
+
// Act
|
|
314
|
+
await applyThinqSnapshotToWasher(mockWasher, snapshot, mockLogger);
|
|
315
|
+
|
|
316
|
+
// Assert
|
|
317
|
+
expect(mockWasher.updateAttribute).toHaveBeenCalled();
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('should use serialNumber when available for logging', async () => {
|
|
321
|
+
// Arrange
|
|
322
|
+
const snapshot = new ThinqSnapshot({
|
|
323
|
+
washerDryer: { state: 'RUNNING' },
|
|
324
|
+
});
|
|
325
|
+
mockWasher.serialNumber = 'serial-123';
|
|
326
|
+
|
|
327
|
+
// Act
|
|
328
|
+
await applyThinqSnapshotToWasher(mockWasher, snapshot, mockLogger);
|
|
329
|
+
|
|
330
|
+
// Assert
|
|
331
|
+
const debugCalls = vi.mocked(mockLogger.debug).mock.calls;
|
|
332
|
+
const entryLog = debugCalls.find((call: any[]) => call[0]?.includes('entry for deviceId'));
|
|
333
|
+
expect(entryLog).toBeDefined();
|
|
334
|
+
expect(entryLog?.at(0)).toContain('serial-123');
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('should fall back to uniqueId when serialNumber is not available', async () => {
|
|
338
|
+
// Arrange
|
|
339
|
+
const snapshot = new ThinqSnapshot({
|
|
340
|
+
washerDryer: { state: 'RUNNING' },
|
|
341
|
+
});
|
|
342
|
+
mockWasher.serialNumber = undefined;
|
|
343
|
+
mockWasher.uniqueId = 'unique-456';
|
|
344
|
+
|
|
345
|
+
// Act
|
|
346
|
+
await applyThinqSnapshotToWasher(mockWasher, snapshot, mockLogger);
|
|
347
|
+
|
|
348
|
+
// Assert
|
|
349
|
+
const debugCalls = vi.mocked(mockLogger.debug).mock.calls;
|
|
350
|
+
const entryLog = debugCalls.find((call: any[]) => call[0]?.includes('entry for deviceId'));
|
|
351
|
+
expect(entryLog).toBeDefined();
|
|
352
|
+
expect(entryLog?.at(0)).toContain('unique-456');
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('should use "unknown" when neither serialNumber nor uniqueId available', async () => {
|
|
356
|
+
// Arrange
|
|
357
|
+
const snapshot = new ThinqSnapshot({
|
|
358
|
+
washerDryer: { state: 'RUNNING' },
|
|
359
|
+
});
|
|
360
|
+
mockWasher.serialNumber = undefined;
|
|
361
|
+
mockWasher.uniqueId = undefined;
|
|
362
|
+
|
|
363
|
+
// Act
|
|
364
|
+
await applyThinqSnapshotToWasher(mockWasher, snapshot, mockLogger);
|
|
365
|
+
|
|
366
|
+
// Assert
|
|
367
|
+
const debugCalls = vi.mocked(mockLogger.debug).mock.calls;
|
|
368
|
+
const entryLog = debugCalls.find((call: any[]) => call[0]?.includes('entry for deviceId'));
|
|
369
|
+
expect(entryLog).toBeDefined();
|
|
370
|
+
expect(entryLog?.at(0)).toContain('unknown');
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
});
|