iobroker.foobar2000 2.2.0 → 2.3.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/.github/copilot-instructions.md +809 -0
- package/.github/dependabot.yml +11 -9
- package/.github/workflows/automerge-dependabot.yml +35 -0
- package/.github/workflows/check-copilot-template.yml +195 -0
- package/.github/workflows/test-and-release.yml +6 -4
- package/CHANGELOG_OLD.md +26 -0
- package/LICENSE +1 -1
- package/README.md +7 -27
- package/foobar2000.js +291 -261
- package/io-package.json +22 -7
- package/package.json +17 -41
- package/.eslintignore +0 -2
- package/.prettierrc.js +0 -9
- package/lib/tools.js +0 -99
- /package/.github/workflows/{dependabot-auto-merge.yml → dependabot-auto-merge.yml.OLD} +0 -0
|
@@ -0,0 +1,809 @@
|
|
|
1
|
+
# ioBroker Adapter Development with GitHub Copilot
|
|
2
|
+
|
|
3
|
+
**Version:** 0.5.7
|
|
4
|
+
**Template Source:** https://github.com/DrozmotiX/ioBroker-Copilot-Instructions
|
|
5
|
+
|
|
6
|
+
This file contains instructions and best practices for GitHub Copilot when working on ioBroker adapter development.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 📑 Table of Contents
|
|
11
|
+
|
|
12
|
+
1. [Project Context](#project-context)
|
|
13
|
+
2. [Code Quality & Standards](#code-quality--standards)
|
|
14
|
+
- [Code Style Guidelines](#code-style-guidelines)
|
|
15
|
+
- [ESLint Configuration](#eslint-configuration)
|
|
16
|
+
3. [Testing](#testing)
|
|
17
|
+
- [Unit Testing](#unit-testing)
|
|
18
|
+
- [Integration Testing](#integration-testing)
|
|
19
|
+
- [API Testing with Credentials](#api-testing-with-credentials)
|
|
20
|
+
4. [Development Best Practices](#development-best-practices)
|
|
21
|
+
- [Dependency Management](#dependency-management)
|
|
22
|
+
- [HTTP Client Libraries](#http-client-libraries)
|
|
23
|
+
- [Error Handling](#error-handling)
|
|
24
|
+
5. [Admin UI Configuration](#admin-ui-configuration)
|
|
25
|
+
- [JSON-Config Setup](#json-config-setup)
|
|
26
|
+
- [Translation Management](#translation-management)
|
|
27
|
+
6. [Documentation](#documentation)
|
|
28
|
+
- [README Updates](#readme-updates)
|
|
29
|
+
- [Changelog Management](#changelog-management)
|
|
30
|
+
7. [CI/CD & GitHub Actions](#cicd--github-actions)
|
|
31
|
+
- [Workflow Configuration](#workflow-configuration)
|
|
32
|
+
- [Testing Integration](#testing-integration)
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Project Context
|
|
37
|
+
|
|
38
|
+
You are working on an ioBroker adapter. ioBroker is an integration platform for the Internet of Things, focused on building smart home and industrial IoT solutions. Adapters are plugins that connect ioBroker to external systems, devices, or services.
|
|
39
|
+
|
|
40
|
+
### Adapter-Specific Context: Foobar2000 Player Control
|
|
41
|
+
|
|
42
|
+
This is the **ioBroker.foobar2000** adapter - a multimedia adapter that provides control interface for the Foobar2000 audio player. Key characteristics:
|
|
43
|
+
|
|
44
|
+
- **Primary Function**: Controls Foobar2000 media player via HTTP API using the foo_httpcontrol plugin
|
|
45
|
+
- **Communication Method**: HTTP GET requests to Foobar2000's web interface (default port 8888)
|
|
46
|
+
- **Key Features**: Playback control, volume management, playlist handling, track information, album art display
|
|
47
|
+
- **Operation Modes**: Local (direct file system access) and Remote (network-based control)
|
|
48
|
+
- **State Polling**: Regular HTTP polling to maintain current player state
|
|
49
|
+
- **Media States**: Comprehensive media player state management (play/pause/stop, seek, shuffle, repeat)
|
|
50
|
+
|
|
51
|
+
### Required External Components
|
|
52
|
+
|
|
53
|
+
- **foo_httpcontrol Plugin**: Must be installed in Foobar2000 for HTTP API access
|
|
54
|
+
- **Configuration Note**: Album art requires `albumart_prefer_embedded = 0` in foobar2000controller config
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Code Quality & Standards
|
|
59
|
+
|
|
60
|
+
### Code Style Guidelines
|
|
61
|
+
|
|
62
|
+
- Follow JavaScript/TypeScript best practices
|
|
63
|
+
- Use async/await for asynchronous operations
|
|
64
|
+
- Implement proper resource cleanup in `unload()` method
|
|
65
|
+
- Use semantic versioning for adapter releases
|
|
66
|
+
- Include proper JSDoc comments for public methods
|
|
67
|
+
|
|
68
|
+
**Timer and Resource Cleanup Example:**
|
|
69
|
+
```javascript
|
|
70
|
+
private connectionTimer?: NodeJS.Timeout;
|
|
71
|
+
|
|
72
|
+
async onReady() {
|
|
73
|
+
this.connectionTimer = setInterval(() => this.checkConnection(), 30000);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
onUnload(callback) {
|
|
77
|
+
try {
|
|
78
|
+
if (this.connectionTimer) {
|
|
79
|
+
clearInterval(this.connectionTimer);
|
|
80
|
+
this.connectionTimer = undefined;
|
|
81
|
+
}
|
|
82
|
+
callback();
|
|
83
|
+
} catch (e) {
|
|
84
|
+
callback();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### ESLint Configuration
|
|
90
|
+
|
|
91
|
+
**CRITICAL:** ESLint validation must run FIRST in your CI/CD pipeline, before any other tests. This "lint-first" approach catches code quality issues early.
|
|
92
|
+
|
|
93
|
+
#### Setup
|
|
94
|
+
```bash
|
|
95
|
+
npm install --save-dev eslint @iobroker/eslint-config
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
#### Configuration (.eslintrc.json)
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"extends": "@iobroker/eslint-config",
|
|
102
|
+
"rules": {
|
|
103
|
+
// Add project-specific rule overrides here if needed
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
#### Package.json Scripts
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"scripts": {
|
|
112
|
+
"lint": "eslint --max-warnings 0 .",
|
|
113
|
+
"lint:fix": "eslint . --fix"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
#### Best Practices
|
|
119
|
+
1. ✅ Run ESLint before committing — fix ALL warnings, not just errors
|
|
120
|
+
2. ✅ Use `lint:fix` for auto-fixable issues
|
|
121
|
+
3. ✅ Don't disable rules without documentation
|
|
122
|
+
4. ✅ Lint all relevant files (main code, tests, build scripts)
|
|
123
|
+
5. ✅ Keep `@iobroker/eslint-config` up to date
|
|
124
|
+
6. ✅ **ESLint warnings are treated as errors in CI** (`--max-warnings 0`). The `lint` script above already includes this flag — run `npm run lint` to match CI behavior locally
|
|
125
|
+
|
|
126
|
+
#### Common Issues
|
|
127
|
+
- **Unused variables**: Remove or prefix with underscore (`_variable`)
|
|
128
|
+
- **Missing semicolons**: Run `npm run lint:fix`
|
|
129
|
+
- **Indentation**: Use 4 spaces (ioBroker standard)
|
|
130
|
+
- **console.log**: Replace with `adapter.log.debug()` or remove
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Testing
|
|
135
|
+
|
|
136
|
+
### Unit Testing
|
|
137
|
+
|
|
138
|
+
- Use Jest as the primary testing framework
|
|
139
|
+
- Create tests for all adapter main functions and helper methods
|
|
140
|
+
- Test error handling scenarios and edge cases
|
|
141
|
+
- Mock external API calls and hardware dependencies
|
|
142
|
+
- For adapters connecting to APIs/devices not reachable by internet, provide example data files
|
|
143
|
+
|
|
144
|
+
**Example Structure:**
|
|
145
|
+
```javascript
|
|
146
|
+
describe('AdapterName', () => {
|
|
147
|
+
let adapter;
|
|
148
|
+
|
|
149
|
+
beforeEach(() => {
|
|
150
|
+
// Setup test adapter instance
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('should initialize correctly', () => {
|
|
154
|
+
// Test adapter initialization
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Integration Testing
|
|
160
|
+
|
|
161
|
+
**CRITICAL:** Use the official `@iobroker/testing` framework. This is the ONLY correct way to test ioBroker adapters.
|
|
162
|
+
|
|
163
|
+
**Official Documentation:** https://github.com/ioBroker/testing
|
|
164
|
+
|
|
165
|
+
#### Framework Structure
|
|
166
|
+
|
|
167
|
+
**✅ Correct Pattern:**
|
|
168
|
+
```javascript
|
|
169
|
+
const path = require('path');
|
|
170
|
+
const { tests } = require('@iobroker/testing');
|
|
171
|
+
|
|
172
|
+
tests.integration(path.join(__dirname, '..'), {
|
|
173
|
+
defineAdditionalTests({ suite }) {
|
|
174
|
+
suite('Test adapter with specific configuration', (getHarness) => {
|
|
175
|
+
let harness;
|
|
176
|
+
|
|
177
|
+
before(() => {
|
|
178
|
+
harness = getHarness();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should configure and start adapter', function () {
|
|
182
|
+
return new Promise(async (resolve, reject) => {
|
|
183
|
+
try {
|
|
184
|
+
// Get adapter object
|
|
185
|
+
const obj = await new Promise((res, rej) => {
|
|
186
|
+
harness.objects.getObject('system.adapter.your-adapter.0', (err, o) => {
|
|
187
|
+
if (err) return rej(err);
|
|
188
|
+
res(o);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
if (!obj) return reject(new Error('Adapter object not found'));
|
|
193
|
+
|
|
194
|
+
// Configure adapter
|
|
195
|
+
Object.assign(obj.native, {
|
|
196
|
+
position: '52.520008,13.404954',
|
|
197
|
+
createHourly: true,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
harness.objects.setObject(obj._id, obj);
|
|
201
|
+
|
|
202
|
+
// Start and wait
|
|
203
|
+
await harness.startAdapterAndWait();
|
|
204
|
+
await new Promise(resolve => setTimeout(resolve, 15000));
|
|
205
|
+
|
|
206
|
+
// Verify states
|
|
207
|
+
const stateIds = await harness.dbConnection.getStateIDs('your-adapter.0.*');
|
|
208
|
+
|
|
209
|
+
if (stateIds.length > 0) {
|
|
210
|
+
console.log('✅ Adapter successfully created states');
|
|
211
|
+
await harness.stopAdapter();
|
|
212
|
+
resolve(true);
|
|
213
|
+
} else {
|
|
214
|
+
reject(new Error('Adapter did not create any states'));
|
|
215
|
+
}
|
|
216
|
+
} catch (error) {
|
|
217
|
+
reject(error);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}).timeout(40000);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
#### Testing Success AND Failure Scenarios
|
|
227
|
+
|
|
228
|
+
**IMPORTANT:** For every "it works" test, implement corresponding "it fails gracefully" tests.
|
|
229
|
+
|
|
230
|
+
**Failure Scenario Example:**
|
|
231
|
+
```javascript
|
|
232
|
+
it('should NOT create daily states when daily is disabled', function () {
|
|
233
|
+
return new Promise(async (resolve, reject) => {
|
|
234
|
+
try {
|
|
235
|
+
harness = getHarness();
|
|
236
|
+
const obj = await new Promise((res, rej) => {
|
|
237
|
+
harness.objects.getObject('system.adapter.your-adapter.0', (err, o) => {
|
|
238
|
+
if (err) return rej(err);
|
|
239
|
+
res(o);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
if (!obj) return reject(new Error('Adapter object not found'));
|
|
244
|
+
|
|
245
|
+
Object.assign(obj.native, {
|
|
246
|
+
createDaily: false, // Daily disabled
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
await new Promise((res, rej) => {
|
|
250
|
+
harness.objects.setObject(obj._id, obj, (err) => {
|
|
251
|
+
if (err) return rej(err);
|
|
252
|
+
res(undefined);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
await harness.startAdapterAndWait();
|
|
257
|
+
await new Promise((res) => setTimeout(res, 20000));
|
|
258
|
+
|
|
259
|
+
const stateIds = await harness.dbConnection.getStateIDs('your-adapter.0.*');
|
|
260
|
+
const dailyStates = stateIds.filter((key) => key.includes('daily'));
|
|
261
|
+
|
|
262
|
+
if (dailyStates.length === 0) {
|
|
263
|
+
console.log('✅ No daily states found as expected');
|
|
264
|
+
resolve(true);
|
|
265
|
+
} else {
|
|
266
|
+
reject(new Error('Expected no daily states but found some'));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
await harness.stopAdapter();
|
|
270
|
+
} catch (error) {
|
|
271
|
+
reject(error);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}).timeout(40000);
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
#### Key Rules
|
|
278
|
+
|
|
279
|
+
1. ✅ Use `@iobroker/testing` framework
|
|
280
|
+
2. ✅ Configure via `harness.objects.setObject()`
|
|
281
|
+
3. ✅ Start via `harness.startAdapterAndWait()`
|
|
282
|
+
4. ✅ Verify states via `harness.states.getState()`
|
|
283
|
+
5. ✅ Allow proper timeouts for async operations
|
|
284
|
+
6. ❌ NEVER test API URLs directly
|
|
285
|
+
7. ❌ NEVER bypass the harness system
|
|
286
|
+
|
|
287
|
+
#### Workflow Dependencies
|
|
288
|
+
|
|
289
|
+
Integration tests should run ONLY after lint and adapter tests pass:
|
|
290
|
+
|
|
291
|
+
```yaml
|
|
292
|
+
integration-tests:
|
|
293
|
+
needs: [check-and-lint, adapter-tests]
|
|
294
|
+
runs-on: ubuntu-22.04
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### API Testing with Credentials
|
|
298
|
+
|
|
299
|
+
For adapters connecting to external APIs requiring authentication:
|
|
300
|
+
|
|
301
|
+
#### Password Encryption for Integration Tests
|
|
302
|
+
|
|
303
|
+
```javascript
|
|
304
|
+
async function encryptPassword(harness, password) {
|
|
305
|
+
const systemConfig = await harness.objects.getObjectAsync("system.config");
|
|
306
|
+
if (!systemConfig?.native?.secret) {
|
|
307
|
+
throw new Error("Could not retrieve system secret for password encryption");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const secret = systemConfig.native.secret;
|
|
311
|
+
let result = '';
|
|
312
|
+
for (let i = 0; i < password.length; ++i) {
|
|
313
|
+
result += String.fromCharCode(secret[i % secret.length].charCodeAt(0) ^ password.charCodeAt(i));
|
|
314
|
+
}
|
|
315
|
+
return result;
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
#### Demo Credentials Testing Pattern
|
|
320
|
+
|
|
321
|
+
- Use provider demo credentials when available (e.g., `demo@api-provider.com` / `demo`)
|
|
322
|
+
- Create separate test file: `test/integration-demo.js`
|
|
323
|
+
- Add npm script: `"test:integration-demo": "mocha test/integration-demo --exit"`
|
|
324
|
+
- Implement clear success/failure criteria
|
|
325
|
+
|
|
326
|
+
**Example Implementation:**
|
|
327
|
+
```javascript
|
|
328
|
+
it("Should connect to API with demo credentials", async () => {
|
|
329
|
+
const encryptedPassword = await encryptPassword(harness, "demo_password");
|
|
330
|
+
|
|
331
|
+
await harness.changeAdapterConfig("your-adapter", {
|
|
332
|
+
native: {
|
|
333
|
+
username: "demo@provider.com",
|
|
334
|
+
password: encryptedPassword,
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
await harness.startAdapter();
|
|
339
|
+
await new Promise(resolve => setTimeout(resolve, 60000));
|
|
340
|
+
|
|
341
|
+
const connectionState = await harness.states.getStateAsync("your-adapter.0.info.connection");
|
|
342
|
+
|
|
343
|
+
if (connectionState?.val === true) {
|
|
344
|
+
console.log("✅ SUCCESS: API connection established");
|
|
345
|
+
return true;
|
|
346
|
+
} else {
|
|
347
|
+
throw new Error("API Test Failed: Expected API connection. Check logs for API errors.");
|
|
348
|
+
}
|
|
349
|
+
}).timeout(120000);
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## Development Best Practices
|
|
355
|
+
|
|
356
|
+
### Dependency Management
|
|
357
|
+
|
|
358
|
+
- Always use `npm` for dependency management
|
|
359
|
+
- Use `npm ci` for installing existing dependencies (respects package-lock.json)
|
|
360
|
+
- Use `npm install` only when adding or updating dependencies
|
|
361
|
+
- Keep dependencies minimal and focused
|
|
362
|
+
- Only update dependencies in separate Pull Requests
|
|
363
|
+
|
|
364
|
+
**When modifying package.json:**
|
|
365
|
+
1. Run `npm install` to sync package-lock.json
|
|
366
|
+
2. Commit both package.json and package-lock.json together
|
|
367
|
+
|
|
368
|
+
**Best Practices:**
|
|
369
|
+
- Prefer built-in Node.js modules when possible
|
|
370
|
+
- Use `@iobroker/adapter-core` for adapter base functionality
|
|
371
|
+
- Avoid deprecated packages
|
|
372
|
+
- Document specific version requirements
|
|
373
|
+
|
|
374
|
+
### HTTP Client Libraries
|
|
375
|
+
|
|
376
|
+
- **Preferred:** Use native `fetch` API (Node.js 20+ required)
|
|
377
|
+
- **Avoid:** `axios` unless specific features are required
|
|
378
|
+
|
|
379
|
+
**Example with fetch:**
|
|
380
|
+
```javascript
|
|
381
|
+
try {
|
|
382
|
+
const response = await fetch('https://api.example.com/data');
|
|
383
|
+
if (!response.ok) {
|
|
384
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
385
|
+
}
|
|
386
|
+
const data = await response.json();
|
|
387
|
+
} catch (error) {
|
|
388
|
+
this.log.error(`API request failed: ${error.message}`);
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
**Other Recommendations:**
|
|
393
|
+
- **Logging:** Use adapter built-in logging (`this.log.*`)
|
|
394
|
+
- **Scheduling:** Use adapter built-in timers and intervals
|
|
395
|
+
- **File operations:** Use Node.js `fs/promises`
|
|
396
|
+
- **Configuration:** Use adapter config system
|
|
397
|
+
|
|
398
|
+
### Error Handling
|
|
399
|
+
|
|
400
|
+
- Always catch and log errors appropriately
|
|
401
|
+
- Use adapter log levels (error, warn, info, debug)
|
|
402
|
+
- Provide meaningful, user-friendly error messages
|
|
403
|
+
- Handle network failures gracefully
|
|
404
|
+
- Implement retry mechanisms where appropriate
|
|
405
|
+
- Always clean up timers, intervals, and resources in `unload()` method
|
|
406
|
+
|
|
407
|
+
**Example:**
|
|
408
|
+
```javascript
|
|
409
|
+
try {
|
|
410
|
+
await this.connectToDevice();
|
|
411
|
+
} catch (error) {
|
|
412
|
+
this.log.error(`Failed to connect to device: ${error.message}`);
|
|
413
|
+
this.setState('info.connection', false, true);
|
|
414
|
+
// Implement retry logic if needed
|
|
415
|
+
}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
## Admin UI Configuration
|
|
421
|
+
|
|
422
|
+
### JSON-Config Setup
|
|
423
|
+
|
|
424
|
+
Use JSON-Config format for modern ioBroker admin interfaces.
|
|
425
|
+
|
|
426
|
+
**Example Structure:**
|
|
427
|
+
```json
|
|
428
|
+
{
|
|
429
|
+
"type": "panel",
|
|
430
|
+
"items": {
|
|
431
|
+
"host": {
|
|
432
|
+
"type": "text",
|
|
433
|
+
"label": "Host address",
|
|
434
|
+
"help": "IP address or hostname of the device"
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
**Guidelines:**
|
|
441
|
+
- ✅ Use consistent naming conventions
|
|
442
|
+
- ✅ Provide sensible default values
|
|
443
|
+
- ✅ Include validation for required fields
|
|
444
|
+
- ✅ Add tooltips for complex options
|
|
445
|
+
- ✅ Ensure translations for all supported languages (minimum English and German)
|
|
446
|
+
- ✅ Write end-user friendly labels, avoid technical jargon
|
|
447
|
+
|
|
448
|
+
### Translation Management
|
|
449
|
+
|
|
450
|
+
**CRITICAL:** Translation files must stay synchronized with `admin/jsonConfig.json`. Orphaned keys or missing translations cause UI issues and PR review delays.
|
|
451
|
+
|
|
452
|
+
#### Overview
|
|
453
|
+
- **Location:** `admin/i18n/{lang}/translations.json` for 11 languages (de, en, es, fr, it, nl, pl, pt, ru, uk, zh-cn)
|
|
454
|
+
- **Source of truth:** `admin/jsonConfig.json` - all `label` and `help` properties must have translations
|
|
455
|
+
- **Command:** `npm run translate` - auto-generates translations but does NOT remove orphaned keys
|
|
456
|
+
- **Formatting:** English uses tabs, other languages use 4 spaces
|
|
457
|
+
|
|
458
|
+
#### Critical Rules
|
|
459
|
+
1. ✅ Keys must match exactly with jsonConfig.json
|
|
460
|
+
2. ✅ No orphaned keys in translation files
|
|
461
|
+
3. ✅ All translations must be in native language (no English fallbacks)
|
|
462
|
+
4. ✅ Keys must be sorted alphabetically
|
|
463
|
+
|
|
464
|
+
#### Workflow for Translation Updates
|
|
465
|
+
|
|
466
|
+
**When modifying admin/jsonConfig.json:**
|
|
467
|
+
|
|
468
|
+
1. Make your changes to labels/help texts
|
|
469
|
+
2. Run automatic translation: `npm run translate`
|
|
470
|
+
3. Create validation script (`scripts/validate-translations.js`):
|
|
471
|
+
|
|
472
|
+
```javascript
|
|
473
|
+
const fs = require('fs');
|
|
474
|
+
const path = require('path');
|
|
475
|
+
const jsonConfig = JSON.parse(fs.readFileSync('admin/jsonConfig.json', 'utf8'));
|
|
476
|
+
|
|
477
|
+
function extractTexts(obj, texts = new Set()) {
|
|
478
|
+
if (typeof obj === 'object' && obj !== null) {
|
|
479
|
+
if (obj.label) texts.add(obj.label);
|
|
480
|
+
if (obj.help) texts.add(obj.help);
|
|
481
|
+
for (const key in obj) {
|
|
482
|
+
extractTexts(obj[key], texts);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return texts;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const requiredTexts = extractTexts(jsonConfig);
|
|
489
|
+
const languages = ['de', 'en', 'es', 'fr', 'it', 'nl', 'pl', 'pt', 'ru', 'uk', 'zh-cn'];
|
|
490
|
+
let hasErrors = false;
|
|
491
|
+
|
|
492
|
+
languages.forEach(lang => {
|
|
493
|
+
const translationPath = path.join('admin', 'i18n', lang, 'translations.json');
|
|
494
|
+
const translations = JSON.parse(fs.readFileSync(translationPath, 'utf8'));
|
|
495
|
+
const translationKeys = new Set(Object.keys(translations));
|
|
496
|
+
|
|
497
|
+
const missing = Array.from(requiredTexts).filter(text => !translationKeys.has(text));
|
|
498
|
+
const orphaned = Array.from(translationKeys).filter(key => !requiredTexts.has(key));
|
|
499
|
+
|
|
500
|
+
console.log(`\n=== ${lang} ===`);
|
|
501
|
+
if (missing.length > 0) {
|
|
502
|
+
console.error('❌ Missing keys:', missing);
|
|
503
|
+
hasErrors = true;
|
|
504
|
+
}
|
|
505
|
+
if (orphaned.length > 0) {
|
|
506
|
+
console.error('❌ Orphaned keys (REMOVE THESE):', orphaned);
|
|
507
|
+
hasErrors = true;
|
|
508
|
+
}
|
|
509
|
+
if (missing.length === 0 && orphaned.length === 0) {
|
|
510
|
+
console.log('✅ All keys match!');
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
process.exit(hasErrors ? 1 : 0);
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
4. Run validation: `node scripts/validate-translations.js`
|
|
518
|
+
5. Remove orphaned keys manually from all translation files
|
|
519
|
+
6. Add missing translations in native languages
|
|
520
|
+
7. Run: `npm run lint && npm run test`
|
|
521
|
+
|
|
522
|
+
#### Add Validation to package.json
|
|
523
|
+
|
|
524
|
+
```json
|
|
525
|
+
{
|
|
526
|
+
"scripts": {
|
|
527
|
+
"translate": "translate-adapter",
|
|
528
|
+
"validate:translations": "node scripts/validate-translations.js",
|
|
529
|
+
"pretest": "npm run lint && npm run validate:translations"
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
#### Translation Checklist
|
|
535
|
+
|
|
536
|
+
Before committing changes to admin UI or translations:
|
|
537
|
+
1. ✅ Validation script shows "All keys match!" for all 11 languages
|
|
538
|
+
2. ✅ No orphaned keys in any translation file
|
|
539
|
+
3. ✅ All translations in native language
|
|
540
|
+
4. ✅ Keys alphabetically sorted
|
|
541
|
+
5. ✅ `npm run lint` passes
|
|
542
|
+
6. ✅ `npm run test` passes
|
|
543
|
+
7. ✅ Admin UI displays correctly
|
|
544
|
+
|
|
545
|
+
---
|
|
546
|
+
|
|
547
|
+
## Documentation
|
|
548
|
+
|
|
549
|
+
### README Updates
|
|
550
|
+
|
|
551
|
+
#### Required Sections
|
|
552
|
+
1. **Installation** - Clear npm/ioBroker admin installation steps
|
|
553
|
+
2. **Configuration** - Detailed configuration options with examples
|
|
554
|
+
3. **Usage** - Practical examples and use cases
|
|
555
|
+
4. **Changelog** - Version history (use "## **WORK IN PROGRESS**" for ongoing changes)
|
|
556
|
+
5. **License** - License information (typically MIT for ioBroker adapters)
|
|
557
|
+
6. **Support** - Links to issues, discussions, community support
|
|
558
|
+
|
|
559
|
+
#### Documentation Standards
|
|
560
|
+
- Use clear, concise language
|
|
561
|
+
- Include code examples for configuration
|
|
562
|
+
- Add screenshots for admin interface when applicable
|
|
563
|
+
- Maintain multilingual support (minimum English and German)
|
|
564
|
+
- Always reference issues in commits and PRs (e.g., "fixes #xx")
|
|
565
|
+
|
|
566
|
+
#### Mandatory README Updates for PRs
|
|
567
|
+
|
|
568
|
+
For **every PR or new feature**, always add a user-friendly entry to README.md:
|
|
569
|
+
|
|
570
|
+
- Add entries under `## **WORK IN PROGRESS**` section
|
|
571
|
+
- Use format: `* (author) **TYPE**: Description of user-visible change`
|
|
572
|
+
- Types: **NEW** (features), **FIXED** (bugs), **ENHANCED** (improvements), **TESTING** (test additions), **CI/CD** (automation)
|
|
573
|
+
- Focus on user impact, not technical details
|
|
574
|
+
|
|
575
|
+
**Example:**
|
|
576
|
+
```markdown
|
|
577
|
+
## **WORK IN PROGRESS**
|
|
578
|
+
|
|
579
|
+
* (DutchmanNL) **FIXED**: Adapter now properly validates login credentials (fixes #25)
|
|
580
|
+
* (DutchmanNL) **NEW**: Added device discovery to simplify initial setup
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
### Changelog Management
|
|
584
|
+
|
|
585
|
+
Follow the [AlCalzone release-script](https://github.com/AlCalzone/release-script) standard.
|
|
586
|
+
|
|
587
|
+
#### Format Requirements
|
|
588
|
+
|
|
589
|
+
```markdown
|
|
590
|
+
# Changelog
|
|
591
|
+
|
|
592
|
+
<!--
|
|
593
|
+
Placeholder for the next version (at the beginning of the line):
|
|
594
|
+
## **WORK IN PROGRESS**
|
|
595
|
+
-->
|
|
596
|
+
|
|
597
|
+
## **WORK IN PROGRESS**
|
|
598
|
+
|
|
599
|
+
- (author) **NEW**: Added new feature X
|
|
600
|
+
- (author) **FIXED**: Fixed bug Y (fixes #25)
|
|
601
|
+
|
|
602
|
+
## v0.1.0 (2023-01-01)
|
|
603
|
+
Initial release
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
#### Workflow Process
|
|
607
|
+
- **During Development:** All changes go under `## **WORK IN PROGRESS**`
|
|
608
|
+
- **For Every PR:** Add user-facing changes to WORK IN PROGRESS section
|
|
609
|
+
- **Before Merge:** Version number and date added when merging to main
|
|
610
|
+
- **Release Process:** Release-script automatically converts placeholder to actual version
|
|
611
|
+
|
|
612
|
+
#### Change Entry Format
|
|
613
|
+
- Format: `- (author) **TYPE**: User-friendly description`
|
|
614
|
+
- Types: **NEW**, **FIXED**, **ENHANCED**
|
|
615
|
+
- Focus on user impact, not technical implementation
|
|
616
|
+
- Reference issues: "fixes #XX" or "solves #XX"
|
|
617
|
+
|
|
618
|
+
---
|
|
619
|
+
|
|
620
|
+
## CI/CD & GitHub Actions
|
|
621
|
+
|
|
622
|
+
### Workflow Configuration
|
|
623
|
+
|
|
624
|
+
#### GitHub Actions Best Practices
|
|
625
|
+
|
|
626
|
+
**Must use ioBroker official testing actions:**
|
|
627
|
+
- `ioBroker/testing-action-check@v1` for lint and package validation
|
|
628
|
+
- `ioBroker/testing-action-adapter@v1` for adapter tests
|
|
629
|
+
- `ioBroker/testing-action-deploy@v1` for automated releases with Trusted Publishing (OIDC)
|
|
630
|
+
|
|
631
|
+
**Configuration:**
|
|
632
|
+
- **Node.js versions:** Test on 20.x, 22.x, 24.x
|
|
633
|
+
- **Platform:** Use ubuntu-22.04
|
|
634
|
+
- **Automated releases:** Deploy to npm on version tags (requires NPM Trusted Publishing)
|
|
635
|
+
- **Monitoring:** Include Sentry release tracking for error monitoring
|
|
636
|
+
|
|
637
|
+
#### Critical: Lint-First Validation Workflow
|
|
638
|
+
|
|
639
|
+
**ALWAYS run ESLint checks BEFORE other tests.** Benefits:
|
|
640
|
+
- Catches code quality issues immediately
|
|
641
|
+
- Prevents wasting CI resources on tests that would fail due to linting errors
|
|
642
|
+
- Provides faster feedback to developers
|
|
643
|
+
- Enforces consistent code quality
|
|
644
|
+
|
|
645
|
+
**Workflow Dependency Configuration:**
|
|
646
|
+
```yaml
|
|
647
|
+
jobs:
|
|
648
|
+
check-and-lint:
|
|
649
|
+
# Runs ESLint and package validation
|
|
650
|
+
# Uses: ioBroker/testing-action-check@v1
|
|
651
|
+
|
|
652
|
+
adapter-tests:
|
|
653
|
+
needs: [check-and-lint] # Wait for linting to pass
|
|
654
|
+
# Run adapter unit tests
|
|
655
|
+
|
|
656
|
+
integration-tests:
|
|
657
|
+
needs: [check-and-lint, adapter-tests] # Wait for both
|
|
658
|
+
# Run integration tests
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
**Key Points:**
|
|
662
|
+
- The `check-and-lint` job has NO dependencies - runs first
|
|
663
|
+
- ALL other test jobs MUST list `check-and-lint` in their `needs` array
|
|
664
|
+
- If linting fails, no other tests run, saving time
|
|
665
|
+
- Fix all ESLint errors before proceeding
|
|
666
|
+
|
|
667
|
+
### Testing Integration
|
|
668
|
+
|
|
669
|
+
#### API Testing in CI/CD
|
|
670
|
+
|
|
671
|
+
For adapters with external API dependencies:
|
|
672
|
+
|
|
673
|
+
```yaml
|
|
674
|
+
demo-api-tests:
|
|
675
|
+
if: contains(github.event.head_commit.message, '[skip ci]') == false
|
|
676
|
+
runs-on: ubuntu-22.04
|
|
677
|
+
|
|
678
|
+
steps:
|
|
679
|
+
- name: Checkout code
|
|
680
|
+
uses: actions/checkout@v4
|
|
681
|
+
|
|
682
|
+
- name: Use Node.js 20.x
|
|
683
|
+
uses: actions/setup-node@v4
|
|
684
|
+
with:
|
|
685
|
+
node-version: 20.x
|
|
686
|
+
cache: 'npm'
|
|
687
|
+
|
|
688
|
+
- name: Install dependencies
|
|
689
|
+
run: npm ci
|
|
690
|
+
|
|
691
|
+
- name: Run demo API tests
|
|
692
|
+
run: npm run test:integration-demo
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
#### Testing Best Practices
|
|
696
|
+
- Run credential tests separately from main test suite
|
|
697
|
+
- Don't make credential tests required for deployment
|
|
698
|
+
- Provide clear failure messages for API issues
|
|
699
|
+
- Use appropriate timeouts for external calls (120+ seconds)
|
|
700
|
+
|
|
701
|
+
#### Package.json Integration
|
|
702
|
+
```json
|
|
703
|
+
{
|
|
704
|
+
"scripts": {
|
|
705
|
+
"test:integration-demo": "mocha test/integration-demo --exit"
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
---
|
|
711
|
+
|
|
712
|
+
## Adapter-Specific Patterns
|
|
713
|
+
|
|
714
|
+
### Foobar2000 HTTP API Communication
|
|
715
|
+
|
|
716
|
+
The adapter communicates with Foobar2000 via HTTP GET requests to the foo_httpcontrol plugin. Use the following patterns:
|
|
717
|
+
|
|
718
|
+
```javascript
|
|
719
|
+
// HTTP command execution pattern
|
|
720
|
+
async executeCommand(command, param) {
|
|
721
|
+
try {
|
|
722
|
+
const response = await this.httpGet(command, [param]);
|
|
723
|
+
if (response && response.data) {
|
|
724
|
+
return response.data;
|
|
725
|
+
}
|
|
726
|
+
} catch (error) {
|
|
727
|
+
this.log.error(`Command ${command} failed: ${error.message}`);
|
|
728
|
+
this.setStateChanged('info.connection', false, true);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Media state update pattern
|
|
733
|
+
updateMediaState(playerData) {
|
|
734
|
+
if (playerData.isplaying) {
|
|
735
|
+
this.setState('state', 'play', true);
|
|
736
|
+
} else if (playerData.ispaused) {
|
|
737
|
+
this.setState('state', 'pause', true);
|
|
738
|
+
} else {
|
|
739
|
+
this.setState('state', 'stop', true);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
- Implement connection retry logic with exponential backoff
|
|
745
|
+
- Handle network timeouts gracefully (default 5 seconds)
|
|
746
|
+
- Parse JSON responses safely with error handling
|
|
747
|
+
- Cache frequently accessed data (like playlist information)
|
|
748
|
+
- Validate HTTP response status codes before processing
|
|
749
|
+
|
|
750
|
+
### Adapter Configuration Structure
|
|
751
|
+
|
|
752
|
+
```javascript
|
|
753
|
+
// Native configuration object structure
|
|
754
|
+
{
|
|
755
|
+
ip: "127.0.0.1", // Foobar2000 server IP
|
|
756
|
+
port: 8888, // HTTP control port
|
|
757
|
+
login: "", // Optional authentication
|
|
758
|
+
password: "", // Optional password
|
|
759
|
+
path: "C:/Program Files (x86)/foobar2000/", // Local installation path
|
|
760
|
+
cmdstart: "", // Start command for local mode
|
|
761
|
+
cmdexit: "" // Exit command for local mode
|
|
762
|
+
}
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
- Detect local vs remote operation mode based on IP address
|
|
766
|
+
- Handle authentication if credentials are provided
|
|
767
|
+
- Validate connection parameters on adapter start
|
|
768
|
+
- Implement connection health checks
|
|
769
|
+
|
|
770
|
+
### State Definitions and Media Controls
|
|
771
|
+
|
|
772
|
+
**Core Media States:**
|
|
773
|
+
- `state`: Current playback state (play/pause/stop/next/previous)
|
|
774
|
+
- `volume`: Volume level (0-100%)
|
|
775
|
+
- `mute`: Mute state (boolean)
|
|
776
|
+
- `seek`: Playback position (0-100%)
|
|
777
|
+
|
|
778
|
+
**Track Information States:**
|
|
779
|
+
- `title`: Current track title
|
|
780
|
+
- `artist`: Current track artist
|
|
781
|
+
- `album`: Current track album
|
|
782
|
+
- `albumArt`: Album art URL or file path
|
|
783
|
+
|
|
784
|
+
**Playlist and Control States:**
|
|
785
|
+
- `playlist`: Current playlist data (JSON string)
|
|
786
|
+
- `itemPlaying`: Currently playing track number
|
|
787
|
+
- `repeat`: Repeat mode (Off/All/One)
|
|
788
|
+
- `shuffle`: Shuffle state (boolean)
|
|
789
|
+
|
|
790
|
+
**Parameter bounds validation:**
|
|
791
|
+
- Volume: 0-100
|
|
792
|
+
- Seek: 0-100%
|
|
793
|
+
|
|
794
|
+
### Foobar2000 Specific Error Scenarios
|
|
795
|
+
|
|
796
|
+
- Player not running or foo_httpcontrol plugin not installed
|
|
797
|
+
- Invalid command parameters or unsupported operations
|
|
798
|
+
- File system access errors in local mode
|
|
799
|
+
- Playlist or track access permissions
|
|
800
|
+
- Album art requires `albumart_prefer_embedded = 0` in foobar2000controller config
|
|
801
|
+
|
|
802
|
+
### Test Coverage Areas for Foobar2000 Adapter
|
|
803
|
+
|
|
804
|
+
- HTTP request handling and error recovery
|
|
805
|
+
- Media state parsing and transformation
|
|
806
|
+
- Playlist management operations
|
|
807
|
+
- Volume control and mute functionality
|
|
808
|
+
- Album art URL generation and caching
|
|
809
|
+
- Mock external HTTP calls to Foobar2000 player during testing
|