iobroker.fakeroku 1.7.0 → 1.7.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 +4 -8
- package/build/main.js +51 -1
- package/build/main.js.map +2 -2
- package/io-package.json +14 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -95,6 +95,10 @@ becomes `true` — or watch `.command` for the last button as text.
|
|
|
95
95
|
### **WORK IN PROGRESS**
|
|
96
96
|
-->
|
|
97
97
|
|
|
98
|
+
### 1.7.1 (2026-09-16)
|
|
99
|
+
|
|
100
|
+
- (krobipd) Fixed: a second instance on the same host is possible again — an instance carried over from an older version still claimed the whole machine.
|
|
101
|
+
|
|
98
102
|
### 1.7.0 (2026-09-16)
|
|
99
103
|
|
|
100
104
|
- (krobipd) Fixed: button datapoints keep their value and their room and function assignment when the adapter starts.
|
|
@@ -128,14 +132,6 @@ becomes `true` — or watch `.command` for the last button as text.
|
|
|
128
132
|
- (krobipd) Fixed: on a host running Docker the adapter could announce itself under a container address no remote can reach. A real network address is preferred now.
|
|
129
133
|
- (krobipd) Fixed: an emulated Roku whose server died while running left the instance showing "connected". It now reports the failure and names the device.
|
|
130
134
|
|
|
131
|
-
### 1.4.0 (2026-09-03)
|
|
132
|
-
- (krobipd) Fixed: renaming an emulated Roku could change its identity on the network, so a paired Harmony or Sofabaton lost the device and had to be set up again.
|
|
133
|
-
- (krobipd) Fixed: a remote key that was pressed when the adapter stopped stayed on for good. All key datapoints are now released at start-up, so the next press works again.
|
|
134
|
-
- (krobipd) Fixed: a device named "info" entered by hand into the configuration replaced the instance's own status channel. The name is refused now and leftovers are removed.
|
|
135
|
-
- (krobipd) Changed: every datapoint now carries a translated name and, where useful, a short description — in all eleven languages, in existing installations as well.
|
|
136
|
-
- (krobipd) Improved: a remote with a globally routable IPv6 address is accepted when it sits in the same network as the ioBroker host, not just on the reserved IPv6 ranges.
|
|
137
|
-
- (krobipd) New: user documentation in English and German, shown in the ioBroker documentation portal.
|
|
138
|
-
|
|
139
135
|
[Older changelogs can be found there](CHANGELOG_OLD.md)
|
|
140
136
|
|
|
141
137
|
## History
|
package/build/main.js
CHANGED
|
@@ -119,11 +119,61 @@ class Fakeroku extends utils.Adapter {
|
|
|
119
119
|
this.on("unload", this.onUnload.bind(this));
|
|
120
120
|
this.deviceManagement = new import_device_management.FakerokuDeviceManagement(this);
|
|
121
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* One-shot repair of this instance's own object — the settings keys and the leftover host claim.
|
|
124
|
+
*
|
|
125
|
+
* js-controller only ever ADDS to an existing instance object: on an update it fills missing
|
|
126
|
+
* `native` keys with the manifest default, and a `common` key the manifest dropped stays behind
|
|
127
|
+
* for ever. Two changes therefore never reached an existing installation on their own:
|
|
128
|
+
* the listen address had to MOVE to `bind` (a read fallback finds the injected default, not the
|
|
129
|
+
* user's value), and `singletonHost` — gone from the manifest since 1.6.0 — still claims the
|
|
130
|
+
* whole host, so nobody could add a second instance although the release said they could.
|
|
131
|
+
*
|
|
132
|
+
* Both are written in ONE merge, so the host restarts the instance at most once; `null` is what
|
|
133
|
+
* removes a key, since a merge cannot delete.
|
|
134
|
+
*
|
|
135
|
+
* @returns true when the object was written — the caller aborts its start, the host restarts
|
|
136
|
+
*/
|
|
137
|
+
async repairInstanceObject() {
|
|
138
|
+
var _a, _b;
|
|
139
|
+
const id = `system.adapter.${this.namespace}`;
|
|
140
|
+
let obj;
|
|
141
|
+
try {
|
|
142
|
+
obj = await this.getForeignObjectAsync(id);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
this.log.warn(`Settings repair skipped \u2014 could not read ${id}: ${(0, import_errors.errText)(err)}`);
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
if (!obj) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
const native = (0, import_native_key_migration.buildNativeKeyPatch)((_a = obj.native) != null ? _a : {}, BIND_KEY_MIGRATIONS);
|
|
151
|
+
const claim = (_b = obj.common) == null ? void 0 : _b.singletonHost;
|
|
152
|
+
const dropsHostClaim = claim !== void 0 && claim !== null;
|
|
153
|
+
if (!Object.keys(native).length && !dropsHostClaim) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
const summary = [
|
|
157
|
+
...Object.keys(native).filter((k) => native[k] !== null).map((k) => `${k} = ${JSON.stringify(native[k])}`),
|
|
158
|
+
...dropsHostClaim ? ["the instance no longer claims the whole host"] : []
|
|
159
|
+
].join(", ");
|
|
160
|
+
try {
|
|
161
|
+
await this.extendForeignObjectAsync(id, {
|
|
162
|
+
...dropsHostClaim ? { common: { singletonHost: null } } : {},
|
|
163
|
+
...Object.keys(native).length ? { native } : {}
|
|
164
|
+
});
|
|
165
|
+
} catch (err) {
|
|
166
|
+
this.log.warn(`Settings repair could not be stored (${(0, import_errors.errText)(err)}) \u2014 retrying on the next start`);
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
this.log.info(`Settings migrated to the standard keys (${summary}) \u2014 this instance restarts once`);
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
122
172
|
/** Create each device's object tree, start its ECP server, then the shared SSDP responder. */
|
|
123
173
|
async onReady() {
|
|
124
174
|
var _a;
|
|
125
175
|
try {
|
|
126
|
-
if (await
|
|
176
|
+
if (await this.repairInstanceObject()) {
|
|
127
177
|
return;
|
|
128
178
|
}
|
|
129
179
|
await this.setState("info.connection", { val: false, ack: true });
|
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, toDeviceRows, type DeviceRow } from \"./lib/device-config\";\nimport { detectLocalIPv4s, detectPrimaryIPv4 } 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 { 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\n/**\n * The listen address moved to `bind` (fleet listen-port standard). Two keys can hold it: the\n * adapter's own `networkInterface`, and `BIND` from the pre-0.5.0 adapter. Both are moved onto\n * `bind`; the helper prefers the source that says something, so a concrete address beats a key\n * left at \"all interfaces\". An empty legacy value becomes \"0.0.0.0\" \u2014 the admin's check skips an\n * instance whose `bind` is falsy, so a migrated \"\" would be as invisible as no key at all.\n */\nconst BIND_KEY_MIGRATIONS: NativeKeyMigration[] = [\n { from: \"networkInterface\", to: \"bind\", coerce: toBindAddress },\n { from: \"BIND\", to: \"bind\", coerce: toBindAddress },\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 /** 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\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 /** Create each device's object tree, start its ECP server, then the shared SSDP responder. */\n private async onReady(): Promise<void> {\n try {\n // The listen address lives under the fleet-standard key `bind` \u2014 the admin's port-conflict\n // check reads `native.port` + `native.bind` and skips every instance without them. On an\n // update js-controller ADDS the missing key with its manifest default and never deletes the\n // old one, so the user's value has to be MOVED: a read fallback would always find the\n // freshly injected default. The write restarts this instance, so nothing may start before it.\n if (await migrateNativeKeys(this, BIND_KEY_MIGRATIONS)) {\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 // 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(this.config.devices);\n\n // Empty AND \"0.0.0.0\" both mean \"auto\": bind all interfaces, advertise the\n // detected primary IP so the adapter runs without configuration. js-controller\n // never rewrites an existing native default, so instances from before 0.5.1\n // still carry \"\" \u2014 both must take the auto path. A concrete IP is honoured as-is.\n const configuredIp = this.config.bind;\n this.bindIp = configuredIp && configuredIp !== \"0.0.0.0\" ? configuredIp : undefined;\n const advertiseIp = this.bindIp ?? detectPrimaryIPv4();\n if (!advertiseIp) {\n this.log.warn(\"No routable IPv4 address found to advertise \u2014 set the network interface in the settings.\");\n // Sweep even here: what belongs in the tree is decided by the configuration,\n // not by the network. A missing cable is no reason to keep a deleted device.\n await this.sweepOrphans(configured);\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 await this.sweepOrphans(configured);\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 if (this.running.length > 0) {\n this.startDiscovery(advertiseIp);\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 * 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 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 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\"))) {\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 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 });\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 // 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 }\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 routable\n * interface so a multi-homed host is discoverable on all its LANs; a chosen interface pins\n * both membership and NOTIFY egress.\n *\n * @param advertiseIp the routable IP to announce\n */\n private startDiscovery(advertiseIp: string): void {\n const membershipInterfaces = this.bindIp ? [this.bindIp] : detectLocalIPv4s();\n const ssdp = this.makeSsdpResponder({\n devices: [...this.running],\n bindIp: this.bindIp,\n advertiseIp,\n membershipInterfaces,\n logger: this.log,\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 a changed host address first, then announce.\n *\n * Only in the automatic case \u2014 a configured interface is the user's decision and does not\n * move under us. Every five minutes is early enough: a controller caches the advertised\n * address for the announced max-age of an hour, so the corrected one reaches it long\n * before the old entry would even expire. Before this the announcement stayed frozen at\n * the address found during onReady, and a DHCP lease change left the discovery pointing\n * at an address nobody serves until someone restarted the instance by hand.\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 creates the manifest's instanceObjects only where they are\n * missing, so a changed name or description never reaches an installation that\n * already has them: the manifest would be correct and the real tree unchanged.\n * extendObject is what carries the change into an existing tree, so an update\n * always lands on 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 27 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 31 (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 rejects it\n // (E1010 \u2014 not in its role list); 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\u201327 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 * 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 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 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 \"${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(String(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 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.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, toDeviceRows, type DeviceRow } from \"./lib/device-config\";\nimport { detectLocalIPv4s, detectPrimaryIPv4 } from \"./lib/detect-ip\";\nimport { errText } from \"./lib/errors\";\nimport { buildNativeKeyPatch, type NativeKeyMigration } from \"./lib/native-key-migration\";\nimport { tDesc, tName, tRaw } from \"./lib/i18n\";\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\n/**\n * The listen address moved to `bind` (fleet listen-port standard). Two keys can hold it: the\n * adapter's own `networkInterface`, and `BIND` from the pre-0.5.0 adapter. Both are moved onto\n * `bind`; the helper prefers the source that says something, so a concrete address beats a key\n * left at \"all interfaces\". An empty legacy value becomes \"0.0.0.0\" \u2014 the admin's check skips an\n * instance whose `bind` is falsy, so a migrated \"\" would be as invisible as no key at all.\n */\nconst BIND_KEY_MIGRATIONS: NativeKeyMigration[] = [\n { from: \"networkInterface\", to: \"bind\", coerce: toBindAddress },\n { from: \"BIND\", to: \"bind\", coerce: toBindAddress },\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 /** 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\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 still claims the\n * whole host, so nobody could add a second instance although the release said they could.\n *\n * Both are written in ONE merge, so the host restarts the instance at most once; `null` is what\n * removes a key, since a merge cannot delete.\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 const native = buildNativeKeyPatch(obj.native ?? {}, BIND_KEY_MIGRATIONS);\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 const dropsHostClaim = claim !== undefined && claim !== null;\n if (!Object.keys(native).length && !dropsHostClaim) {\n return false;\n }\n const summary = [\n ...Object.keys(native)\n .filter(k => native[k] !== null)\n .map(k => `${k} = ${JSON.stringify(native[k])}`),\n ...(dropsHostClaim ? [\"the instance no longer claims the whole host\"] : []),\n ].join(\", \");\n try {\n await this.extendForeignObjectAsync(id, {\n ...(dropsHostClaim ? { common: { singletonHost: null } } : {}),\n ...(Object.keys(native).length ? { native } : {}),\n });\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(`Settings migrated to the standard keys (${summary}) \u2014 this instance 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 // 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(this.config.devices);\n\n // Empty AND \"0.0.0.0\" both mean \"auto\": bind all interfaces, advertise the\n // detected primary IP so the adapter runs without configuration. js-controller\n // never rewrites an existing native default, so instances from before 0.5.1\n // still carry \"\" \u2014 both must take the auto path. A concrete IP is honoured as-is.\n const configuredIp = this.config.bind;\n this.bindIp = configuredIp && configuredIp !== \"0.0.0.0\" ? configuredIp : undefined;\n const advertiseIp = this.bindIp ?? detectPrimaryIPv4();\n if (!advertiseIp) {\n this.log.warn(\"No routable IPv4 address found to advertise \u2014 set the network interface in the settings.\");\n // Sweep even here: what belongs in the tree is decided by the configuration,\n // not by the network. A missing cable is no reason to keep a deleted device.\n await this.sweepOrphans(configured);\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 await this.sweepOrphans(configured);\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 if (this.running.length > 0) {\n this.startDiscovery(advertiseIp);\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 * 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 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 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\"))) {\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 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 });\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 // 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 }\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 routable\n * interface so a multi-homed host is discoverable on all its LANs; a chosen interface pins\n * both membership and NOTIFY egress.\n *\n * @param advertiseIp the routable IP to announce\n */\n private startDiscovery(advertiseIp: string): void {\n const membershipInterfaces = this.bindIp ? [this.bindIp] : detectLocalIPv4s();\n const ssdp = this.makeSsdpResponder({\n devices: [...this.running],\n bindIp: this.bindIp,\n advertiseIp,\n membershipInterfaces,\n logger: this.log,\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 a changed host address first, then announce.\n *\n * Only in the automatic case \u2014 a configured interface is the user's decision and does not\n * move under us. Every five minutes is early enough: a controller caches the advertised\n * address for the announced max-age of an hour, so the corrected one reaches it long\n * before the old entry would even expire. Before this the announcement stayed frozen at\n * the address found during onReady, and a DHCP lease change left the discovery pointing\n * at an address nobody serves until someone restarted the instance by hand.\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 creates the manifest's instanceObjects only where they are\n * missing, so a changed name or description never reaches an installation that\n * already has them: the manifest would be correct and the real tree unchanged.\n * extendObject is what carries the change into an existing tree, so an update\n * always lands on 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 27 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 31 (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 rejects it\n // (E1010 \u2014 not in its role list); 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\u201327 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 * 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 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 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 \"${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(String(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 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.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": []
|
|
7
7
|
}
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "fakeroku",
|
|
4
|
-
"version": "1.7.
|
|
4
|
+
"version": "1.7.1",
|
|
5
5
|
"news": {
|
|
6
|
+
"1.7.1": {
|
|
7
|
+
"en": "A second instance on the same host is possible again; an instance carried over from an older version still claimed the whole machine.",
|
|
8
|
+
"de": "Eine zweite Instanz auf demselben Rechner ist wieder möglich; eine aus einer älteren Version übernommene Instanz beanspruchte den ganzen Rechner.",
|
|
9
|
+
"ru": "Второй экземпляр на том же хосте снова возможен; экземпляр, перенесённый из более старой версии, занимал весь компьютер целиком.",
|
|
10
|
+
"pt": "Uma segunda instância no mesmo anfitrião volta a ser possível; uma instância vinda de uma versão mais antiga reclamava a máquina inteira.",
|
|
11
|
+
"nl": "Een tweede instantie op dezelfde host is weer mogelijk; een instantie uit een oudere versie claimde de hele machine.",
|
|
12
|
+
"fr": "Une deuxième instance sur le même hôte est de nouveau possible ; une instance venue d'une version plus ancienne réclamait toute la machine.",
|
|
13
|
+
"it": "Una seconda istanza sullo stesso host è di nuovo possibile; un'istanza proveniente da una versione precedente rivendicava l'intera macchina.",
|
|
14
|
+
"es": "Una segunda instancia en el mismo host vuelve a ser posible; una instancia procedente de una versión anterior reclamaba toda la máquina.",
|
|
15
|
+
"pl": "Druga instancja na tym samym hoście jest znów możliwa; instancja przeniesiona ze starszej wersji zajmowała całą maszynę.",
|
|
16
|
+
"uk": "Другий екземпляр на тому самому хості знову можливий; екземпляр, перенесений зі старішої версії, займав усю машину.",
|
|
17
|
+
"zh-cn": "同一主机上可以再次添加第二个实例;从旧版本沿用下来的实例会独占整台机器。"
|
|
18
|
+
},
|
|
6
19
|
"1.7.0": {
|
|
7
20
|
"en": "Button datapoints keep their value and their room and function assignment, and the instance restarts once right after this update.",
|
|
8
21
|
"de": "Tastendatenpunkte behalten ihren Wert und ihre Raum- und Funktionszuordnung, und die Instanz startet direkt nach diesem Update einmal neu.",
|
|
@@ -80,19 +93,6 @@
|
|
|
80
93
|
"pl": "Naprawia awarię przy nieprawidłowym klawiszu, akceptuje piloty IPv6, ogranicza każdy Roku do 25 poleceń/s, pokazuje właściwą ikonę, zgłasza Roku OS 15.0, wymaga admin >= 8.0.11.",
|
|
81
94
|
"uk": "Виправлено збій при некоректному натисканні, приймаються IPv6-пульти, ліміт 25 команд/с на Roku, правильна іконка, повідомляється Roku OS 15.0, потрібен admin >= 8.0.11.",
|
|
82
95
|
"zh-cn": "修复格式错误按键导致的崩溃,接受 IPv6 遥控器,每个 Roku 限制为每秒 25 条命令,显示正确图标,报告 Roku OS 15.0,需要 admin >= 8.0.11。"
|
|
83
|
-
},
|
|
84
|
-
"1.2.0": {
|
|
85
|
-
"en": "Honest status: the instance reports connected only while every configured Roku runs. Shutdown waits for its last write. Plus optional error reporting via Sentry.",
|
|
86
|
-
"de": "Ehrlicher Status: Die Instanz meldet nur dann verbunden, wenn jeder konfigurierte Roku läuft. Das Beenden wartet auf den letzten Schreibvorgang. Dazu optionale Fehlerberichte über Sentry.",
|
|
87
|
-
"ru": "Честный статус: экземпляр сообщает о подключении, только когда работают все настроенные Roku. Остановка дожидается последней записи. Плюс необязательная отправка ошибок через Sentry.",
|
|
88
|
-
"pt": "Estado honesto: a instância só indica ligado enquanto todos os Roku configurados estiverem a funcionar. A paragem aguarda a última escrita. Além disso, relatórios de erro opcionais via Sentry.",
|
|
89
|
-
"nl": "Eerlijke status: de instantie meldt alleen verbonden zolang elke geconfigureerde Roku draait. Het stoppen wacht op de laatste schrijfactie. Plus optionele foutrapportage via Sentry.",
|
|
90
|
-
"fr": "Statut honnête : l'instance n'indique connecté que si chaque Roku configuré fonctionne. L'arrêt attend sa dernière écriture. Plus des rapports d'erreurs facultatifs via Sentry.",
|
|
91
|
-
"it": "Stato onesto: l'istanza segnala connesso solo finché ogni Roku configurato è in funzione. L'arresto attende l'ultima scrittura. Inoltre segnalazione errori opzionale tramite Sentry.",
|
|
92
|
-
"es": "Estado honesto: la instancia solo indica conectado mientras cada Roku configurado esté funcionando. La parada espera a su última escritura. Además, informes de errores opcionales mediante Sentry.",
|
|
93
|
-
"pl": "Uczciwy status: instancja zgłasza połączenie tylko wtedy, gdy działa każdy skonfigurowany Roku. Zatrzymanie czeka na ostatni zapis. Dodatkowo opcjonalne raportowanie błędów przez Sentry.",
|
|
94
|
-
"uk": "Чесний статус: екземпляр повідомляє про з'єднання, лише поки працює кожен налаштований Roku. Зупинка чекає на останній запис. Крім того, необов'язкові звіти про помилки через Sentry.",
|
|
95
|
-
"zh-cn": "诚实的状态:仅当每个已配置的 Roku 都在运行时,实例才报告已连接。停止时会等待最后一次写入完成。此外还提供可选的 Sentry 错误报告。"
|
|
96
96
|
}
|
|
97
97
|
},
|
|
98
98
|
"titleLang": {
|
package/package.json
CHANGED