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,392 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
extractWasherStopCommand,
|
|
5
|
+
type WasherStopCommandPayload,
|
|
6
|
+
} from '../../../platform/thinq/thinqWasherStopCommandResolver.js';
|
|
7
|
+
|
|
8
|
+
describe('thinqWasherStopCommandResolver', () => {
|
|
9
|
+
describe('extractWasherStopCommand', () => {
|
|
10
|
+
it('should extract WMStop command when present', () => {
|
|
11
|
+
// Arrange
|
|
12
|
+
const deviceModel = {
|
|
13
|
+
ControlWifi: {
|
|
14
|
+
WMStop: {
|
|
15
|
+
command: 'Set',
|
|
16
|
+
dataKey: 'washerDryer.state',
|
|
17
|
+
dataValue: 'STOP',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Act
|
|
23
|
+
const result = extractWasherStopCommand(deviceModel);
|
|
24
|
+
|
|
25
|
+
// Assert
|
|
26
|
+
expect(result).toBeDefined();
|
|
27
|
+
expect(result?.command).toBe('Set');
|
|
28
|
+
expect(result?.dataKey).toBe('washerDryer.state');
|
|
29
|
+
expect(result?.dataValue).toBe('STOP');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should fall back to WMOff when WMStop is absent', () => {
|
|
33
|
+
// Arrange
|
|
34
|
+
const deviceModel = {
|
|
35
|
+
ControlWifi: {
|
|
36
|
+
WMOff: {
|
|
37
|
+
command: 'Set',
|
|
38
|
+
dataKey: 'washerDryer.state',
|
|
39
|
+
dataValue: 'OFF',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Act
|
|
45
|
+
const result = extractWasherStopCommand(deviceModel);
|
|
46
|
+
|
|
47
|
+
// Assert
|
|
48
|
+
expect(result).toBeDefined();
|
|
49
|
+
expect(result?.dataValue).toBe('OFF');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should prefer WMStop over WMOff when both present', () => {
|
|
53
|
+
// Arrange
|
|
54
|
+
const deviceModel = {
|
|
55
|
+
ControlWifi: {
|
|
56
|
+
WMStop: {
|
|
57
|
+
command: 'Set',
|
|
58
|
+
dataKey: 'washerDryer.state',
|
|
59
|
+
dataValue: 'STOP',
|
|
60
|
+
},
|
|
61
|
+
WMOff: {
|
|
62
|
+
command: 'Set',
|
|
63
|
+
dataKey: 'washerDryer.state',
|
|
64
|
+
dataValue: 'OFF',
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Act
|
|
70
|
+
const result = extractWasherStopCommand(deviceModel);
|
|
71
|
+
|
|
72
|
+
// Assert
|
|
73
|
+
expect(result?.dataValue).toBe('STOP');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should return undefined when both WMStop and WMOff are absent', () => {
|
|
77
|
+
// Arrange
|
|
78
|
+
const deviceModel = {
|
|
79
|
+
ControlWifi: {
|
|
80
|
+
OtherCommand: {
|
|
81
|
+
dataKey: 'some.state',
|
|
82
|
+
dataValue: 'VALUE',
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Act
|
|
88
|
+
const result = extractWasherStopCommand(deviceModel);
|
|
89
|
+
|
|
90
|
+
// Assert
|
|
91
|
+
expect(result).toBeUndefined();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should return undefined when ControlWifi is absent', () => {
|
|
95
|
+
// Arrange
|
|
96
|
+
const deviceModel = {
|
|
97
|
+
OtherProperty: {
|
|
98
|
+
WMStop: {
|
|
99
|
+
dataKey: 'washerDryer.state',
|
|
100
|
+
dataValue: 'STOP',
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Act
|
|
106
|
+
const result = extractWasherStopCommand(deviceModel);
|
|
107
|
+
|
|
108
|
+
// Assert
|
|
109
|
+
expect(result).toBeUndefined();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should return undefined for empty deviceModel', () => {
|
|
113
|
+
// Arrange
|
|
114
|
+
const deviceModel = {};
|
|
115
|
+
|
|
116
|
+
// Act
|
|
117
|
+
const result = extractWasherStopCommand(deviceModel);
|
|
118
|
+
|
|
119
|
+
// Assert
|
|
120
|
+
expect(result).toBeUndefined();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should return undefined when deviceModel is not an object', () => {
|
|
124
|
+
// Arrange
|
|
125
|
+
const deviceModel: any = null;
|
|
126
|
+
|
|
127
|
+
// Act
|
|
128
|
+
const result = extractWasherStopCommand(deviceModel);
|
|
129
|
+
|
|
130
|
+
// Assert
|
|
131
|
+
expect(result).toBeUndefined();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should handle WMStop as non-object gracefully', () => {
|
|
135
|
+
// Arrange
|
|
136
|
+
const deviceModel = {
|
|
137
|
+
ControlWifi: {
|
|
138
|
+
WMStop: 'string-value',
|
|
139
|
+
WMOff: {
|
|
140
|
+
dataKey: 'washerDryer.state',
|
|
141
|
+
dataValue: 'OFF',
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Act
|
|
147
|
+
const result = extractWasherStopCommand(deviceModel);
|
|
148
|
+
|
|
149
|
+
// Assert
|
|
150
|
+
expect(result).toBeDefined();
|
|
151
|
+
expect(result?.dataValue).toBe('OFF');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should include optional command field when present', () => {
|
|
155
|
+
// Arrange
|
|
156
|
+
const deviceModel = {
|
|
157
|
+
ControlWifi: {
|
|
158
|
+
WMStop: {
|
|
159
|
+
command: 'Operation',
|
|
160
|
+
dataKey: 'washerDryer.state',
|
|
161
|
+
dataValue: 'STOP',
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Act
|
|
167
|
+
const result = extractWasherStopCommand(deviceModel);
|
|
168
|
+
|
|
169
|
+
// Assert
|
|
170
|
+
expect(result?.command).toBe('Operation');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should omit command field when absent', () => {
|
|
174
|
+
// Arrange
|
|
175
|
+
const deviceModel = {
|
|
176
|
+
ControlWifi: {
|
|
177
|
+
WMStop: {
|
|
178
|
+
dataKey: 'washerDryer.state',
|
|
179
|
+
dataValue: 'STOP',
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// Act
|
|
185
|
+
const result = extractWasherStopCommand(deviceModel);
|
|
186
|
+
|
|
187
|
+
// Assert
|
|
188
|
+
expect(result?.command).toBeUndefined();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should handle dataSetList when present', () => {
|
|
192
|
+
// Arrange
|
|
193
|
+
const deviceModel = {
|
|
194
|
+
ControlWifi: {
|
|
195
|
+
WMStop: {
|
|
196
|
+
dataKey: 'washerDryer.state',
|
|
197
|
+
dataValue: 'STOP',
|
|
198
|
+
dataSetList: {
|
|
199
|
+
course: 'value1',
|
|
200
|
+
spin: 'value2',
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Act
|
|
207
|
+
const result = extractWasherStopCommand(deviceModel);
|
|
208
|
+
|
|
209
|
+
// Assert
|
|
210
|
+
expect(result?.dataSetList).toBeDefined();
|
|
211
|
+
expect(result?.dataSetList?.course).toBe('value1');
|
|
212
|
+
expect(result?.dataSetList?.spin).toBe('value2');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should fallback to data field when dataSetList is absent', () => {
|
|
216
|
+
// Arrange
|
|
217
|
+
const deviceModel = {
|
|
218
|
+
ControlWifi: {
|
|
219
|
+
WMStop: {
|
|
220
|
+
dataKey: 'washerDryer.state',
|
|
221
|
+
dataValue: 'STOP',
|
|
222
|
+
data: {
|
|
223
|
+
course: 'value1',
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// Act
|
|
230
|
+
const result = extractWasherStopCommand(deviceModel);
|
|
231
|
+
|
|
232
|
+
// Assert
|
|
233
|
+
expect(result?.dataSetList).toBeDefined();
|
|
234
|
+
expect(result?.dataSetList?.course).toBe('value1');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should prefer dataSetList over data field', () => {
|
|
238
|
+
// Arrange
|
|
239
|
+
const deviceModel = {
|
|
240
|
+
ControlWifi: {
|
|
241
|
+
WMStop: {
|
|
242
|
+
dataKey: 'washerDryer.state',
|
|
243
|
+
dataValue: 'STOP',
|
|
244
|
+
dataSetList: {
|
|
245
|
+
course: 'from-dataSetList',
|
|
246
|
+
},
|
|
247
|
+
data: {
|
|
248
|
+
course: 'from-data',
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// Act
|
|
255
|
+
const result = extractWasherStopCommand(deviceModel);
|
|
256
|
+
|
|
257
|
+
// Assert
|
|
258
|
+
expect(result?.dataSetList?.course).toBe('from-dataSetList');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should handle dataValue as any type (not just string)', () => {
|
|
262
|
+
// Arrange
|
|
263
|
+
const deviceModel = {
|
|
264
|
+
ControlWifi: {
|
|
265
|
+
WMStop: {
|
|
266
|
+
dataKey: 'washerDryer.state',
|
|
267
|
+
dataValue: 123,
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// Act
|
|
273
|
+
const result = extractWasherStopCommand(deviceModel);
|
|
274
|
+
|
|
275
|
+
// Assert
|
|
276
|
+
expect(result?.dataValue).toBe(123);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should handle boolean dataValue', () => {
|
|
280
|
+
// Arrange
|
|
281
|
+
const deviceModel = {
|
|
282
|
+
ControlWifi: {
|
|
283
|
+
WMStop: {
|
|
284
|
+
dataKey: 'washerDryer.state',
|
|
285
|
+
dataValue: true,
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// Act
|
|
291
|
+
const result = extractWasherStopCommand(deviceModel);
|
|
292
|
+
|
|
293
|
+
// Assert
|
|
294
|
+
expect(result?.dataValue).toBe(true);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should handle null dataValue', () => {
|
|
298
|
+
// Arrange
|
|
299
|
+
const deviceModel = {
|
|
300
|
+
ControlWifi: {
|
|
301
|
+
WMStop: {
|
|
302
|
+
dataKey: 'washerDryer.state',
|
|
303
|
+
dataValue: null,
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// Act
|
|
309
|
+
const result = extractWasherStopCommand(deviceModel);
|
|
310
|
+
|
|
311
|
+
// Assert
|
|
312
|
+
expect(result?.dataValue).toBeNull();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('should omit dataSetList when neither dataSetList nor data are objects', () => {
|
|
316
|
+
// Arrange
|
|
317
|
+
const deviceModel = {
|
|
318
|
+
ControlWifi: {
|
|
319
|
+
WMStop: {
|
|
320
|
+
dataKey: 'washerDryer.state',
|
|
321
|
+
dataValue: 'STOP',
|
|
322
|
+
dataSetList: 'not-an-object',
|
|
323
|
+
data: 'also-not-an-object',
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// Act
|
|
329
|
+
const result = extractWasherStopCommand(deviceModel);
|
|
330
|
+
|
|
331
|
+
// Assert
|
|
332
|
+
expect(result?.dataSetList).toBeUndefined();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('should handle ControlWifi as non-object gracefully', () => {
|
|
336
|
+
// Arrange
|
|
337
|
+
const deviceModel = {
|
|
338
|
+
ControlWifi: 'not-an-object',
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
// Act
|
|
342
|
+
const result = extractWasherStopCommand(deviceModel);
|
|
343
|
+
|
|
344
|
+
// Assert
|
|
345
|
+
expect(result).toBeUndefined();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('should handle deeply nested undefined properties gracefully', () => {
|
|
349
|
+
// Arrange
|
|
350
|
+
const deviceModel = {
|
|
351
|
+
ControlWifi: {
|
|
352
|
+
WMStop: {
|
|
353
|
+
dataKey: undefined,
|
|
354
|
+
dataValue: undefined,
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
// Act
|
|
360
|
+
const result = extractWasherStopCommand(deviceModel);
|
|
361
|
+
|
|
362
|
+
// Assert
|
|
363
|
+
expect(result).toBeDefined();
|
|
364
|
+
expect(result?.dataKey).toBeUndefined();
|
|
365
|
+
expect(result?.dataValue).toBeUndefined();
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('should include all fields from WMStop in payload', () => {
|
|
369
|
+
// Arrange
|
|
370
|
+
const deviceModel = {
|
|
371
|
+
ControlWifi: {
|
|
372
|
+
WMStop: {
|
|
373
|
+
command: 'Operation',
|
|
374
|
+
dataKey: 'washerDryer.state',
|
|
375
|
+
dataValue: 'STOP',
|
|
376
|
+
dataSetList: { course: 'express' },
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// Act
|
|
382
|
+
const result = extractWasherStopCommand(deviceModel);
|
|
383
|
+
|
|
384
|
+
// Assert
|
|
385
|
+
expect(result).toBeDefined();
|
|
386
|
+
expect(result?.command).toBe('Operation');
|
|
387
|
+
expect(result?.dataKey).toBe('washerDryer.state');
|
|
388
|
+
expect(result?.dataValue).toBe('STOP');
|
|
389
|
+
expect(result?.dataSetList).toBeDefined();
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { ENGINES, PLUGIN_NAME, PLUGIN_URL, PLUGIN_VERSION } from '../settings.js';
|
|
8
|
+
|
|
9
|
+
describe('settings', () => {
|
|
10
|
+
it('should export PLUGIN_NAME matching package.json name', () => {
|
|
11
|
+
const packageJsonPath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json');
|
|
12
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as Record<string, unknown>;
|
|
13
|
+
expect(PLUGIN_NAME).toBe(pkg.name);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should export PLUGIN_VERSION matching package.json version', () => {
|
|
17
|
+
const packageJsonPath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json');
|
|
18
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as Record<string, unknown>;
|
|
19
|
+
expect(PLUGIN_VERSION).toBe(pkg.version);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should export PLUGIN_URL matching package.json homepage', () => {
|
|
23
|
+
const packageJsonPath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json');
|
|
24
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as Record<string, unknown>;
|
|
25
|
+
expect(PLUGIN_URL).toBe(pkg.homepage);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should export ENGINES matching package.json engines', () => {
|
|
29
|
+
const packageJsonPath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json');
|
|
30
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as Record<string, unknown>;
|
|
31
|
+
expect(ENGINES).toEqual(pkg.engines);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Masks a secret string, leaving only the last `visibleChars` characters visible (e.g. `****abcd`). */
|
|
2
|
+
export function maskSecret(value: string | undefined, visibleChars = 4): string {
|
|
3
|
+
if (!value) {
|
|
4
|
+
return '';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
if (value.length <= visibleChars) {
|
|
8
|
+
return '*'.repeat(value.length);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return '*'.repeat(value.length - visibleChars) + value.slice(-visibleChars);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Field names (case-insensitive) whose values are masked by `maskSensitiveFields` before being logged. */
|
|
15
|
+
const SENSITIVE_FIELD_NAMES = new Set([
|
|
16
|
+
'refresh_token',
|
|
17
|
+
'refreshtoken',
|
|
18
|
+
'access_token',
|
|
19
|
+
'accesstoken',
|
|
20
|
+
'password',
|
|
21
|
+
'client_secret',
|
|
22
|
+
'clientsecret',
|
|
23
|
+
'secret',
|
|
24
|
+
'x-emp-token',
|
|
25
|
+
'authorization',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Returns a shallow-safe, deep-masked copy of a plain object/record for debug logging: any key matching a known
|
|
30
|
+
* secret field name (refresh/access token, password, client secret, auth headers) is masked via `maskSecret`;
|
|
31
|
+
* everything else is copied as-is. Intended for logging outgoing request headers/bodies without leaking secrets.
|
|
32
|
+
*/
|
|
33
|
+
export function maskSensitiveFields(source: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
|
|
34
|
+
if (!source) {
|
|
35
|
+
return source;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const result: Record<string, unknown> = {};
|
|
39
|
+
for (const [key, value] of Object.entries(source)) {
|
|
40
|
+
if (SENSITIVE_FIELD_NAMES.has(key.toLowerCase())) {
|
|
41
|
+
result[key] = typeof value === 'string' ? maskSecret(value) : value;
|
|
42
|
+
} else if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
43
|
+
result[key] = maskSensitiveFields(value as Record<string, unknown>);
|
|
44
|
+
} else {
|
|
45
|
+
result[key] = value;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"incremental": true,
|
|
4
|
+
"composite": true,
|
|
5
|
+
"target": "esnext",
|
|
6
|
+
"lib": ["ESNext"],
|
|
7
|
+
"module": "nodenext",
|
|
8
|
+
"moduleResolution": "nodenext",
|
|
9
|
+
"resolveJsonModule": true,
|
|
10
|
+
"allowJs": true,
|
|
11
|
+
"checkJs": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": true,
|
|
14
|
+
"sourceMap": true,
|
|
15
|
+
"preserveConstEnums": true,
|
|
16
|
+
"allowSyntheticDefaultImports": true,
|
|
17
|
+
"esModuleInterop": true,
|
|
18
|
+
"forceConsistentCasingInFileNames": true,
|
|
19
|
+
"strict": true,
|
|
20
|
+
"noImplicitAny": true,
|
|
21
|
+
"alwaysStrict": true,
|
|
22
|
+
"noImplicitOverride": true,
|
|
23
|
+
"skipLibCheck": true
|
|
24
|
+
}
|
|
25
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"incremental": false,
|
|
5
|
+
"composite": false,
|
|
6
|
+
"declaration": false,
|
|
7
|
+
"declarationMap": false,
|
|
8
|
+
"sourceMap": false,
|
|
9
|
+
"removeComments": true
|
|
10
|
+
},
|
|
11
|
+
"include": ["src/**/*.ts"],
|
|
12
|
+
"exclude": ["**/*.spec.ts", "**/*.test.ts", "**/__test__/*"]
|
|
13
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// eslint-disable
|
|
2
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
3
|
+
// @ts-nocheck
|
|
4
|
+
// vite.config.ts
|
|
5
|
+
|
|
6
|
+
// This Vitest configuration is designed for a TypeScript project.
|
|
7
|
+
|
|
8
|
+
import { defineConfig } from 'vitest/config';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import tsconfigPaths from 'vite-tsconfig-paths';
|
|
11
|
+
|
|
12
|
+
export default defineConfig({
|
|
13
|
+
plugins: [tsconfigPaths()],
|
|
14
|
+
test: {
|
|
15
|
+
include: ['src/**/*.test.ts', 'src/**/*.spec.ts'],
|
|
16
|
+
exclude: ['dist', 'node_modules', 'src/pluginTemplate/**', 'src/cli/**', 'src/cli.ts'],
|
|
17
|
+
globals: true,
|
|
18
|
+
clearMocks: true,
|
|
19
|
+
restoreMocks: true,
|
|
20
|
+
environment: 'node',
|
|
21
|
+
maxWorkers: 100,
|
|
22
|
+
coverage: {
|
|
23
|
+
provider: 'v8', // default, but explicit
|
|
24
|
+
reporter: ['text', 'lcov'],
|
|
25
|
+
include: ['src/**/*.{ts,tsx,js,jsx}'],
|
|
26
|
+
exclude: [
|
|
27
|
+
// Exclude test files that may live under src
|
|
28
|
+
'src/**/*.test.{ts,tsx,js,jsx}',
|
|
29
|
+
'src/**/*.spec.{ts,tsx,js,jsx}',
|
|
30
|
+
// Exclude helper file(s) not meant for coverage
|
|
31
|
+
'src/tests/**/*.ts',
|
|
32
|
+
// Exclude type declaration files
|
|
33
|
+
'src/**/*.d.ts',
|
|
34
|
+
],
|
|
35
|
+
thresholds: {
|
|
36
|
+
statements: 75,
|
|
37
|
+
branches: 75,
|
|
38
|
+
functions: 75,
|
|
39
|
+
lines: 75,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Memory Archive
|
|
2
|
+
|
|
3
|
+
Older entries pruned from `.claude/memory.md` to respect its per-section cap. Consult when working on the areas named below.
|
|
4
|
+
|
|
5
|
+
## Common Pitfalls (archived)
|
|
6
|
+
|
|
7
|
+
- `classifyIslands` (`legacyIslandDetection.ts`) size-heuristic bug (Jul 27, fixed): was gating per `hasNoWallOrZoneData` map-wide; now evaluates per-island as last-resort fallback — deleted 4-line variable, removed gate from condition, updated doc comment.
|
|
8
|
+
- Implementer must NOT run full builds or the whole test suite — its gate is `format:ci` → `lint:fix:ci` → `type-check:ci` only; `build:local:ci`/`test:ci` belong to compiler/test-writer.
|
|
9
|
+
- `platformRunner.ts:120` writes `activeMapId` BEFORE `handleActiveMapChanged` — a guard inside the handler can't prevent `activeMapId` desync, and the same-map guard (`:119`) then swallows an identical-mapId retry.
|
|
10
|
+
- `SELECT_AREAS` empty-input path (`roborockVacuumCleaner.ts:164-176`) must NOT call `trySwitchMap` — keep empty vs explicit branches structurally separate with early `return`, else V10/V1 (`activeMapId=-1`) fires unguarded `switchMap` on every global-clean.
|
|
11
|
+
- ESLint `preserve-caught-error` requires re-thrown errors to carry `{ cause: err }` — omitting it fails `lint:fix:ci` even when the message embeds the original error text.
|
|
12
|
+
- ESLint `no-base-to-string` prohibits stringifying `err.cause` directly in templates (`String(err.cause)`) — check `if (cause instanceof Error)` first, then safely access `.message`; this prevents accidental `[object Object]` in logs.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Reference Workspaces (allowlist)
|
|
2
|
+
|
|
3
|
+
External codebases agents may consult, **read-only**. Investigator and technical-architect may ONLY read paths listed here — anything not listed is out of bounds.
|
|
4
|
+
|
|
5
|
+
**Priority = list order.** Agents consult top-first and stop early once the question is answered.
|
|
6
|
+
|
|
7
|
+
Added for the `migrate-lg-thinq-to-matterbridge` task (Sep 8, 2026).
|
|
8
|
+
|
|
9
|
+
| Name | Path | Notes |
|
|
10
|
+
| ------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
11
|
+
| matterbridge-example-dynamic-platform | `/Volumes/ExternalSSD/code/references/matterbridge-example-dynamic-platform` | Canonical Matterbridge dynamic-platform plugin shape: lifecycle hooks, device factories (`matterbridge/devices`), config schema, build tooling. |
|
|
12
|
+
| homebridge-lg-thinq | `/Volumes/ExternalSSD/code/references/homebridge-lg-thinq` | Source logic to port: LG ThinQ cloud OAuth/auth flow, API client, device discovery, device type classes, capability mapping, MQTT/polling event model. |
|
|
13
|
+
| ioBroker.lg-thinq | `/Volumes/ExternalSSD/code/references/ioBroker.lg-thinq` | Second, independent LG ThinQ cloud implementation (JS, by TA2k, v1.2.2): per-device-type modules in `lib/` (e.g. `air_conditioning.js`, `heat_pump.js`), model JSON capability tables in `lib/modelJsonUri`, `main.js` auth/poll/MQTT flow. Use to cross-check payloads/scales/field names against homebridge-lg-thinq. |
|
|
14
|
+
| rethink | `/Volumes/ExternalSSD/code/references/rethink` | anszom/rethink (GPL-2.0, TypeScript): a LOCAL replacement for the LG ThinQ cloud that talks to appliances over LG's local wire protocol (TLV over a local MQTT broker) and exposes them to Home Assistant. READ ONLY for ideas, never copy code (license). Best for how the AC itself behaves: `cloud/devices/RAC_056905_WW.ts` is the user's exact AC model (field-to-feature mapping, ranges, quirks such as the energy sensor bias). `bridge/thinqApi.ts`, `oauth2.ts`, `thinq2connection.ts`, `tools/lgcloud-monitor.ts` show how the real cloud is reached. It does NOT use the official control API, so it cannot explain cloud error codes or `airState.*` names; match fields by meaning. |
|
|
15
|
+
| homebridge-webos-tv | `/Volumes/ExternalSSD/code/references/homebridge-webos-tv` | SECOND source to port: LG webOS TV control over local network/websocket (SSAP protocol), NOT the ThinQ cloud API — distinct auth/connection model (local client-key pairing). |
|
|
16
|
+
| matterbridge-roborock-vacuum-plugin | `/Volumes/ExternalSSD/code/matterbridge-roborock-vacuum-plugin` | Sibling plugin, closest structural precedent for a prior Homebridge→Matterbridge migration in this family (folder layout, matterbridge API usage, config/build tooling, node-persist token storage). |
|
|
17
|
+
|
|
18
|
+
## Rules
|
|
19
|
+
|
|
20
|
+
- Read-only — never modify, format, or stage files in these paths.
|
|
21
|
+
- CodeGraph does NOT work here (it indexes this repo only) — use Grep/Glob/Read.
|
|
22
|
+
- Cite findings as `<workspace-name>/path/to/file:line` in answers.
|
|
23
|
+
- To add a workspace: append a row here (with a one-line note on what it is good for) — no other change needed.
|