iobroker.fakeroku 0.2.3 → 0.4.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.
@@ -0,0 +1,917 @@
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
+ This is the **fakeroku** adapter, which emulates Roku devices to enable integration with Logitech Harmony Hubs. The adapter creates virtual Roku devices that can be discovered and controlled by Harmony Hubs, allowing ioBroker to respond to Harmony Hub commands as if it were a real Roku device.
41
+
42
+ ## Adapter-Specific Context
43
+
44
+ - **Adapter Name:** fakeroku
45
+ - **Primary Function:** Emulates Roku devices for Logitech Harmony Hub integration
46
+ - **Key Dependencies:**
47
+ - `http-headers` for HTTP response handling
48
+ - `@iobroker/adapter-core` for ioBroker integration
49
+ - UDP multicast for device discovery (SSDP protocol)
50
+ - **Configuration Requirements:**
51
+ - LAN IP address (not 0.0.0.0)
52
+ - Multicast IP (default: 239.255.255.250)
53
+ - Virtual Roku devices configuration (name, port, UUID)
54
+ - **Network Protocols:**
55
+ - HTTP server for command reception
56
+ - UDP multicast for SSDP device discovery
57
+ - ECP (External Control Protocol) for Roku command handling
58
+
59
+ ---
60
+
61
+ ## Code Quality & Standards
62
+
63
+ ### Code Style Guidelines
64
+
65
+ - Follow JavaScript/TypeScript best practices
66
+ - Use async/await for asynchronous operations
67
+ - Implement proper resource cleanup in `unload()` method
68
+ - Use semantic versioning for adapter releases
69
+ - Include proper JSDoc comments for public methods
70
+
71
+ **Timer and Resource Cleanup Example:**
72
+ ```javascript
73
+ private connectionTimer?: NodeJS.Timeout;
74
+
75
+ async onReady() {
76
+ this.connectionTimer = setInterval(() => this.checkConnection(), 30000);
77
+ }
78
+
79
+ onUnload(callback) {
80
+ try {
81
+ if (this.connectionTimer) {
82
+ clearInterval(this.connectionTimer);
83
+ this.connectionTimer = undefined;
84
+ }
85
+ callback();
86
+ } catch (e) {
87
+ callback();
88
+ }
89
+ }
90
+ ```
91
+
92
+ ### ESLint Configuration
93
+
94
+ **CRITICAL:** ESLint validation must run FIRST in your CI/CD pipeline, before any other tests. This "lint-first" approach catches code quality issues early.
95
+
96
+ #### Setup
97
+ ```bash
98
+ npm install --save-dev eslint @iobroker/eslint-config
99
+ ```
100
+
101
+ #### Configuration (.eslintrc.json)
102
+ ```json
103
+ {
104
+ "extends": "@iobroker/eslint-config",
105
+ "rules": {
106
+ // Add project-specific rule overrides here if needed
107
+ }
108
+ }
109
+ ```
110
+
111
+ #### Package.json Scripts
112
+ ```json
113
+ {
114
+ "scripts": {
115
+ "lint": "eslint --max-warnings 0 .",
116
+ "lint:fix": "eslint . --fix"
117
+ }
118
+ }
119
+ ```
120
+
121
+ #### Best Practices
122
+ 1. ✅ Run ESLint before committing — fix ALL warnings, not just errors
123
+ 2. ✅ Use `lint:fix` for auto-fixable issues
124
+ 3. ✅ Don't disable rules without documentation
125
+ 4. ✅ Lint all relevant files (main code, tests, build scripts)
126
+ 5. ✅ Keep `@iobroker/eslint-config` up to date
127
+ 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
128
+
129
+ #### Common Issues
130
+ - **Unused variables**: Remove or prefix with underscore (`_variable`)
131
+ - **Missing semicolons**: Run `npm run lint:fix`
132
+ - **Indentation**: Use 4 spaces (ioBroker standard)
133
+ - **console.log**: Replace with `adapter.log.debug()` or remove
134
+
135
+ ---
136
+
137
+
138
+
139
+ ## Testing
140
+
141
+ ### Unit Testing
142
+
143
+ - Use Jest as the primary testing framework
144
+ - Create tests for all adapter main functions and helper methods
145
+ - Test error handling scenarios and edge cases
146
+ - Mock external API calls and hardware dependencies
147
+ - For adapters connecting to APIs/devices not reachable by internet, provide example data files
148
+
149
+ **Example Structure:**
150
+ ```javascript
151
+ describe('AdapterName', () => {
152
+ let adapter;
153
+
154
+ beforeEach(() => {
155
+ // Setup test adapter instance
156
+ });
157
+
158
+ test('should initialize correctly', () => {
159
+ // Test adapter initialization
160
+ });
161
+ });
162
+ ```
163
+
164
+ ### Integration Testing
165
+
166
+ **CRITICAL:** Use the official `@iobroker/testing` framework. This is the ONLY correct way to test ioBroker adapters.
167
+
168
+ **Official Documentation:** https://github.com/ioBroker/testing
169
+
170
+ #### Framework Structure
171
+
172
+ **✅ Correct Pattern:**
173
+ ```javascript
174
+ const path = require('path');
175
+ const { tests } = require('@iobroker/testing');
176
+
177
+ tests.integration(path.join(__dirname, '..'), {
178
+ defineAdditionalTests({ suite }) {
179
+ suite('Test adapter with specific configuration', (getHarness) => {
180
+ let harness;
181
+
182
+ before(() => {
183
+ harness = getHarness();
184
+ });
185
+
186
+ it('should configure and start adapter', function () {
187
+ return new Promise(async (resolve, reject) => {
188
+ try {
189
+ // Get adapter object
190
+ const obj = await new Promise((res, rej) => {
191
+ harness.objects.getObject('system.adapter.your-adapter.0', (err, o) => {
192
+ if (err) return rej(err);
193
+ res(o);
194
+ });
195
+ });
196
+
197
+ if (!obj) return reject(new Error('Adapter object not found'));
198
+
199
+ // Configure adapter
200
+ Object.assign(obj.native, {
201
+ position: '52.520008,13.404954',
202
+ createHourly: true,
203
+ });
204
+
205
+ harness.objects.setObject(obj._id, obj);
206
+
207
+ // Start and wait
208
+ await harness.startAdapterAndWait();
209
+ await new Promise(resolve => setTimeout(resolve, 15000));
210
+
211
+ // Verify states
212
+ const stateIds = await harness.dbConnection.getStateIDs('your-adapter.0.*');
213
+
214
+ if (stateIds.length > 0) {
215
+ console.log('✅ Adapter successfully created states');
216
+ await harness.stopAdapter();
217
+ resolve(true);
218
+ } else {
219
+ reject(new Error('Adapter did not create any states'));
220
+ }
221
+ } catch (error) {
222
+ reject(error);
223
+ }
224
+ });
225
+ }).timeout(40000);
226
+ });
227
+ }
228
+ });
229
+ ```
230
+
231
+ #### Testing Success AND Failure Scenarios
232
+
233
+ **IMPORTANT:** For every "it works" test, implement corresponding "it fails gracefully" tests.
234
+
235
+ **Failure Scenario Example:**
236
+ ```javascript
237
+ it('should NOT create daily states when daily is disabled', function () {
238
+ return new Promise(async (resolve, reject) => {
239
+ try {
240
+ harness = getHarness();
241
+ const obj = await new Promise((res, rej) => {
242
+ harness.objects.getObject('system.adapter.your-adapter.0', (err, o) => {
243
+ if (err) return rej(err);
244
+ res(o);
245
+ });
246
+ });
247
+
248
+ if (!obj) return reject(new Error('Adapter object not found'));
249
+
250
+ Object.assign(obj.native, {
251
+ createDaily: false, // Daily disabled
252
+ });
253
+
254
+ await new Promise((res, rej) => {
255
+ harness.objects.setObject(obj._id, obj, (err) => {
256
+ if (err) return rej(err);
257
+ res(undefined);
258
+ });
259
+ });
260
+
261
+ await harness.startAdapterAndWait();
262
+ await new Promise((res) => setTimeout(res, 20000));
263
+
264
+ const stateIds = await harness.dbConnection.getStateIDs('your-adapter.0.*');
265
+ const dailyStates = stateIds.filter((key) => key.includes('daily'));
266
+
267
+ if (dailyStates.length === 0) {
268
+ console.log('✅ No daily states found as expected');
269
+ resolve(true);
270
+ } else {
271
+ reject(new Error('Expected no daily states but found some'));
272
+ }
273
+
274
+ await harness.stopAdapter();
275
+ } catch (error) {
276
+ reject(error);
277
+ }
278
+ });
279
+ }).timeout(40000);
280
+ ```
281
+
282
+ #### Key Rules
283
+
284
+ 1. ✅ Use `@iobroker/testing` framework
285
+ 2. ✅ Configure via `harness.objects.setObject()`
286
+ 3. ✅ Start via `harness.startAdapterAndWait()`
287
+ 4. ✅ Verify states via `harness.states.getState()`
288
+ 5. ✅ Allow proper timeouts for async operations
289
+ 6. ❌ NEVER test API URLs directly
290
+ 7. ❌ NEVER bypass the harness system
291
+
292
+ #### Workflow Dependencies
293
+
294
+ Integration tests should run ONLY after lint and adapter tests pass:
295
+
296
+ ```yaml
297
+ integration-tests:
298
+ needs: [check-and-lint, adapter-tests]
299
+ runs-on: ubuntu-22.04
300
+ ```
301
+
302
+ ### API Testing with Credentials
303
+
304
+ For adapters connecting to external APIs requiring authentication:
305
+
306
+ #### Password Encryption for Integration Tests
307
+
308
+ ```javascript
309
+ async function encryptPassword(harness, password) {
310
+ const systemConfig = await harness.objects.getObjectAsync("system.config");
311
+ if (!systemConfig?.native?.secret) {
312
+ throw new Error("Could not retrieve system secret for password encryption");
313
+ }
314
+
315
+ const secret = systemConfig.native.secret;
316
+ let result = '';
317
+ for (let i = 0; i < password.length; ++i) {
318
+ result += String.fromCharCode(secret[i % secret.length].charCodeAt(0) ^ password.charCodeAt(i));
319
+ }
320
+ return result;
321
+ }
322
+ ```
323
+
324
+ #### Demo Credentials Testing Pattern
325
+
326
+ - Use provider demo credentials when available (e.g., `demo@api-provider.com` / `demo`)
327
+ - Create separate test file: `test/integration-demo.js`
328
+ - Add npm script: `"test:integration-demo": "mocha test/integration-demo --exit"`
329
+ - Implement clear success/failure criteria
330
+
331
+ **Example Implementation:**
332
+ ```javascript
333
+ it("Should connect to API with demo credentials", async () => {
334
+ const encryptedPassword = await encryptPassword(harness, "demo_password");
335
+
336
+ await harness.changeAdapterConfig("your-adapter", {
337
+ native: {
338
+ username: "demo@provider.com",
339
+ password: encryptedPassword,
340
+ }
341
+ });
342
+
343
+ await harness.startAdapter();
344
+ await new Promise(resolve => setTimeout(resolve, 60000));
345
+
346
+ const connectionState = await harness.states.getStateAsync("your-adapter.0.info.connection");
347
+
348
+ if (connectionState?.val === true) {
349
+ console.log("✅ SUCCESS: API connection established");
350
+ return true;
351
+ } else {
352
+ throw new Error("API Test Failed: Expected API connection. Check logs for API errors.");
353
+ }
354
+ }).timeout(120000);
355
+ ```
356
+
357
+ ---
358
+
359
+ ## Development Best Practices
360
+
361
+ ### Dependency Management
362
+
363
+ - Always use `npm` for dependency management
364
+ - Use `npm ci` for installing existing dependencies (respects package-lock.json)
365
+ - Use `npm install` only when adding or updating dependencies
366
+ - Keep dependencies minimal and focused
367
+ - Only update dependencies in separate Pull Requests
368
+
369
+ **When modifying package.json:**
370
+ 1. Run `npm install` to sync package-lock.json
371
+ 2. Commit both package.json and package-lock.json together
372
+
373
+ **Best Practices:**
374
+ - Prefer built-in Node.js modules when possible
375
+ - Use `@iobroker/adapter-core` for adapter base functionality
376
+ - Avoid deprecated packages
377
+ - Document specific version requirements
378
+
379
+ ### HTTP Client Libraries
380
+
381
+ - **Preferred:** Use native `fetch` API (Node.js 20+ required)
382
+ - **Avoid:** `axios` unless specific features are required
383
+
384
+ **Example with fetch:**
385
+ ```javascript
386
+ try {
387
+ const response = await fetch('https://api.example.com/data');
388
+ if (!response.ok) {
389
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
390
+ }
391
+ const data = await response.json();
392
+ } catch (error) {
393
+ this.log.error(`API request failed: ${error.message}`);
394
+ }
395
+ ```
396
+
397
+ **Other Recommendations:**
398
+ - **Logging:** Use adapter built-in logging (`this.log.*`)
399
+ - **Scheduling:** Use adapter built-in timers and intervals
400
+ - **File operations:** Use Node.js `fs/promises`
401
+ - **Configuration:** Use adapter config system
402
+
403
+ ### Error Handling
404
+
405
+ - Always catch and log errors appropriately
406
+ - Use adapter log levels (error, warn, info, debug)
407
+ - Provide meaningful, user-friendly error messages
408
+ - Handle network failures gracefully
409
+ - Implement retry mechanisms where appropriate
410
+ - Always clean up timers, intervals, and resources in `unload()` method
411
+
412
+ **Example:**
413
+ ```javascript
414
+ try {
415
+ await this.connectToDevice();
416
+ } catch (error) {
417
+ this.log.error(`Failed to connect to device: ${error.message}`);
418
+ this.setState('info.connection', false, true);
419
+ // Implement retry logic if needed
420
+ }
421
+ ```
422
+
423
+ ---
424
+
425
+ ## Admin UI Configuration
426
+
427
+ ### JSON-Config Setup
428
+
429
+ Use JSON-Config format for modern ioBroker admin interfaces.
430
+
431
+ **Example Structure:**
432
+ ```json
433
+ {
434
+ "type": "panel",
435
+ "items": {
436
+ "host": {
437
+ "type": "text",
438
+ "label": "Host address",
439
+ "help": "IP address or hostname of the device"
440
+ }
441
+ }
442
+ }
443
+ ```
444
+
445
+ **Guidelines:**
446
+ - ✅ Use consistent naming conventions
447
+ - ✅ Provide sensible default values
448
+ - ✅ Include validation for required fields
449
+ - ✅ Add tooltips for complex options
450
+ - ✅ Ensure translations for all supported languages (minimum English and German)
451
+ - ✅ Write end-user friendly labels, avoid technical jargon
452
+
453
+ ### Translation Management
454
+
455
+ **CRITICAL:** Translation files must stay synchronized with `admin/jsonConfig.json`. Orphaned keys or missing translations cause UI issues and PR review delays.
456
+
457
+ #### Overview
458
+ - **Location:** `admin/i18n/{lang}/translations.json` for 11 languages (de, en, es, fr, it, nl, pl, pt, ru, uk, zh-cn)
459
+ - **Source of truth:** `admin/jsonConfig.json` - all `label` and `help` properties must have translations
460
+ - **Command:** `npm run translate` - auto-generates translations but does NOT remove orphaned keys
461
+ - **Formatting:** English uses tabs, other languages use 4 spaces
462
+
463
+ #### Critical Rules
464
+ 1. ✅ Keys must match exactly with jsonConfig.json
465
+ 2. ✅ No orphaned keys in translation files
466
+ 3. ✅ All translations must be in native language (no English fallbacks)
467
+ 4. ✅ Keys must be sorted alphabetically
468
+
469
+ #### Workflow for Translation Updates
470
+
471
+ **When modifying admin/jsonConfig.json:**
472
+
473
+ 1. Make your changes to labels/help texts
474
+ 2. Run automatic translation: `npm run translate`
475
+ 3. Create validation script (`scripts/validate-translations.js`):
476
+
477
+ ```javascript
478
+ const fs = require('fs');
479
+ const path = require('path');
480
+ const jsonConfig = JSON.parse(fs.readFileSync('admin/jsonConfig.json', 'utf8'));
481
+
482
+ function extractTexts(obj, texts = new Set()) {
483
+ if (typeof obj === 'object' && obj !== null) {
484
+ if (obj.label) texts.add(obj.label);
485
+ if (obj.help) texts.add(obj.help);
486
+ for (const key in obj) {
487
+ extractTexts(obj[key], texts);
488
+ }
489
+ }
490
+ return texts;
491
+ }
492
+
493
+ const requiredTexts = extractTexts(jsonConfig);
494
+ const languages = ['de', 'en', 'es', 'fr', 'it', 'nl', 'pl', 'pt', 'ru', 'uk', 'zh-cn'];
495
+ let hasErrors = false;
496
+
497
+ languages.forEach(lang => {
498
+ const translationPath = path.join('admin', 'i18n', lang, 'translations.json');
499
+ const translations = JSON.parse(fs.readFileSync(translationPath, 'utf8'));
500
+ const translationKeys = new Set(Object.keys(translations));
501
+
502
+ const missing = Array.from(requiredTexts).filter(text => !translationKeys.has(text));
503
+ const orphaned = Array.from(translationKeys).filter(key => !requiredTexts.has(key));
504
+
505
+ console.log(`\n=== ${lang} ===`);
506
+ if (missing.length > 0) {
507
+ console.error('❌ Missing keys:', missing);
508
+ hasErrors = true;
509
+ }
510
+ if (orphaned.length > 0) {
511
+ console.error('❌ Orphaned keys (REMOVE THESE):', orphaned);
512
+ hasErrors = true;
513
+ }
514
+ if (missing.length === 0 && orphaned.length === 0) {
515
+ console.log('✅ All keys match!');
516
+ }
517
+ });
518
+
519
+ process.exit(hasErrors ? 1 : 0);
520
+ ```
521
+
522
+ 4. Run validation: `node scripts/validate-translations.js`
523
+ 5. Remove orphaned keys manually from all translation files
524
+ 6. Add missing translations in native languages
525
+ 7. Run: `npm run lint && npm run test`
526
+
527
+ #### Add Validation to package.json
528
+
529
+ ```json
530
+ {
531
+ "scripts": {
532
+ "translate": "translate-adapter",
533
+ "validate:translations": "node scripts/validate-translations.js",
534
+ "pretest": "npm run lint && npm run validate:translations"
535
+ }
536
+ }
537
+ ```
538
+
539
+ #### Translation Checklist
540
+
541
+ Before committing changes to admin UI or translations:
542
+ 1. ✅ Validation script shows "All keys match!" for all 11 languages
543
+ 2. ✅ No orphaned keys in any translation file
544
+ 3. ✅ All translations in native language
545
+ 4. ✅ Keys alphabetically sorted
546
+ 5. ✅ `npm run lint` passes
547
+ 6. ✅ `npm run test` passes
548
+ 7. ✅ Admin UI displays correctly
549
+
550
+ ---
551
+
552
+ ## Documentation
553
+
554
+ ### README Updates
555
+
556
+ #### Required Sections
557
+ 1. **Installation** - Clear npm/ioBroker admin installation steps
558
+ 2. **Configuration** - Detailed configuration options with examples
559
+ 3. **Usage** - Practical examples and use cases
560
+ 4. **Changelog** - Version history (use "## **WORK IN PROGRESS**" for ongoing changes)
561
+ 5. **License** - License information (typically MIT for ioBroker adapters)
562
+ 6. **Support** - Links to issues, discussions, community support
563
+
564
+ #### Documentation Standards
565
+ - Use clear, concise language
566
+ - Include code examples for configuration
567
+ - Add screenshots for admin interface when applicable
568
+ - Maintain multilingual support (minimum English and German)
569
+ - Always reference issues in commits and PRs (e.g., "fixes #xx")
570
+
571
+ #### Mandatory README Updates for PRs
572
+
573
+ For **every PR or new feature**, always add a user-friendly entry to README.md:
574
+
575
+ - Add entries under `## **WORK IN PROGRESS**` section
576
+ - Use format: `* (author) **TYPE**: Description of user-visible change`
577
+ - Types: **NEW** (features), **FIXED** (bugs), **ENHANCED** (improvements), **TESTING** (test additions), **CI/CD** (automation)
578
+ - Focus on user impact, not technical details
579
+
580
+ **Example:**
581
+ ```markdown
582
+ ## **WORK IN PROGRESS**
583
+
584
+ * (DutchmanNL) **FIXED**: Adapter now properly validates login credentials (fixes #25)
585
+ * (DutchmanNL) **NEW**: Added device discovery to simplify initial setup
586
+ ```
587
+
588
+ ### Changelog Management
589
+
590
+ Follow the [AlCalzone release-script](https://github.com/AlCalzone/release-script) standard.
591
+
592
+ #### Format Requirements
593
+
594
+ ```markdown
595
+ # Changelog
596
+
597
+ <!--
598
+ Placeholder for the next version (at the beginning of the line):
599
+ ## **WORK IN PROGRESS**
600
+ -->
601
+
602
+ ## **WORK IN PROGRESS**
603
+
604
+ - (author) **NEW**: Added new feature X
605
+ - (author) **FIXED**: Fixed bug Y (fixes #25)
606
+
607
+ ## v0.1.0 (2023-01-01)
608
+ Initial release
609
+ ```
610
+
611
+ #### Workflow Process
612
+ - **During Development:** All changes go under `## **WORK IN PROGRESS**`
613
+ - **For Every PR:** Add user-facing changes to WORK IN PROGRESS section
614
+ - **Before Merge:** Version number and date added when merging to main
615
+ - **Release Process:** Release-script automatically converts placeholder to actual version
616
+
617
+ #### Change Entry Format
618
+ - Format: `- (author) **TYPE**: User-friendly description`
619
+ - Types: **NEW**, **FIXED**, **ENHANCED**
620
+ - Focus on user impact, not technical implementation
621
+ - Reference issues: "fixes #XX" or "solves #XX"
622
+
623
+ ---
624
+
625
+ ## CI/CD & GitHub Actions
626
+
627
+ ### Workflow Configuration
628
+
629
+ #### GitHub Actions Best Practices
630
+
631
+ **Must use ioBroker official testing actions:**
632
+ - `ioBroker/testing-action-check@v1` for lint and package validation
633
+ - `ioBroker/testing-action-adapter@v1` for adapter tests
634
+ - `ioBroker/testing-action-deploy@v1` for automated releases with Trusted Publishing (OIDC)
635
+
636
+ **Configuration:**
637
+ - **Node.js versions:** Test on 20.x, 22.x, 24.x
638
+ - **Platform:** Use ubuntu-22.04
639
+ - **Automated releases:** Deploy to npm on version tags (requires NPM Trusted Publishing)
640
+ - **Monitoring:** Include Sentry release tracking for error monitoring
641
+
642
+ #### Critical: Lint-First Validation Workflow
643
+
644
+ **ALWAYS run ESLint checks BEFORE other tests.** Benefits:
645
+ - Catches code quality issues immediately
646
+ - Prevents wasting CI resources on tests that would fail due to linting errors
647
+ - Provides faster feedback to developers
648
+ - Enforces consistent code quality
649
+
650
+ **Workflow Dependency Configuration:**
651
+ ```yaml
652
+ jobs:
653
+ check-and-lint:
654
+ # Runs ESLint and package validation
655
+ # Uses: ioBroker/testing-action-check@v1
656
+
657
+ adapter-tests:
658
+ needs: [check-and-lint] # Wait for linting to pass
659
+ # Run adapter unit tests
660
+
661
+ integration-tests:
662
+ needs: [check-and-lint, adapter-tests] # Wait for both
663
+ # Run integration tests
664
+ ```
665
+
666
+ **Key Points:**
667
+ - The `check-and-lint` job has NO dependencies - runs first
668
+ - ALL other test jobs MUST list `check-and-lint` in their `needs` array
669
+ - If linting fails, no other tests run, saving time
670
+ - Fix all ESLint errors before proceeding
671
+
672
+ ### Testing Integration
673
+
674
+ #### API Testing in CI/CD
675
+
676
+ For adapters with external API dependencies:
677
+
678
+ ```yaml
679
+ demo-api-tests:
680
+ if: contains(github.event.head_commit.message, '[skip ci]') == false
681
+ runs-on: ubuntu-22.04
682
+
683
+ steps:
684
+ - name: Checkout code
685
+ uses: actions/checkout@v4
686
+
687
+ - name: Use Node.js 20.x
688
+ uses: actions/setup-node@v4
689
+ with:
690
+ node-version: 20.x
691
+ cache: 'npm'
692
+
693
+ - name: Install dependencies
694
+ run: npm ci
695
+
696
+ - name: Run demo API tests
697
+ run: npm run test:integration-demo
698
+ ```
699
+
700
+ #### Testing Best Practices
701
+ - Run credential tests separately from main test suite
702
+ - Don't make credential tests required for deployment
703
+ - Provide clear failure messages for API issues
704
+ - Use appropriate timeouts for external calls (120+ seconds)
705
+
706
+ #### Package.json Integration
707
+ ```json
708
+ {
709
+ "scripts": {
710
+ "test:integration-demo": "mocha test/integration-demo --exit"
711
+ }
712
+ }
713
+ ```
714
+
715
+ ---
716
+
717
+ ### Network Service Development (Fakeroku-Specific)
718
+
719
+ #### HTTP Server Setup
720
+ ```javascript
721
+ const http = require('http');
722
+
723
+ class FakerokuAdapter extends utils.Adapter {
724
+ constructor(options = {}) {
725
+ super(options);
726
+ this.httpServers = new Map();
727
+ }
728
+
729
+ async createHttpServer(device) {
730
+ const server = http.createServer((req, res) => {
731
+ this.handleHttpRequest(req, res, device);
732
+ });
733
+
734
+ server.listen(device.port, this.config.BIND, () => {
735
+ this.log.info(`HTTP server for ${device.name} listening on port ${device.port}`);
736
+ });
737
+
738
+ this.httpServers.set(device.name, server);
739
+ return server;
740
+ }
741
+
742
+ handleHttpRequest(req, res, device) {
743
+ const url = req.url;
744
+ this.log.debug(`Received request: ${req.method} ${url} for device ${device.name}`);
745
+
746
+ if (url.startsWith('/keypress/')) {
747
+ const key = url.substring(10);
748
+ this.createKeyState(device, key);
749
+ res.writeHead(200);
750
+ res.end();
751
+ } else if (url === '/query/device-info') {
752
+ this.sendDeviceInfo(res, device);
753
+ }
754
+ }
755
+ }
756
+ ```
757
+
758
+ #### UDP Multicast for Device Discovery
759
+ ```javascript
760
+ const dgram = require('dgram');
761
+
762
+ async setupSSDP() {
763
+ this.ssdpSocket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
764
+
765
+ this.ssdpSocket.on('message', (message, remote) => {
766
+ const msg = message.toString();
767
+ if (msg.includes('M-SEARCH') && msg.includes('roku:ecp')) {
768
+ this.respondToSSDP(remote);
769
+ }
770
+ });
771
+
772
+ this.ssdpSocket.bind(1900, () => {
773
+ this.ssdpSocket.addMembership('239.255.255.250');
774
+ this.log.info('SSDP server bound to multicast group');
775
+ });
776
+ }
777
+ ```
778
+
779
+ #### State Creation for Commands
780
+ ```javascript
781
+ async createKeyState(device, key) {
782
+ const stateId = `${device.name}.${key}`;
783
+
784
+ await this.setObjectNotExistsAsync(stateId, {
785
+ type: 'state',
786
+ common: {
787
+ name: `Key: ${key}`,
788
+ type: 'boolean',
789
+ role: 'button',
790
+ read: true,
791
+ write: true,
792
+ },
793
+ native: {
794
+ device: device.name,
795
+ key: key
796
+ },
797
+ });
798
+
799
+ await this.setStateAsync(stateId, { val: true, ack: true });
800
+
801
+ // Reset after short delay
802
+ setTimeout(async () => {
803
+ await this.setStateAsync(stateId, { val: false, ack: true });
804
+ }, 100);
805
+ }
806
+ ```
807
+
808
+
809
+
810
+ ---
811
+
812
+ ### Network Testing Best Practices (Fakeroku-Specific)
813
+
814
+ For adapters that create network services like fakeroku:
815
+
816
+ #### Mock Network Environment Testing
817
+ ```javascript
818
+ const { tests } = require('@iobroker/testing');
819
+ const http = require('http');
820
+ const dgram = require('dgram');
821
+
822
+ tests.integration(path.join(__dirname, '..'), {
823
+ defineAdditionalTests({ suite }) {
824
+ suite('Network Service Testing', (getHarness) => {
825
+ let harness;
826
+ let mockHarmonyHub;
827
+
828
+ before(async () => {
829
+ harness = getHarness();
830
+
831
+ // Create mock Harmony Hub that sends SSDP requests
832
+ mockHarmonyHub = dgram.createSocket('udp4');
833
+ });
834
+
835
+ after(async () => {
836
+ if (mockHarmonyHub) {
837
+ mockHarmonyHub.close();
838
+ }
839
+ });
840
+
841
+ it('should respond to Harmony Hub discovery', async function() {
842
+ this.timeout(30000);
843
+
844
+ // Configure adapter with test device
845
+ await harness.changeAdapterConfig('fakeroku', {
846
+ native: {
847
+ BIND: '127.0.0.1',
848
+ devices: [{
849
+ name: 'TestRoku',
850
+ port: 9093,
851
+ uuid: 'test-12345'
852
+ }]
853
+ }
854
+ });
855
+
856
+ await harness.startAdapter();
857
+ await new Promise(resolve => setTimeout(resolve, 5000));
858
+
859
+ // Simulate Harmony Hub discovery request
860
+ const discoveryMessage = 'M-SEARCH * HTTP/1.1\r\nHOST: 239.255.255.250:1900\r\nMAN: "ssdp:discover"\r\nST: roku:ecp\r\n\r\n';
861
+
862
+ return new Promise((resolve, reject) => {
863
+ mockHarmonyHub.on('message', (message, remote) => {
864
+ const response = message.toString();
865
+ if (response.includes('roku:ecp') && response.includes('TestRoku')) {
866
+ resolve();
867
+ }
868
+ });
869
+
870
+ mockHarmonyHub.send(discoveryMessage, 1900, '239.255.255.250');
871
+
872
+ setTimeout(() => reject(new Error('No SSDP response received')), 10000);
873
+ });
874
+ });
875
+ });
876
+ }
877
+ });
878
+ ```
879
+
880
+ #### HTTP Command Testing
881
+ ```javascript
882
+ it('should handle ECP commands via HTTP', async function() {
883
+ this.timeout(20000);
884
+
885
+ await harness.startAdapter();
886
+ await new Promise(resolve => setTimeout(resolve, 5000));
887
+
888
+ // Send keypress command to adapter
889
+ const response = await new Promise((resolve, reject) => {
890
+ const req = http.request({
891
+ hostname: '127.0.0.1',
892
+ port: 9093,
893
+ path: '/keypress/Home',
894
+ method: 'POST'
895
+ }, (res) => {
896
+ resolve(res);
897
+ });
898
+
899
+ req.on('error', reject);
900
+ req.end();
901
+ });
902
+
903
+ expect(response.statusCode).toBe(200);
904
+
905
+ // Verify state was created
906
+ await new Promise(resolve => setTimeout(resolve, 1000));
907
+ const homeKeyState = await new Promise((resolve, reject) => {
908
+ harness.states.getState('fakeroku.0.TestRoku.Home', (err, state) => {
909
+ if (err) return reject(err);
910
+ resolve(state);
911
+ });
912
+ });
913
+
914
+ expect(homeKeyState).toBeTruthy();
915
+ expect(homeKeyState.val).toBe(true);
916
+ });
917
+ ```