iobroker.fakeroku 1.8.0 → 1.8.1
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/README.md +5 -14
- package/build/lib/native-key-migration.js +44 -8
- package/build/lib/native-key-migration.js.map +2 -2
- package/build/main.js +17 -44
- package/build/main.js.map +2 -2
- package/io-package.json +37 -37
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ so they accept the emulated device instead of giving up on it.
|
|
|
29
29
|
|
|
30
30
|
## Sentry / Error reporting
|
|
31
31
|
|
|
32
|
-
**This adapter uses Sentry libraries to automatically report exceptions and code errors to the developers.** Reporting
|
|
32
|
+
**This adapter uses Sentry libraries to automatically report exceptions and code errors to the developers.** Reporting is active by default. It stays off when the ioBroker diagnostics setting is `none` (`diag` in the system configuration), when data reporting is disabled for this instance or its host (`disableDataReporting`), and on CI systems. A report contains the error with its stack trace and technical context such as versions and platform, plus an anonymous installation ID.
|
|
33
33
|
|
|
34
34
|
For details and how to disable it, see the [Sentry plugin documentation](https://github.com/ioBroker/plugin-sentry#plugin-sentry). Error reporting requires js-controller 3.0 or newer.
|
|
35
35
|
|
|
@@ -100,6 +100,10 @@ becomes `true` — or watch `.command` for the last button as text.
|
|
|
100
100
|
Placeholder for the next version (at the beginning of the line):
|
|
101
101
|
### **WORK IN PROGRESS**
|
|
102
102
|
-->
|
|
103
|
+
### 1.8.1 (2026-09-27)
|
|
104
|
+
|
|
105
|
+
- (krobipd) Improved: the note the Admin shows before an update from 0.x is short now: the apps folder is removed, app launches arrive as launch:<id> in command.
|
|
106
|
+
|
|
103
107
|
### 1.8.0 (2026-09-25)
|
|
104
108
|
|
|
105
109
|
- (krobipd) Fixed: Home Assistant and openHAB can set up the emulated Roku again — it now answers the active-app, media-player and TV-channel queries they send.
|
|
@@ -131,19 +135,6 @@ becomes `true` — or watch `.command` for the last button as text.
|
|
|
131
135
|
|
|
132
136
|
- (krobipd) Changed: installing straight from GitHub is no longer offered — the adapter is built before publishing, so it is installed from the ioBroker repository instead.
|
|
133
137
|
|
|
134
|
-
### 1.6.0 (2026-09-07)
|
|
135
|
-
|
|
136
|
-
- (krobipd) Fixed: saving a device in the admin could change its identity on the network, so a paired Harmony or Sofabaton lost it.
|
|
137
|
-
- (krobipd) Fixed: with the device list open twice, editing or deleting a card could hit a different emulated Roku than the one clicked.
|
|
138
|
-
- (krobipd) Fixed: releasing a key was dropped while the adapter shed a flood of commands, so the key could stay pressed for half a minute.
|
|
139
|
-
- (krobipd) Fixed: an ECP port still held after a restart left that device dead until you restarted the instance; it is retried every minute now.
|
|
140
|
-
- (krobipd) Fixed: stopping the instance now takes the emulated Rokus out of the remote's list instead of leaving them there for up to an hour.
|
|
141
|
-
- (krobipd) Fixed: an emulated Roku whose server died is no longer offered for discovery.
|
|
142
|
-
- (krobipd) Fixed: a configured port no server can bind falls back to 8060 instead of leaving the device unstarted.
|
|
143
|
-
- (krobipd) Changed: the device dialog refuses a reserved or colliding name right away instead of reporting it after saving.
|
|
144
|
-
- (krobipd) Changed: the adapter can now run in compact mode, sharing one process with other adapters instead of claiming its own.
|
|
145
|
-
- (krobipd) Changed: more than one instance may run on the same machine again; only the ports have to differ.
|
|
146
|
-
|
|
147
138
|
[Older changelogs can be found there](CHANGELOG_OLD.md)
|
|
148
139
|
|
|
149
140
|
## History
|
|
@@ -18,12 +18,29 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
18
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
19
|
var native_key_migration_exports = {};
|
|
20
20
|
__export(native_key_migration_exports, {
|
|
21
|
+
KEPT_COMMON_KEYS: () => KEPT_COMMON_KEYS,
|
|
22
|
+
buildCommonKeyPatch: () => buildCommonKeyPatch,
|
|
21
23
|
buildNativeKeyPatch: () => buildNativeKeyPatch,
|
|
22
24
|
migrateNativeKeys: () => migrateNativeKeys
|
|
23
25
|
});
|
|
24
26
|
module.exports = __toCommonJS(native_key_migration_exports);
|
|
27
|
+
const KEPT_COMMON_KEYS = [
|
|
28
|
+
"title",
|
|
29
|
+
"schedule",
|
|
30
|
+
"restartSchedule",
|
|
31
|
+
"mode",
|
|
32
|
+
"loglevel",
|
|
33
|
+
"enabled",
|
|
34
|
+
"custom",
|
|
35
|
+
"tier",
|
|
36
|
+
"visWidgets",
|
|
37
|
+
"localLinks",
|
|
38
|
+
"compact",
|
|
39
|
+
"supportedMessages"
|
|
40
|
+
];
|
|
25
41
|
const isRename = (m) => "from" in m;
|
|
26
42
|
const isDrop = (m) => "drop" in m;
|
|
43
|
+
const isCommonDrop = (m) => "commonDrop" in m;
|
|
27
44
|
const isPresent = (v) => v !== void 0 && v !== null;
|
|
28
45
|
const isMeaningful = (v) => {
|
|
29
46
|
if (!isPresent(v)) {
|
|
@@ -68,7 +85,7 @@ function buildNativeKeyPatch(native, migrations) {
|
|
|
68
85
|
}
|
|
69
86
|
}
|
|
70
87
|
for (const m of migrations) {
|
|
71
|
-
if (isRename(m) || isDrop(m) || !isPresent(native[m.key])) {
|
|
88
|
+
if (isRename(m) || isDrop(m) || isCommonDrop(m) || !isPresent(native[m.key])) {
|
|
72
89
|
continue;
|
|
73
90
|
}
|
|
74
91
|
const coerced = m.coerce(native[m.key]);
|
|
@@ -78,28 +95,45 @@ function buildNativeKeyPatch(native, migrations) {
|
|
|
78
95
|
}
|
|
79
96
|
return patch;
|
|
80
97
|
}
|
|
98
|
+
function buildCommonKeyPatch(common, migrations) {
|
|
99
|
+
const patch = {};
|
|
100
|
+
for (const m of migrations) {
|
|
101
|
+
if (isCommonDrop(m) && !KEPT_COMMON_KEYS.includes(m.commonDrop) && isPresent(common[m.commonDrop])) {
|
|
102
|
+
patch[m.commonDrop] = null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return patch;
|
|
106
|
+
}
|
|
81
107
|
async function migrateNativeKeys(adapter, migrations, describeError) {
|
|
82
108
|
const id = `system.adapter.${adapter.namespace}`;
|
|
83
109
|
let native;
|
|
110
|
+
let common;
|
|
84
111
|
try {
|
|
85
112
|
const obj = await adapter.getForeignObjectAsync(id);
|
|
86
113
|
native = obj == null ? void 0 : obj.native;
|
|
114
|
+
common = obj == null ? void 0 : obj.common;
|
|
87
115
|
} catch (err) {
|
|
88
116
|
adapter.log.warn(`Settings migration skipped \u2014 could not read ${id}: ${describeError(err)}`);
|
|
89
117
|
return false;
|
|
90
118
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
const patch = buildNativeKeyPatch(native, migrations);
|
|
119
|
+
const patch = native ? buildNativeKeyPatch(native, migrations) : {};
|
|
120
|
+
const commonPatch = common ? buildCommonKeyPatch(common, migrations) : {};
|
|
95
121
|
const touched = Object.keys(patch);
|
|
96
|
-
|
|
122
|
+
const commonTouched = Object.keys(commonPatch);
|
|
123
|
+
if (touched.length === 0 && commonTouched.length === 0) {
|
|
97
124
|
return false;
|
|
98
125
|
}
|
|
99
126
|
const summary = touched.filter((k) => patch[k] !== null).map((k) => `${k} = ${JSON.stringify(patch[k])}`).join(", ");
|
|
100
|
-
const removed = touched.filter((k) => patch[k] === null).join(", ");
|
|
127
|
+
const removed = [...touched.filter((k) => patch[k] === null), ...commonTouched.map((k) => `common.${k}`)].join(", ");
|
|
128
|
+
const write = {};
|
|
129
|
+
if (touched.length > 0) {
|
|
130
|
+
write.native = patch;
|
|
131
|
+
}
|
|
132
|
+
if (commonTouched.length > 0) {
|
|
133
|
+
write.common = commonPatch;
|
|
134
|
+
}
|
|
101
135
|
try {
|
|
102
|
-
await adapter.extendForeignObjectAsync(id,
|
|
136
|
+
await adapter.extendForeignObjectAsync(id, write);
|
|
103
137
|
adapter.log.info(
|
|
104
138
|
summary ? `Settings migrated to the standard keys (${summary}) \u2014 this instance restarts once` : `Obsolete settings removed (${removed}) \u2014 this instance restarts once`
|
|
105
139
|
);
|
|
@@ -121,6 +155,8 @@ async function migrateNativeKeys(adapter, migrations, describeError) {
|
|
|
121
155
|
}
|
|
122
156
|
// Annotate the CommonJS export names for ESM import in node:
|
|
123
157
|
0 && (module.exports = {
|
|
158
|
+
KEPT_COMMON_KEYS,
|
|
159
|
+
buildCommonKeyPatch,
|
|
124
160
|
buildNativeKeyPatch,
|
|
125
161
|
migrateNativeKeys
|
|
126
162
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/native-key-migration.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * One-shot migration of instance settings keys (`system.adapter.<ns>.native`) \u2014 a key that\n * was renamed or whose value type changed is carried over on the first start after the\n * update, so an existing installation keeps what its user configured; a key an earlier\n * version declared and this one no longer reads is dropped (nulled), so it does not stay in\n * every existing installation for good (js-controller never deletes a native key).\n *\n * Why this exists (fleet standard \"listen-port declaration\", 2026-09-15): the admin's\n * port-conflict check only sees instances carrying `native.port` AND `native.bind`, so a\n * listen address stored under any other key (`host`, `bindAddress`, `networkInterface`) is\n * invisible to it. Renaming the key in the manifest is not enough \u2014 on an update\n * js-controller ADDS every missing native key with its manifest default and never deletes\n * the old one, so after the update both keys exist: the new one with the default, the old\n * one with the user's value. A read fallback `new || old` therefore always picks the default.\n * The old value must be moved explicitly, and the old key nulled (a merge cannot delete).\n *\n * Contract for every rename onto `bind`: supply a `coerce` that turns an empty legacy value into\n * \"0.0.0.0\" \u2014 the admin's port-conflict check skips an instance whose `bind` is falsy\n * (`if (!instance.native?.bind) return;` in ConfigPort), so a migrated `\"\"` would leave the\n * adapter exactly as invisible as before. The helper itself moves values verbatim.\n *\n * Two rules the write obeys, both learned in hassemu's legacy migration:\n * - Merge, never write the whole object \u2014 `extendForeignObjectAsync` with only the touched\n * keys; `null` survives the merge (`undefined` would be skipped) and makes the old key\n * falsy, which is all a later read needs.\n * - A write to the own instance object restarts the instance \u2014 so the caller aborts its\n * start when this reports a write, instead of binding a port in a process about to go down.\n *\n * Fleet master: `Entwicklung/.consistency-master/src/lib/native-key-migration.ts`. Every adapter\n * that migrates native keys carries this file and its test byte for byte \u2014 the release run\n * (consistency level 1) reports any difference. Change the master, then copy; never the copy.\n * The file imports nothing adapter-specific: the caller hands in its own error-text helper.\n */\n\n/** Rename: the old value wins over the freshly added default; the old key is nulled. */\nexport interface NativeKeyRename {\n /** The key the value used to live under. */\n from: string;\n /** The key the value moves to. */\n to: string;\n /** Optional value conversion on the way (e.g. an empty legacy string \u2192 \"0.0.0.0\"). */\n coerce?: (old: unknown) => unknown;\n}\n\n/** In-place coercion (same key), e.g. the manifest default `\"8080\"` \u2192 `8080`. */\nexport interface NativeKeyCoercion {\n /** The key whose value type changed. */\n key: string;\n /** Returns the value in its new form; an unchanged result writes nothing. */\n coerce: (value: unknown) => unknown;\n}\n\n/** Drop: a key an earlier version declared and this one no longer reads \u2014 nulled when it still holds a value. */\nexport interface NativeKeyDrop {\n /** The obsolete key. */\n drop: string;\n}\n\nexport type NativeKeyMigration = NativeKeyRename | NativeKeyCoercion | NativeKeyDrop;\n\n/** The adapter surface the migration needs \u2014 object I/O, logging, the in-memory config. */\nexport interface NativeKeyMigrationAdapter {\n /** Instance namespace, e.g. `adapter.0`. */\n namespace: string;\n /** Adapter log \u2014 one info line per migration, warnings for a failed read or write. */\n log: { info: (msg: string) => void; warn: (msg: string) => void };\n /** `adapter.config` \u2014 patched in memory only when the write fails. */\n config: object;\n /** Reads the instance object. */\n getForeignObjectAsync(id: string): Promise<unknown>;\n /** Merges the touched native keys into the instance object. */\n extendForeignObjectAsync(id: string, obj: { native: Record<string, unknown> }): Promise<unknown>;\n}\n\nconst isRename = (m: NativeKeyMigration): m is NativeKeyRename => \"from\" in m;\n\nconst isDrop = (m: NativeKeyMigration): m is NativeKeyDrop => \"drop\" in m;\n\nconst isPresent = (v: unknown): boolean => v !== undefined && v !== null;\n\n/**\n * A value that says something \u2014 when several old keys compete for one new key, a source\n * holding nothing but the manifest default (\"\" or \"listen everywhere\") must not beat a\n * source holding the concrete address the user once entered.\n *\n * @param v the candidate value\n */\nconst isMeaningful = (v: unknown): boolean => {\n if (!isPresent(v)) {\n return false;\n }\n if (typeof v === \"string\") {\n const s = v.trim();\n return s !== \"\" && s !== \"0.0.0.0\";\n }\n return true;\n};\n\n/**\n * A coercion result the migration can store \u2014 `undefined` and `NaN` would either be\n * skipped by the merge or come back as `null`, and a value that never settles would\n * restart the instance on every start.\n *\n * @param v the coerced value\n */\nconst isStorable = (v: unknown): boolean => v !== undefined && !(typeof v === \"number\" && Number.isNaN(v));\n\n/**\n * Computes the native patch for the given migrations \u2014 pure, so the decision is testable\n * without an adapter.\n *\n * Renames targeting the same key are evaluated together, in order: the first source whose\n * value is meaningful wins; when none is, the first present one (its coercion may still turn\n * an empty legacy value into a sensible one). Every present source is nulled. A coercion\n * writes only when the coerced value differs from the stored one. A drop nulls its key when it\n * still holds a value \u2014 an absent or already nulled key writes nothing.\n *\n * @param native the instance's current native settings\n * @param migrations the renames and coercions to apply\n * @returns the keys to merge into native \u2014 empty when nothing needs to change\n */\nexport function buildNativeKeyPatch(\n native: Record<string, unknown>,\n migrations: NativeKeyMigration[],\n): Record<string, unknown> {\n const patch: Record<string, unknown> = {};\n\n const renamesByTarget = new Map<string, NativeKeyRename[]>();\n for (const m of migrations) {\n if (isRename(m)) {\n const group = renamesByTarget.get(m.to) ?? [];\n group.push(m);\n renamesByTarget.set(m.to, group);\n }\n }\n for (const [to, group] of renamesByTarget) {\n const present = group.filter(r => isPresent(native[r.from]));\n if (present.length === 0) {\n continue;\n }\n const winner = present.find(r => isMeaningful(native[r.from])) ?? present[0];\n const value = winner.coerce ? winner.coerce(native[winner.from]) : native[winner.from];\n if (!isStorable(value)) {\n // Nothing to carry over \u2014 leave the old keys untouched rather than null a value\n // that never reached its new key.\n continue;\n }\n patch[to] = value;\n for (const r of present) {\n patch[r.from] = null;\n }\n }\n\n for (const m of migrations) {\n if (isDrop(m) && isPresent(native[m.drop])) {\n patch[m.drop] = null;\n }\n }\n\n for (const m of migrations) {\n if (isRename(m) || isDrop(m) || !isPresent(native[m.key])) {\n continue;\n }\n const coerced = m.coerce(native[m.key]);\n if (isStorable(coerced) && !Object.is(coerced, native[m.key])) {\n patch[m.key] = coerced;\n }\n }\n return patch;\n}\n\n/**\n * Applies the migrations to `system.adapter.<ns>` with ONE merge of the touched keys.\n *\n * @param adapter the adapter (object I/O, log, in-memory config)\n * @param migrations the renames and coercions to apply\n * @param describeError the adapter's error-text helper (one per repository) \u2014 renders whatever\n * the object store threw, so a rejected plain object never reads `[object Object]`\n * @returns true when the instance object was written \u2014 the caller must abort its start,\n * the host restarts the instance with the migrated settings. false when nothing had to\n * change, or when the write failed: then the in-memory config already carries the\n * migrated values and the start continues with them (the write is retried next start).\n */\nexport async function migrateNativeKeys(\n adapter: NativeKeyMigrationAdapter,\n migrations: NativeKeyMigration[],\n describeError: (err: unknown) => string,\n): Promise<boolean> {\n const id = `system.adapter.${adapter.namespace}`;\n let native: Record<string, unknown> | undefined;\n try {\n const obj = (await adapter.getForeignObjectAsync(id)) as { native?: Record<string, unknown> } | null | undefined;\n native = obj?.native;\n } catch (err) {\n adapter.log.warn(`Settings migration skipped \u2014 could not read ${id}: ${describeError(err)}`);\n return false;\n }\n if (!native) {\n return false;\n }\n const patch = buildNativeKeyPatch(native, migrations);\n const touched = Object.keys(patch);\n if (touched.length === 0) {\n return false;\n }\n const summary = touched\n .filter(k => patch[k] !== null)\n .map(k => `${k} = ${JSON.stringify(patch[k])}`)\n .join(\", \");\n const removed = touched.filter(k => patch[k] === null).join(\", \");\n try {\n await adapter.extendForeignObjectAsync(id, { native: patch });\n adapter.log.info(\n summary\n ? `Settings migrated to the standard keys (${summary}) \u2014 this instance restarts once`\n : `Obsolete settings removed (${removed}) \u2014 this instance restarts once`,\n );\n return true;\n } catch (err) {\n adapter.log.warn(\n `Settings migration could not be stored (${describeError(err)}) \u2014 ${summary ? `using ${summary}` : `ignoring ${removed}`} for this run`,\n );\n const config = adapter.config as Record<string, unknown>;\n for (const k of touched) {\n if (patch[k] === null) {\n delete config[k];\n } else {\n config[k] = patch[k];\n }\n }\n return false;\n }\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;
|
|
4
|
+
"sourcesContent": ["/**\n * One-shot migration of instance settings keys (`system.adapter.<ns>.native`) \u2014 a key that\n * was renamed or whose value type changed is carried over on the first start after the\n * update, so an existing installation keeps what its user configured; a key an earlier\n * version declared and this one no longer reads is dropped (nulled), so it does not stay in\n * every existing installation for good (js-controller never deletes a native key).\n *\n * Why this exists (fleet standard \"listen-port declaration\", 2026-09-15): the admin's\n * port-conflict check only sees instances carrying `native.port` AND `native.bind`, so a\n * listen address stored under any other key (`host`, `bindAddress`, `networkInterface`) is\n * invisible to it. Renaming the key in the manifest is not enough \u2014 on an update\n * js-controller ADDS every missing native key with its manifest default and never deletes\n * the old one, so after the update both keys exist: the new one with the default, the old\n * one with the user's value. A read fallback `new || old` therefore always picks the default.\n * The old value must be moved explicitly, and the old key nulled (a merge cannot delete).\n *\n * Contract for every rename onto `bind`: supply a `coerce` that turns an empty legacy value into\n * \"0.0.0.0\" \u2014 the admin's port-conflict check skips an instance whose `bind` is falsy\n * (`if (!instance.native?.bind) return;` in ConfigPort), so a migrated `\"\"` would leave the\n * adapter exactly as invisible as before. The helper itself moves values verbatim.\n *\n * Two rules the write obeys, both learned in hassemu's legacy migration:\n * - Merge, never write the whole object \u2014 `extendForeignObjectAsync` with only the touched\n * keys; `null` survives the merge (`undefined` would be skipped) and makes the old key\n * falsy, which is all a later read needs.\n * - A write to the own instance object restarts the instance \u2014 so the caller aborts its\n * start when this reports a write, instead of binding a port in a process about to go down.\n *\n * Fleet master: `Entwicklung/.consistency-master/src/lib/native-key-migration.ts`. Every adapter\n * that migrates native keys carries this file and its test byte for byte \u2014 the release run\n * (consistency level 1) reports any difference. Change the master, then copy; never the copy.\n * The file imports nothing adapter-specific: the caller hands in its own error-text helper.\n *\n * `common` keys too (since 2026-09-25): js-controller MERGES the manifest's `common` into the instance object on every\n * update and never removes a key (v7.2.2 `setupUpload.ts` `extendCommon`, only `visWidgets`/`localLinks` are deleted\n * and `compact` overwritten) \u2014 a key an earlier manifest declared stays in every existing instance for good, e.g.\n * `nondeletable` (the instance can no longer be deleted) or `singletonHost`. `{ commonDrop: \"<key>\" }` nulls it in the\n * same single write as the native changes, so the update still costs one restart. The keys js-controller keeps for the\n * user or maintains itself are never touched, whatever the table says (`KEPT_COMMON_KEYS`).\n *\n * Order within one call: every rename is applied before any drop, in one write \u2014 a takeover of a dropped key belongs\n * in the same table as `{ from, to }`; a takeover in the adapter's own code must run before this call.\n */\n\n/** Rename: the old value wins over the freshly added default; the old key is nulled. */\nexport interface NativeKeyRename {\n /** The key the value used to live under. */\n from: string;\n /** The key the value moves to. */\n to: string;\n /** Optional value conversion on the way (e.g. an empty legacy string \u2192 \"0.0.0.0\"). */\n coerce?: (old: unknown) => unknown;\n}\n\n/** In-place coercion (same key), e.g. the manifest default `\"8080\"` \u2192 `8080`. */\nexport interface NativeKeyCoercion {\n /** The key whose value type changed. */\n key: string;\n /** Returns the value in its new form; an unchanged result writes nothing. */\n coerce: (value: unknown) => unknown;\n}\n\n/** Drop: a key an earlier version declared and this one no longer reads \u2014 nulled when it still holds a value. */\nexport interface NativeKeyDrop {\n /** The obsolete key. */\n drop: string;\n}\n\n/** Common drop: a key an earlier manifest declared in `common` and this one no longer does \u2014 nulled when it still holds a value. */\nexport interface CommonKeyDrop {\n /** The obsolete `common` key, spelled exactly as the old manifest had it. */\n commonDrop: string;\n}\n\nexport type NativeKeyMigration = NativeKeyRename | NativeKeyCoercion | NativeKeyDrop | CommonKeyDrop;\n\n/**\n * `common` keys a drop never touches: js-controller keeps them for the user on every update (`preserveAttributes` in\n * v7.2.2 `setupUpload.ts` \u2014 the instance title, schedules, mode, log level, enabled flag, custom settings, tier) or\n * maintains them itself (`visWidgets`, `localLinks`, `compact`); `supportedMessages` is repaired by the package check\n * `messagebox-repair`.\n */\nexport const KEPT_COMMON_KEYS: readonly string[] = [\n \"title\",\n \"schedule\",\n \"restartSchedule\",\n \"mode\",\n \"loglevel\",\n \"enabled\",\n \"custom\",\n \"tier\",\n \"visWidgets\",\n \"localLinks\",\n \"compact\",\n \"supportedMessages\",\n];\n\n/** The adapter surface the migration needs \u2014 object I/O, logging, the in-memory config. */\nexport interface NativeKeyMigrationAdapter {\n /** Instance namespace, e.g. `adapter.0`. */\n namespace: string;\n /** Adapter log \u2014 one info line per migration, warnings for a failed read or write. */\n log: { info: (msg: string) => void; warn: (msg: string) => void };\n /** `adapter.config` \u2014 patched in memory only when the write fails. */\n config: object;\n /** Reads the instance object. */\n getForeignObjectAsync(id: string): Promise<unknown>;\n /** Merges the touched native and common keys into the instance object. */\n extendForeignObjectAsync(\n id: string,\n obj: { native?: Record<string, unknown>; common?: Record<string, unknown> },\n ): Promise<unknown>;\n}\n\nconst isRename = (m: NativeKeyMigration): m is NativeKeyRename => \"from\" in m;\n\nconst isDrop = (m: NativeKeyMigration): m is NativeKeyDrop => \"drop\" in m;\n\nconst isCommonDrop = (m: NativeKeyMigration): m is CommonKeyDrop => \"commonDrop\" in m;\n\nconst isPresent = (v: unknown): boolean => v !== undefined && v !== null;\n\n/**\n * A value that says something \u2014 when several old keys compete for one new key, a source\n * holding nothing but the manifest default (\"\" or \"listen everywhere\") must not beat a\n * source holding the concrete address the user once entered.\n *\n * @param v the candidate value\n */\nconst isMeaningful = (v: unknown): boolean => {\n if (!isPresent(v)) {\n return false;\n }\n if (typeof v === \"string\") {\n const s = v.trim();\n return s !== \"\" && s !== \"0.0.0.0\";\n }\n return true;\n};\n\n/**\n * A coercion result the migration can store \u2014 `undefined` and `NaN` would either be\n * skipped by the merge or come back as `null`, and a value that never settles would\n * restart the instance on every start.\n *\n * @param v the coerced value\n */\nconst isStorable = (v: unknown): boolean => v !== undefined && !(typeof v === \"number\" && Number.isNaN(v));\n\n/**\n * Computes the native patch for the given migrations \u2014 pure, so the decision is testable\n * without an adapter.\n *\n * Renames targeting the same key are evaluated together, in order: the first source whose\n * value is meaningful wins; when none is, the first present one (its coercion may still turn\n * an empty legacy value into a sensible one). Every present source is nulled. A coercion\n * writes only when the coerced value differs from the stored one. A drop nulls its key when it\n * still holds a value \u2014 an absent or already nulled key writes nothing.\n *\n * @param native the instance's current native settings\n * @param migrations the renames and coercions to apply\n * @returns the keys to merge into native \u2014 empty when nothing needs to change\n */\nexport function buildNativeKeyPatch(\n native: Record<string, unknown>,\n migrations: NativeKeyMigration[],\n): Record<string, unknown> {\n const patch: Record<string, unknown> = {};\n\n const renamesByTarget = new Map<string, NativeKeyRename[]>();\n for (const m of migrations) {\n if (isRename(m)) {\n const group = renamesByTarget.get(m.to) ?? [];\n group.push(m);\n renamesByTarget.set(m.to, group);\n }\n }\n for (const [to, group] of renamesByTarget) {\n const present = group.filter(r => isPresent(native[r.from]));\n if (present.length === 0) {\n continue;\n }\n const winner = present.find(r => isMeaningful(native[r.from])) ?? present[0];\n const value = winner.coerce ? winner.coerce(native[winner.from]) : native[winner.from];\n if (!isStorable(value)) {\n // Nothing to carry over \u2014 leave the old keys untouched rather than null a value\n // that never reached its new key.\n continue;\n }\n patch[to] = value;\n for (const r of present) {\n patch[r.from] = null;\n }\n }\n\n for (const m of migrations) {\n if (isDrop(m) && isPresent(native[m.drop])) {\n patch[m.drop] = null;\n }\n }\n\n for (const m of migrations) {\n if (isRename(m) || isDrop(m) || isCommonDrop(m) || !isPresent(native[m.key])) {\n continue;\n }\n const coerced = m.coerce(native[m.key]);\n if (isStorable(coerced) && !Object.is(coerced, native[m.key])) {\n patch[m.key] = coerced;\n }\n }\n return patch;\n}\n\n/**\n * Computes the common patch \u2014 every `commonDrop` whose key still holds a value is nulled; a key in\n * `KEPT_COMMON_KEYS` never is.\n *\n * @param common the instance's current common part\n * @param migrations the table (renames, coercions and native drops are ignored here)\n * @returns the keys to merge into common \u2014 empty when nothing needs to change\n */\nexport function buildCommonKeyPatch(\n common: Record<string, unknown>,\n migrations: NativeKeyMigration[],\n): Record<string, unknown> {\n const patch: Record<string, unknown> = {};\n for (const m of migrations) {\n if (isCommonDrop(m) && !KEPT_COMMON_KEYS.includes(m.commonDrop) && isPresent(common[m.commonDrop])) {\n patch[m.commonDrop] = null;\n }\n }\n return patch;\n}\n\n/**\n * Applies the migrations to `system.adapter.<ns>` with ONE merge of the touched keys.\n *\n * @param adapter the adapter (object I/O, log, in-memory config)\n * @param migrations the renames and coercions to apply\n * @param describeError the adapter's error-text helper (one per repository) \u2014 renders whatever\n * the object store threw, so a rejected plain object never reads `[object Object]`\n * @returns true when the instance object was written \u2014 the caller must abort its start,\n * the host restarts the instance with the migrated settings. false when nothing had to\n * change, or when the write failed: then the in-memory config already carries the\n * migrated values and the start continues with them (the write is retried next start).\n */\nexport async function migrateNativeKeys(\n adapter: NativeKeyMigrationAdapter,\n migrations: NativeKeyMigration[],\n describeError: (err: unknown) => string,\n): Promise<boolean> {\n const id = `system.adapter.${adapter.namespace}`;\n let native: Record<string, unknown> | undefined;\n let common: Record<string, unknown> | undefined;\n try {\n const obj = (await adapter.getForeignObjectAsync(id)) as\n { native?: Record<string, unknown>; common?: Record<string, unknown> } | null | undefined;\n native = obj?.native;\n common = obj?.common;\n } catch (err) {\n adapter.log.warn(`Settings migration skipped \u2014 could not read ${id}: ${describeError(err)}`);\n return false;\n }\n const patch = native ? buildNativeKeyPatch(native, migrations) : {};\n const commonPatch = common ? buildCommonKeyPatch(common, migrations) : {};\n const touched = Object.keys(patch);\n const commonTouched = Object.keys(commonPatch);\n if (touched.length === 0 && commonTouched.length === 0) {\n return false;\n }\n const summary = touched\n .filter(k => patch[k] !== null)\n .map(k => `${k} = ${JSON.stringify(patch[k])}`)\n .join(\", \");\n const removed = [...touched.filter(k => patch[k] === null), ...commonTouched.map(k => `common.${k}`)].join(\", \");\n const write: { native?: Record<string, unknown>; common?: Record<string, unknown> } = {};\n if (touched.length > 0) {\n write.native = patch;\n }\n if (commonTouched.length > 0) {\n write.common = commonPatch;\n }\n try {\n await adapter.extendForeignObjectAsync(id, write);\n adapter.log.info(\n summary\n ? `Settings migrated to the standard keys (${summary}) \u2014 this instance restarts once`\n : `Obsolete settings removed (${removed}) \u2014 this instance restarts once`,\n );\n return true;\n } catch (err) {\n adapter.log.warn(\n `Settings migration could not be stored (${describeError(err)}) \u2014 ${summary ? `using ${summary}` : `ignoring ${removed}`} for this run`,\n );\n const config = adapter.config as Record<string, unknown>;\n for (const k of touched) {\n if (patch[k] === null) {\n delete config[k];\n } else {\n config[k] = patch[k];\n }\n }\n return false;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/build/main.js
CHANGED
|
@@ -61,10 +61,11 @@ const BIND_WAIT_STEP_MS = 1e4;
|
|
|
61
61
|
const BIND_WAIT_MAX_MS = 12e4;
|
|
62
62
|
const DISCOVERY_RETRY_INTERVAL_MS = 6e4;
|
|
63
63
|
const DROPPED_NATIVE_KEYS = [{ drop: "HTTP_PORT" }, { drop: "MULTICAST_IP" }, { drop: "UUID" }];
|
|
64
|
+
const DROPPED_COMMON_KEYS = [{ commonDrop: "license" }, { commonDrop: "singletonHost" }];
|
|
64
65
|
function bindKeyMigrations(native) {
|
|
65
66
|
const hasInterfaceKey = native.networkInterface !== void 0 && native.networkInterface !== null;
|
|
66
67
|
const bind = hasInterfaceKey ? [{ from: "networkInterface", to: "bind", coerce: toBindAddress }, { drop: "BIND" }] : [{ from: "BIND", to: "bind", coerce: toBindAddress }];
|
|
67
|
-
return [...bind, ...DROPPED_NATIVE_KEYS];
|
|
68
|
+
return [...bind, ...DROPPED_NATIVE_KEYS, ...DROPPED_COMMON_KEYS];
|
|
68
69
|
}
|
|
69
70
|
function toBindAddress(old) {
|
|
70
71
|
return typeof old === "string" && old.trim() ? old.trim() : "0.0.0.0";
|
|
@@ -131,26 +132,24 @@ class Fakeroku extends utils.Adapter {
|
|
|
131
132
|
this.deviceManagement = new import_device_management.FakerokuDeviceManagement(this);
|
|
132
133
|
}
|
|
133
134
|
/**
|
|
134
|
-
* One-shot repair of this instance's own object — the settings keys and the leftover
|
|
135
|
+
* One-shot repair of this instance's own object — the settings keys and the leftover `common` keys.
|
|
135
136
|
*
|
|
136
137
|
* js-controller only ever ADDS to an existing instance object: on an update it fills missing
|
|
137
138
|
* `native` keys with the manifest default, and a `common` key the manifest dropped stays behind
|
|
138
139
|
* for ever. Two changes therefore never reached an existing installation on their own:
|
|
139
140
|
* the listen address had to MOVE to `bind` (a read fallback finds the injected default, not the
|
|
140
|
-
* user's value), and `singletonHost`
|
|
141
|
-
*
|
|
142
|
-
* is still the adapter's to remove).
|
|
141
|
+
* user's value), and `license`/`singletonHost` stayed in the instance object (read by nobody:
|
|
142
|
+
* `createInstance` reads the ADAPTER object, but a dead key is still the adapter's to remove).
|
|
143
143
|
*
|
|
144
|
-
*
|
|
145
|
-
* the fleet master)
|
|
146
|
-
* migrated values into this run's config instead
|
|
147
|
-
*
|
|
148
|
-
* installation that needs both restarts twice, once each, and never again.
|
|
144
|
+
* Everything goes through the fleet helper (`lib/native-key-migration.ts`, byte-identical with
|
|
145
|
+
* the fleet master) in ONE write, so the update costs one restart: it merges only the touched
|
|
146
|
+
* keys, and when the write fails it carries the migrated values into this run's config instead
|
|
147
|
+
* of starting on the injected default.
|
|
149
148
|
*
|
|
150
149
|
* @returns true when the object was written — the caller aborts its start, the host restarts
|
|
151
150
|
*/
|
|
152
151
|
async repairInstanceObject() {
|
|
153
|
-
var _a
|
|
152
|
+
var _a;
|
|
154
153
|
const id = `system.adapter.${this.namespace}`;
|
|
155
154
|
let obj;
|
|
156
155
|
try {
|
|
@@ -162,21 +161,7 @@ class Fakeroku extends utils.Adapter {
|
|
|
162
161
|
if (!obj) {
|
|
163
162
|
return false;
|
|
164
163
|
}
|
|
165
|
-
|
|
166
|
-
return true;
|
|
167
|
-
}
|
|
168
|
-
const claim = (_b = obj.common) == null ? void 0 : _b.singletonHost;
|
|
169
|
-
if (claim === void 0 || claim === null) {
|
|
170
|
-
return false;
|
|
171
|
-
}
|
|
172
|
-
try {
|
|
173
|
-
await this.extendForeignObjectAsync(id, { common: { singletonHost: null } });
|
|
174
|
-
} catch (err) {
|
|
175
|
-
this.log.warn(`Settings repair could not be stored (${(0, import_errors.errText)(err)}) \u2014 retrying on the next start`);
|
|
176
|
-
return false;
|
|
177
|
-
}
|
|
178
|
-
this.log.info("The instance no longer claims the whole host \u2014 it restarts once");
|
|
179
|
-
return true;
|
|
164
|
+
return (0, import_native_key_migration.migrateNativeKeys)(this, bindKeyMigrations((_a = obj.native) != null ? _a : {}), import_errors.errText);
|
|
180
165
|
}
|
|
181
166
|
/** Create each device's object tree, start its ECP server, then the shared SSDP responder. */
|
|
182
167
|
async onReady() {
|
|
@@ -577,28 +562,16 @@ class Fakeroku extends utils.Adapter {
|
|
|
577
562
|
* extendObject is what carries the name into an existing tree, so an update always lands on
|
|
578
563
|
* every datapoint, not just on fresh installs.
|
|
579
564
|
*
|
|
580
|
-
*
|
|
581
|
-
*
|
|
565
|
+
* Only name and description: the manifest owns the rest of the shape, and js-controller writes
|
|
566
|
+
* it back on every start — the object type included (`extend(true, old, manifest)`), which is
|
|
567
|
+
* also what repairs an `info` channel a hand-edited device row named "info" once turned into a
|
|
568
|
+
* device object (see the reserved-id guard in startDevices).
|
|
582
569
|
*/
|
|
583
570
|
async refreshOwnObjects() {
|
|
584
571
|
await Promise.all([
|
|
585
|
-
this.extendObject("info", {
|
|
586
|
-
type: "channel",
|
|
587
|
-
common: { name: (0, import_i18n.tName)("channelInfo") },
|
|
588
|
-
native: {}
|
|
589
|
-
}),
|
|
572
|
+
this.extendObject("info", { common: { name: (0, import_i18n.tName)("channelInfo") } }),
|
|
590
573
|
this.extendObject("info.connection", {
|
|
591
|
-
|
|
592
|
-
common: {
|
|
593
|
-
name: (0, import_i18n.tName)("connectionStatus"),
|
|
594
|
-
desc: (0, import_i18n.tDesc)("connectionStatusDesc"),
|
|
595
|
-
type: "boolean",
|
|
596
|
-
role: "indicator.connected",
|
|
597
|
-
read: true,
|
|
598
|
-
write: false,
|
|
599
|
-
def: false
|
|
600
|
-
},
|
|
601
|
-
native: {}
|
|
574
|
+
common: { name: (0, import_i18n.tName)("connectionStatus"), desc: (0, import_i18n.tDesc)("connectionStatusDesc") }
|
|
602
575
|
})
|
|
603
576
|
]);
|
|
604
577
|
}
|
package/build/main.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/main.ts"],
|
|
4
|
-
"sourcesContent": ["import * as utils from \"@iobroker/adapter-core\";\nimport { I18n } from \"@iobroker/adapter-core\";\nimport { join } from \"node:path\";\nimport { FakerokuDeviceManagement } from \"./device-management\";\nimport type { RokuAdvert } from \"./discovery/ssdp-messages\";\nimport { RokuSsdpResponder } from \"./discovery/ssdp-responder\";\nimport { DEFAULT_APPS } from \"./ecp/device-info\";\nimport { COMMAND_TYPES, type CommandEvent } from \"./ecp/ecp-command\";\nimport { EcpHttpServer } from \"./ecp/ecp-http-server\";\nimport { commandToStateWrite, type DeviceType, keysForType } from \"./ecp/state-model\";\nimport { RESERVED_IDS } from \"./lib/constants\";\nimport { deviceObjectId, deviceTreeOf, toDeviceRows, type DeviceRow } from \"./lib/device-config\";\nimport { randomIdentity } from \"./lib/device-identity\";\nimport {\n detectLocalIPv4s,\n detectLocalNets,\n detectPrimaryIPv4,\n hasLocalAddress,\n localAddressFor,\n netsOfInterface,\n} from \"./lib/detect-ip\";\nimport { errText } from \"./lib/errors\";\nimport { migrateNativeKeys, type NativeKeyMigration } from \"./lib/native-key-migration\";\nimport { tDesc, tName, tRaw } from \"./lib/i18n\";\nimport { isLanClient } from \"./lib/lan-guard\";\nimport { planNativePrune, planObjectCleanup } from \"./lib/object-cleanup\";\nimport { RateGate } from \"./lib/rate-gate\";\n\n/** Managed timeout for a stuck SSDP start (a busy port 1900 must not hang onReady). */\nconst SSDP_START_TIMEOUT_MS = 5000;\n/** Proactive ssdp:alive interval so controllers find the device without searching. */\nconst SSDP_NOTIFY_INTERVAL_MS = 300_000;\n/** How long a keypress pulses its keys.<Key> state true before falling back to false. */\nconst KEY_PULSE_MS = 50;\n/** Safety cap for a held key: a keydown with no matching keyup resets after this. */\nconst HOLD_MAX_MS = 30_000;\n/** Commands accepted per second and emulated Roku; the excess is dropped (see lib/rate-gate.ts). */\nconst MAX_COMMANDS_PER_SECOND = 25;\n/** How often at most the dropped-commands warning repeats per device. */\nconst RATE_WARN_INTERVAL_MS = 60_000;\n/** How long to wait before trying a device whose ECP port was busy at start-up again. */\nconst DEVICE_RETRY_INTERVAL_MS = 60_000;\n/** How often a chosen interface address that is missing at start-up is looked for again. */\nconst BIND_WAIT_STEP_MS = 10_000;\n/**\n * How long a missing chosen address is waited for. Long enough for a host whose network comes up\n * after ioBroker (Wi-Fi, DHCP, a restart after a power cut); after that the address is taken as\n * gone, and serving on another one would leave the network the user chose.\n */\nconst BIND_WAIT_MAX_MS = 120_000;\n/** How often \"all interfaces\" looks for an address to start discovery with when it had none. */\nconst DISCOVERY_RETRY_INTERVAL_MS = 60_000;\n\n/**\n * Keys the 0.1.x adapter declared and nothing reads any more: its own HTTP port and multicast\n * address (both fixed by the Roku protocol today) and one global identity (every device carries its\n * own now). js-controller never deletes a native key, so without the drop they stay in every\n * installation that came from there.\n */\nconst DROPPED_NATIVE_KEYS: NativeKeyMigration[] = [{ drop: \"HTTP_PORT\" }, { drop: \"MULTICAST_IP\" }, { drop: \"UUID\" }];\n\n/**\n * The listen address moved to `bind` (fleet listen-port standard). Which legacy key holds the\n * user's CURRENT choice depends on the versions the installation went through:\n *\n * - `networkInterface` exists only on an instance that ran 0.6.0\u20131.6.1. Its admin showed exactly\n * that key, and the adapter bound to it \u2014 \"\" meant all interfaces. It is the setting the user\n * saw last, so it wins, and a `BIND` still lying next to it is only cleared. The old adapter's\n * `BIND` had to be a concrete LAN address (it was also the advertised location), so letting it\n * win would pin an instance that ran on \"all interfaces\" for months to an address from years ago.\n * - Without `networkInterface` the instance comes straight from the pre-0.6.0 adapter, and `BIND`\n * is its setting.\n *\n * An empty legacy value becomes \"0.0.0.0\" \u2014 the admin's check skips an instance whose `bind` is\n * falsy, so a migrated \"\" would be as invisible as no key at all.\n *\n * @param native the instance's stored native settings\n * @returns the migrations for this installation, the obsolete 0.1.x keys included\n */\nfunction bindKeyMigrations(native: Record<string, unknown>): NativeKeyMigration[] {\n const hasInterfaceKey = native.networkInterface !== undefined && native.networkInterface !== null;\n const bind: NativeKeyMigration[] = hasInterfaceKey\n ? [{ from: \"networkInterface\", to: \"bind\", coerce: toBindAddress }, { drop: \"BIND\" }]\n : [{ from: \"BIND\", to: \"bind\", coerce: toBindAddress }];\n return [...bind, ...DROPPED_NATIVE_KEYS];\n}\n\n/**\n * A legacy listen address in the form the admin can read.\n *\n * @param old the stored legacy value\n * @returns the address, or \"0.0.0.0\" when it holds nothing usable\n */\nfunction toBindAddress(old: unknown): string {\n return typeof old === \"string\" && old.trim() ? old.trim() : \"0.0.0.0\";\n}\n\n/** An emulated Roku whose ECP server did not come up yet \u2014 its objects exist, only the server is missing. */\ninterface PendingDevice {\n /** The id-safe device path segment. */\n deviceId: string;\n /** The configured device name, for log lines the user can act on. */\n friendlyName: string;\n /** The advert (identity + port) this device would answer under. */\n advert: RokuAdvert;\n /** The emulated device type. */\n deviceType: DeviceType;\n}\n\n/**\n * ioBroker.fakeroku \u2014 Roku emulator (input side).\n *\n * Emulates one or more Roku devices on the LAN so that ECP/SSDP remotes\n * (Logitech Harmony or a Sofabaton X1/X2) trigger events in ioBroker:\n * a keypress lands in `<device>.command` and pulses `<device>.keys.<Key>`.\n */\nexport class Fakeroku extends utils.Adapter {\n private ssdp: RokuSsdpResponder | undefined;\n private notifyTimer: ioBroker.Interval | undefined;\n private readonly ecpServers = new Map<string, EcpHttpServer>();\n private readonly pulseTimers = new Set<ioBroker.Timeout>();\n /** Per held key id, its watchdog timer \u2014 so a keydown without a keyup cannot pin it true forever. */\n private readonly holdTimers = new Map<string, ioBroker.Timeout>();\n /** Per device, the key names it exposes \u2014 so a keypress only writes keys this device carries. */\n private readonly deviceKeys = new Map<string, ReadonlySet<string>>();\n /** Per device, its command rate gate \u2014 the write-flood protection for the states database. */\n private readonly commandGates = new Map<string, RateGate>();\n /** Per device, when the dropped-commands warning was last written. */\n private readonly rateWarnedAt = new Map<string, number>();\n /** Per device id, the name the user gave it \u2014 for log lines the user can recognise. */\n private readonly deviceNames = new Map<string, string>();\n /** The devices that are actually listening \u2014 the basis for info.connection. */\n private readonly running: RokuAdvert[] = [];\n /** Devices whose ECP server did not start; retried on a timer until they come up. */\n private pending: PendingDevice[] = [];\n /** The retry timer for {@link pending}, armed only while something is waiting. */\n private retryTimer: ioBroker.Timeout | undefined;\n /** The timer that starts discovery once the host has an address (\"all interfaces\" only). */\n private discoveryTimer: ioBroker.Timeout | undefined;\n\n /**\n * Set as the very first thing onUnload does. Everything that can still be in flight at\n * that moment asks it before it writes or starts anything: the minute retry can be sitting\n * in `await server.start()` when the host says stop, and would then register a server into\n * an already-cleared map and write info.connection TRUE after the closing FALSE.\n */\n private stopping = false;\n /** How many configured devices are expected to listen \u2014 the target info.connection compares against. */\n private expectedDevices = 0;\n /** The interface to bind to, or undefined for \"all\" \u2014 kept for the retry after onReady returned. */\n private bindIp: string | undefined;\n /**\n * Device-manager backend: the emulated Rokus as cards with add/edit/delete.\n *\n * Nothing reads this field, and that is not an oversight: dm-utils subscribes to\n * the adapter's `message` event from its own constructor, so creating the object\n * IS the wiring. The field keeps it visible \u2014 and owned \u2014 instead of leaving a\n * bare `new` in the constructor that reads like a mistake.\n */\n private readonly deviceManagement: FakerokuDeviceManagement;\n\n // Construction seams for the two network-facing collaborators. Production uses\n // the real classes; the orchestration tests swap them for fakes so onReady's\n // wiring (per-device isolation, SSDP degradation, timers) is testable without\n // binding a port. Behaviour is unchanged \u2014 same constructors, same arguments.\n private makeEcpServer: (options: ConstructorParameters<typeof EcpHttpServer>[0]) => EcpHttpServer = options =>\n new EcpHttpServer(options);\n private makeSsdpResponder: (options: ConstructorParameters<typeof RokuSsdpResponder>[0]) => RokuSsdpResponder =\n options => new RokuSsdpResponder(options);\n\n /**\n * @param options adapter options passed through by js-controller\n */\n public constructor(options: Partial<utils.AdapterOptions> = {}) {\n super({\n ...options,\n name: \"fakeroku\",\n });\n\n this.on(\"ready\", this.onReady.bind(this));\n this.on(\"unload\", this.onUnload.bind(this));\n this.deviceManagement = new FakerokuDeviceManagement(this);\n }\n\n /**\n * One-shot repair of this instance's own object \u2014 the settings keys and the leftover host claim.\n *\n * js-controller only ever ADDS to an existing instance object: on an update it fills missing\n * `native` keys with the manifest default, and a `common` key the manifest dropped stays behind\n * for ever. Two changes therefore never reached an existing installation on their own:\n * the listen address had to MOVE to `bind` (a read fallback finds the injected default, not the\n * user's value), and `singletonHost` \u2014 gone from the manifest since 1.6.0 \u2014 stayed in the\n * instance object (read by nobody: `createInstance` reads the ADAPTER object, but a dead key\n * is still the adapter's to remove).\n *\n * The settings go through the fleet helper (`lib/native-key-migration.ts`, byte-identical with\n * the fleet master): it merges only the touched keys, and when the write fails it carries the\n * migrated values into this run's config instead of starting on the injected default. The host\n * claim is a `common` key, which the helper does not touch, so it is its own write; an\n * installation that needs both restarts twice, once each, and never again.\n *\n * @returns true when the object was written \u2014 the caller aborts its start, the host restarts\n */\n private async repairInstanceObject(): Promise<boolean> {\n const id = `system.adapter.${this.namespace}`;\n let obj: { common?: Record<string, unknown>; native?: Record<string, unknown> } | null | undefined;\n try {\n obj = await this.getForeignObjectAsync(id);\n } catch (err) {\n this.log.warn(`Settings repair skipped \u2014 could not read ${id}: ${errText(err)}`);\n return false;\n }\n if (!obj) {\n return false;\n }\n if (await migrateNativeKeys(this, bindKeyMigrations(obj.native ?? {}), errText)) {\n return true;\n }\n // `null` is the state AFTER a repair \u2014 treating it as present would rewrite on every start\n // and restart the instance for ever.\n const claim = obj.common?.singletonHost;\n if (claim === undefined || claim === null) {\n return false;\n }\n try {\n await this.extendForeignObjectAsync(id, { common: { singletonHost: null } });\n } catch (err) {\n this.log.warn(`Settings repair could not be stored (${errText(err)}) \u2014 retrying on the next start`);\n return false;\n }\n this.log.info(\"The instance no longer claims the whole host \u2014 it restarts once\");\n return true;\n }\n\n /** Create each device's object tree, start its ECP server, then the shared SSDP responder. */\n private async onReady(): Promise<void> {\n try {\n // Repair what an update leaves behind in the instance object, before anything binds a port.\n // The write restarts this instance, so nothing may start before it.\n if (await this.repairInstanceObject()) {\n return;\n }\n\n await this.setState(\"info.connection\", { val: false, ack: true });\n await I18n.init(join(this.adapterDir, \"admin\"), this);\n await this.refreshOwnObjects();\n\n // The object tree as it is before this start touches it: the rows resolve their object id\n // and \u2014 for a row from before 0.7.0 \u2014 their type against it.\n const owned = await this.readOwnObjects();\n if (this.stopping) {\n return;\n }\n\n // Read the device list BEFORE anything can return early: the orphan sweep below\n // has to know the configured set at every exit, or a tree the user just deleted\n // stays in the database for good (there is no second path that removes it).\n // `null` separates \"the user removed the last device\" (an empty array \u2014 sweep)\n // from \"there is no devices key at all\" (never configured, or a config we could\n // not read \u2014 sweep nothing, or we would delete a tree we cannot account for).\n // lib/device-config.ts is the ONE normaliser; the device manager reads the same\n // rows, so an edit can never resolve a different identity than the one advertised.\n const configured = toDeviceRows(\n this.config.devices,\n deviceTreeOf(owned.keys(), id => owned.get(id)?.type),\n );\n if (configured && (await this.persistNewIdentities(configured, owned))) {\n return;\n }\n\n // Empty AND \"0.0.0.0\" both mean \"auto\": bind all interfaces, answer every network with the\n // host's own address in it. js-controller never rewrites an existing native default, so\n // instances from before 0.5.1 still carry \"\" \u2014 both must take the auto path.\n const configuredIp = this.config.bind;\n this.bindIp = configuredIp && configuredIp !== \"0.0.0.0\" ? configuredIp : undefined;\n\n // A chosen interface is the user's decision: everything stays in its network. An address no\n // interface carries cannot be served \u2014 and serving on another one would leave the network\n // the user chose. It is waited for briefly (a host whose network comes up after ioBroker),\n // then reported and nothing starts.\n if (this.bindIp && !(await this.awaitBindAddress(this.bindIp))) {\n if (this.stopping) {\n return;\n }\n this.log.error(\n `The network interface address ${this.bindIp} does not exist on this host \u2014 choose another network interface in the instance settings.`,\n );\n await this.sweepOrphans(configured);\n return;\n }\n if (this.stopping) {\n return;\n }\n\n if (!configured || configured.length === 0) {\n this.log.warn(\"No emulated Roku devices configured.\");\n await this.sweepOrphans(configured);\n return;\n }\n\n this.expectedDevices = configured.length;\n await this.startDevices(configured);\n if (this.stopping) {\n return;\n }\n await this.sweepOrphans(configured);\n if (this.stopping) {\n return;\n }\n\n if (this.running.length === 0 && this.pending.length === 0) {\n // Nothing is controllable and nothing is worth retrying \u2014 leave info.connection false.\n this.log.error(\"No emulated Roku device could be started \u2014 check the configured names for conflicts.\");\n return;\n }\n\n const advertiseIp = this.bindIp ?? detectPrimaryIPv4();\n if (this.running.length > 0) {\n if (advertiseIp) {\n this.startDiscovery(advertiseIp);\n } else {\n // The ECP servers listen on every interface and need no address; only discovery has\n // nothing to announce yet. It starts as soon as the host has an address.\n this.log.warn(\n \"No routable IPv4 address yet \u2014 the emulated Rokus are listening; discovery starts as soon as the host has an address.\",\n );\n this.scheduleDiscoveryStart();\n }\n }\n await this.reportConnectionState(advertiseIp);\n this.scheduleDeviceRetry();\n } catch (e) {\n this.log.error(`onReady failed: ${errText(e)}`);\n }\n }\n\n /**\n * Wait until an interface carries the chosen address \u2014 at most {@link BIND_WAIT_MAX_MS}, looking\n * every {@link BIND_WAIT_STEP_MS}. Returns at once when the address is there (the normal case).\n *\n * @param address the chosen interface address\n * @returns true once an interface carries it; false when it did not appear or the host said stop\n */\n private async awaitBindAddress(address: string): Promise<boolean> {\n for (let waited = 0; ; waited += BIND_WAIT_STEP_MS) {\n if (hasLocalAddress(address, detectLocalNets())) {\n return true;\n }\n if (this.stopping || waited >= BIND_WAIT_MAX_MS) {\n return false;\n }\n if (waited === 0) {\n this.log.info(`Waiting for the network interface address ${address} to come up \u2026`);\n }\n await new Promise<void>(resolve => {\n // The managed timer refuses during shutdown and hands back nothing \u2014 then there is\n // nothing to wait for.\n if (!this.setTimeout(resolve, BIND_WAIT_STEP_MS)) {\n resolve();\n }\n });\n }\n }\n\n /**\n * Look for an address to start discovery with, once a minute, until one is there (\"all\n * interfaces\" with no routable IPv4 at start-up: the ECP servers already listen).\n */\n private scheduleDiscoveryStart(): void {\n const timer = this.setTimeout(() => {\n this.discoveryTimer = undefined;\n void this.tryStartDiscovery();\n }, DISCOVERY_RETRY_INTERVAL_MS);\n if (timer) {\n this.discoveryTimer = timer;\n }\n }\n\n /** One attempt of {@link scheduleDiscoveryStart}; dropped with `void`, so it catches its own failures. */\n private async tryStartDiscovery(): Promise<void> {\n try {\n if (this.stopping || this.ssdp) {\n return;\n }\n const advertiseIp = detectPrimaryIPv4();\n if (!advertiseIp) {\n this.scheduleDiscoveryStart();\n return;\n }\n this.startDiscovery(advertiseIp);\n await this.reportConnectionState(advertiseIp);\n } catch (e) {\n this.log.warn(`Starting discovery failed: ${errText(e)}`);\n }\n }\n\n /**\n * The trust boundary of both services: a client in one of the host's own networks \u2014 with a\n * chosen interface only in that interface's network. Read fresh on every question, so an address\n * change is followed instead of frozen at start-up.\n *\n * @param address the client address\n * @returns true if the client may be answered\n */\n private readonly isOwnClient = (address: string | undefined): boolean =>\n isLanClient(address, () => {\n const nets = detectLocalNets();\n return this.bindIp ? netsOfInterface(this.bindIp, nets) : nets;\n });\n\n /**\n * Create the object tree and start the ECP server for every configured device.\n *\n * Each device is isolated: a busy port, a reserved name or a colliding object id takes\n * down that device only. A device whose objects exist but whose server did not start goes\n * into {@link pending} and is retried \u2014 that case is a restart race far more often than a\n * real conflict, and without the retry the user has to restart the instance by hand.\n *\n * @param configured the normalised device rows\n */\n private async startDevices(configured: readonly DeviceRow[]): Promise<void> {\n const seenIds = new Set<string>();\n for (const row of configured) {\n // The host said stop: nothing more may start, and nothing may land in the queue onUnload\n // just emptied.\n if (this.stopping) {\n return;\n }\n const deviceId = deviceObjectId(row);\n // Two configured names can sanitize to the same object id \u2014 the admin guards\n // against it, but a hand-edited config could still carry it. Skip the duplicate\n // instead of letting two devices fight over one object tree.\n if (seenIds.has(deviceId)) {\n this.log.warn(`Emulated Roku \"${row.name}\" maps to an object id already in use (${deviceId}) \u2014 skipping it.`);\n continue;\n }\n // The dialog refuses these names, but native.devices is hand-editable. A\n // device called \"info\" would rewrite the adapter's own info channel into a\n // device object and hang its command/keys states under info.connection.\n if (RESERVED_IDS.has(deviceId)) {\n this.log.warn(\n `Emulated Roku \"${row.name}\" maps to the object id \"${deviceId}\", which the adapter reserves for its own status \u2014 skipping it.`,\n );\n continue;\n }\n seenIds.add(deviceId);\n if (row.identityReplaced) {\n this.log.warn(`Emulated Roku \"${row.name}\" has an unusable device id in its config \u2014 using a derived one.`);\n }\n if (row.portReplaced) {\n this.log.warn(`Emulated Roku \"${row.name}\" has an unusable ECP port in its config \u2014 using ${row.port}.`);\n }\n const keys = keysForType(row.type);\n try {\n await this.createDeviceStates(deviceId, row.name, keys);\n } catch (e) {\n this.log.warn(`Emulated Roku \"${row.name}\" could not be created: ${errText(e)} \u2014 skipping it.`);\n continue;\n }\n this.deviceKeys.set(deviceId, new Set(keys));\n this.deviceNames.set(deviceId, row.name);\n const device: PendingDevice = {\n deviceId,\n friendlyName: row.name,\n advert: { uuid: row.identity, port: row.port },\n deviceType: row.type,\n };\n if (!(await this.startDeviceServer(device, \"start\")) && !this.stopping) {\n this.pending.push(device);\n }\n }\n }\n\n /**\n * Start one device's ECP server and register it as running.\n *\n * @param device the device to start\n * @param phase whether this is the initial start (warn) or a retry (debug \u2014 the warning was written once)\n * @returns true if the server is listening\n */\n private async startDeviceServer(device: PendingDevice, phase: \"start\" | \"retry\"): Promise<boolean> {\n // The host said stop while this device's objects were being created: bind nothing.\n if (this.stopping) {\n return false;\n }\n let server: EcpHttpServer | undefined;\n try {\n server = this.makeEcpServer({\n device: device.advert,\n friendlyName: device.friendlyName,\n apps: DEFAULT_APPS,\n deviceType: device.deviceType,\n bindIp: this.bindIp,\n logger: this.log,\n onCommand: cmd => this.applyCommand(device.deviceId, cmd),\n onFatalError: () => this.onEcpFatal(device),\n isClientAllowed: this.isOwnClient,\n });\n await server.start();\n // The host may have said stop while we were binding. Registering now would put the\n // server into a map onUnload has already emptied \u2014 nothing would ever close it.\n if (this.stopping) {\n server.stop();\n return false;\n }\n this.ecpServers.set(device.deviceId, server);\n this.running.push(device.advert);\n this.ssdp?.addDevice(device.advert);\n if (phase === \"retry\") {\n this.log.info(`Emulated Roku \"${device.friendlyName}\" is listening on port ${device.advert.port} again.`);\n }\n return true;\n } catch (e) {\n // One device's failure (a busy ECP port) must not take the others down.\n // Close whatever the failed start left behind, so nothing outlives this turn.\n server?.stop();\n const detail = `${errText(e)} \u2014 retrying every ${DEVICE_RETRY_INTERVAL_MS / 1000} s`;\n if (phase === \"start\") {\n this.log.warn(\n `Emulated Roku \"${device.friendlyName}\" could not start on port ${device.advert.port}: ${detail}`,\n );\n } else {\n this.log.debug(\n `Emulated Roku \"${device.friendlyName}\" still cannot start on port ${device.advert.port}: ${detail}`,\n );\n }\n return false;\n }\n }\n\n /** Arm the retry timer while any device is still waiting for its port; a no-op otherwise. */\n private scheduleDeviceRetry(): void {\n if (this.retryTimer || this.pending.length === 0) {\n return;\n }\n const timer = this.setTimeout(() => {\n this.retryTimer = undefined;\n void this.retryPendingDevices();\n }, DEVICE_RETRY_INTERVAL_MS);\n if (timer) {\n this.retryTimer = timer;\n }\n }\n\n /**\n * Try the devices that could not start yet. A port taken at boot is usually a restart\n * race \u2014 the previous process still holds it \u2014 so this recovers on its own instead of\n * leaving a dead device behind a red instance until someone restarts by hand.\n */\n private async retryPendingDevices(): Promise<void> {\n // The timer drops this call with `void`, so nothing may escape it: a rejection here \u2014 the\n // status write failing while the states database hiccups \u2014 would be an unhandled rejection,\n // and js-controller ends the instance over it.\n try {\n // No stopping check at the entry: onUnload sets the flag and empties the queue in the\n // same synchronous block, so a call that arrives afterwards finds nothing to do, and a\n // call already inside the loop is caught after the bind and again at the tail. A guard\n // here could never fire \u2014 measured, it survived its own mutation needle.\n const stillPending: PendingDevice[] = [];\n let recovered = false;\n for (const device of this.pending) {\n if (await this.startDeviceServer(device, \"retry\")) {\n recovered = true;\n } else {\n stillPending.push(device);\n }\n }\n if (this.stopping) {\n // The host said stop while we were binding. Assigning the list back would re-fill the\n // queue onUnload just emptied, and scheduleDeviceRetry would then arm a new timer \u2014\n // which js-controller refuses during shutdown, with a warning nobody can explain.\n return;\n }\n this.pending = stillPending;\n if (recovered) {\n // Discovery may never have started (every device failed at boot) \u2014 bring it up now.\n const advertiseIp = this.bindIp ?? detectPrimaryIPv4();\n if (advertiseIp && !this.ssdp) {\n this.startDiscovery(advertiseIp);\n }\n await this.reportConnectionState(advertiseIp);\n }\n this.scheduleDeviceRetry();\n } catch (e) {\n this.log.warn(`Retrying the waiting emulated Rokus failed: ${errText(e)}`);\n // No stopping check: onUnload empties the queue, and scheduleDeviceRetry arms nothing for\n // an empty one \u2014 measured, a guard here survived its own mutation needle.\n this.scheduleDeviceRetry();\n }\n }\n\n /**\n * Start the SSDP responder for the devices that are listening.\n *\n * Discovery is only an aid; the ECP servers already make the adapter controllable, so a\n * busy port 1900 (or a stuck bind) degrades to \"discovery off, already-paired remotes\n * still work\" instead of failing the whole start-up. In the auto case join every real\n * interface and answer each network with the host's own address in it, so a multi-homed\n * host is discoverable \u2014 and reachable \u2014 on all its LANs; a chosen interface pins\n * membership, NOTIFY egress and every answer to itself.\n *\n * @param advertiseIp the routable IP to announce\n */\n private startDiscovery(advertiseIp: string): void {\n const bindIp = this.bindIp;\n const membershipInterfaces = bindIp\n ? [{ iface: detectLocalNets().find(net => net.address === bindIp)?.iface ?? bindIp, address: bindIp }]\n : detectLocalIPv4s();\n const ssdp = this.makeSsdpResponder({\n devices: [...this.running],\n bindIp,\n advertiseIp,\n membershipInterfaces,\n logger: this.log,\n isClientAllowed: this.isOwnClient,\n // \"All interfaces\": every search is answered with the host's address in the searcher's own\n // network. A chosen interface answers with its address only (the responder uses bindIp).\n advertiseFor: bindIp ? undefined : remote => localAddressFor(remote, detectLocalNets()),\n onFatalError: () => this.onSsdpFatal(),\n });\n this.ssdp = ssdp;\n this.startWithTimeout(ssdp.start(), SSDP_START_TIMEOUT_MS).then(\n () => {\n // The bind resolved after the host asked us to stop: announcing now would put a\n // device back into the network we just said goodbye to, and this.setInterval would\n // refuse with \"setInterval called, but adapter is shutting down\".\n if (this.stopping) {\n ssdp.stop();\n return;\n }\n ssdp.announce();\n const timer = this.setInterval(() => this.announceTick(), SSDP_NOTIFY_INTERVAL_MS);\n if (timer) {\n this.notifyTimer = timer;\n }\n },\n (e: unknown) => {\n this.log.warn(\n `SSDP discovery unavailable: ${errText(e)} \u2014 already-paired remotes still work; set the network interface if devices are not found.`,\n );\n // A start that only timed out can still bind later. Close it, or the socket\n // outlives the reference dropped here and keeps answering with nobody to stop it.\n ssdp.stop();\n if (this.ssdp === ssdp) {\n this.ssdp = undefined;\n }\n },\n );\n }\n\n /**\n * One NOTIFY pass: follow the host first, then announce.\n *\n * Only in the automatic case \u2014 a chosen interface is the user's decision and does not move under\n * us. Every pass hands the responder the interfaces the host has NOW: one that came up since the\n * start (a second network card, a VLAN) is joined and announced on, one that went away is\n * dropped, and a changed fallback address (a DHCP lease change) is logged once. Every five\n * minutes is early enough: a controller caches the announced address for its max-age of an hour.\n */\n private announceTick(): void {\n if (!this.bindIp) {\n const current = detectPrimaryIPv4();\n if (current && this.ssdp?.refreshAdvertise(current, detectLocalIPv4s())) {\n this.log.info(`Host address changed \u2014 the emulated Rokus are now advertised on ${current}.`);\n }\n }\n this.ssdp?.announce();\n }\n\n /**\n * Write info.connection and say in the log what the instance is doing.\n *\n * \"Connected\" means EVERY configured Roku is listening, not just some of them. A device\n * whose port is taken (or whose name collides) is skipped with a warning naming it;\n * reporting \"connected\" anyway would hide a broken configuration behind the devices that\n * did come up, and the user would only find out when the remote stops working. Config rows\n * without a usable name are filtered out before this point and deliberately do not count \u2014\n * every device that fails has said so in the log.\n *\n * @param advertiseIp the announced IP, for the log line\n */\n private async reportConnectionState(advertiseIp: string): Promise<void> {\n const allStarted = this.running.length === this.expectedDevices;\n await this.setState(\"info.connection\", { val: allStarted, ack: true });\n // The retry path can hand in an empty address (detectPrimaryIPv4 found nothing), and\n // \"advertising on (discovery off)\" reads like a truncated line rather than a finding.\n const where = `advertising on ${advertiseIp || \"no routable IPv4\"}${this.ssdp ? \"\" : \" (discovery off)\"}`;\n if (allStarted) {\n this.log.info(`Emulating ${this.running.length} Roku device(s), ${where}`);\n } else {\n this.log.error(\n `Only ${this.running.length} of ${this.expectedDevices} configured Roku device(s) could be started, ${where} \u2014 fix the cause reported above; the instance stays disconnected until every device runs.`,\n );\n }\n }\n\n /**\n * Re-apply the adapter's OWN objects \u2014 the `info` channel and `info.connection`\n * \u2014 on every start.\n *\n * js-controller extends the manifest's instanceObjects on every start, but it preserves\n * `common.name` of an existing object (7.2.2 `_extendObjects`, `preserve: { common: [\"name\"] }`):\n * a changed description or role arrives by itself, a changed NAME only on a fresh install.\n * extendObject is what carries the name into an existing tree, so an update always lands on\n * every datapoint, not just on fresh installs.\n *\n * It also repairs the `info` channel after a hand-edited device row named\n * \"info\" turned it into a device object (see the reserved-id guard in startDevices).\n */\n private async refreshOwnObjects(): Promise<void> {\n await Promise.all([\n this.extendObject(\"info\", {\n type: \"channel\",\n common: { name: tName(\"channelInfo\") },\n native: {},\n }),\n this.extendObject(\"info.connection\", {\n type: \"state\",\n common: {\n name: tName(\"connectionStatus\"),\n desc: tDesc(\"connectionStatusDesc\"),\n type: \"boolean\",\n role: \"indicator.connected\",\n read: true,\n write: false,\n def: false,\n },\n native: {},\n }),\n ]);\n }\n\n /**\n * Create the fixed object tree for one emulated Roku: the device, `command` +\n * `commandType`, and one `sensor` boolean state per key the device type exposes \u2014\n * all up front, so the tree is usable before any key is ever pressed.\n *\n * Every key state is also RESET to false here. A key is a momentary signal, but\n * nothing writes its release when the adapter goes down: a keypress pulses true\n * and schedules the false 50 ms later, a keydown holds true until its keyup, and\n * onUnload drops both timers without writing. So a stop inside that window \u2014 or\n * a crash, or a controller that never sent its keyup \u2014 leaves the key true in\n * the database for good, and a rule watching for the next press never sees an\n * edge again. The reset belongs on STARTUP, not into onUnload: only startup also\n * covers the crash, and up to 31 writes per device would eat the shutdown budget that\n * today comfortably carries a single one. setStateChanged writes only where the\n * value actually differs, so a healthy tree costs nothing.\n *\n * The writes go out together: they address different objects, and doing 20 (a player)\n * or 35 (a TV) of them strictly one after another made start-up wait for one round trip\n * per datapoint \u2014 the key reset right below has always been parallel.\n *\n * @param deviceId the id-safe device path segment\n * @param friendlyName the configured device name\n * @param keys the key names to create for this device (from its type)\n */\n private async createDeviceStates(deviceId: string, friendlyName: string, keys: readonly string[]): Promise<void> {\n await Promise.all([\n // The device name is the user's own text \u2014 nothing to translate, but it must\n // still BE a translation object like every other common.name (tRaw).\n this.extendObject(deviceId, { type: \"device\", common: { name: tRaw(friendlyName) }, native: {} }),\n this.extendObject(`${deviceId}.command`, {\n type: \"state\",\n common: {\n name: tName(\"stateLastCommand\"),\n desc: tDesc(\"stateLastCommandDesc\"),\n type: \"string\",\n role: \"text\",\n read: true,\n write: false,\n def: \"\",\n },\n native: {},\n }),\n this.extendObject(`${deviceId}.commandType`, {\n type: \"state\",\n common: {\n name: tName(\"stateLastCommandType\"),\n desc: tDesc(\"stateLastCommandTypeDesc\"),\n type: \"string\",\n role: \"text\",\n read: true,\n write: false,\n def: \"\",\n // The fixed verb list, so the admin shows the value as a label. Plain strings\n // only \u2014 a translation object here crashes the admin's object view.\n states: Object.fromEntries(COMMAND_TYPES.map(verb => [verb, verb])),\n },\n native: {},\n }),\n this.extendObject(`${deviceId}.keys`, {\n type: \"channel\",\n common: { name: tName(\"channelKeys\"), desc: tDesc(\"channelKeysDesc\") },\n native: {},\n }),\n ...keys.map(key =>\n this.extendObject(`${deviceId}.keys.${key}`, {\n type: \"state\",\n // \"sensor\" = generic boolean read-only (active/inactive). The docs suggest\n // button.press for a keypress-as-state, but the repochecker requires button.press to\n // be read:false, write:true (config_StateRoles) \u2014 a key state is read-only, so\n // button.press fails with E1010; sensor is the gate-conformant fit.\n // The name is the ECP key identifier and identical in every language, but\n // it still has to BE a translation object (tRaw), never a bare string.\n // No desc: the key name already says everything there is to say. The\n // decision itself is recorded in test/self-explaining.json, where the\n // object-inventory gate reads it \u2014 silence has to be a decision, not a gap.\n common: { name: tRaw(key), type: \"boolean\", role: \"sensor\", read: true, write: false, def: false },\n native: {},\n }),\n ),\n ]);\n await Promise.all(keys.map(key => this.setStateChangedAsync(`${deviceId}.keys.${key}`, { val: false, ack: true })));\n }\n\n /**\n * Run the orphan sweep for the current configuration \u2014 from EVERY exit of\n * onReady, which is the point of it existing as its own method.\n *\n * The sweep used to sit on the happy path only, so removing the last emulated\n * Roku (or a host without a routable address) left its device object, its\n * `command` / `commandType` and its 16\u201331 key states behind with nothing that\n * would ever remove them: the user deletes a device in the admin and keeps its\n * datapoints forever. The adapter answers for its own datapoints\n * (`feedback_adapter_verantwortet_datenpunkte`), so the sweep runs whenever the\n * configuration could be read.\n *\n * `configured === null` is that condition, and it is not pedantry: with no `devices`\n * key at all (a never-configured instance, or a config we could not read) an\n * empty set would mean \"nothing is configured, delete everything\" \u2014 trading a\n * tree that stays for a tree that is gone.\n *\n * @param configured the configured rows, or null when there is no readable list\n */\n private async sweepOrphans(configured: readonly DeviceRow[] | null): Promise<void> {\n if (!configured) {\n return;\n }\n await this.cleanupOrphans(new Set(configured.map(row => deviceObjectId(row))));\n }\n\n /**\n * The adapter's objects, keyed relative to the namespace.\n *\n * @returns the object dump\n */\n private async readOwnObjects(): Promise<Map<string, ioBroker.Object>> {\n const objects = await this.getAdapterObjectsAsync();\n const prefix = `${this.namespace}.`;\n const owned = new Map<string, ioBroker.Object>();\n for (const [id, obj] of Object.entries(objects)) {\n if (id.startsWith(prefix)) {\n owned.set(id.slice(prefix.length), obj);\n }\n }\n return owned;\n }\n\n /**\n * Give every device that has never been announced its own identity and fix its object id \u2014\n * on its very first start, before anything announces it.\n *\n * \"Never announced\" is a row without a stored `uuid` whose object tree does not exist yet: the\n * manifest's default device of a fresh instance, or a hand-written row. Its identity would\n * otherwise be derived from its name, and every installation \u2014 every instance on one host too \u2014\n * announcing a device called \"Roku\" would share one USN. A row whose tree exists has been\n * announced, maybe paired, and keeps the identity it has.\n *\n * Writing `native` restarts the instance, so the start ends here when something was written.\n *\n * @param rows the configured rows\n * @param owned the object tree as it is\n * @returns true when the config was written \u2014 the caller aborts its start\n */\n private async persistNewIdentities(\n rows: readonly DeviceRow[],\n owned: ReadonlyMap<string, ioBroker.Object>,\n ): Promise<boolean> {\n const fresh = rows.filter(row => row.identityDerived && !owned.has(row.objectId));\n if (fresh.length === 0) {\n return false;\n }\n const devices = rows.map(row => ({\n name: row.storedName,\n port: row.port,\n type: row.type,\n uuid: fresh.includes(row) ? randomIdentity() : row.identity,\n objectId: row.objectId,\n }));\n try {\n await this.extendForeignObjectAsync(`system.adapter.${this.namespace}`, { native: { devices } });\n } catch (e) {\n // Not fatal: the device starts with the identity derived from its name, as before.\n this.log.warn(`New emulated Roku could not get its own identity (${errText(e)}) \u2014 trying again next start`);\n return false;\n }\n this.log.info(\n `New emulated Roku ${fresh.map(row => `\"${row.name}\"`).join(\", \")} got its own network identity \u2014 the instance restarts once`,\n );\n return true;\n }\n\n /**\n * Remove objects left over from an earlier version or config \u2014 the legacy\n * `apps` node, keys no longer standard, and whole device sub-trees no longer\n * configured (a renamed/removed device). The adapter otherwise only ever\n * creates objects, so without this the tree would accrete stale entries.\n *\n * The same pass also strips attributes an older version wrote into a state's `native`\n * and this one does not (see {@link pruneStateNative}) \u2014 the object dump is already in\n * hand here, so recognising them costs nothing.\n *\n * @param configuredDeviceIds the id-safe names of the currently configured devices\n */\n private async cleanupOrphans(configuredDeviceIds: ReadonlySet<string>): Promise<void> {\n // A dump read NOW, after this start created and renamed its objects: the native repair\n // below writes an object back whole, and a dump from before the start would write the\n // names and descriptions of the previous version back over the ones just set.\n const owned = await this.readOwnObjects();\n const toDelete = planObjectCleanup([...owned.keys()], configuredDeviceIds, this.deviceKeys);\n for (const id of toDelete) {\n await this.delObjectAsync(id, { recursive: true }).catch((e: unknown) => {\n this.log.debug(`cleanup: could not delete ${id}: ${errText(e)}`);\n });\n }\n if (toDelete.length > 0) {\n this.log.debug(`Removed ${toDelete.length} orphaned object(s) from an earlier version or config.`);\n }\n await this.pruneStateNative(owned, new Set(toDelete));\n }\n\n /**\n * Strip `native` attributes an earlier version wrote and this one no longer does.\n *\n * The pre-0.5.0 adapter created every key state with `native: { url: \"keys/Home\" }`.\n * `extendObject` cannot remove that: it merges, so an attribute only the stored object\n * carries survives every further update \u2014 and writing `null` would not delete it either,\n * it would store `null`. So an installation upgraded from <= 0.4.0 carries a dead\n * attribute on every key datapoint, and the adapter answers for its own datapoints.\n *\n * Removing one therefore rewrites the object in a single `setForeignObject`, handing back\n * exactly what was read with `native` emptied \u2014 `common` included, because that is where\n * `common.custom` lives, the user's own history/chart configuration.\n *\n * It used to be a `delObject` followed by an `extendObject`. That pair replaces an object,\n * but it is the wrong tool here: `delObject` on a state ALSO deletes the state's value and\n * removes its id from every enum it belongs to. Repairing a dead attribute that way cost\n * the user the recorded value and the room the datapoint was sorted into \u2014 and the\n * re-created object came back carrying `common.def` as its value, which reads like data\n * rather than like a loss. `setForeignObject` touches neither. (The discouraged call is\n * `setObject`, repochecker S5054, and only because a blind full write overwrites what the\n * user changed; writing back what was just read does not.)\n *\n * The write fails safely: if it does not happen, the attribute stays where it was and the\n * tree is exactly as before. The reason is in the log.\n *\n * @param owned the adapter's objects, keyed relative to the namespace\n * @param deleted the ids the sweep just removed \u2014 no point writing to those\n */\n private async pruneStateNative(\n owned: ReadonlyMap<string, ioBroker.Object>,\n deleted: ReadonlySet<string>,\n ): Promise<void> {\n const stale = planNativePrune(owned, deleted);\n for (const [id, obj] of stale) {\n try {\n // Non-recursive: a channel or device carrying the leftover keeps its children,\n // which are read and repaired on their own turn.\n // One write, never a delete plus a re-create: from/ts/user are left out so the\n // controller stamps the write freshly \u2014 it only adopts them when they are absent.\n const { from: _from, ts: _ts, user: _user, ...keep } = obj;\n // The cast only collapses the union: ioBroker.Object spans every object kind\n // (instance, host, enum, ...), while a write accepts device/channel/state/folder.\n // The shape is the one just read back, so nothing is asserted away here.\n await this.setForeignObject(`${this.namespace}.${id}`, {\n ...keep,\n native: {},\n } as\n | ioBroker.SettableDeviceObject\n | ioBroker.SettableChannelObject\n | ioBroker.SettableFolderObject\n | ioBroker.SettableStateObject);\n } catch (e: unknown) {\n this.log.debug(`cleanup: could not rewrite ${id}: ${errText(e)}`);\n }\n }\n if (stale.length > 0) {\n this.log.debug(`Removed a stale native attribute from ${stale.length} object(s) of an earlier version.`);\n }\n }\n\n /**\n * Apply a received ECP command to this device's states: record it in `command`\n * / `commandType`, and pulse or hold the standard key if it is one.\n *\n * Returns whether the command was applied: the ECP server logs only what got\n * through, so the rate gate covers the log as well as the states database.\n *\n * @param deviceId the id-safe device path segment\n * @param cmd the parsed ECP command\n * @returns true if the command was applied, false if the rate gate dropped it\n */\n private applyCommand(deviceId: string, cmd: CommandEvent): boolean {\n const write = commandToStateWrite(cmd);\n // The release of a key that is actually HELD never passes through the rate gate.\n // Dropping a keypress costs one event; dropping the keyup of a held key leaves it true\n // until the 30 s watchdog, so the flood protection would be the thing that falsifies\n // the tree. It also costs one write instead of four, so it is not what the gate exists\n // to stop.\n // The exemption asks whether there is something to release, not what the request looks\n // like: without the watchdog check a flood of keyup requests for a key nobody is holding\n // would bypass the gate entirely - and since the server logs only what was applied, it\n // would take a log line with it on every request.\n const releaseOf = write.holdKey?.value === false ? `${deviceId}.keys.${write.holdKey.key}` : null;\n const isRelease = releaseOf !== null && this.holdTimers.has(releaseOf);\n if (!isRelease && !this.admitCommand(deviceId)) {\n return false;\n }\n this.writeState(`${deviceId}.command`, write.command);\n this.writeState(`${deviceId}.commandType`, write.commandType);\n // Only write keys.<Key> if THIS device's type carries the key \u2014 a player has no\n // TV key objects, so a stray TV keypress lands in `command` only, never a missing state.\n const keys = this.deviceKeys.get(deviceId);\n if (write.pulseKey && keys?.has(write.pulseKey)) {\n const id = `${deviceId}.keys.${write.pulseKey}`;\n // A keypress on a key that is currently HELD ends the hold: its watchdog would\n // otherwise fire later and write a release for a key the pulse already released.\n this.clearHoldTimer(id);\n this.writeState(id, true);\n const timer = this.setTimeout(() => {\n if (timer) {\n this.pulseTimers.delete(timer);\n }\n // A keydown that arrived inside the pulse window owns the key now: ECP defines a\n // keypress as pressing down AND releasing, so it is a finished act and a later\n // keydown starts a new one. Writing the pulse's release here would end a hold that\n // is still going on, while its watchdog stays armed. The keyup writes the release.\n if (!this.holdTimers.has(id)) {\n this.writeState(id, false);\n }\n }, KEY_PULSE_MS);\n if (timer) {\n this.pulseTimers.add(timer);\n }\n } else if (write.holdKey && keys?.has(write.holdKey.key)) {\n const id = `${deviceId}.keys.${write.holdKey.key}`;\n this.writeState(id, write.holdKey.value);\n // A keydown holds the key true until its keyup. Arm a watchdog so a lost keyup\n // (controller disconnects mid-press) cannot pin the key true forever; a keyup\n // clears it, and a repeated keydown re-arms it.\n this.clearHoldTimer(id);\n if (write.holdKey.value) {\n const timer = this.setTimeout(() => {\n this.holdTimers.delete(id);\n this.writeState(id, false);\n }, HOLD_MAX_MS);\n if (timer) {\n this.holdTimers.set(id, timer);\n }\n }\n }\n return true;\n }\n\n /**\n * Disarm the hold watchdog of one key, if it has one.\n *\n * @param id the full key state id\n */\n private clearHoldTimer(id: string): void {\n const pending = this.holdTimers.get(id);\n if (pending) {\n this.clearTimeout(pending);\n this.holdTimers.delete(id);\n }\n }\n\n /**\n * The rate gate in front of every state write: MAX_COMMANDS_PER_SECOND per device,\n * the excess is dropped and reported once per RATE_WARN_INTERVAL_MS. Every accepted\n * command costs the states database three writes plus one more when the pulse\n * ends \u2014 a flooding device in the LAN would otherwise slow the whole host.\n *\n * @param deviceId the id-safe device path segment\n * @returns true if the command may be applied\n */\n private admitCommand(deviceId: string): boolean {\n const now = Date.now();\n let gate = this.commandGates.get(deviceId);\n if (!gate) {\n gate = new RateGate(MAX_COMMANDS_PER_SECOND, now);\n this.commandGates.set(deviceId, gate);\n }\n if (gate.allow(now)) {\n return true;\n }\n if (now - (this.rateWarnedAt.get(deviceId) ?? 0) >= RATE_WARN_INTERVAL_MS) {\n this.rateWarnedAt.set(deviceId, now);\n this.log.warn(\n `Emulated Roku \"${this.deviceNames.get(deviceId) ?? deviceId}\" receives more than ${MAX_COMMANDS_PER_SECOND} commands per second \u2014 dropping the excess (a misbehaving controller?)`,\n );\n }\n return false;\n }\n\n /**\n * Fire-and-forget state write for the command hot path. The states exist (created\n * up front in createDeviceStates), so there is no read-before-write; a rejection \u2014\n * the states database already closed while a remote still sends \u2014 is traced at\n * debug, because an unhandled one would crash the adapter over one lost keypress.\n *\n * @param id the state id relative to the namespace\n * @param val the value to write\n */\n private writeState(id: string, val: string | boolean): void {\n this.setState(id, { val, ack: true }).catch((e: unknown) => {\n this.log.debug(`State write ${id} failed: ${errText(e)}`);\n });\n }\n\n /**\n * One emulated Roku's ECP server died at runtime (a server error after a good\n * start). That device answers nothing any more, so the instance is no longer\n * \"every configured Roku is listening\" \u2014 revise the status instead of leaving a\n * green instance behind a device that is gone, and stop announcing it, or discovery\n * would keep pointing remotes at a port nobody serves. The other devices keep\n * running, and the message names the one that failed.\n *\n * No retry here, deliberately: a port that was busy at boot is a restart race and\n * heals; a server that died while running died for a reason this code does not know,\n * and retrying it on a timer would be a log-flood generator.\n *\n * @param device the device whose server died\n */\n private onEcpFatal(device: PendingDevice): void {\n this.log.error(\n `Emulated Roku \"${device.friendlyName}\" stopped answering after a server error \u2014 restart the instance to bring it back.`,\n );\n // Close it before forgetting it: once it is out of the map neither this path nor\n // onUnload can ever reach it again, and in compact mode the port would stay taken for\n // the lifetime of the whole host process. stop() is idempotent and safe on a dead\n // server. The SSDP responder already does exactly this in its own fatal path.\n this.ecpServers.get(device.deviceId)?.stop();\n this.ecpServers.delete(device.deviceId);\n const at = this.running.indexOf(device.advert);\n if (at >= 0) {\n this.running.splice(at, 1);\n }\n this.ssdp?.removeDevice(device.advert.uuid);\n this.setState(\"info.connection\", { val: false, ack: true }).catch((e: unknown) => {\n this.log.debug(`Connection state write failed: ${errText(e)}`);\n });\n }\n\n /**\n * The SSDP responder died at runtime (a socket error after a good start). Stop\n * announcing into the dead socket and drop the discovery aid. The ECP servers\n * keep working, so info.connection \u2014 which reflects ECP readiness \u2014 stays true.\n */\n private onSsdpFatal(): void {\n if (this.notifyTimer) {\n this.clearInterval(this.notifyTimer);\n this.notifyTimer = undefined;\n }\n this.ssdp = undefined;\n this.log.warn(\n \"SSDP discovery stopped after a socket error \u2014 already-paired remotes still work; restart the instance to re-enable discovery.\",\n );\n }\n\n /**\n * Bound await: reject if the SSDP start doesn't settle in time, so a stuck\n * port-1900 bind degrades to \"discovery off\" instead of hanging the adapter.\n *\n * @param promise the SSDP start promise\n * @param ms the timeout in milliseconds\n * @returns a promise that settles with the start result or a timeout error\n */\n private startWithTimeout(promise: Promise<void>, ms: number): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const timer = this.setTimeout(() => reject(new Error(`SSDP start timed out after ${ms} ms`)), ms);\n const clear = (): void => {\n if (timer) {\n this.clearTimeout(timer);\n }\n };\n promise.then(\n () => {\n clear();\n resolve();\n },\n (e: unknown) => {\n clear();\n // Lint (prefer-promise-reject-errors) wants an Error here; errText downstream\n // would cope with anything, so this is the rule's shape, not a second safeguard.\n reject(e instanceof Error ? e : new Error(errText(e)));\n },\n );\n });\n }\n\n /**\n * Teardown: drop the timers and sockets synchronously, then report done only\n * once the farewell and the last write have landed.\n *\n * `info.connection` is the only status this adapter carries, and nothing else\n * resets it: the host means to, but writes its reset to the namespace root\n * instead of the datapoint (js-controller#3472). So if the final write is\n * lost, the instance shows \"connected\" while the adapter is off.\n *\n * A fire-and-forget write plus an immediate callback is a race, not a\n * guaranteed loss \u2014 measured on 1.1.0, it still arrived, because without\n * `common.supportedMessages.stopInstance` the process ends in an orderly way\n * and flushes what is pending. Waiting closes the race for the slow or busy\n * case, and it is safe for the same reason: no `stopInstance` means the host\n * grants the full `common.stopTimeout` instead of killing the process.\n *\n * The farewell (`ssdp:byebye`) rides in the same wait: without it a controller keeps the\n * emulated Roku in its list for up to the announced hour and sends key presses into a\n * port nobody serves.\n *\n * @param callback function to invoke once teardown is complete\n */\n private onUnload(callback: () => void): void {\n try {\n // First of all, before any teardown: whatever is still in flight must not start or\n // write anything from here on.\n this.stopping = true;\n if (this.notifyTimer) {\n this.clearInterval(this.notifyTimer);\n this.notifyTimer = undefined;\n }\n if (this.retryTimer) {\n this.clearTimeout(this.retryTimer);\n this.retryTimer = undefined;\n }\n if (this.discoveryTimer) {\n this.clearTimeout(this.discoveryTimer);\n this.discoveryTimer = undefined;\n }\n for (const t of this.pulseTimers) {\n this.clearTimeout(t);\n }\n this.pulseTimers.clear();\n for (const t of this.holdTimers.values()) {\n this.clearTimeout(t);\n }\n this.holdTimers.clear();\n // The ECP servers close right here, synchronously: nothing about the farewell needs\n // them, and a keep-alive connection left open drags teardown toward a SIGKILL.\n for (const s of this.ecpServers.values()) {\n s.stop();\n }\n this.ecpServers.clear();\n this.deviceKeys.clear();\n this.deviceNames.clear();\n this.commandGates.clear();\n this.rateWarnedAt.clear();\n this.running.length = 0;\n this.pending = [];\n // The SSDP socket is the one thing that cannot close yet \u2014 the farewell goes out\n // through it. It is closed in the callback below, whichever way that arrives.\n const farewell = this.ssdp ? this.ssdp.byebye() : Promise.resolve();\n const connection = this.setState(\"info.connection\", { val: false, ack: true })\n // A rejected write must not become an unhandled rejection \u2014 that is a\n // crash (exit code 6) instead of an orderly stop. The trace stays at\n // debug: it explains a stale \"connected\" afterwards, and nobody can act\n // on it while the adapter is already going down.\n .catch((e: unknown) => {\n this.log.debug(`Final connection write failed: ${errText(e)}`);\n });\n void Promise.all([farewell, connection]).finally(() => {\n this.ssdp?.stop();\n this.ssdp = undefined;\n callback();\n });\n } catch {\n callback();\n }\n }\n}\n\nif (require.main !== module) {\n // Export the constructor in compact mode\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) => new Fakeroku(options);\n} else {\n // Start the instance directly\n (() => new Fakeroku())();\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import * as utils from \"@iobroker/adapter-core\";\nimport { I18n } from \"@iobroker/adapter-core\";\nimport { join } from \"node:path\";\nimport { FakerokuDeviceManagement } from \"./device-management\";\nimport type { RokuAdvert } from \"./discovery/ssdp-messages\";\nimport { RokuSsdpResponder } from \"./discovery/ssdp-responder\";\nimport { DEFAULT_APPS } from \"./ecp/device-info\";\nimport { COMMAND_TYPES, type CommandEvent } from \"./ecp/ecp-command\";\nimport { EcpHttpServer } from \"./ecp/ecp-http-server\";\nimport { commandToStateWrite, type DeviceType, keysForType } from \"./ecp/state-model\";\nimport { RESERVED_IDS } from \"./lib/constants\";\nimport { deviceObjectId, deviceTreeOf, toDeviceRows, type DeviceRow } from \"./lib/device-config\";\nimport { randomIdentity } from \"./lib/device-identity\";\nimport {\n detectLocalIPv4s,\n detectLocalNets,\n detectPrimaryIPv4,\n hasLocalAddress,\n localAddressFor,\n netsOfInterface,\n} from \"./lib/detect-ip\";\nimport { errText } from \"./lib/errors\";\nimport { migrateNativeKeys, type NativeKeyMigration } from \"./lib/native-key-migration\";\nimport { tDesc, tName, tRaw } from \"./lib/i18n\";\nimport { isLanClient } from \"./lib/lan-guard\";\nimport { planNativePrune, planObjectCleanup } from \"./lib/object-cleanup\";\nimport { RateGate } from \"./lib/rate-gate\";\n\n/** Managed timeout for a stuck SSDP start (a busy port 1900 must not hang onReady). */\nconst SSDP_START_TIMEOUT_MS = 5000;\n/** Proactive ssdp:alive interval so controllers find the device without searching. */\nconst SSDP_NOTIFY_INTERVAL_MS = 300_000;\n/** How long a keypress pulses its keys.<Key> state true before falling back to false. */\nconst KEY_PULSE_MS = 50;\n/** Safety cap for a held key: a keydown with no matching keyup resets after this. */\nconst HOLD_MAX_MS = 30_000;\n/** Commands accepted per second and emulated Roku; the excess is dropped (see lib/rate-gate.ts). */\nconst MAX_COMMANDS_PER_SECOND = 25;\n/** How often at most the dropped-commands warning repeats per device. */\nconst RATE_WARN_INTERVAL_MS = 60_000;\n/** How long to wait before trying a device whose ECP port was busy at start-up again. */\nconst DEVICE_RETRY_INTERVAL_MS = 60_000;\n/** How often a chosen interface address that is missing at start-up is looked for again. */\nconst BIND_WAIT_STEP_MS = 10_000;\n/**\n * How long a missing chosen address is waited for. Long enough for a host whose network comes up\n * after ioBroker (Wi-Fi, DHCP, a restart after a power cut); after that the address is taken as\n * gone, and serving on another one would leave the network the user chose.\n */\nconst BIND_WAIT_MAX_MS = 120_000;\n/** How often \"all interfaces\" looks for an address to start discovery with when it had none. */\nconst DISCOVERY_RETRY_INTERVAL_MS = 60_000;\n\n/**\n * Keys the 0.1.x adapter declared and nothing reads any more: its own HTTP port and multicast\n * address (both fixed by the Roku protocol today) and one global identity (every device carries its\n * own now). js-controller never deletes a native key, so without the drop they stay in every\n * installation that came from there.\n */\nconst DROPPED_NATIVE_KEYS: NativeKeyMigration[] = [{ drop: \"HTTP_PORT\" }, { drop: \"MULTICAST_IP\" }, { drop: \"UUID\" }];\n\n/**\n * `common` keys an earlier manifest declared and this one no longer does: `license` (0.1.1, replaced\n * by `licenseInformation`) and `singletonHost` (0.2.0, gone since 1.6.0). js-controller merges\n * `common` on every update and never removes a key, so they stay in every installation that came\n * from there; the fleet helper nulls them in the same single write as the settings.\n */\nconst DROPPED_COMMON_KEYS: NativeKeyMigration[] = [{ commonDrop: \"license\" }, { commonDrop: \"singletonHost\" }];\n\n/**\n * The listen address moved to `bind` (fleet listen-port standard). Which legacy key holds the\n * user's CURRENT choice depends on the versions the installation went through:\n *\n * - `networkInterface` exists only on an instance that ran 0.6.0\u20131.6.1. Its admin showed exactly\n * that key, and the adapter bound to it \u2014 \"\" meant all interfaces. It is the setting the user\n * saw last, so it wins, and a `BIND` still lying next to it is only cleared. The old adapter's\n * `BIND` had to be a concrete LAN address (it was also the advertised location), so letting it\n * win would pin an instance that ran on \"all interfaces\" for months to an address from years ago.\n * - Without `networkInterface` the instance comes straight from the pre-0.6.0 adapter, and `BIND`\n * is its setting.\n *\n * An empty legacy value becomes \"0.0.0.0\" \u2014 the admin's check skips an instance whose `bind` is\n * falsy, so a migrated \"\" would be as invisible as no key at all.\n *\n * @param native the instance's stored native settings\n * @returns the migrations for this installation, the obsolete 0.1.x keys included\n */\nfunction bindKeyMigrations(native: Record<string, unknown>): NativeKeyMigration[] {\n const hasInterfaceKey = native.networkInterface !== undefined && native.networkInterface !== null;\n const bind: NativeKeyMigration[] = hasInterfaceKey\n ? [{ from: \"networkInterface\", to: \"bind\", coerce: toBindAddress }, { drop: \"BIND\" }]\n : [{ from: \"BIND\", to: \"bind\", coerce: toBindAddress }];\n return [...bind, ...DROPPED_NATIVE_KEYS, ...DROPPED_COMMON_KEYS];\n}\n\n/**\n * A legacy listen address in the form the admin can read.\n *\n * @param old the stored legacy value\n * @returns the address, or \"0.0.0.0\" when it holds nothing usable\n */\nfunction toBindAddress(old: unknown): string {\n return typeof old === \"string\" && old.trim() ? old.trim() : \"0.0.0.0\";\n}\n\n/** An emulated Roku whose ECP server did not come up yet \u2014 its objects exist, only the server is missing. */\ninterface PendingDevice {\n /** The id-safe device path segment. */\n deviceId: string;\n /** The configured device name, for log lines the user can act on. */\n friendlyName: string;\n /** The advert (identity + port) this device would answer under. */\n advert: RokuAdvert;\n /** The emulated device type. */\n deviceType: DeviceType;\n}\n\n/**\n * ioBroker.fakeroku \u2014 Roku emulator (input side).\n *\n * Emulates one or more Roku devices on the LAN so that ECP/SSDP remotes\n * (Logitech Harmony or a Sofabaton X1/X2) trigger events in ioBroker:\n * a keypress lands in `<device>.command` and pulses `<device>.keys.<Key>`.\n */\nexport class Fakeroku extends utils.Adapter {\n private ssdp: RokuSsdpResponder | undefined;\n private notifyTimer: ioBroker.Interval | undefined;\n private readonly ecpServers = new Map<string, EcpHttpServer>();\n private readonly pulseTimers = new Set<ioBroker.Timeout>();\n /** Per held key id, its watchdog timer \u2014 so a keydown without a keyup cannot pin it true forever. */\n private readonly holdTimers = new Map<string, ioBroker.Timeout>();\n /** Per device, the key names it exposes \u2014 so a keypress only writes keys this device carries. */\n private readonly deviceKeys = new Map<string, ReadonlySet<string>>();\n /** Per device, its command rate gate \u2014 the write-flood protection for the states database. */\n private readonly commandGates = new Map<string, RateGate>();\n /** Per device, when the dropped-commands warning was last written. */\n private readonly rateWarnedAt = new Map<string, number>();\n /** Per device id, the name the user gave it \u2014 for log lines the user can recognise. */\n private readonly deviceNames = new Map<string, string>();\n /** The devices that are actually listening \u2014 the basis for info.connection. */\n private readonly running: RokuAdvert[] = [];\n /** Devices whose ECP server did not start; retried on a timer until they come up. */\n private pending: PendingDevice[] = [];\n /** The retry timer for {@link pending}, armed only while something is waiting. */\n private retryTimer: ioBroker.Timeout | undefined;\n /** The timer that starts discovery once the host has an address (\"all interfaces\" only). */\n private discoveryTimer: ioBroker.Timeout | undefined;\n\n /**\n * Set as the very first thing onUnload does. Everything that can still be in flight at\n * that moment asks it before it writes or starts anything: the minute retry can be sitting\n * in `await server.start()` when the host says stop, and would then register a server into\n * an already-cleared map and write info.connection TRUE after the closing FALSE.\n */\n private stopping = false;\n /** How many configured devices are expected to listen \u2014 the target info.connection compares against. */\n private expectedDevices = 0;\n /** The interface to bind to, or undefined for \"all\" \u2014 kept for the retry after onReady returned. */\n private bindIp: string | undefined;\n /**\n * Device-manager backend: the emulated Rokus as cards with add/edit/delete.\n *\n * Nothing reads this field, and that is not an oversight: dm-utils subscribes to\n * the adapter's `message` event from its own constructor, so creating the object\n * IS the wiring. The field keeps it visible \u2014 and owned \u2014 instead of leaving a\n * bare `new` in the constructor that reads like a mistake.\n */\n private readonly deviceManagement: FakerokuDeviceManagement;\n\n // Construction seams for the two network-facing collaborators. Production uses\n // the real classes; the orchestration tests swap them for fakes so onReady's\n // wiring (per-device isolation, SSDP degradation, timers) is testable without\n // binding a port. Behaviour is unchanged \u2014 same constructors, same arguments.\n private makeEcpServer: (options: ConstructorParameters<typeof EcpHttpServer>[0]) => EcpHttpServer = options =>\n new EcpHttpServer(options);\n private makeSsdpResponder: (options: ConstructorParameters<typeof RokuSsdpResponder>[0]) => RokuSsdpResponder =\n options => new RokuSsdpResponder(options);\n\n /**\n * @param options adapter options passed through by js-controller\n */\n public constructor(options: Partial<utils.AdapterOptions> = {}) {\n super({\n ...options,\n name: \"fakeroku\",\n });\n\n this.on(\"ready\", this.onReady.bind(this));\n this.on(\"unload\", this.onUnload.bind(this));\n this.deviceManagement = new FakerokuDeviceManagement(this);\n }\n\n /**\n * One-shot repair of this instance's own object \u2014 the settings keys and the leftover `common` keys.\n *\n * js-controller only ever ADDS to an existing instance object: on an update it fills missing\n * `native` keys with the manifest default, and a `common` key the manifest dropped stays behind\n * for ever. Two changes therefore never reached an existing installation on their own:\n * the listen address had to MOVE to `bind` (a read fallback finds the injected default, not the\n * user's value), and `license`/`singletonHost` stayed in the instance object (read by nobody:\n * `createInstance` reads the ADAPTER object, but a dead key is still the adapter's to remove).\n *\n * Everything goes through the fleet helper (`lib/native-key-migration.ts`, byte-identical with\n * the fleet master) in ONE write, so the update costs one restart: it merges only the touched\n * keys, and when the write fails it carries the migrated values into this run's config instead\n * of starting on the injected default.\n *\n * @returns true when the object was written \u2014 the caller aborts its start, the host restarts\n */\n private async repairInstanceObject(): Promise<boolean> {\n const id = `system.adapter.${this.namespace}`;\n let obj: { common?: Record<string, unknown>; native?: Record<string, unknown> } | null | undefined;\n try {\n obj = await this.getForeignObjectAsync(id);\n } catch (err) {\n this.log.warn(`Settings repair skipped \u2014 could not read ${id}: ${errText(err)}`);\n return false;\n }\n if (!obj) {\n return false;\n }\n return migrateNativeKeys(this, bindKeyMigrations(obj.native ?? {}), errText);\n }\n\n /** Create each device's object tree, start its ECP server, then the shared SSDP responder. */\n private async onReady(): Promise<void> {\n try {\n // Repair what an update leaves behind in the instance object, before anything binds a port.\n // The write restarts this instance, so nothing may start before it.\n if (await this.repairInstanceObject()) {\n return;\n }\n\n await this.setState(\"info.connection\", { val: false, ack: true });\n await I18n.init(join(this.adapterDir, \"admin\"), this);\n await this.refreshOwnObjects();\n\n // The object tree as it is before this start touches it: the rows resolve their object id\n // and \u2014 for a row from before 0.7.0 \u2014 their type against it.\n const owned = await this.readOwnObjects();\n if (this.stopping) {\n return;\n }\n\n // Read the device list BEFORE anything can return early: the orphan sweep below\n // has to know the configured set at every exit, or a tree the user just deleted\n // stays in the database for good (there is no second path that removes it).\n // `null` separates \"the user removed the last device\" (an empty array \u2014 sweep)\n // from \"there is no devices key at all\" (never configured, or a config we could\n // not read \u2014 sweep nothing, or we would delete a tree we cannot account for).\n // lib/device-config.ts is the ONE normaliser; the device manager reads the same\n // rows, so an edit can never resolve a different identity than the one advertised.\n const configured = toDeviceRows(\n this.config.devices,\n deviceTreeOf(owned.keys(), id => owned.get(id)?.type),\n );\n if (configured && (await this.persistNewIdentities(configured, owned))) {\n return;\n }\n\n // Empty AND \"0.0.0.0\" both mean \"auto\": bind all interfaces, answer every network with the\n // host's own address in it. js-controller never rewrites an existing native default, so\n // instances from before 0.5.1 still carry \"\" \u2014 both must take the auto path.\n const configuredIp = this.config.bind;\n this.bindIp = configuredIp && configuredIp !== \"0.0.0.0\" ? configuredIp : undefined;\n\n // A chosen interface is the user's decision: everything stays in its network. An address no\n // interface carries cannot be served \u2014 and serving on another one would leave the network\n // the user chose. It is waited for briefly (a host whose network comes up after ioBroker),\n // then reported and nothing starts.\n if (this.bindIp && !(await this.awaitBindAddress(this.bindIp))) {\n if (this.stopping) {\n return;\n }\n this.log.error(\n `The network interface address ${this.bindIp} does not exist on this host \u2014 choose another network interface in the instance settings.`,\n );\n await this.sweepOrphans(configured);\n return;\n }\n if (this.stopping) {\n return;\n }\n\n if (!configured || configured.length === 0) {\n this.log.warn(\"No emulated Roku devices configured.\");\n await this.sweepOrphans(configured);\n return;\n }\n\n this.expectedDevices = configured.length;\n await this.startDevices(configured);\n if (this.stopping) {\n return;\n }\n await this.sweepOrphans(configured);\n if (this.stopping) {\n return;\n }\n\n if (this.running.length === 0 && this.pending.length === 0) {\n // Nothing is controllable and nothing is worth retrying \u2014 leave info.connection false.\n this.log.error(\"No emulated Roku device could be started \u2014 check the configured names for conflicts.\");\n return;\n }\n\n const advertiseIp = this.bindIp ?? detectPrimaryIPv4();\n if (this.running.length > 0) {\n if (advertiseIp) {\n this.startDiscovery(advertiseIp);\n } else {\n // The ECP servers listen on every interface and need no address; only discovery has\n // nothing to announce yet. It starts as soon as the host has an address.\n this.log.warn(\n \"No routable IPv4 address yet \u2014 the emulated Rokus are listening; discovery starts as soon as the host has an address.\",\n );\n this.scheduleDiscoveryStart();\n }\n }\n await this.reportConnectionState(advertiseIp);\n this.scheduleDeviceRetry();\n } catch (e) {\n this.log.error(`onReady failed: ${errText(e)}`);\n }\n }\n\n /**\n * Wait until an interface carries the chosen address \u2014 at most {@link BIND_WAIT_MAX_MS}, looking\n * every {@link BIND_WAIT_STEP_MS}. Returns at once when the address is there (the normal case).\n *\n * @param address the chosen interface address\n * @returns true once an interface carries it; false when it did not appear or the host said stop\n */\n private async awaitBindAddress(address: string): Promise<boolean> {\n for (let waited = 0; ; waited += BIND_WAIT_STEP_MS) {\n if (hasLocalAddress(address, detectLocalNets())) {\n return true;\n }\n if (this.stopping || waited >= BIND_WAIT_MAX_MS) {\n return false;\n }\n if (waited === 0) {\n this.log.info(`Waiting for the network interface address ${address} to come up \u2026`);\n }\n await new Promise<void>(resolve => {\n // The managed timer refuses during shutdown and hands back nothing \u2014 then there is\n // nothing to wait for.\n if (!this.setTimeout(resolve, BIND_WAIT_STEP_MS)) {\n resolve();\n }\n });\n }\n }\n\n /**\n * Look for an address to start discovery with, once a minute, until one is there (\"all\n * interfaces\" with no routable IPv4 at start-up: the ECP servers already listen).\n */\n private scheduleDiscoveryStart(): void {\n const timer = this.setTimeout(() => {\n this.discoveryTimer = undefined;\n void this.tryStartDiscovery();\n }, DISCOVERY_RETRY_INTERVAL_MS);\n if (timer) {\n this.discoveryTimer = timer;\n }\n }\n\n /** One attempt of {@link scheduleDiscoveryStart}; dropped with `void`, so it catches its own failures. */\n private async tryStartDiscovery(): Promise<void> {\n try {\n if (this.stopping || this.ssdp) {\n return;\n }\n const advertiseIp = detectPrimaryIPv4();\n if (!advertiseIp) {\n this.scheduleDiscoveryStart();\n return;\n }\n this.startDiscovery(advertiseIp);\n await this.reportConnectionState(advertiseIp);\n } catch (e) {\n this.log.warn(`Starting discovery failed: ${errText(e)}`);\n }\n }\n\n /**\n * The trust boundary of both services: a client in one of the host's own networks \u2014 with a\n * chosen interface only in that interface's network. Read fresh on every question, so an address\n * change is followed instead of frozen at start-up.\n *\n * @param address the client address\n * @returns true if the client may be answered\n */\n private readonly isOwnClient = (address: string | undefined): boolean =>\n isLanClient(address, () => {\n const nets = detectLocalNets();\n return this.bindIp ? netsOfInterface(this.bindIp, nets) : nets;\n });\n\n /**\n * Create the object tree and start the ECP server for every configured device.\n *\n * Each device is isolated: a busy port, a reserved name or a colliding object id takes\n * down that device only. A device whose objects exist but whose server did not start goes\n * into {@link pending} and is retried \u2014 that case is a restart race far more often than a\n * real conflict, and without the retry the user has to restart the instance by hand.\n *\n * @param configured the normalised device rows\n */\n private async startDevices(configured: readonly DeviceRow[]): Promise<void> {\n const seenIds = new Set<string>();\n for (const row of configured) {\n // The host said stop: nothing more may start, and nothing may land in the queue onUnload\n // just emptied.\n if (this.stopping) {\n return;\n }\n const deviceId = deviceObjectId(row);\n // Two configured names can sanitize to the same object id \u2014 the admin guards\n // against it, but a hand-edited config could still carry it. Skip the duplicate\n // instead of letting two devices fight over one object tree.\n if (seenIds.has(deviceId)) {\n this.log.warn(`Emulated Roku \"${row.name}\" maps to an object id already in use (${deviceId}) \u2014 skipping it.`);\n continue;\n }\n // The dialog refuses these names, but native.devices is hand-editable. A\n // device called \"info\" would rewrite the adapter's own info channel into a\n // device object and hang its command/keys states under info.connection.\n if (RESERVED_IDS.has(deviceId)) {\n this.log.warn(\n `Emulated Roku \"${row.name}\" maps to the object id \"${deviceId}\", which the adapter reserves for its own status \u2014 skipping it.`,\n );\n continue;\n }\n seenIds.add(deviceId);\n if (row.identityReplaced) {\n this.log.warn(`Emulated Roku \"${row.name}\" has an unusable device id in its config \u2014 using a derived one.`);\n }\n if (row.portReplaced) {\n this.log.warn(`Emulated Roku \"${row.name}\" has an unusable ECP port in its config \u2014 using ${row.port}.`);\n }\n const keys = keysForType(row.type);\n try {\n await this.createDeviceStates(deviceId, row.name, keys);\n } catch (e) {\n this.log.warn(`Emulated Roku \"${row.name}\" could not be created: ${errText(e)} \u2014 skipping it.`);\n continue;\n }\n this.deviceKeys.set(deviceId, new Set(keys));\n this.deviceNames.set(deviceId, row.name);\n const device: PendingDevice = {\n deviceId,\n friendlyName: row.name,\n advert: { uuid: row.identity, port: row.port },\n deviceType: row.type,\n };\n if (!(await this.startDeviceServer(device, \"start\")) && !this.stopping) {\n this.pending.push(device);\n }\n }\n }\n\n /**\n * Start one device's ECP server and register it as running.\n *\n * @param device the device to start\n * @param phase whether this is the initial start (warn) or a retry (debug \u2014 the warning was written once)\n * @returns true if the server is listening\n */\n private async startDeviceServer(device: PendingDevice, phase: \"start\" | \"retry\"): Promise<boolean> {\n // The host said stop while this device's objects were being created: bind nothing.\n if (this.stopping) {\n return false;\n }\n let server: EcpHttpServer | undefined;\n try {\n server = this.makeEcpServer({\n device: device.advert,\n friendlyName: device.friendlyName,\n apps: DEFAULT_APPS,\n deviceType: device.deviceType,\n bindIp: this.bindIp,\n logger: this.log,\n onCommand: cmd => this.applyCommand(device.deviceId, cmd),\n onFatalError: () => this.onEcpFatal(device),\n isClientAllowed: this.isOwnClient,\n });\n await server.start();\n // The host may have said stop while we were binding. Registering now would put the\n // server into a map onUnload has already emptied \u2014 nothing would ever close it.\n if (this.stopping) {\n server.stop();\n return false;\n }\n this.ecpServers.set(device.deviceId, server);\n this.running.push(device.advert);\n this.ssdp?.addDevice(device.advert);\n if (phase === \"retry\") {\n this.log.info(`Emulated Roku \"${device.friendlyName}\" is listening on port ${device.advert.port} again.`);\n }\n return true;\n } catch (e) {\n // One device's failure (a busy ECP port) must not take the others down.\n // Close whatever the failed start left behind, so nothing outlives this turn.\n server?.stop();\n const detail = `${errText(e)} \u2014 retrying every ${DEVICE_RETRY_INTERVAL_MS / 1000} s`;\n if (phase === \"start\") {\n this.log.warn(\n `Emulated Roku \"${device.friendlyName}\" could not start on port ${device.advert.port}: ${detail}`,\n );\n } else {\n this.log.debug(\n `Emulated Roku \"${device.friendlyName}\" still cannot start on port ${device.advert.port}: ${detail}`,\n );\n }\n return false;\n }\n }\n\n /** Arm the retry timer while any device is still waiting for its port; a no-op otherwise. */\n private scheduleDeviceRetry(): void {\n if (this.retryTimer || this.pending.length === 0) {\n return;\n }\n const timer = this.setTimeout(() => {\n this.retryTimer = undefined;\n void this.retryPendingDevices();\n }, DEVICE_RETRY_INTERVAL_MS);\n if (timer) {\n this.retryTimer = timer;\n }\n }\n\n /**\n * Try the devices that could not start yet. A port taken at boot is usually a restart\n * race \u2014 the previous process still holds it \u2014 so this recovers on its own instead of\n * leaving a dead device behind a red instance until someone restarts by hand.\n */\n private async retryPendingDevices(): Promise<void> {\n // The timer drops this call with `void`, so nothing may escape it: a rejection here \u2014 the\n // status write failing while the states database hiccups \u2014 would be an unhandled rejection,\n // and js-controller ends the instance over it.\n try {\n // No stopping check at the entry: onUnload sets the flag and empties the queue in the\n // same synchronous block, so a call that arrives afterwards finds nothing to do, and a\n // call already inside the loop is caught after the bind and again at the tail. A guard\n // here could never fire \u2014 measured, it survived its own mutation needle.\n const stillPending: PendingDevice[] = [];\n let recovered = false;\n for (const device of this.pending) {\n if (await this.startDeviceServer(device, \"retry\")) {\n recovered = true;\n } else {\n stillPending.push(device);\n }\n }\n if (this.stopping) {\n // The host said stop while we were binding. Assigning the list back would re-fill the\n // queue onUnload just emptied, and scheduleDeviceRetry would then arm a new timer \u2014\n // which js-controller refuses during shutdown, with a warning nobody can explain.\n return;\n }\n this.pending = stillPending;\n if (recovered) {\n // Discovery may never have started (every device failed at boot) \u2014 bring it up now.\n const advertiseIp = this.bindIp ?? detectPrimaryIPv4();\n if (advertiseIp && !this.ssdp) {\n this.startDiscovery(advertiseIp);\n }\n await this.reportConnectionState(advertiseIp);\n }\n this.scheduleDeviceRetry();\n } catch (e) {\n this.log.warn(`Retrying the waiting emulated Rokus failed: ${errText(e)}`);\n // No stopping check: onUnload empties the queue, and scheduleDeviceRetry arms nothing for\n // an empty one \u2014 measured, a guard here survived its own mutation needle.\n this.scheduleDeviceRetry();\n }\n }\n\n /**\n * Start the SSDP responder for the devices that are listening.\n *\n * Discovery is only an aid; the ECP servers already make the adapter controllable, so a\n * busy port 1900 (or a stuck bind) degrades to \"discovery off, already-paired remotes\n * still work\" instead of failing the whole start-up. In the auto case join every real\n * interface and answer each network with the host's own address in it, so a multi-homed\n * host is discoverable \u2014 and reachable \u2014 on all its LANs; a chosen interface pins\n * membership, NOTIFY egress and every answer to itself.\n *\n * @param advertiseIp the routable IP to announce\n */\n private startDiscovery(advertiseIp: string): void {\n const bindIp = this.bindIp;\n const membershipInterfaces = bindIp\n ? [{ iface: detectLocalNets().find(net => net.address === bindIp)?.iface ?? bindIp, address: bindIp }]\n : detectLocalIPv4s();\n const ssdp = this.makeSsdpResponder({\n devices: [...this.running],\n bindIp,\n advertiseIp,\n membershipInterfaces,\n logger: this.log,\n isClientAllowed: this.isOwnClient,\n // \"All interfaces\": every search is answered with the host's address in the searcher's own\n // network. A chosen interface answers with its address only (the responder uses bindIp).\n advertiseFor: bindIp ? undefined : remote => localAddressFor(remote, detectLocalNets()),\n onFatalError: () => this.onSsdpFatal(),\n });\n this.ssdp = ssdp;\n this.startWithTimeout(ssdp.start(), SSDP_START_TIMEOUT_MS).then(\n () => {\n // The bind resolved after the host asked us to stop: announcing now would put a\n // device back into the network we just said goodbye to, and this.setInterval would\n // refuse with \"setInterval called, but adapter is shutting down\".\n if (this.stopping) {\n ssdp.stop();\n return;\n }\n ssdp.announce();\n const timer = this.setInterval(() => this.announceTick(), SSDP_NOTIFY_INTERVAL_MS);\n if (timer) {\n this.notifyTimer = timer;\n }\n },\n (e: unknown) => {\n this.log.warn(\n `SSDP discovery unavailable: ${errText(e)} \u2014 already-paired remotes still work; set the network interface if devices are not found.`,\n );\n // A start that only timed out can still bind later. Close it, or the socket\n // outlives the reference dropped here and keeps answering with nobody to stop it.\n ssdp.stop();\n if (this.ssdp === ssdp) {\n this.ssdp = undefined;\n }\n },\n );\n }\n\n /**\n * One NOTIFY pass: follow the host first, then announce.\n *\n * Only in the automatic case \u2014 a chosen interface is the user's decision and does not move under\n * us. Every pass hands the responder the interfaces the host has NOW: one that came up since the\n * start (a second network card, a VLAN) is joined and announced on, one that went away is\n * dropped, and a changed fallback address (a DHCP lease change) is logged once. Every five\n * minutes is early enough: a controller caches the announced address for its max-age of an hour.\n */\n private announceTick(): void {\n if (!this.bindIp) {\n const current = detectPrimaryIPv4();\n if (current && this.ssdp?.refreshAdvertise(current, detectLocalIPv4s())) {\n this.log.info(`Host address changed \u2014 the emulated Rokus are now advertised on ${current}.`);\n }\n }\n this.ssdp?.announce();\n }\n\n /**\n * Write info.connection and say in the log what the instance is doing.\n *\n * \"Connected\" means EVERY configured Roku is listening, not just some of them. A device\n * whose port is taken (or whose name collides) is skipped with a warning naming it;\n * reporting \"connected\" anyway would hide a broken configuration behind the devices that\n * did come up, and the user would only find out when the remote stops working. Config rows\n * without a usable name are filtered out before this point and deliberately do not count \u2014\n * every device that fails has said so in the log.\n *\n * @param advertiseIp the announced IP, for the log line\n */\n private async reportConnectionState(advertiseIp: string): Promise<void> {\n const allStarted = this.running.length === this.expectedDevices;\n await this.setState(\"info.connection\", { val: allStarted, ack: true });\n // The retry path can hand in an empty address (detectPrimaryIPv4 found nothing), and\n // \"advertising on (discovery off)\" reads like a truncated line rather than a finding.\n const where = `advertising on ${advertiseIp || \"no routable IPv4\"}${this.ssdp ? \"\" : \" (discovery off)\"}`;\n if (allStarted) {\n this.log.info(`Emulating ${this.running.length} Roku device(s), ${where}`);\n } else {\n this.log.error(\n `Only ${this.running.length} of ${this.expectedDevices} configured Roku device(s) could be started, ${where} \u2014 fix the cause reported above; the instance stays disconnected until every device runs.`,\n );\n }\n }\n\n /**\n * Re-apply the adapter's OWN objects \u2014 the `info` channel and `info.connection`\n * \u2014 on every start.\n *\n * js-controller extends the manifest's instanceObjects on every start, but it preserves\n * `common.name` of an existing object (7.2.2 `_extendObjects`, `preserve: { common: [\"name\"] }`):\n * a changed description or role arrives by itself, a changed NAME only on a fresh install.\n * extendObject is what carries the name into an existing tree, so an update always lands on\n * every datapoint, not just on fresh installs.\n *\n * Only name and description: the manifest owns the rest of the shape, and js-controller writes\n * it back on every start \u2014 the object type included (`extend(true, old, manifest)`), which is\n * also what repairs an `info` channel a hand-edited device row named \"info\" once turned into a\n * device object (see the reserved-id guard in startDevices).\n */\n private async refreshOwnObjects(): Promise<void> {\n await Promise.all([\n this.extendObject(\"info\", { common: { name: tName(\"channelInfo\") } }),\n this.extendObject(\"info.connection\", {\n common: { name: tName(\"connectionStatus\"), desc: tDesc(\"connectionStatusDesc\") },\n }),\n ]);\n }\n\n /**\n * Create the fixed object tree for one emulated Roku: the device, `command` +\n * `commandType`, and one `sensor` boolean state per key the device type exposes \u2014\n * all up front, so the tree is usable before any key is ever pressed.\n *\n * Every key state is also RESET to false here. A key is a momentary signal, but\n * nothing writes its release when the adapter goes down: a keypress pulses true\n * and schedules the false 50 ms later, a keydown holds true until its keyup, and\n * onUnload drops both timers without writing. So a stop inside that window \u2014 or\n * a crash, or a controller that never sent its keyup \u2014 leaves the key true in\n * the database for good, and a rule watching for the next press never sees an\n * edge again. The reset belongs on STARTUP, not into onUnload: only startup also\n * covers the crash, and up to 31 writes per device would eat the shutdown budget that\n * today comfortably carries a single one. setStateChanged writes only where the\n * value actually differs, so a healthy tree costs nothing.\n *\n * The writes go out together: they address different objects, and doing 20 (a player)\n * or 35 (a TV) of them strictly one after another made start-up wait for one round trip\n * per datapoint \u2014 the key reset right below has always been parallel.\n *\n * @param deviceId the id-safe device path segment\n * @param friendlyName the configured device name\n * @param keys the key names to create for this device (from its type)\n */\n private async createDeviceStates(deviceId: string, friendlyName: string, keys: readonly string[]): Promise<void> {\n await Promise.all([\n // The device name is the user's own text \u2014 nothing to translate, but it must\n // still BE a translation object like every other common.name (tRaw).\n this.extendObject(deviceId, { type: \"device\", common: { name: tRaw(friendlyName) }, native: {} }),\n this.extendObject(`${deviceId}.command`, {\n type: \"state\",\n common: {\n name: tName(\"stateLastCommand\"),\n desc: tDesc(\"stateLastCommandDesc\"),\n type: \"string\",\n role: \"text\",\n read: true,\n write: false,\n def: \"\",\n },\n native: {},\n }),\n this.extendObject(`${deviceId}.commandType`, {\n type: \"state\",\n common: {\n name: tName(\"stateLastCommandType\"),\n desc: tDesc(\"stateLastCommandTypeDesc\"),\n type: \"string\",\n role: \"text\",\n read: true,\n write: false,\n def: \"\",\n // The fixed verb list, so the admin shows the value as a label. Plain strings\n // only \u2014 a translation object here crashes the admin's object view.\n states: Object.fromEntries(COMMAND_TYPES.map(verb => [verb, verb])),\n },\n native: {},\n }),\n this.extendObject(`${deviceId}.keys`, {\n type: \"channel\",\n common: { name: tName(\"channelKeys\"), desc: tDesc(\"channelKeysDesc\") },\n native: {},\n }),\n ...keys.map(key =>\n this.extendObject(`${deviceId}.keys.${key}`, {\n type: \"state\",\n // \"sensor\" = generic boolean read-only (active/inactive). The docs suggest\n // button.press for a keypress-as-state, but the repochecker requires button.press to\n // be read:false, write:true (config_StateRoles) \u2014 a key state is read-only, so\n // button.press fails with E1010; sensor is the gate-conformant fit.\n // The name is the ECP key identifier and identical in every language, but\n // it still has to BE a translation object (tRaw), never a bare string.\n // No desc: the key name already says everything there is to say. The\n // decision itself is recorded in test/self-explaining.json, where the\n // object-inventory gate reads it \u2014 silence has to be a decision, not a gap.\n common: { name: tRaw(key), type: \"boolean\", role: \"sensor\", read: true, write: false, def: false },\n native: {},\n }),\n ),\n ]);\n await Promise.all(keys.map(key => this.setStateChangedAsync(`${deviceId}.keys.${key}`, { val: false, ack: true })));\n }\n\n /**\n * Run the orphan sweep for the current configuration \u2014 from EVERY exit of\n * onReady, which is the point of it existing as its own method.\n *\n * The sweep used to sit on the happy path only, so removing the last emulated\n * Roku (or a host without a routable address) left its device object, its\n * `command` / `commandType` and its 16\u201331 key states behind with nothing that\n * would ever remove them: the user deletes a device in the admin and keeps its\n * datapoints forever. The adapter answers for its own datapoints\n * (`feedback_adapter_verantwortet_datenpunkte`), so the sweep runs whenever the\n * configuration could be read.\n *\n * `configured === null` is that condition, and it is not pedantry: with no `devices`\n * key at all (a never-configured instance, or a config we could not read) an\n * empty set would mean \"nothing is configured, delete everything\" \u2014 trading a\n * tree that stays for a tree that is gone.\n *\n * @param configured the configured rows, or null when there is no readable list\n */\n private async sweepOrphans(configured: readonly DeviceRow[] | null): Promise<void> {\n if (!configured) {\n return;\n }\n await this.cleanupOrphans(new Set(configured.map(row => deviceObjectId(row))));\n }\n\n /**\n * The adapter's objects, keyed relative to the namespace.\n *\n * @returns the object dump\n */\n private async readOwnObjects(): Promise<Map<string, ioBroker.Object>> {\n const objects = await this.getAdapterObjectsAsync();\n const prefix = `${this.namespace}.`;\n const owned = new Map<string, ioBroker.Object>();\n for (const [id, obj] of Object.entries(objects)) {\n if (id.startsWith(prefix)) {\n owned.set(id.slice(prefix.length), obj);\n }\n }\n return owned;\n }\n\n /**\n * Give every device that has never been announced its own identity and fix its object id \u2014\n * on its very first start, before anything announces it.\n *\n * \"Never announced\" is a row without a stored `uuid` whose object tree does not exist yet: the\n * manifest's default device of a fresh instance, or a hand-written row. Its identity would\n * otherwise be derived from its name, and every installation \u2014 every instance on one host too \u2014\n * announcing a device called \"Roku\" would share one USN. A row whose tree exists has been\n * announced, maybe paired, and keeps the identity it has.\n *\n * Writing `native` restarts the instance, so the start ends here when something was written.\n *\n * @param rows the configured rows\n * @param owned the object tree as it is\n * @returns true when the config was written \u2014 the caller aborts its start\n */\n private async persistNewIdentities(\n rows: readonly DeviceRow[],\n owned: ReadonlyMap<string, ioBroker.Object>,\n ): Promise<boolean> {\n const fresh = rows.filter(row => row.identityDerived && !owned.has(row.objectId));\n if (fresh.length === 0) {\n return false;\n }\n const devices = rows.map(row => ({\n name: row.storedName,\n port: row.port,\n type: row.type,\n uuid: fresh.includes(row) ? randomIdentity() : row.identity,\n objectId: row.objectId,\n }));\n try {\n await this.extendForeignObjectAsync(`system.adapter.${this.namespace}`, { native: { devices } });\n } catch (e) {\n // Not fatal: the device starts with the identity derived from its name, as before.\n this.log.warn(`New emulated Roku could not get its own identity (${errText(e)}) \u2014 trying again next start`);\n return false;\n }\n this.log.info(\n `New emulated Roku ${fresh.map(row => `\"${row.name}\"`).join(\", \")} got its own network identity \u2014 the instance restarts once`,\n );\n return true;\n }\n\n /**\n * Remove objects left over from an earlier version or config \u2014 the legacy\n * `apps` node, keys no longer standard, and whole device sub-trees no longer\n * configured (a renamed/removed device). The adapter otherwise only ever\n * creates objects, so without this the tree would accrete stale entries.\n *\n * The same pass also strips attributes an older version wrote into a state's `native`\n * and this one does not (see {@link pruneStateNative}) \u2014 the object dump is already in\n * hand here, so recognising them costs nothing.\n *\n * @param configuredDeviceIds the id-safe names of the currently configured devices\n */\n private async cleanupOrphans(configuredDeviceIds: ReadonlySet<string>): Promise<void> {\n // A dump read NOW, after this start created and renamed its objects: the native repair\n // below writes an object back whole, and a dump from before the start would write the\n // names and descriptions of the previous version back over the ones just set.\n const owned = await this.readOwnObjects();\n const toDelete = planObjectCleanup([...owned.keys()], configuredDeviceIds, this.deviceKeys);\n for (const id of toDelete) {\n await this.delObjectAsync(id, { recursive: true }).catch((e: unknown) => {\n this.log.debug(`cleanup: could not delete ${id}: ${errText(e)}`);\n });\n }\n if (toDelete.length > 0) {\n this.log.debug(`Removed ${toDelete.length} orphaned object(s) from an earlier version or config.`);\n }\n await this.pruneStateNative(owned, new Set(toDelete));\n }\n\n /**\n * Strip `native` attributes an earlier version wrote and this one no longer does.\n *\n * The pre-0.5.0 adapter created every key state with `native: { url: \"keys/Home\" }`.\n * `extendObject` cannot remove that: it merges, so an attribute only the stored object\n * carries survives every further update \u2014 and writing `null` would not delete it either,\n * it would store `null`. So an installation upgraded from <= 0.4.0 carries a dead\n * attribute on every key datapoint, and the adapter answers for its own datapoints.\n *\n * Removing one therefore rewrites the object in a single `setForeignObject`, handing back\n * exactly what was read with `native` emptied \u2014 `common` included, because that is where\n * `common.custom` lives, the user's own history/chart configuration.\n *\n * It used to be a `delObject` followed by an `extendObject`. That pair replaces an object,\n * but it is the wrong tool here: `delObject` on a state ALSO deletes the state's value and\n * removes its id from every enum it belongs to. Repairing a dead attribute that way cost\n * the user the recorded value and the room the datapoint was sorted into \u2014 and the\n * re-created object came back carrying `common.def` as its value, which reads like data\n * rather than like a loss. `setForeignObject` touches neither. (The discouraged call is\n * `setObject`, repochecker S5054, and only because a blind full write overwrites what the\n * user changed; writing back what was just read does not.)\n *\n * The write fails safely: if it does not happen, the attribute stays where it was and the\n * tree is exactly as before. The reason is in the log.\n *\n * @param owned the adapter's objects, keyed relative to the namespace\n * @param deleted the ids the sweep just removed \u2014 no point writing to those\n */\n private async pruneStateNative(\n owned: ReadonlyMap<string, ioBroker.Object>,\n deleted: ReadonlySet<string>,\n ): Promise<void> {\n const stale = planNativePrune(owned, deleted);\n for (const [id, obj] of stale) {\n try {\n // Non-recursive: a channel or device carrying the leftover keeps its children,\n // which are read and repaired on their own turn.\n // One write, never a delete plus a re-create: from/ts/user are left out so the\n // controller stamps the write freshly \u2014 it only adopts them when they are absent.\n const { from: _from, ts: _ts, user: _user, ...keep } = obj;\n // The cast only collapses the union: ioBroker.Object spans every object kind\n // (instance, host, enum, ...), while a write accepts device/channel/state/folder.\n // The shape is the one just read back, so nothing is asserted away here.\n await this.setForeignObject(`${this.namespace}.${id}`, {\n ...keep,\n native: {},\n } as\n | ioBroker.SettableDeviceObject\n | ioBroker.SettableChannelObject\n | ioBroker.SettableFolderObject\n | ioBroker.SettableStateObject);\n } catch (e: unknown) {\n this.log.debug(`cleanup: could not rewrite ${id}: ${errText(e)}`);\n }\n }\n if (stale.length > 0) {\n this.log.debug(`Removed a stale native attribute from ${stale.length} object(s) of an earlier version.`);\n }\n }\n\n /**\n * Apply a received ECP command to this device's states: record it in `command`\n * / `commandType`, and pulse or hold the standard key if it is one.\n *\n * Returns whether the command was applied: the ECP server logs only what got\n * through, so the rate gate covers the log as well as the states database.\n *\n * @param deviceId the id-safe device path segment\n * @param cmd the parsed ECP command\n * @returns true if the command was applied, false if the rate gate dropped it\n */\n private applyCommand(deviceId: string, cmd: CommandEvent): boolean {\n const write = commandToStateWrite(cmd);\n // The release of a key that is actually HELD never passes through the rate gate.\n // Dropping a keypress costs one event; dropping the keyup of a held key leaves it true\n // until the 30 s watchdog, so the flood protection would be the thing that falsifies\n // the tree. It also costs one write instead of four, so it is not what the gate exists\n // to stop.\n // The exemption asks whether there is something to release, not what the request looks\n // like: without the watchdog check a flood of keyup requests for a key nobody is holding\n // would bypass the gate entirely - and since the server logs only what was applied, it\n // would take a log line with it on every request.\n const releaseOf = write.holdKey?.value === false ? `${deviceId}.keys.${write.holdKey.key}` : null;\n const isRelease = releaseOf !== null && this.holdTimers.has(releaseOf);\n if (!isRelease && !this.admitCommand(deviceId)) {\n return false;\n }\n this.writeState(`${deviceId}.command`, write.command);\n this.writeState(`${deviceId}.commandType`, write.commandType);\n // Only write keys.<Key> if THIS device's type carries the key \u2014 a player has no\n // TV key objects, so a stray TV keypress lands in `command` only, never a missing state.\n const keys = this.deviceKeys.get(deviceId);\n if (write.pulseKey && keys?.has(write.pulseKey)) {\n const id = `${deviceId}.keys.${write.pulseKey}`;\n // A keypress on a key that is currently HELD ends the hold: its watchdog would\n // otherwise fire later and write a release for a key the pulse already released.\n this.clearHoldTimer(id);\n this.writeState(id, true);\n const timer = this.setTimeout(() => {\n if (timer) {\n this.pulseTimers.delete(timer);\n }\n // A keydown that arrived inside the pulse window owns the key now: ECP defines a\n // keypress as pressing down AND releasing, so it is a finished act and a later\n // keydown starts a new one. Writing the pulse's release here would end a hold that\n // is still going on, while its watchdog stays armed. The keyup writes the release.\n if (!this.holdTimers.has(id)) {\n this.writeState(id, false);\n }\n }, KEY_PULSE_MS);\n if (timer) {\n this.pulseTimers.add(timer);\n }\n } else if (write.holdKey && keys?.has(write.holdKey.key)) {\n const id = `${deviceId}.keys.${write.holdKey.key}`;\n this.writeState(id, write.holdKey.value);\n // A keydown holds the key true until its keyup. Arm a watchdog so a lost keyup\n // (controller disconnects mid-press) cannot pin the key true forever; a keyup\n // clears it, and a repeated keydown re-arms it.\n this.clearHoldTimer(id);\n if (write.holdKey.value) {\n const timer = this.setTimeout(() => {\n this.holdTimers.delete(id);\n this.writeState(id, false);\n }, HOLD_MAX_MS);\n if (timer) {\n this.holdTimers.set(id, timer);\n }\n }\n }\n return true;\n }\n\n /**\n * Disarm the hold watchdog of one key, if it has one.\n *\n * @param id the full key state id\n */\n private clearHoldTimer(id: string): void {\n const pending = this.holdTimers.get(id);\n if (pending) {\n this.clearTimeout(pending);\n this.holdTimers.delete(id);\n }\n }\n\n /**\n * The rate gate in front of every state write: MAX_COMMANDS_PER_SECOND per device,\n * the excess is dropped and reported once per RATE_WARN_INTERVAL_MS. Every accepted\n * command costs the states database three writes plus one more when the pulse\n * ends \u2014 a flooding device in the LAN would otherwise slow the whole host.\n *\n * @param deviceId the id-safe device path segment\n * @returns true if the command may be applied\n */\n private admitCommand(deviceId: string): boolean {\n const now = Date.now();\n let gate = this.commandGates.get(deviceId);\n if (!gate) {\n gate = new RateGate(MAX_COMMANDS_PER_SECOND, now);\n this.commandGates.set(deviceId, gate);\n }\n if (gate.allow(now)) {\n return true;\n }\n if (now - (this.rateWarnedAt.get(deviceId) ?? 0) >= RATE_WARN_INTERVAL_MS) {\n this.rateWarnedAt.set(deviceId, now);\n this.log.warn(\n `Emulated Roku \"${this.deviceNames.get(deviceId) ?? deviceId}\" receives more than ${MAX_COMMANDS_PER_SECOND} commands per second \u2014 dropping the excess (a misbehaving controller?)`,\n );\n }\n return false;\n }\n\n /**\n * Fire-and-forget state write for the command hot path. The states exist (created\n * up front in createDeviceStates), so there is no read-before-write; a rejection \u2014\n * the states database already closed while a remote still sends \u2014 is traced at\n * debug, because an unhandled one would crash the adapter over one lost keypress.\n *\n * @param id the state id relative to the namespace\n * @param val the value to write\n */\n private writeState(id: string, val: string | boolean): void {\n this.setState(id, { val, ack: true }).catch((e: unknown) => {\n this.log.debug(`State write ${id} failed: ${errText(e)}`);\n });\n }\n\n /**\n * One emulated Roku's ECP server died at runtime (a server error after a good\n * start). That device answers nothing any more, so the instance is no longer\n * \"every configured Roku is listening\" \u2014 revise the status instead of leaving a\n * green instance behind a device that is gone, and stop announcing it, or discovery\n * would keep pointing remotes at a port nobody serves. The other devices keep\n * running, and the message names the one that failed.\n *\n * No retry here, deliberately: a port that was busy at boot is a restart race and\n * heals; a server that died while running died for a reason this code does not know,\n * and retrying it on a timer would be a log-flood generator.\n *\n * @param device the device whose server died\n */\n private onEcpFatal(device: PendingDevice): void {\n this.log.error(\n `Emulated Roku \"${device.friendlyName}\" stopped answering after a server error \u2014 restart the instance to bring it back.`,\n );\n // Close it before forgetting it: once it is out of the map neither this path nor\n // onUnload can ever reach it again, and in compact mode the port would stay taken for\n // the lifetime of the whole host process. stop() is idempotent and safe on a dead\n // server. The SSDP responder already does exactly this in its own fatal path.\n this.ecpServers.get(device.deviceId)?.stop();\n this.ecpServers.delete(device.deviceId);\n const at = this.running.indexOf(device.advert);\n if (at >= 0) {\n this.running.splice(at, 1);\n }\n this.ssdp?.removeDevice(device.advert.uuid);\n this.setState(\"info.connection\", { val: false, ack: true }).catch((e: unknown) => {\n this.log.debug(`Connection state write failed: ${errText(e)}`);\n });\n }\n\n /**\n * The SSDP responder died at runtime (a socket error after a good start). Stop\n * announcing into the dead socket and drop the discovery aid. The ECP servers\n * keep working, so info.connection \u2014 which reflects ECP readiness \u2014 stays true.\n */\n private onSsdpFatal(): void {\n if (this.notifyTimer) {\n this.clearInterval(this.notifyTimer);\n this.notifyTimer = undefined;\n }\n this.ssdp = undefined;\n this.log.warn(\n \"SSDP discovery stopped after a socket error \u2014 already-paired remotes still work; restart the instance to re-enable discovery.\",\n );\n }\n\n /**\n * Bound await: reject if the SSDP start doesn't settle in time, so a stuck\n * port-1900 bind degrades to \"discovery off\" instead of hanging the adapter.\n *\n * @param promise the SSDP start promise\n * @param ms the timeout in milliseconds\n * @returns a promise that settles with the start result or a timeout error\n */\n private startWithTimeout(promise: Promise<void>, ms: number): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const timer = this.setTimeout(() => reject(new Error(`SSDP start timed out after ${ms} ms`)), ms);\n const clear = (): void => {\n if (timer) {\n this.clearTimeout(timer);\n }\n };\n promise.then(\n () => {\n clear();\n resolve();\n },\n (e: unknown) => {\n clear();\n // Lint (prefer-promise-reject-errors) wants an Error here; errText downstream\n // would cope with anything, so this is the rule's shape, not a second safeguard.\n reject(e instanceof Error ? e : new Error(errText(e)));\n },\n );\n });\n }\n\n /**\n * Teardown: drop the timers and sockets synchronously, then report done only\n * once the farewell and the last write have landed.\n *\n * `info.connection` is the only status this adapter carries, and nothing else\n * resets it: the host means to, but writes its reset to the namespace root\n * instead of the datapoint (js-controller#3472). So if the final write is\n * lost, the instance shows \"connected\" while the adapter is off.\n *\n * A fire-and-forget write plus an immediate callback is a race, not a\n * guaranteed loss \u2014 measured on 1.1.0, it still arrived, because without\n * `common.supportedMessages.stopInstance` the process ends in an orderly way\n * and flushes what is pending. Waiting closes the race for the slow or busy\n * case, and it is safe for the same reason: no `stopInstance` means the host\n * grants the full `common.stopTimeout` instead of killing the process.\n *\n * The farewell (`ssdp:byebye`) rides in the same wait: without it a controller keeps the\n * emulated Roku in its list for up to the announced hour and sends key presses into a\n * port nobody serves.\n *\n * @param callback function to invoke once teardown is complete\n */\n private onUnload(callback: () => void): void {\n try {\n // First of all, before any teardown: whatever is still in flight must not start or\n // write anything from here on.\n this.stopping = true;\n if (this.notifyTimer) {\n this.clearInterval(this.notifyTimer);\n this.notifyTimer = undefined;\n }\n if (this.retryTimer) {\n this.clearTimeout(this.retryTimer);\n this.retryTimer = undefined;\n }\n if (this.discoveryTimer) {\n this.clearTimeout(this.discoveryTimer);\n this.discoveryTimer = undefined;\n }\n for (const t of this.pulseTimers) {\n this.clearTimeout(t);\n }\n this.pulseTimers.clear();\n for (const t of this.holdTimers.values()) {\n this.clearTimeout(t);\n }\n this.holdTimers.clear();\n // The ECP servers close right here, synchronously: nothing about the farewell needs\n // them, and a keep-alive connection left open drags teardown toward a SIGKILL.\n for (const s of this.ecpServers.values()) {\n s.stop();\n }\n this.ecpServers.clear();\n this.deviceKeys.clear();\n this.deviceNames.clear();\n this.commandGates.clear();\n this.rateWarnedAt.clear();\n this.running.length = 0;\n this.pending = [];\n // The SSDP socket is the one thing that cannot close yet \u2014 the farewell goes out\n // through it. It is closed in the callback below, whichever way that arrives.\n const farewell = this.ssdp ? this.ssdp.byebye() : Promise.resolve();\n const connection = this.setState(\"info.connection\", { val: false, ack: true })\n // A rejected write must not become an unhandled rejection \u2014 that is a\n // crash (exit code 6) instead of an orderly stop. The trace stays at\n // debug: it explains a stale \"connected\" afterwards, and nobody can act\n // on it while the adapter is already going down.\n .catch((e: unknown) => {\n this.log.debug(`Final connection write failed: ${errText(e)}`);\n });\n void Promise.all([farewell, connection]).finally(() => {\n this.ssdp?.stop();\n this.ssdp = undefined;\n callback();\n });\n } catch {\n callback();\n }\n }\n}\n\nif (require.main !== module) {\n // Export the constructor in compact mode\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) => new Fakeroku(options);\n} else {\n // Start the instance directly\n (() => new Fakeroku())();\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["_a"]
|
|
7
7
|
}
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "fakeroku",
|
|
4
|
-
"version": "1.8.
|
|
4
|
+
"version": "1.8.1",
|
|
5
5
|
"news": {
|
|
6
|
+
"1.8.1": {
|
|
7
|
+
"en": "The note the Admin shows before an update from 0.x is short now: the apps folder is removed, app launches arrive as launch:<id> in command.",
|
|
8
|
+
"de": "Der Hinweis, den der Admin vor einem Update von 0.x zeigt, ist jetzt kurz: der Ordner apps fällt weg, App-Starts kommen als launch:<id> in command.",
|
|
9
|
+
"ru": "Уведомление, которое Admin показывает перед обновлением с 0.x, теперь короткое: папка apps удалена, запуск приложений приходит как launch:<id> в command.",
|
|
10
|
+
"pt": "A nota que o Admin mostra antes de atualizar a partir da 0.x agora é curta: a pasta apps foi removida, os inícios de apps chegam como launch:<id> em command.",
|
|
11
|
+
"nl": "De melding die de Admin vóór een update vanaf 0.x toont, is nu kort: de map apps vervalt, app-starts komen binnen als launch:<id> in command.",
|
|
12
|
+
"fr": "La note affichée par l'Admin avant une mise à jour depuis 0.x est courte : le dossier apps est supprimé, les lancements d'applis arrivent en launch:<id> dans command.",
|
|
13
|
+
"it": "La nota che l'Admin mostra prima di aggiornare dalla 0.x ora è breve: la cartella apps viene rimossa, gli avvii delle app arrivano come launch:<id> in command.",
|
|
14
|
+
"es": "El aviso que muestra el Admin antes de actualizar desde 0.x ahora es breve: se elimina la carpeta apps, los inicios de apps llegan como launch:<id> en command.",
|
|
15
|
+
"pl": "Komunikat, który Admin pokazuje przed aktualizacją z 0.x, jest teraz krótki: folder apps zostaje usunięty, uruchomienia aplikacji przychodzą jako launch:<id> w command.",
|
|
16
|
+
"uk": "Повідомлення, яке Admin показує перед оновленням з 0.x, тепер коротке: папку apps видалено, запуски застосунків надходять як launch:<id> у command.",
|
|
17
|
+
"zh-cn": "Admin 在从 0.x 升级前显示的提示现在很简短:apps 文件夹已移除,应用启动以 launch:<id> 形式出现在 command 中。"
|
|
18
|
+
},
|
|
6
19
|
"1.8.0": {
|
|
7
20
|
"en": "Home Assistant and openHAB can set up the emulated Roku again, keys in any upper or lower case work, and spaces typed in Home Assistant arrive as spaces\nAn upgrade from the old adapter keeps every object tree with its rooms and history, and renaming a device no longer moves its datapoints\nOnly devices in the host's own networks are answered; a chosen network interface keeps everything in its network and is never swapped for another\nAn instance started before the network is up starts its devices and adds discovery later; the device dialog greys out OK for a taken name or port\nNew: the TV profile adds power on, power, sleep and tuner input keys; new devices get their own network identity",
|
|
8
21
|
"de": "Home Assistant und openHAB können den emulierten Roku wieder einrichten, Tasten in beliebiger Groß- und Kleinschreibung wirken, Leerzeichen aus Home Assistant kommen an\nEin Umstieg vom alten Adapter behält jeden Objektbaum samt Räumen und History, und Umbenennen verschiebt die Datenpunkte eines Geräts nicht mehr\nBeantwortet werden nur Geräte in den eigenen Netzen des Hosts; eine gewählte Netzwerkschnittstelle hält alles in ihrem Netz und wird nie ersetzt\nStartet die Instanz vor dem Netzwerk, laufen die Geräte trotzdem und die Erkennung folgt später; der Gerätedialog sperrt OK bei belegtem Namen oder Port\nNeu: das TV-Profil bringt die Tasten Einschalten, Ein/Aus, Sleep und Tuner-Eingang; neue Geräte erhalten eine eigene Netzwerk-Identität",
|
|
@@ -80,19 +93,6 @@
|
|
|
80
93
|
"pl": "Usunięcie ostatniego emulowanego Roku usuwa jego punkty danych, host z Dockerem nie ogłasza już nieosiągalnego adresu, a awaria jest zgłaszana.",
|
|
81
94
|
"uk": "Видалення останнього емульованого Roku прибирає його точки даних, хост із Docker не оголошує недосяжну адресу, а про відмову повідомляється.",
|
|
82
95
|
"zh-cn": "删除最后一个模拟 Roku 会一并移除其数据点,运行 Docker 的主机不再公告无法访问的地址,设备失效会被报告。"
|
|
83
|
-
},
|
|
84
|
-
"1.4.0": {
|
|
85
|
-
"en": "Renaming a Roku keeps the remote paired, a stuck key is released at start-up, all datapoints get translated names, and IPv6 remotes from your network are accepted.",
|
|
86
|
-
"de": "Umbenennen behält die Kopplung der Fernbedienung, eine hängende Taste wird beim Start freigegeben, alle Datenpunkte bekommen übersetzte Namen, IPv6-Fernbedienungen werden akzeptiert.",
|
|
87
|
-
"ru": "Переименование сохраняет сопряжение пульта, зависшая кнопка сбрасывается при старте, все точки данных получают переведённые названия, IPv6-пульты принимаются.",
|
|
88
|
-
"pt": "Mudar o nome mantém o emparelhamento, uma tecla presa é libertada no arranque, todos os pontos de dados recebem nomes traduzidos, comandos IPv6 são aceites.",
|
|
89
|
-
"nl": "Hernoemen behoudt de koppeling, een blijvende toets wordt bij de start vrijgegeven, alle datapunten krijgen vertaalde namen, IPv6-afstandsbedieningen worden geaccepteerd.",
|
|
90
|
-
"fr": "Renommer conserve l'appairage, une touche bloquée est libérée au démarrage, tous les points de données ont des noms traduits, les télécommandes IPv6 sont acceptées.",
|
|
91
|
-
"it": "Rinominare mantiene l'associazione, un tasto bloccato viene rilasciato all'avvio, tutti i punti dati hanno nomi tradotti, i telecomandi IPv6 sono accettati.",
|
|
92
|
-
"es": "Renombrar mantiene el emparejamiento, una tecla atascada se libera al iniciar, todos los puntos de datos tienen nombres traducidos, se aceptan mandos IPv6.",
|
|
93
|
-
"pl": "Zmiana nazwy zachowuje parowanie, zablokowany klawisz jest zwalniany przy starcie, wszystkie punkty danych mają przetłumaczone nazwy, piloty IPv6 są akceptowane.",
|
|
94
|
-
"uk": "Перейменування зберігає сполучення, залипла кнопка звільняється під час запуску, усі точки даних мають перекладені назви, IPv6-пульти приймаються.",
|
|
95
|
-
"zh-cn": "重命名后遥控器仍保持配对,卡住的按键在启动时释放,所有数据点获得翻译后的名称,接受本网络的 IPv6 遥控器。"
|
|
96
96
|
}
|
|
97
97
|
},
|
|
98
98
|
"titleLang": {
|
|
@@ -166,7 +166,7 @@
|
|
|
166
166
|
],
|
|
167
167
|
"globalDependencies": [
|
|
168
168
|
{
|
|
169
|
-
"admin": ">=8.0.
|
|
169
|
+
"admin": ">=8.0.14"
|
|
170
170
|
}
|
|
171
171
|
],
|
|
172
172
|
"adminUI": {
|
|
@@ -185,30 +185,30 @@
|
|
|
185
185
|
]
|
|
186
186
|
},
|
|
187
187
|
"title": {
|
|
188
|
-
"en": "
|
|
189
|
-
"de": "
|
|
190
|
-
"ru": "
|
|
191
|
-
"pt": "
|
|
192
|
-
"nl": "
|
|
193
|
-
"fr": "
|
|
194
|
-
"it": "
|
|
195
|
-
"es": "
|
|
196
|
-
"pl": "
|
|
197
|
-
"uk": "
|
|
198
|
-
"zh-cn": "
|
|
188
|
+
"en": "The apps folder is removed",
|
|
189
|
+
"de": "Ordner apps fällt weg",
|
|
190
|
+
"ru": "Папка apps удалена",
|
|
191
|
+
"pt": "A pasta apps foi removida",
|
|
192
|
+
"nl": "De map apps vervalt",
|
|
193
|
+
"fr": "Le dossier apps est supprimé",
|
|
194
|
+
"it": "La cartella apps viene rimossa",
|
|
195
|
+
"es": "Se elimina la carpeta apps",
|
|
196
|
+
"pl": "Folder apps zostaje usunięty",
|
|
197
|
+
"uk": "Папку apps видалено",
|
|
198
|
+
"zh-cn": "apps 文件夹已移除"
|
|
199
199
|
},
|
|
200
200
|
"text": {
|
|
201
|
-
"en": "
|
|
202
|
-
"de": "
|
|
203
|
-
"ru": "
|
|
204
|
-
"pt": "Os
|
|
205
|
-
"nl": "
|
|
206
|
-
"fr": "
|
|
207
|
-
"it": "
|
|
208
|
-
"es": "
|
|
209
|
-
"pl": "
|
|
210
|
-
"uk": "
|
|
211
|
-
"zh-cn": "
|
|
201
|
+
"en": "App launches now arrive as launch:<id> in the command datapoint.",
|
|
202
|
+
"de": "App-Starts kommen jetzt als launch:<id> im Datenpunkt command.",
|
|
203
|
+
"ru": "Запуск приложений теперь приходит как launch:<id> в точке данных command.",
|
|
204
|
+
"pt": "Os inícios de apps chegam agora como launch:<id> no ponto de dados command.",
|
|
205
|
+
"nl": "App-starts komen nu binnen als launch:<id> in het datapunt command.",
|
|
206
|
+
"fr": "Les lancements d'applis arrivent désormais sous la forme launch:<id> dans le point de données command.",
|
|
207
|
+
"it": "Gli avvii delle app arrivano ora come launch:<id> nel datapoint command.",
|
|
208
|
+
"es": "Los inicios de apps llegan ahora como launch:<id> en el punto de datos command.",
|
|
209
|
+
"pl": "Uruchomienia aplikacji przychodzą teraz jako launch:<id> w punkcie danych command.",
|
|
210
|
+
"uk": "Запуски застосунків тепер надходять як launch:<id> у точці даних command.",
|
|
211
|
+
"zh-cn": "应用启动现以 launch:<id> 形式出现在数据点 command 中。"
|
|
212
212
|
},
|
|
213
213
|
"level": "warn",
|
|
214
214
|
"buttons": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iobroker.fakeroku",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.1",
|
|
4
4
|
"description": "ioBroker adapter that emulates Roku devices, letting ECP/SSDP remotes (Logitech Harmony, Sofabaton) trigger events in ioBroker",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "krobi",
|
|
@@ -49,11 +49,11 @@
|
|
|
49
49
|
"@tsconfig/node22": "^22.0.5",
|
|
50
50
|
"@types/iobroker": "npm:@iobroker/types@^7.2.3",
|
|
51
51
|
"@types/node": "^22",
|
|
52
|
-
"@vitest/coverage-v8": "^5.0.
|
|
53
|
-
"iobroker-adapter-checks": "^0.
|
|
52
|
+
"@vitest/coverage-v8": "^5.0.2",
|
|
53
|
+
"iobroker-adapter-checks": "^0.19.0",
|
|
54
54
|
"rimraf": "^6.1.3",
|
|
55
55
|
"typescript": "~6.0.3",
|
|
56
|
-
"vitest": "^5.0.
|
|
56
|
+
"vitest": "^5.0.2"
|
|
57
57
|
},
|
|
58
58
|
"main": "build/main.js",
|
|
59
59
|
"files": [
|