iobroker.yamaha 2.9.0 → 2.9.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 CHANGED
@@ -74,6 +74,10 @@ Details on all settings, the object tree and the ports the adapter uses are in t
74
74
  Placeholder for the next version (at the beginning of the line):
75
75
  ### **WORK IN PROGRESS**
76
76
  -->
77
+ ### 2.9.1 (2026-09-12)
78
+
79
+ - (krobipd) Fixed: A receiver the network search found keeps its datapoints when you add a device by hand — they stay with their history and are marked offline instead of deleted
80
+
77
81
  ### 2.9.0 (2026-09-12)
78
82
 
79
83
  - (krobipd) New: Devices you enter by hand and devices the network search finds now run side by side — entering one receiver no longer takes every found one out of the instance
@@ -108,15 +112,6 @@ Details on all settings, the object tree and the ports the adapter uses are in t
108
112
  - (krobipd) New: A datapoint the receiver reveals later now appears at once — a function it starts answering, a value it reports for the first time, a status field it begins delivering.
109
113
  - (krobipd) Changed: A datapoint that never carried a value is removed only after two starts confirm it, so a receiver left in standby no longer loses datapoints it still has.
110
114
 
111
- ### 2.6.0 (2026-09-09)
112
-
113
- - (krobipd) Fixed: input and sound program lists now offer only what the receiver itself declares or proves it has, instead of every value any Yamaha may have (#619)
114
- - (krobipd) Fixed: the 2008 receiver generation gets volume, mute and sound program back; HDMI output, aspect, resolution and decoder lists carry the values the receiver reports
115
- - (krobipd) New: HD Radio and Sirius on the US models, zone balance, pre-out mode and zone scenes, party volume keys, HDMI video mode, lip sync, a second trigger output and speaker pattern
116
- - (krobipd) New: on older XML receivers the enhancer, CINEMA DSP 3D, speaker A/B, Zone B, a zone-wide cursor pad, transport keys and zone names; MusicCast gains standby-through and speaker pattern
117
- - (krobipd) Improved: a receiver is set up from its own declaration of zones and inputs, so it comes online faster and is learned again by itself after an update that changes how it is read
118
- - (krobipd) Improved: the first connection to a YNCA receiver asks fewer questions, so its datapoints appear sooner
119
-
120
115
  [Older changelogs can be found there](CHANGELOG_OLD.md)
121
116
 
122
117
  ## History
@@ -112,13 +112,13 @@ function mergeDiscovered(known, found, onCollision) {
112
112
  }
113
113
  return [...byId.values()];
114
114
  }
115
- function staleObjects(existing, deviceIds, namespace) {
115
+ function staleObjects(existing, deviceIds, namespace, remembered = /* @__PURE__ */ new Set()) {
116
116
  if (deviceIds.size === 0) {
117
117
  return [];
118
118
  }
119
119
  const isKept = (fullId) => {
120
120
  const top = stripNamespace(fullId, namespace).split(".")[0];
121
- return top === "info" || deviceIds.has(top);
121
+ return top === "info" || deviceIds.has(top) || remembered.has(top);
122
122
  };
123
123
  return existing.filter((id) => !isKept(id)).sort((a, b) => b.length - a.length);
124
124
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/pure-helpers.ts"],
4
- "sourcesContent": ["import type { DeviceRecord } from \"./types\";\nimport type { DiscoveredDevice } from \"./discovery\";\n\ninterface ConfiguredDevice {\n name?: string;\n ip: string;\n}\n\n/**\n * True when a raw config row carries a non-empty ip (the name is optional \u2014 a row\n * with an ip but no name is valid and falls back to the ip as its id, so a device\n * is never silently dropped just because its name was left blank).\n *\n * @param entry a raw config row from the admin device table\n * @returns whether the row is a valid configured device\n */\nfunction isConfiguredDevice(entry: unknown): entry is ConfiguredDevice {\n if (typeof entry !== \"object\" || entry === null) {\n return false;\n }\n const candidate = entry as { name?: unknown; ip?: unknown };\n return (\n typeof candidate.ip === \"string\" &&\n candidate.ip.length > 0 &&\n (candidate.name === undefined || typeof candidate.name === \"string\")\n );\n}\n\n/**\n * Make a string safe for use as an ioBroker object id segment.\n *\n * @param raw the raw string (e.g. a device name)\n * @returns the string with id-unsafe characters replaced by underscores\n */\nexport function sanitizeId(raw: string): string {\n return raw.replace(/[^A-Za-z0-9\\-_]/g, \"_\");\n}\n\n/**\n * Strip the adapter namespace (e.g. `yamaha.0`) from a full state id, leaving the\n * device-relative path (e.g. `living.power`).\n *\n * @param fullId the full state id\n * @param namespace the adapter namespace\n * @returns the id relative to the adapter instance\n */\nexport function stripNamespace(fullId: string, namespace: string): string {\n return fullId.slice(namespace.length + 1);\n}\n\n/**\n * Turn the admin device table (untrusted native config) into device records.\n * Invalid rows are dropped, as are rows whose id collides with the adapter's own\n * reserved `info` branch or with an id already taken \u2014 two names that sanitise to\n * the same id (e.g. \"Living Room\" and \"Living.Room\") would otherwise share one\n * object tree. A dropped collision is reported via `onCollision` so the device does\n * not just silently \"not exist\" for the user.\n *\n * @param raw the raw `native.devices` value\n * @param onCollision called with the dropped row's name/ip and the clashing id\n * @returns validated, de-duplicated device records\n */\nexport function parseDevices(raw: unknown, onCollision?: (dropped: string, takenId: string) => void): DeviceRecord[] {\n if (!Array.isArray(raw)) {\n return [];\n }\n const records: DeviceRecord[] = [];\n const taken = new Set<string>([\"info\"]); // reserved: the adapter's own info channel\n for (const entry of raw) {\n if (!isConfiguredDevice(entry)) {\n continue;\n }\n // Fall back to the ip as the id when the name is blank, so the device still appears\n // instead of vanishing silently.\n const id = sanitizeId(entry.name && entry.name.length > 0 ? entry.name : entry.ip);\n if (taken.has(id)) {\n onCollision?.(entry.name || entry.ip, id);\n continue;\n }\n taken.add(id);\n records.push({ id, ip: entry.ip, source: \"manual\" });\n }\n return records;\n}\n\n/**\n * The set of devices the adapter runs: the manual table plus the discovered store, keyed by id.\n *\n * A manual entry WINS over a discovered one of the same id \u2014 the user typed that address, and\n * the network search must not overwrite it. Before 2.9.0 the two were mutually exclusive (a\n * filled table turned the search off entirely), so making one discovered device manual dropped\n * every other one from the run and `cleanupStaleObjects` deleted their trees.\n *\n * @param manual the rows from the instance's device table\n * @param discovered the records remembered from the network search\n * @returns one record per id, manual first, each tagged with where it came from\n */\nexport function unionDevices(manual: readonly DeviceRecord[], discovered: readonly DeviceRecord[]): DeviceRecord[] {\n const byId = new Map<string, DeviceRecord>();\n for (const device of manual) {\n byId.set(device.id, { ...device, source: \"manual\" });\n }\n for (const device of discovered) {\n if (byId.has(device.id)) {\n continue;\n }\n // An address a manual device already occupies would mean two records talking to one\n // receiver \u2014 the manual one owns it.\n if ([...byId.values()].some(known => known.ip === device.ip)) {\n continue;\n }\n byId.set(device.id, { ...device, source: \"discovered\" });\n }\n return [...byId.values()];\n}\n\n/**\n * Merge freshly discovered devices into the set already known from earlier runs\n * (the auto-discovery standby protection). A known device is kept even when this\n * run's scan did not find it \u2014 a receiver in deep standby answers no SSDP, and its\n * object tree must survive.\n *\n * Identity is the device ID (derived from the friendly name it advertises, or from its\n * address when it advertises none) \u2014 NOT its address. Keyed by address, a receiver that\n * moved to a new address by DHCP was lost for good: the remembered record kept the old\n * address, the same receiver found at the new one produced the same id, and the id clash\n * dropped it. It stayed offline with no way back, because the id is what the whole object\n * tree hangs off. Now the same id simply carries the new address over.\n *\n * A genuine clash remains a clash: a DIFFERENT device sitting on an address another\n * record already claims is skipped and reported through `onCollision`, so a missing\n * device is explainable rather than silent.\n *\n * @param known the device records remembered from earlier runs\n * @param found the devices discovered this run\n * @param onCollision called with the dropped device's name/ip and the clashing id\n * @returns the merged records, one per device id\n */\nexport function mergeDiscovered(\n known: DeviceRecord[],\n found: DiscoveredDevice[],\n onCollision?: (dropped: string, takenId: string) => void,\n): DeviceRecord[] {\n const byId = new Map<string, DeviceRecord>();\n for (const device of known) {\n // \"info\" is the adapter's own channel \u2014 a device may never claim it.\n if (device.id === \"info\" || byId.has(device.id)) {\n continue;\n }\n byId.set(device.id, { ...device });\n }\n for (const device of found) {\n const label = device.name || device.ip;\n const id = sanitizeId(label);\n const remembered = byId.get(id);\n if (remembered) {\n // Same device, possibly at a new address \u2014 carry the address over, keep the id.\n remembered.ip = device.ip;\n continue;\n }\n const ipOwner = [...byId.values()].find(record => record.ip === device.ip);\n if (id === \"info\" || ipOwner) {\n onCollision?.(label, ipOwner?.id ?? id);\n continue;\n }\n byId.set(id, { id, ip: device.ip });\n }\n return [...byId.values()];\n}\n\n/**\n * From all object ids under the instance, pick the stale ones to delete on start:\n * everything that does not belong to a configured device and is outside the\n * adapter's own `info` branch. Removes the previous adapter's whole object tree\n * and any device dropped from the config. Keyed on the configured device ids\n * (not on what was created this run), so it works with the async connect where a\n * device's tree may only appear later \u2014 a configured device's subtree is kept\n * regardless of whether it has connected yet. Deepest first, so children go\n * before their parents.\n *\n * @param existing all object ids currently under the instance\n * @param deviceIds the ids of the currently configured devices\n * @param namespace the adapter namespace (e.g. `yamaha.0`)\n * @returns the stale ids to delete, deepest first\n */\nexport function staleObjects(existing: string[], deviceIds: Set<string>, namespace: string): string[] {\n // No configured devices \u2192 never wipe the whole tree (a user who cleared the\n // device table by accident would otherwise lose every object in one pass).\n if (deviceIds.size === 0) {\n return [];\n }\n const isKept = (fullId: string): boolean => {\n const top = stripNamespace(fullId, namespace).split(\".\")[0];\n return top === \"info\" || deviceIds.has(top);\n };\n return existing.filter(id => !isKept(id)).sort((a, b) => b.length - a.length);\n}\n\n/**\n * Relative state ids an earlier version created under a different path and that this\n * version has renamed or moved. On start-up the old object is deleted so it does not\n * linger orphaned beside the new one \u2014 {@link staleObjects} only removes whole\n * non-configured device trees, not renamed states inside a device that is kept.\n */\n/**\n * The per-source playback-block states of the pre-2.0.0 tree \u2014 the same 19 states\n * used to sit under every player source folder, almost all permanently empty. In\n * 2.0.0 ONE flat block per zone replaces them; these copies are removed on the\n * first start (the sources that keep genuinely own states keep those untouched).\n */\nconst V2_PLAYER_BLOCK_STATES = [\n \"playback\",\n \"artist\",\n \"album\",\n \"track\",\n \"station\",\n \"channelName\",\n \"totalTime\",\n \"elapsedTime\",\n \"repeat\",\n \"shuffle\",\n \"albumArt\",\n \"source\",\n \"play\",\n \"pause\",\n \"stop\",\n \"next\",\n \"prev\",\n \"repeatToggle\",\n \"shuffleToggle\",\n];\n\n/** The source folders that stay in 2.0.0 (own states remain) but lose their block copy. */\nconst V2_SLIMMED_SOURCES = [\n \"netPlayer\",\n \"cd\",\n \"netRadio\",\n \"server\",\n \"usb\",\n \"napster\",\n \"pandora\",\n \"rhapsody\",\n \"sirius\",\n \"pc\",\n \"airplay\",\n \"bluetooth\",\n // Folders that came back with own content after the 2.0.0 cut (iPod/iPod USB: MODE, 2026-09-06;\n // Spotify: the RX-A850 presets, 2026-09-09) \u2014 their 1.x playback copies still have to go.\n \"ipod\",\n \"ipodUsb\",\n \"spotify\",\n];\n\nexport const RENAMED_STATE_IDS = [\n // v2.8.0: the volume datapoint now carries the scale the receiver itself displays, so the two\n // derived states are gone \u2014 `actualVolume` was an exact duplicate of `volume` on every device\n // declaring a single scale, and `actualVolumeMode` a dropdown with one entry there. `inputText`\n // repeated the label the `input` dropdown carries since 2.7.2, and only on MusicCast devices.\n \"actualVolume\",\n \"actualVolumeMode\",\n \"inputText\",\n \"hdmiOut\",\n \"directMode\",\n \"masterPower\",\n \"party\",\n \"partyMute\",\n \"distributionEnable\",\n \"partyEnable\",\n \"remoteCode\",\n // v1.0.0 multiroom regroup: the MusicCast-Link states moved into their own\n // multiroom.group folder, so the tree itself tells device-group from all-zones scope.\n \"multiroom.distributionEnable\",\n // Short-lived v1.0.0 pre-cut id \u2014 mislabeled \"active\", the field means \"enabled for use\".\n \"multiroom.group.streamingActive\",\n \"multiroom.role\",\n \"multiroom.groupId\",\n \"multiroom.groupName\",\n \"multiroom.serverZone\",\n \"multiroom.clientList\",\n \"multiroom.linkClient\",\n \"multiroom.leaveGroup\",\n // ---- v2.0.0 tree rework ------------------------------------------------------\n // Player unification: the per-source copies of the playback block are gone (the\n // slimmed folders keep only their own preset/pairing/drive states).\n ...V2_SLIMMED_SOURCES.flatMap(source => V2_PLAYER_BLOCK_STATES.map(state => `player.${source}.${state}`)),\n // Scenes: the twelve per-name datapoints became the recall dropdown + scene.list.\n ...Array.from({ length: 12 }, (_unused, i) => `scene.name${i + 1}`),\n // Tuner unification: ONE band/frequency/preset; the DAB subunit's FM half moved\n // onto the flat tuner ids, the two per-band frequencies became tuner.frequency.\n \"tuner.amFrequency\",\n \"tuner.fmFrequency\",\n \"tuner.dab.band\",\n \"tuner.dab.preset\",\n \"tuner.dab.fmPreset\",\n \"tuner.dab.fmFrequency\",\n \"tuner.dab.fmSearchMode\",\n \"tuner.dab.fmRdsService\",\n \"tuner.dab.fmRdsProgramType\",\n \"tuner.dab.fmRdsText\",\n \"tuner.dab.fmRdsClock\",\n \"tuner.dab.fmStereo\",\n \"tuner.dab.fmTuned\",\n \"tuner.dab.audioMode\",\n // Sound polish: equalizer and signal info each moved into their own subfolder.\n \"sound.equalizerMode\",\n \"sound.equalizerLow\",\n \"sound.equalizerMid\",\n \"sound.equalizerHigh\",\n \"sound.signalFormat\",\n \"sound.signalSampling\",\n \"sound.signalBits\",\n \"sound.signalBitrate\",\n // HDMI polish: the lip-sync offsets moved into the hdmi folder (the lipSync\n // channel itself is in RENAMED_CHANNELS); the A/B toggles joined the speakers.\n \"advanced.speakerA\",\n \"advanced.speakerB\",\n];\n\n/**\n * Old channel prefixes whose whole subtree this version moved out \u2014 the `system`\n * grab-bag is gone (model/firmware \u2192 info, HDMI outputs \u2192 hdmi, speaker patterns \u2192\n * speakers, input names \u2192 inputNames, master power \u2192 multiroom.masterPower). The channel and\n * every state under it are removed.\n */\nexport const RENAMED_CHANNELS = [\n // pre-0.11 system folder\n \"system\",\n // v0.18.1 multiroom regroup: zone2/3/4, zoneB, flat multiroom states moved under multiroom/.\n \"zone2\",\n \"zone3\",\n \"zone4\",\n \"zoneB\",\n // v1.0.0: stray per-zone copies of the device-global YXC states (the zone loop used to\n // prefix them too, yielding multiroom.zoneN.multiroom.* junk) are swept away.\n \"multiroom.zone2.multiroom\",\n \"multiroom.zone3.multiroom\",\n \"multiroom.zone4.multiroom\",\n // Regrouping: media sources moved under player/, DAB under tuner/, dist \u2192 multiroom. The old\n // flat channels (and their whole subtree) are deleted so the new grouped ones do not sit\n // beside orphaned copies on an upgraded instance.\n \"netRadio\",\n \"server\",\n \"usb\",\n \"spotify\",\n \"deezer\",\n \"tidal\",\n \"napster\",\n \"pandora\",\n \"rhapsody\",\n \"sirius\",\n \"airplay\",\n \"bluetooth\",\n \"pc\",\n \"musicCastLink\",\n \"ipod\",\n \"ipodUsb\",\n \"netPlayer\",\n \"cd\",\n \"dab\",\n \"dist\",\n // Sound/Advanced regroup: DSP/tone-tuning states moved under sound.*, setup-only states\n // (+ the speakers/initialVolume/inputNames subtrees, already dotted before) under advanced.*.\n // {@link renamedObjectIds} also checks these against a stripped zone2/3/4 prefix, so one\n // entry here catches a MAIN state and its zoned copies (e.g. \"straight\" and \"zone2.straight\").\n \"straight\",\n \"enhancer\",\n \"pureDirect\",\n \"direct\",\n \"adaptiveDrc\",\n \"surroundAI\",\n \"surroundDecoder\",\n \"cinemaDsp3d\",\n \"extraBass\",\n \"bass\",\n \"treble\",\n \"subwooferTrim\",\n \"balance\",\n \"dialogueLevel\",\n \"dialogueLift\",\n \"dtsDialogueControl\",\n \"monaural\",\n \"surround3d\",\n \"adaptiveDspLevel\",\n \"audioSelect\",\n \"linkControl\",\n \"linkAudioDelay\",\n \"linkAudioQuality\",\n \"contentsDisplay\",\n \"equalizerLow\",\n \"equalizerMid\",\n \"equalizerHigh\",\n \"clearVoice\",\n \"bassExtension\",\n \"ypaoVolume\",\n \"maxVolume\",\n \"speakerA\",\n \"speakerB\",\n \"speakers\",\n \"initialVolume\",\n \"inputNames\",\n // ---- v2.0.0 tree rework: the always-empty source channels are gone entirely\n // (their playback lives in the flat per-zone block), lip sync moved into hdmi.\n // `player.ipod`/`player.ipodUsb` came BACK on 2026-09-06: the official command list gives\n // both sources an own `MODE` (Normal/Extended), so the folders carry genuine content again\n // and must not be deleted on every start \u2014 the migration-table guard in pure-helpers.test.ts\n // is what caught the contradiction. `player.spotify` followed on 2026-09-09: the RX-A850 list\n // gives Spotify a preset recall and store, so its folder carries content again.\n \"player.deezer\",\n \"player.tidal\",\n \"player.musicCastLink\",\n \"lipSync\",\n];\n\n/**\n * Read-capable state objects under a configured device whose state NEVER carried a\n * value (no value, no last-change). They are over-declarations of an earlier adapter\n * version \u2014 today's claim-with-proof creation would not make them \u2014 and deleting them\n * is lossless: there is no value and no history to lose, and anything a transport\n * legitimately claims is recreated right after, when the device connects. Runs once\n * per adapter version (the caller keeps a marker), so a freshly created state that is\n * merely waiting for its first value does not flap on every start. Excluded: buttons\n * and other write-only states (naturally valueless) and the `info.` header the adapter\n * itself maintains for every device, connected or not. A recording setting\n * (`common.custom`) is deliberately NO factor: whether anyone records a datapoint is\n * the user's business and says nothing about whether the datapoint belongs in the\n * tree \u2014 that is solely the adapter's responsibility (krobi, 2.0.3). On a never-filled\n * state there is nothing recorded to lose anyway.\n *\n * @param objects all objects under the instance, keyed by full id\n * @param states all states under the instance, keyed by full id\n * @param deviceIds the ids of the devices to sweep\n * @param namespace the adapter namespace (e.g. `yamaha.0`)\n * @returns the full ids of never-filled read-capable states\n */\nexport function neverWrittenStateIds(\n objects: Record<string, { type?: string; common?: unknown } | undefined>,\n states: Record<string, { val?: unknown; lc?: number } | null | undefined>,\n deviceIds: Set<string>,\n namespace: string,\n): string[] {\n const ids: string[] = [];\n for (const [fullId, object] of Object.entries(objects)) {\n const common = object?.common as { read?: boolean } | undefined;\n if (object?.type !== \"state\" || common?.read === false) {\n continue;\n }\n const relative = stripNamespace(fullId, namespace);\n const top = relative.split(\".\")[0];\n if (!deviceIds.has(top) || relative.slice(top.length + 1).startsWith(\"info.\")) {\n continue;\n }\n const state = states[fullId];\n if (!state || ((state.val === null || state.val === undefined) && !state.lc)) {\n ids.push(fullId);\n }\n }\n return ids;\n}\n\n/**\n * Channels under a connected device that carry no datapoint anywhere below them \u2014 the folders a\n * tree rework empties but never removes.\n *\n * Both existing sweeps miss these on purpose: {@link staleObjects} only knows whole device trees\n * and named paths, and {@link neverWrittenStateIds} filters on `type === \"state\"`. So a folder\n * whose datapoints the v2.0.0 migration deleted (`player.server` \u2014 the SERVER source keeps no own\n * datapoint in the new tree) survives as an empty promise in the object tree, and every future\n * rework that empties a folder instead of listing it would leave the same kind of leftover.\n *\n * Nested empties resolve by themselves: a channel counts as filled only when a STATE exists\n * somewhere beneath it, so a folder holding nothing but other empty folders goes too. Deepest\n * first, so children are deleted before their parents.\n *\n * @param objects all objects currently under the instance\n * @param deviceIds the devices that have connected in this run (an offline device is left alone)\n * @param namespace the adapter namespace (e.g. `yamaha.0`)\n * @returns the full channel ids to delete, deepest first\n */\nexport function childlessChannelIds(\n objects: Record<string, { type?: string } | undefined>,\n deviceIds: Set<string>,\n namespace: string,\n): string[] {\n // Every path segment that has a datapoint below it, collected once over all objects.\n const filled = new Set<string>();\n for (const [fullId, object] of Object.entries(objects)) {\n if (object?.type !== \"state\") {\n continue;\n }\n for (let cut = fullId.lastIndexOf(\".\"); cut > 0; cut = fullId.lastIndexOf(\".\", cut - 1)) {\n filled.add(fullId.slice(0, cut));\n }\n }\n const ids: string[] = [];\n for (const [fullId, object] of Object.entries(objects)) {\n if (object?.type !== \"channel\" || filled.has(fullId)) {\n continue;\n }\n const relative = stripNamespace(fullId, namespace);\n if (deviceIds.has(relative.split(\".\")[0])) {\n ids.push(fullId);\n }\n }\n return ids.sort((a, b) => b.length - a.length);\n}\n\n/**\n * The full ids of renamed old states (and old channel subtrees) that still exist\n * under a configured device, to be deleted on start-up so no orphan lingers beside\n * the new object. Deepest first, so children go before their parents.\n *\n * @param existing all object ids currently under the instance\n * @param deviceIds the ids of the currently configured devices\n * @param namespace the adapter namespace (e.g. `yamaha.0`)\n * @returns the full old ids to delete, deepest first\n */\nexport function renamedObjectIds(existing: string[], deviceIds: Set<string>, namespace: string): string[] {\n const stale: string[] = [];\n for (const deviceId of deviceIds) {\n const base = `${namespace}.${deviceId}.`;\n for (const full of existing) {\n if (!full.startsWith(base)) {\n continue;\n }\n const rel = full.slice(base.length);\n // Strip an optional zone prefix before matching too, so one RENAMED_CHANNELS entry\n // (e.g. \"straight\") catches both the MAIN state and its zoned copies \u2014 the old flat\n // \"zone2.\" form and today's \"multiroom.zone2.\" form alike (v2.0.0 renames live in\n // zoned folders, e.g. multiroom.zone2.scene.name1).\n const zone = /^(?:multiroom\\.)?zone[234]\\./.exec(rel)?.[0] ?? \"\";\n const template = rel.slice(zone.length);\n const renamedState = RENAMED_STATE_IDS.includes(rel) || RENAMED_STATE_IDS.includes(template);\n const underRenamedChannel = RENAMED_CHANNELS.some(\n ch => rel === ch || rel.startsWith(`${ch}.`) || template === ch || template.startsWith(`${ch}.`),\n );\n if (renamedState || underRenamedChannel) {\n stale.push(full);\n }\n }\n }\n return stale.sort((a, b) => b.length - a.length);\n}\n\n/**\n * The device row to carry over from the previous adapter's single-device config,\n * or undefined if nothing needs migrating. The old yamaha stored one receiver as\n * `config.ip` (older installs: `config.IP`); the new adapter uses a `devices`\n * table. Only migrates when the table is still empty, so it runs once.\n *\n * The old value could be a hostname (its HTTP client resolved names) and could\n * carry a `:port` suffix (its HTTP library split host:port). A port suffix would\n * break every transport here \u2014 YNCA is TCP :50000, YXC/XML build `http://<ip>\u2026`\n * \u2014 so it is stripped; the host/IP itself is carried over as-is.\n *\n * @param config the instance's native config\n * @returns the row to add to the devices table, or undefined\n */\nexport function legacyDeviceRow(config: Record<string, unknown>): { name: string; ip: string } | undefined {\n if (Array.isArray(config.devices) && config.devices.length > 0) {\n return undefined;\n }\n const raw =\n typeof config.ip === \"string\" && config.ip\n ? config.ip\n : typeof config.IP === \"string\" && config.IP\n ? config.IP\n : undefined;\n if (!raw) {\n return undefined;\n }\n const ip = raw.trim().replace(/:\\d+$/, \"\");\n return ip ? { name: ip, ip } : undefined;\n}\n\n/**\n * How trustworthy a display-name candidate is. A name the device carries for itself\n * (the MusicCast zone name a user typed in the app) beats its model designation.\n */\nexport const LABEL_RANK = { model: 1, deviceName: 2 } as const;\n\n/** Rank of a display-name candidate \u2014 see {@link LABEL_RANK}. */\nexport type LabelRank = (typeof LABEL_RANK)[keyof typeof LABEL_RANK];\n\n/**\n * Zone names that say nothing about the device \u2014 a receiver ships with these and a\n * user who never renamed the zone would end up with \"Main Zone\" as the device name.\n */\nconst GENERIC_ZONE_NAMES = new Set([\"main\", \"main zone\", \"mainzone\", \"zone\", \"zone 1\", \"zone1\"]);\n\n/**\n * Is this candidate worth showing as a device name?\n *\n * @param candidate the reported name\n * @returns true when it carries information about this particular device\n */\nexport function isUsefulDeviceName(candidate: string | undefined): boolean {\n const trimmed = (candidate ?? \"\").trim();\n return trimmed.length > 0 && !GENERIC_ZONE_NAMES.has(trimmed.toLowerCase());\n}\n\n/**\n * The display name to write onto a device object, or undefined to leave it alone.\n *\n * An upgraded instance carries the IP as its device name: the previous adapter knew\n * only an IP, so the migration had nothing else to call the device, and the object id\n * \u2014 which must not change, every history and visualisation binding hangs off it \u2014\n * became that IP. This decides when the adapter may replace that placeholder with\n * something a user recognises.\n *\n * Two things must never be overwritten: a name the user typed, and a better name by a\n * weaker source. The adapter therefore only writes over its own placeholder (the id\n * itself) or over what it wrote last, and only when the new candidate ranks at least\n * as high as the one behind the current name.\n *\n * @param current the device object's present `common.name`\n * @param deviceId the object id, which is also the placeholder name\n * @param candidate the newly reported name\n * @param rank how trustworthy the candidate is\n * @param ownName the name this adapter wrote last for the device, if any\n * @param ownRank the rank behind {@link ownName}\n * @returns the name to write, or undefined when the current name stays\n */\nexport function nextDeviceLabel(\n current: string | undefined,\n deviceId: string,\n candidate: string | undefined,\n rank: LabelRank,\n ownName?: string,\n ownRank?: LabelRank,\n): string | undefined {\n const wanted = (candidate ?? \"\").trim();\n if (!isUsefulDeviceName(wanted) || wanted === current) {\n return undefined;\n }\n const isPlaceholder = current === undefined || current === deviceId;\n const isOurs = ownName !== undefined && current === ownName;\n if (!isPlaceholder && !isOurs) {\n return undefined; // the user named this device \u2014 theirs wins\n }\n if (isOurs && ownRank !== undefined && rank < ownRank) {\n return undefined; // do not fall back from a device name to its model\n }\n return wanted;\n}\n\n/** The numeric bounds an ioBroker state object can carry. */\nexport interface BoundFields {\n /** The smallest value the datapoint declares. */\n min?: number;\n /** The largest value the datapoint declares. */\n max?: number;\n /** The grid the datapoint's values sit on. */\n step?: number;\n}\n\n/**\n * The bound fields, in the order a clearing write lists them. Kept as one list so the snapshot,\n * the comparison and the clearing write can never drift apart.\n */\nexport const BOUND_FIELDS = [\"min\", \"max\", \"step\"] as const;\n\n/**\n * The bounds a stored object carries, with anything that is not a number treated as absent \u2014 an\n * object written by an older version, or by hand in the admin, can hold a string there.\n *\n * @param common the stored object's `common` part\n * @returns its numeric bounds\n */\nexport function boundsOfCommon(common: { min?: unknown; max?: unknown; step?: unknown } | undefined): BoundFields {\n const bounds: BoundFields = {};\n for (const field of BOUND_FIELDS) {\n const value = common?.[field];\n if (typeof value === \"number\") {\n bounds[field] = value;\n }\n }\n return bounds;\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { DeviceRecord } from \"./types\";\nimport type { DiscoveredDevice } from \"./discovery\";\n\ninterface ConfiguredDevice {\n name?: string;\n ip: string;\n}\n\n/**\n * True when a raw config row carries a non-empty ip (the name is optional \u2014 a row\n * with an ip but no name is valid and falls back to the ip as its id, so a device\n * is never silently dropped just because its name was left blank).\n *\n * @param entry a raw config row from the admin device table\n * @returns whether the row is a valid configured device\n */\nfunction isConfiguredDevice(entry: unknown): entry is ConfiguredDevice {\n if (typeof entry !== \"object\" || entry === null) {\n return false;\n }\n const candidate = entry as { name?: unknown; ip?: unknown };\n return (\n typeof candidate.ip === \"string\" &&\n candidate.ip.length > 0 &&\n (candidate.name === undefined || typeof candidate.name === \"string\")\n );\n}\n\n/**\n * Make a string safe for use as an ioBroker object id segment.\n *\n * @param raw the raw string (e.g. a device name)\n * @returns the string with id-unsafe characters replaced by underscores\n */\nexport function sanitizeId(raw: string): string {\n return raw.replace(/[^A-Za-z0-9\\-_]/g, \"_\");\n}\n\n/**\n * Strip the adapter namespace (e.g. `yamaha.0`) from a full state id, leaving the\n * device-relative path (e.g. `living.power`).\n *\n * @param fullId the full state id\n * @param namespace the adapter namespace\n * @returns the id relative to the adapter instance\n */\nexport function stripNamespace(fullId: string, namespace: string): string {\n return fullId.slice(namespace.length + 1);\n}\n\n/**\n * Turn the admin device table (untrusted native config) into device records.\n * Invalid rows are dropped, as are rows whose id collides with the adapter's own\n * reserved `info` branch or with an id already taken \u2014 two names that sanitise to\n * the same id (e.g. \"Living Room\" and \"Living.Room\") would otherwise share one\n * object tree. A dropped collision is reported via `onCollision` so the device does\n * not just silently \"not exist\" for the user.\n *\n * @param raw the raw `native.devices` value\n * @param onCollision called with the dropped row's name/ip and the clashing id\n * @returns validated, de-duplicated device records\n */\nexport function parseDevices(raw: unknown, onCollision?: (dropped: string, takenId: string) => void): DeviceRecord[] {\n if (!Array.isArray(raw)) {\n return [];\n }\n const records: DeviceRecord[] = [];\n const taken = new Set<string>([\"info\"]); // reserved: the adapter's own info channel\n for (const entry of raw) {\n if (!isConfiguredDevice(entry)) {\n continue;\n }\n // Fall back to the ip as the id when the name is blank, so the device still appears\n // instead of vanishing silently.\n const id = sanitizeId(entry.name && entry.name.length > 0 ? entry.name : entry.ip);\n if (taken.has(id)) {\n onCollision?.(entry.name || entry.ip, id);\n continue;\n }\n taken.add(id);\n records.push({ id, ip: entry.ip, source: \"manual\" });\n }\n return records;\n}\n\n/**\n * The set of devices the adapter runs: the manual table plus the discovered store, keyed by id.\n *\n * A manual entry WINS over a discovered one of the same id \u2014 the user typed that address, and\n * the network search must not overwrite it. Before 2.9.0 the two were mutually exclusive (a\n * filled table turned the search off entirely), so making one discovered device manual dropped\n * every other one from the run and `cleanupStaleObjects` deleted their trees.\n *\n * @param manual the rows from the instance's device table\n * @param discovered the records remembered from the network search\n * @returns one record per id, manual first, each tagged with where it came from\n */\nexport function unionDevices(manual: readonly DeviceRecord[], discovered: readonly DeviceRecord[]): DeviceRecord[] {\n const byId = new Map<string, DeviceRecord>();\n for (const device of manual) {\n byId.set(device.id, { ...device, source: \"manual\" });\n }\n for (const device of discovered) {\n if (byId.has(device.id)) {\n continue;\n }\n // An address a manual device already occupies would mean two records talking to one\n // receiver \u2014 the manual one owns it.\n if ([...byId.values()].some(known => known.ip === device.ip)) {\n continue;\n }\n byId.set(device.id, { ...device, source: \"discovered\" });\n }\n return [...byId.values()];\n}\n\n/**\n * Merge freshly discovered devices into the set already known from earlier runs\n * (the auto-discovery standby protection). A known device is kept even when this\n * run's scan did not find it \u2014 a receiver in deep standby answers no SSDP, and its\n * object tree must survive.\n *\n * Identity is the device ID (derived from the friendly name it advertises, or from its\n * address when it advertises none) \u2014 NOT its address. Keyed by address, a receiver that\n * moved to a new address by DHCP was lost for good: the remembered record kept the old\n * address, the same receiver found at the new one produced the same id, and the id clash\n * dropped it. It stayed offline with no way back, because the id is what the whole object\n * tree hangs off. Now the same id simply carries the new address over.\n *\n * A genuine clash remains a clash: a DIFFERENT device sitting on an address another\n * record already claims is skipped and reported through `onCollision`, so a missing\n * device is explainable rather than silent.\n *\n * @param known the device records remembered from earlier runs\n * @param found the devices discovered this run\n * @param onCollision called with the dropped device's name/ip and the clashing id\n * @returns the merged records, one per device id\n */\nexport function mergeDiscovered(\n known: DeviceRecord[],\n found: DiscoveredDevice[],\n onCollision?: (dropped: string, takenId: string) => void,\n): DeviceRecord[] {\n const byId = new Map<string, DeviceRecord>();\n for (const device of known) {\n // \"info\" is the adapter's own channel \u2014 a device may never claim it.\n if (device.id === \"info\" || byId.has(device.id)) {\n continue;\n }\n byId.set(device.id, { ...device });\n }\n for (const device of found) {\n const label = device.name || device.ip;\n const id = sanitizeId(label);\n const remembered = byId.get(id);\n if (remembered) {\n // Same device, possibly at a new address \u2014 carry the address over, keep the id.\n remembered.ip = device.ip;\n continue;\n }\n const ipOwner = [...byId.values()].find(record => record.ip === device.ip);\n if (id === \"info\" || ipOwner) {\n onCollision?.(label, ipOwner?.id ?? id);\n continue;\n }\n byId.set(id, { id, ip: device.ip });\n }\n return [...byId.values()];\n}\n\n/**\n * From all object ids under the instance, pick the stale ones to delete on start:\n * everything that does not belong to a configured device and is outside the\n * adapter's own `info` branch. Removes the previous adapter's whole object tree\n * and any device dropped from the config. Keyed on the configured device ids\n * (not on what was created this run), so it works with the async connect where a\n * device's tree may only appear later \u2014 a configured device's subtree is kept\n * regardless of whether it has connected yet. Deepest first, so children go\n * before their parents.\n *\n * A device the discovery store still remembers is kept too, even when it is not part of\n * this run: switching the network search off is not a delete. Only the card's delete\n * button removes a device \u2014 it takes the record out of the store, and the id stops being\n * remembered here at the same moment. Without that a single hand-entered receiver used to\n * take every found one's tree with it, history and VIS bindings included.\n *\n * @param existing all object ids currently under the instance\n * @param deviceIds the ids of the currently configured devices\n * @param namespace the adapter namespace (e.g. `yamaha.0`)\n * @param remembered ids the discovery store still holds that are not running this time\n * @returns the stale ids to delete, deepest first\n */\nexport function staleObjects(\n existing: string[],\n deviceIds: Set<string>,\n namespace: string,\n remembered: ReadonlySet<string> = new Set(),\n): string[] {\n // No configured devices \u2192 never wipe the whole tree (a user who cleared the\n // device table by accident would otherwise lose every object in one pass).\n // Anchored on the RUNNING set: a run with nothing to run deletes nothing at all.\n if (deviceIds.size === 0) {\n return [];\n }\n const isKept = (fullId: string): boolean => {\n const top = stripNamespace(fullId, namespace).split(\".\")[0];\n return top === \"info\" || deviceIds.has(top) || remembered.has(top);\n };\n return existing.filter(id => !isKept(id)).sort((a, b) => b.length - a.length);\n}\n\n/**\n * Relative state ids an earlier version created under a different path and that this\n * version has renamed or moved. On start-up the old object is deleted so it does not\n * linger orphaned beside the new one \u2014 {@link staleObjects} only removes whole\n * non-configured device trees, not renamed states inside a device that is kept.\n */\n/**\n * The per-source playback-block states of the pre-2.0.0 tree \u2014 the same 19 states\n * used to sit under every player source folder, almost all permanently empty. In\n * 2.0.0 ONE flat block per zone replaces them; these copies are removed on the\n * first start (the sources that keep genuinely own states keep those untouched).\n */\nconst V2_PLAYER_BLOCK_STATES = [\n \"playback\",\n \"artist\",\n \"album\",\n \"track\",\n \"station\",\n \"channelName\",\n \"totalTime\",\n \"elapsedTime\",\n \"repeat\",\n \"shuffle\",\n \"albumArt\",\n \"source\",\n \"play\",\n \"pause\",\n \"stop\",\n \"next\",\n \"prev\",\n \"repeatToggle\",\n \"shuffleToggle\",\n];\n\n/** The source folders that stay in 2.0.0 (own states remain) but lose their block copy. */\nconst V2_SLIMMED_SOURCES = [\n \"netPlayer\",\n \"cd\",\n \"netRadio\",\n \"server\",\n \"usb\",\n \"napster\",\n \"pandora\",\n \"rhapsody\",\n \"sirius\",\n \"pc\",\n \"airplay\",\n \"bluetooth\",\n // Folders that came back with own content after the 2.0.0 cut (iPod/iPod USB: MODE, 2026-09-06;\n // Spotify: the RX-A850 presets, 2026-09-09) \u2014 their 1.x playback copies still have to go.\n \"ipod\",\n \"ipodUsb\",\n \"spotify\",\n];\n\nexport const RENAMED_STATE_IDS = [\n // v2.8.0: the volume datapoint now carries the scale the receiver itself displays, so the two\n // derived states are gone \u2014 `actualVolume` was an exact duplicate of `volume` on every device\n // declaring a single scale, and `actualVolumeMode` a dropdown with one entry there. `inputText`\n // repeated the label the `input` dropdown carries since 2.7.2, and only on MusicCast devices.\n \"actualVolume\",\n \"actualVolumeMode\",\n \"inputText\",\n \"hdmiOut\",\n \"directMode\",\n \"masterPower\",\n \"party\",\n \"partyMute\",\n \"distributionEnable\",\n \"partyEnable\",\n \"remoteCode\",\n // v1.0.0 multiroom regroup: the MusicCast-Link states moved into their own\n // multiroom.group folder, so the tree itself tells device-group from all-zones scope.\n \"multiroom.distributionEnable\",\n // Short-lived v1.0.0 pre-cut id \u2014 mislabeled \"active\", the field means \"enabled for use\".\n \"multiroom.group.streamingActive\",\n \"multiroom.role\",\n \"multiroom.groupId\",\n \"multiroom.groupName\",\n \"multiroom.serverZone\",\n \"multiroom.clientList\",\n \"multiroom.linkClient\",\n \"multiroom.leaveGroup\",\n // ---- v2.0.0 tree rework ------------------------------------------------------\n // Player unification: the per-source copies of the playback block are gone (the\n // slimmed folders keep only their own preset/pairing/drive states).\n ...V2_SLIMMED_SOURCES.flatMap(source => V2_PLAYER_BLOCK_STATES.map(state => `player.${source}.${state}`)),\n // Scenes: the twelve per-name datapoints became the recall dropdown + scene.list.\n ...Array.from({ length: 12 }, (_unused, i) => `scene.name${i + 1}`),\n // Tuner unification: ONE band/frequency/preset; the DAB subunit's FM half moved\n // onto the flat tuner ids, the two per-band frequencies became tuner.frequency.\n \"tuner.amFrequency\",\n \"tuner.fmFrequency\",\n \"tuner.dab.band\",\n \"tuner.dab.preset\",\n \"tuner.dab.fmPreset\",\n \"tuner.dab.fmFrequency\",\n \"tuner.dab.fmSearchMode\",\n \"tuner.dab.fmRdsService\",\n \"tuner.dab.fmRdsProgramType\",\n \"tuner.dab.fmRdsText\",\n \"tuner.dab.fmRdsClock\",\n \"tuner.dab.fmStereo\",\n \"tuner.dab.fmTuned\",\n \"tuner.dab.audioMode\",\n // Sound polish: equalizer and signal info each moved into their own subfolder.\n \"sound.equalizerMode\",\n \"sound.equalizerLow\",\n \"sound.equalizerMid\",\n \"sound.equalizerHigh\",\n \"sound.signalFormat\",\n \"sound.signalSampling\",\n \"sound.signalBits\",\n \"sound.signalBitrate\",\n // HDMI polish: the lip-sync offsets moved into the hdmi folder (the lipSync\n // channel itself is in RENAMED_CHANNELS); the A/B toggles joined the speakers.\n \"advanced.speakerA\",\n \"advanced.speakerB\",\n];\n\n/**\n * Old channel prefixes whose whole subtree this version moved out \u2014 the `system`\n * grab-bag is gone (model/firmware \u2192 info, HDMI outputs \u2192 hdmi, speaker patterns \u2192\n * speakers, input names \u2192 inputNames, master power \u2192 multiroom.masterPower). The channel and\n * every state under it are removed.\n */\nexport const RENAMED_CHANNELS = [\n // pre-0.11 system folder\n \"system\",\n // v0.18.1 multiroom regroup: zone2/3/4, zoneB, flat multiroom states moved under multiroom/.\n \"zone2\",\n \"zone3\",\n \"zone4\",\n \"zoneB\",\n // v1.0.0: stray per-zone copies of the device-global YXC states (the zone loop used to\n // prefix them too, yielding multiroom.zoneN.multiroom.* junk) are swept away.\n \"multiroom.zone2.multiroom\",\n \"multiroom.zone3.multiroom\",\n \"multiroom.zone4.multiroom\",\n // Regrouping: media sources moved under player/, DAB under tuner/, dist \u2192 multiroom. The old\n // flat channels (and their whole subtree) are deleted so the new grouped ones do not sit\n // beside orphaned copies on an upgraded instance.\n \"netRadio\",\n \"server\",\n \"usb\",\n \"spotify\",\n \"deezer\",\n \"tidal\",\n \"napster\",\n \"pandora\",\n \"rhapsody\",\n \"sirius\",\n \"airplay\",\n \"bluetooth\",\n \"pc\",\n \"musicCastLink\",\n \"ipod\",\n \"ipodUsb\",\n \"netPlayer\",\n \"cd\",\n \"dab\",\n \"dist\",\n // Sound/Advanced regroup: DSP/tone-tuning states moved under sound.*, setup-only states\n // (+ the speakers/initialVolume/inputNames subtrees, already dotted before) under advanced.*.\n // {@link renamedObjectIds} also checks these against a stripped zone2/3/4 prefix, so one\n // entry here catches a MAIN state and its zoned copies (e.g. \"straight\" and \"zone2.straight\").\n \"straight\",\n \"enhancer\",\n \"pureDirect\",\n \"direct\",\n \"adaptiveDrc\",\n \"surroundAI\",\n \"surroundDecoder\",\n \"cinemaDsp3d\",\n \"extraBass\",\n \"bass\",\n \"treble\",\n \"subwooferTrim\",\n \"balance\",\n \"dialogueLevel\",\n \"dialogueLift\",\n \"dtsDialogueControl\",\n \"monaural\",\n \"surround3d\",\n \"adaptiveDspLevel\",\n \"audioSelect\",\n \"linkControl\",\n \"linkAudioDelay\",\n \"linkAudioQuality\",\n \"contentsDisplay\",\n \"equalizerLow\",\n \"equalizerMid\",\n \"equalizerHigh\",\n \"clearVoice\",\n \"bassExtension\",\n \"ypaoVolume\",\n \"maxVolume\",\n \"speakerA\",\n \"speakerB\",\n \"speakers\",\n \"initialVolume\",\n \"inputNames\",\n // ---- v2.0.0 tree rework: the always-empty source channels are gone entirely\n // (their playback lives in the flat per-zone block), lip sync moved into hdmi.\n // `player.ipod`/`player.ipodUsb` came BACK on 2026-09-06: the official command list gives\n // both sources an own `MODE` (Normal/Extended), so the folders carry genuine content again\n // and must not be deleted on every start \u2014 the migration-table guard in pure-helpers.test.ts\n // is what caught the contradiction. `player.spotify` followed on 2026-09-09: the RX-A850 list\n // gives Spotify a preset recall and store, so its folder carries content again.\n \"player.deezer\",\n \"player.tidal\",\n \"player.musicCastLink\",\n \"lipSync\",\n];\n\n/**\n * Read-capable state objects under a configured device whose state NEVER carried a\n * value (no value, no last-change). They are over-declarations of an earlier adapter\n * version \u2014 today's claim-with-proof creation would not make them \u2014 and deleting them\n * is lossless: there is no value and no history to lose, and anything a transport\n * legitimately claims is recreated right after, when the device connects. Runs once\n * per adapter version (the caller keeps a marker), so a freshly created state that is\n * merely waiting for its first value does not flap on every start. Excluded: buttons\n * and other write-only states (naturally valueless) and the `info.` header the adapter\n * itself maintains for every device, connected or not. A recording setting\n * (`common.custom`) is deliberately NO factor: whether anyone records a datapoint is\n * the user's business and says nothing about whether the datapoint belongs in the\n * tree \u2014 that is solely the adapter's responsibility (krobi, 2.0.3). On a never-filled\n * state there is nothing recorded to lose anyway.\n *\n * @param objects all objects under the instance, keyed by full id\n * @param states all states under the instance, keyed by full id\n * @param deviceIds the ids of the devices to sweep\n * @param namespace the adapter namespace (e.g. `yamaha.0`)\n * @returns the full ids of never-filled read-capable states\n */\nexport function neverWrittenStateIds(\n objects: Record<string, { type?: string; common?: unknown } | undefined>,\n states: Record<string, { val?: unknown; lc?: number } | null | undefined>,\n deviceIds: Set<string>,\n namespace: string,\n): string[] {\n const ids: string[] = [];\n for (const [fullId, object] of Object.entries(objects)) {\n const common = object?.common as { read?: boolean } | undefined;\n if (object?.type !== \"state\" || common?.read === false) {\n continue;\n }\n const relative = stripNamespace(fullId, namespace);\n const top = relative.split(\".\")[0];\n if (!deviceIds.has(top) || relative.slice(top.length + 1).startsWith(\"info.\")) {\n continue;\n }\n const state = states[fullId];\n if (!state || ((state.val === null || state.val === undefined) && !state.lc)) {\n ids.push(fullId);\n }\n }\n return ids;\n}\n\n/**\n * Channels under a connected device that carry no datapoint anywhere below them \u2014 the folders a\n * tree rework empties but never removes.\n *\n * Both existing sweeps miss these on purpose: {@link staleObjects} only knows whole device trees\n * and named paths, and {@link neverWrittenStateIds} filters on `type === \"state\"`. So a folder\n * whose datapoints the v2.0.0 migration deleted (`player.server` \u2014 the SERVER source keeps no own\n * datapoint in the new tree) survives as an empty promise in the object tree, and every future\n * rework that empties a folder instead of listing it would leave the same kind of leftover.\n *\n * Nested empties resolve by themselves: a channel counts as filled only when a STATE exists\n * somewhere beneath it, so a folder holding nothing but other empty folders goes too. Deepest\n * first, so children are deleted before their parents.\n *\n * @param objects all objects currently under the instance\n * @param deviceIds the devices that have connected in this run (an offline device is left alone)\n * @param namespace the adapter namespace (e.g. `yamaha.0`)\n * @returns the full channel ids to delete, deepest first\n */\nexport function childlessChannelIds(\n objects: Record<string, { type?: string } | undefined>,\n deviceIds: Set<string>,\n namespace: string,\n): string[] {\n // Every path segment that has a datapoint below it, collected once over all objects.\n const filled = new Set<string>();\n for (const [fullId, object] of Object.entries(objects)) {\n if (object?.type !== \"state\") {\n continue;\n }\n for (let cut = fullId.lastIndexOf(\".\"); cut > 0; cut = fullId.lastIndexOf(\".\", cut - 1)) {\n filled.add(fullId.slice(0, cut));\n }\n }\n const ids: string[] = [];\n for (const [fullId, object] of Object.entries(objects)) {\n if (object?.type !== \"channel\" || filled.has(fullId)) {\n continue;\n }\n const relative = stripNamespace(fullId, namespace);\n if (deviceIds.has(relative.split(\".\")[0])) {\n ids.push(fullId);\n }\n }\n return ids.sort((a, b) => b.length - a.length);\n}\n\n/**\n * The full ids of renamed old states (and old channel subtrees) that still exist\n * under a configured device, to be deleted on start-up so no orphan lingers beside\n * the new object. Deepest first, so children go before their parents.\n *\n * @param existing all object ids currently under the instance\n * @param deviceIds the ids of the currently configured devices\n * @param namespace the adapter namespace (e.g. `yamaha.0`)\n * @returns the full old ids to delete, deepest first\n */\nexport function renamedObjectIds(existing: string[], deviceIds: Set<string>, namespace: string): string[] {\n const stale: string[] = [];\n for (const deviceId of deviceIds) {\n const base = `${namespace}.${deviceId}.`;\n for (const full of existing) {\n if (!full.startsWith(base)) {\n continue;\n }\n const rel = full.slice(base.length);\n // Strip an optional zone prefix before matching too, so one RENAMED_CHANNELS entry\n // (e.g. \"straight\") catches both the MAIN state and its zoned copies \u2014 the old flat\n // \"zone2.\" form and today's \"multiroom.zone2.\" form alike (v2.0.0 renames live in\n // zoned folders, e.g. multiroom.zone2.scene.name1).\n const zone = /^(?:multiroom\\.)?zone[234]\\./.exec(rel)?.[0] ?? \"\";\n const template = rel.slice(zone.length);\n const renamedState = RENAMED_STATE_IDS.includes(rel) || RENAMED_STATE_IDS.includes(template);\n const underRenamedChannel = RENAMED_CHANNELS.some(\n ch => rel === ch || rel.startsWith(`${ch}.`) || template === ch || template.startsWith(`${ch}.`),\n );\n if (renamedState || underRenamedChannel) {\n stale.push(full);\n }\n }\n }\n return stale.sort((a, b) => b.length - a.length);\n}\n\n/**\n * The device row to carry over from the previous adapter's single-device config,\n * or undefined if nothing needs migrating. The old yamaha stored one receiver as\n * `config.ip` (older installs: `config.IP`); the new adapter uses a `devices`\n * table. Only migrates when the table is still empty, so it runs once.\n *\n * The old value could be a hostname (its HTTP client resolved names) and could\n * carry a `:port` suffix (its HTTP library split host:port). A port suffix would\n * break every transport here \u2014 YNCA is TCP :50000, YXC/XML build `http://<ip>\u2026`\n * \u2014 so it is stripped; the host/IP itself is carried over as-is.\n *\n * @param config the instance's native config\n * @returns the row to add to the devices table, or undefined\n */\nexport function legacyDeviceRow(config: Record<string, unknown>): { name: string; ip: string } | undefined {\n if (Array.isArray(config.devices) && config.devices.length > 0) {\n return undefined;\n }\n const raw =\n typeof config.ip === \"string\" && config.ip\n ? config.ip\n : typeof config.IP === \"string\" && config.IP\n ? config.IP\n : undefined;\n if (!raw) {\n return undefined;\n }\n const ip = raw.trim().replace(/:\\d+$/, \"\");\n return ip ? { name: ip, ip } : undefined;\n}\n\n/**\n * How trustworthy a display-name candidate is. A name the device carries for itself\n * (the MusicCast zone name a user typed in the app) beats its model designation.\n */\nexport const LABEL_RANK = { model: 1, deviceName: 2 } as const;\n\n/** Rank of a display-name candidate \u2014 see {@link LABEL_RANK}. */\nexport type LabelRank = (typeof LABEL_RANK)[keyof typeof LABEL_RANK];\n\n/**\n * Zone names that say nothing about the device \u2014 a receiver ships with these and a\n * user who never renamed the zone would end up with \"Main Zone\" as the device name.\n */\nconst GENERIC_ZONE_NAMES = new Set([\"main\", \"main zone\", \"mainzone\", \"zone\", \"zone 1\", \"zone1\"]);\n\n/**\n * Is this candidate worth showing as a device name?\n *\n * @param candidate the reported name\n * @returns true when it carries information about this particular device\n */\nexport function isUsefulDeviceName(candidate: string | undefined): boolean {\n const trimmed = (candidate ?? \"\").trim();\n return trimmed.length > 0 && !GENERIC_ZONE_NAMES.has(trimmed.toLowerCase());\n}\n\n/**\n * The display name to write onto a device object, or undefined to leave it alone.\n *\n * An upgraded instance carries the IP as its device name: the previous adapter knew\n * only an IP, so the migration had nothing else to call the device, and the object id\n * \u2014 which must not change, every history and visualisation binding hangs off it \u2014\n * became that IP. This decides when the adapter may replace that placeholder with\n * something a user recognises.\n *\n * Two things must never be overwritten: a name the user typed, and a better name by a\n * weaker source. The adapter therefore only writes over its own placeholder (the id\n * itself) or over what it wrote last, and only when the new candidate ranks at least\n * as high as the one behind the current name.\n *\n * @param current the device object's present `common.name`\n * @param deviceId the object id, which is also the placeholder name\n * @param candidate the newly reported name\n * @param rank how trustworthy the candidate is\n * @param ownName the name this adapter wrote last for the device, if any\n * @param ownRank the rank behind {@link ownName}\n * @returns the name to write, or undefined when the current name stays\n */\nexport function nextDeviceLabel(\n current: string | undefined,\n deviceId: string,\n candidate: string | undefined,\n rank: LabelRank,\n ownName?: string,\n ownRank?: LabelRank,\n): string | undefined {\n const wanted = (candidate ?? \"\").trim();\n if (!isUsefulDeviceName(wanted) || wanted === current) {\n return undefined;\n }\n const isPlaceholder = current === undefined || current === deviceId;\n const isOurs = ownName !== undefined && current === ownName;\n if (!isPlaceholder && !isOurs) {\n return undefined; // the user named this device \u2014 theirs wins\n }\n if (isOurs && ownRank !== undefined && rank < ownRank) {\n return undefined; // do not fall back from a device name to its model\n }\n return wanted;\n}\n\n/** The numeric bounds an ioBroker state object can carry. */\nexport interface BoundFields {\n /** The smallest value the datapoint declares. */\n min?: number;\n /** The largest value the datapoint declares. */\n max?: number;\n /** The grid the datapoint's values sit on. */\n step?: number;\n}\n\n/**\n * The bound fields, in the order a clearing write lists them. Kept as one list so the snapshot,\n * the comparison and the clearing write can never drift apart.\n */\nexport const BOUND_FIELDS = [\"min\", \"max\", \"step\"] as const;\n\n/**\n * The bounds a stored object carries, with anything that is not a number treated as absent \u2014 an\n * object written by an older version, or by hand in the admin, can hold a string there.\n *\n * @param common the stored object's `common` part\n * @returns its numeric bounds\n */\nexport function boundsOfCommon(common: { min?: unknown; max?: unknown; step?: unknown } | undefined): BoundFields {\n const bounds: BoundFields = {};\n for (const field of BOUND_FIELDS) {\n const value = common?.[field];\n if (typeof value === \"number\") {\n bounds[field] = value;\n }\n }\n return bounds;\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/build/main.js CHANGED
@@ -194,9 +194,11 @@ class Yamaha extends utils.Adapter {
194
194
  for (const device of devices) {
195
195
  this.knownDeviceIps.add(device.ip);
196
196
  }
197
+ const idle = this.discovering ? [] : await this.rememberedButIdle(devices);
197
198
  await this.snapshotExistingDatapoints();
198
- await this.cleanupStaleObjects(new Set(devices.map((device) => device.id)));
199
+ await this.cleanupStaleObjects(new Set(devices.map((device) => device.id)), new Set(idle.map((device) => device.id)));
199
200
  await this.ensureInstanceInfoObjects();
201
+ await this.markIdleDevicesOffline(idle);
200
202
  await this.subscribeToStates();
201
203
  const pushReceiver = new import_push_receiver.YxcPushReceiver({
202
204
  log: { debug: (message) => this.log.debug(message), warn: (message) => this.log.warn(message) },
@@ -322,6 +324,59 @@ class Yamaha extends utils.Adapter {
322
324
  const mode = (_a = this.config.discovery) != null ? _a : "auto";
323
325
  return mode === "always" || mode === "auto" && manualCount === 0;
324
326
  }
327
+ /**
328
+ * The devices the discovery store still remembers that are NOT part of this run: the network
329
+ * search is off, so nothing looked for them. They keep their objects — switching the search
330
+ * off is a configuration change, not a delete, and only the card's delete button removes a
331
+ * device (it takes the record out of the store in the same step).
332
+ *
333
+ * @param running the devices this run does start
334
+ * @returns the remembered records that stay idle
335
+ */
336
+ async rememberedButIdle(running) {
337
+ const runningIds = new Set(running.map((device) => device.id));
338
+ const remembered = await (0, import_discovered_store.readDiscovered)((0, import_discovered_store_deps.discoveredStoreDeps)(this));
339
+ return remembered.filter((device) => !runningIds.has(device.id));
340
+ }
341
+ /**
342
+ * Stamp the idle devices disconnected and say once why they are idle. ioBroker keeps a
343
+ * state's last value forever, so a kept tree would otherwise still claim "connected,
344
+ * YNCA ✓" while nothing is talking to the device — the same lie the disconnected stamp in
345
+ * {@link startDevice} prevents for a device that does run.
346
+ *
347
+ * Every write is guarded on the object being there: nothing starts these devices, so
348
+ * nothing creates their header either, and a blind write would leave bare orphan states
349
+ * behind for a device whose tree is already gone.
350
+ *
351
+ * @param idle the remembered devices that do not run this time
352
+ */
353
+ async markIdleDevicesOffline(idle) {
354
+ var _a;
355
+ if (idle.length === 0) {
356
+ return;
357
+ }
358
+ for (const device of idle) {
359
+ const ids = [
360
+ `${device.id}.info.connection`,
361
+ ...TRANSPORT_IDS.map((protocol) => `${device.id}.info.transports.${protocol}`)
362
+ ];
363
+ for (const id of ids) {
364
+ if (await this.getObjectAsync(id)) {
365
+ await this.setState(id, { val: false, ack: true });
366
+ }
367
+ }
368
+ }
369
+ const names = idle.map((device) => device.id).join(", ");
370
+ if (((_a = this.config.discovery) != null ? _a : "auto") === "never") {
371
+ this.log.info(
372
+ `${idle.length} remembered device(s) stay idle \u2014 the network search is off (${names}); their objects are kept, use the delete button on a card to remove one`
373
+ );
374
+ } else {
375
+ this.log.warn(
376
+ `${idle.length} remembered device(s) are not running: the device list is filled and the network search is set to Automatic (${names}) \u2014 their objects are kept; set the search to Always to run them next to the devices you entered`
377
+ );
378
+ }
379
+ }
325
380
  /**
326
381
  * Arm a background search because an auto-found device is offline — the only way back to a
327
382
  * receiver that moved to another address, since it answers at the remembered one no more.
@@ -588,11 +643,12 @@ class Yamaha extends utils.Adapter {
588
643
  * subtree is kept whether or not it has connected yet.
589
644
  *
590
645
  * @param deviceIds the ids of the currently configured devices
646
+ * @param remembered ids the discovery store still holds that are idle this run
591
647
  */
592
- async cleanupStaleObjects(deviceIds) {
648
+ async cleanupStaleObjects(deviceIds, remembered) {
593
649
  const allObjects = await this.getAdapterObjectsAsync();
594
650
  const existing = Object.keys(allObjects);
595
- const stale = (0, import_pure_helpers.staleObjects)(existing, deviceIds, this.namespace);
651
+ const stale = (0, import_pure_helpers.staleObjects)(existing, deviceIds, this.namespace, remembered);
596
652
  const renamed = (0, import_pure_helpers.renamedObjectIds)(existing, deviceIds, this.namespace);
597
653
  const config = this.config;
598
654
  const disabled = existing.filter((full) => {
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 { createSocket } from \"node:dgram\";\nimport { get as httpGet } from \"node:http\";\nimport { networkInterfaces } from \"node:os\";\nimport { attemptDevice } from \"./lib/attempt-device\";\nimport { searchInterfaces } from \"./lib/network-interfaces\";\nimport { isGroupEnabled } from \"./lib/catalog/groups\";\nimport type { ObjectDef } from \"./lib/catalog/types\";\nimport {\n asPercentObject,\n fromPercent,\n isAmpVolumeId,\n toPercent,\n volumeBoundsOf,\n type VolumeBounds,\n} from \"./lib/catalog/volume-percent\";\nimport { iconForModel } from \"./lib/device-type\";\nimport {\n BOUND_FIELDS,\n type BoundFields,\n boundsOfCommon,\n childlessChannelIds,\n LABEL_RANK,\n type LabelRank,\n legacyDeviceRow,\n mergeDiscovered,\n neverWrittenStateIds,\n nextDeviceLabel,\n parseDevices,\n unionDevices,\n renamedObjectIds,\n staleObjects,\n stripNamespace,\n} from \"./lib/pure-helpers\";\nimport { errorMessage, MAX_HTTP_BODY_BYTES } from \"./lib/util\";\nimport { tName } from \"./lib/i18n\";\nimport { discoverYamaha } from \"./lib/discovery\";\nimport { readDiscovered, readIgnored, writeDiscovered } from \"./lib/discovered-store\";\nimport { discoveredStoreDeps, ignoredStoreDeps } from \"./lib/discovered-store-deps\";\nimport { YxcPushReceiver } from \"./lib/yxc/push-receiver\";\nimport { YamahaDeviceManagement } from \"./device-management\";\nimport type { DeviceSource, DeviceRecord } from \"./lib/types\";\nimport { DeviceSupervisor, type ConnectionHandle } from \"./lib/lifecycle/device-supervisor\";\nimport { ReconnectStrategy } from \"./lib/lifecycle/reconnect-strategy\";\nimport { ReachabilityDedup } from \"./lib/lifecycle/reachability-dedup\";\nimport type { YncaSubunitCache } from \"./lib/ynca/subunit-cache\";\nimport type { ProbeMemory } from \"./lib/lifecycle/probe-memory\";\nimport { DeviceProfileStore } from \"./lib/lifecycle/capability-profile\";\n\n/** Supervisor reconnect backoff bounds (exponential: 1s, 2s \u2026 capped at 60s). */\nconst RECONNECT_BASE_MS = 1000;\nconst RECONNECT_MAX_MS = 60000;\n\n/** Abort a discovery description fetch after this long, so a dead device cannot hang it. */\nconst FETCH_TIMEOUT_MS = 4000;\n\n/** How often the discovery M-SEARCH is repeated \u2014 multicast is lossy, one dropped packet must not hide a receiver. */\nconst SSDP_SEARCH_BURST = 3;\n/** Spacing between the repeated M-SEARCH sends, inside the collect window. */\nconst SSDP_SEARCH_INTERVAL_MS = 1000;\n\n/** The three transports in attempt order \u2014 also the per-transport `info.transports.*` state ids. */\nconst TRANSPORT_IDS = [\"ynca\", \"yxc\", \"xml\"] as const;\n\n/**\n * How long the datapoint balance waits for quiet before it logs. Devices connect\n * asynchronously and in parallel, so the line has to outlast the slowest of them.\n */\nconst DATAPOINT_BALANCE_SETTLE_MS = 5000;\n\n/**\n * Shortest gap between two network searches triggered by an offline auto-found device. A\n * receiver that moved to another address answers nowhere else, so the search is the only way\n * back to it \u2014 but a device that is simply switched off must not turn that into a scan loop.\n */\nconst REDISCOVER_MIN_INTERVAL_MS = 300000;\n\n/**\n * How long a device object's native writes are collected before ONE extendObject carries them\n * (the probe memory persists on every change \u2014 dozens within a first connect's first second).\n */\nconst NATIVE_PERSIST_WINDOW_MS = 250;\n\n/**\n * A map or set keyed by namespace-relative state ids \u2014 the shape {@link YamahaAdapter.forgetUnder}\n * prunes when a device goes. Structural on purpose: `Map<string, T>` and `Set<string>` both fit.\n */\ninterface StateKeyedCache {\n keys(): IterableIterator<string>;\n delete(key: string): boolean;\n}\n\n/** A device's native patch waiting for its coalescing window to end. */\ninterface PendingNative {\n /** The merged patch (latest value per key wins). */\n native: Record<string, unknown>;\n /** The window timer; undefined when the adapter refused one (shutdown) and the write ran at once. */\n timer?: ioBroker.Timeout;\n}\n\n/**\n * ioBroker.yamaha \u2014 controls Yamaha AV receivers and MusicCast devices.\n *\n * Each configured device is driven by a supervisor that keeps a multi-transport\n * handle online: every protocol the device answers \u2014 YNCA (amp control over a held\n * TCP connection, event-pushed), YXC (MusicCast, push + poll), XML/YNC (pre-2010,\n * polled over HTTP) \u2014 connects in parallel on one object tree, each datapoint owned\n * by the best-fitting transport. All YXC devices share one UDP push receiver, keyed\n * by source IP.\n */\nexport class Yamaha extends utils.Adapter {\n private readonly supervisors: DeviceSupervisor[] = [];\n /** deviceId \u2192 its supervisor, so a state change goes to ONE device, not to all of them. */\n /**\n * What each volume datapoint declares on the DEVICE'S OWN scale, while percent mode replaces\n * that declaration with 0\u2026100 %. Filled by `upsertObject` \u2014 the object is always written before\n * any value for it \u2014 and read by both value directions, so the conversion has exactly one\n * source. Keyed by the full state id, so zones and devices never mix.\n */\n private readonly volumeScales = new Map<string, VolumeBounds>();\n /**\n * Per volume datapoint, the definition the coordinator produced BEFORE percent had its say \u2014\n * what the live switch rebuilds from, so turning it changes the object without a restart.\n */\n private readonly volumeDefs = new Map<string, ObjectDef>();\n /**\n * Per device, whether its volume datapoints read 0\u2026100 %. A device setting, not an instance\n * one: the adapter serves several receivers and 2.8.0's single checkbox hit all of them.\n */\n private readonly volumePercent = new Map<string, boolean>();\n /**\n * The instance-wide percent switch of 2.8.0, read once per start. It decides what a device\n * that has not been asked yet inherits \u2014 see {@link ensureDeviceHeader}.\n */\n private legacyVolumePercent = false;\n\n private readonly supervisorById = new Map<string, DeviceSupervisor>();\n private readonly deviceConnected = new Map<string, boolean>();\n /** deviceId \u2192 the record it is currently running with, so an address change is visible. */\n private readonly deviceRecords = new Map<string, DeviceRecord>();\n /** The addresses of all supervised devices \u2014 a multiroom group resolves its clients through it. */\n private readonly knownDeviceIps = new Set<string>();\n /** Whether the network search runs in this instance \u2014 see {@link searchesTheNetwork}. */\n private discovering = false;\n /** Armed while an auto-found device is offline: the search that can bring it back. */\n private rediscoverTimer: ioBroker.Timeout | undefined;\n /** When the last background search ran, so the retry cannot become a scan loop. */\n private lastRediscovery = 0;\n private pushReceiver: YxcPushReceiver | undefined;\n /**\n * Set the moment teardown begins: a connect attempt still in flight then resolves into\n * a closing adapter and must not arm keepalives/timers any more \u2014 the framework would\n * refuse them anyway, but with a warn line per attempt (\"setInterval called, but\n * adapter is shutting down\", seen live on the 1.7.0 upgrade restart).\n */\n private unloading = false;\n /** Device-manager backend: the receivers as cards with add/edit/delete. */\n private readonly deviceManagement: YamahaDeviceManagement;\n /**\n * Every datapoint that existed when this run started, filled ONCE before the cleanup and\n * before any device connects. Without it the balance below would report the whole tree as\n * new on every restart: `upsertObject` runs `extendObject` on every state it touches (the\n * role/unit retrofit), so \"did the create path run?\" is not the same question as \"is this\n * datapoint new?\".\n */\n private readonly knownDatapoints = new Set<string>();\n /**\n * The `common.states` map every existing datapoint carried when this run started, then the\n * map last written by this run \u2014 what a clearing write has to be judged against (#619).\n * Filled from the same start-up read as {@link knownDatapoints}; no per-state database read.\n */\n private readonly storedStates = new Map<string, Record<string, string>>();\n /**\n * The numeric bounds every existing datapoint carried when this run started, then the ones\n * last written by this run \u2014 judged the same way {@link storedStates} is, from the one\n * start-up read, never a database read per state.\n *\n * A bound the new definition DROPS has to be cleared explicitly: `extendObject` merges, so an\n * old `min`/`max` outlives the definition that put it there forever. Measured on\n * `tuner.frequency`, whose FM-only envelope had to go once a DAB receiver reported 180064 kHz\n * into it \u2014 without a clearing write exactly the installations with the problem would keep it.\n */\n private readonly storedBounds = new Map<string, BoundFields>();\n /** State ids (namespace-relative) some transport upserted in THIS run \u2014 live claims. */\n private readonly touchedThisRun = new Set<string>();\n /** Devices that reported connected at least once in this run (gates the orphan purge). */\n private readonly readyDevices = new Set<string>();\n private createdDatapoints = 0;\n private removedDatapoints = 0;\n /** Debounce for the balance line, so one config change produces ONE line, not one per device. */\n private balanceTimer: ioBroker.Timeout | undefined;\n /** Set when the start-up snapshot failed \u2014 a balance without it would be wrong, so none is written. */\n private balanceDisabled = false;\n /**\n * True while the settle pass runs. Its own purges report their removals, which used to\n * re-arm the timer and run the whole pass a second time five seconds later \u2014 two more full\n * reads of the object tree for a round that could only ever remove nothing (audit 2026-09-06).\n */\n private balanceSettling = false;\n /**\n * Latched after the first failed database write, so an outage warns once and the\n * repeats stay at debug until a write goes through again (nut2 `failedUps` pattern).\n */\n private stateWritesFailing = false;\n /** Per device, its capability profile (probe memory, YNCA snapshot, purge marker) \u2014 see loadDeviceProfile. */\n private readonly profiles = new Map<string, DeviceProfileStore>();\n /** Per device, the native patch inside its coalescing window (see persistDeviceNative). */\n private readonly pendingNative = new Map<string, PendingNative>();\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: \"yamaha\",\n });\n\n this.on(\"ready\", this.onReady.bind(this));\n this.on(\"stateChange\", this.onStateChange.bind(this));\n this.on(\"unload\", this.onUnload.bind(this));\n this.deviceManagement = new YamahaDeviceManagement(this);\n }\n\n /** Start a supervisor for each configured device, then subscribe to state changes. */\n private async onReady(): Promise<void> {\n try {\n this.log.info('starting \u2014 a \"ready\" message will follow for each device');\n await this.setState(\"info.connection\", { val: false, ack: true });\n await this.migrateLegacyDevice();\n await this.migrateGroupZones();\n // The running set is the UNION of the device table and the discovery store, and whether\n // the network search runs at all is its own setting. Until 2.9.0 the table WAS the switch\n // \u2014 filled meant manual, empty meant auto \u2014 so the two could never be combined, and\n // turning a single discovered device into a manual one dropped every other one from the\n // run (their trees went with them). XML/pre-2010 devices never answer SSDP, so they are\n // always added by hand; that is exactly the case the mixed mode exists for.\n const configured = parseDevices(this.config.devices, (dropped, takenId) =>\n this.log.warn(`device \"${dropped}\" skipped \u2014 its object id \"${takenId}\" is already used by another device`),\n );\n // The 2.8.0 instance-wide percent switch: read from the instance OBJECT, not from\n // `this.config`. The key is out of the config schema now, so `this.config` is no longer a\n // source to build on \u2014 the object still carries what the user chose.\n const instance = await this.getForeignObjectAsync(`system.adapter.${this.namespace}`);\n this.legacyVolumePercent =\n (instance?.native as { volumeAsPercent?: unknown } | undefined)?.volumeAsPercent === true;\n this.discovering = this.searchesTheNetwork(configured.length);\n const devices = unionDevices(configured, this.discovering ? await this.autoDiscover() : []);\n if (this.unloading) {\n // Stopped during the initial network search: it resolves on its own clock, and\n // everything below \u2014 push socket, subscriptions, device sockets and timers \u2014\n // would come up on an instance that is already gone, with nothing left to close it.\n return;\n }\n // The start-up search (blocking first setup, or the background one below) is the first\n // search of this run \u2014 an offline device must not fire another one right behind it.\n this.lastRediscovery = Date.now();\n for (const device of devices) {\n this.knownDeviceIps.add(device.ip);\n }\n // Before the cleanup and before any device connects \u2014 see knownDatapoints.\n await this.snapshotExistingDatapoints();\n await this.cleanupStaleObjects(new Set(devices.map(device => device.id)));\n await this.ensureInstanceInfoObjects();\n await this.subscribeToStates();\n const pushReceiver = new YxcPushReceiver({\n log: { debug: message => this.log.debug(message), warn: message => this.log.warn(message) },\n schedule: (cb, ms) => this.setTimeout(cb, ms),\n cancel: handle => this.clearTimeout(handle),\n });\n pushReceiver.start();\n this.pushReceiver = pushReceiver;\n if (configured.length > 0) {\n this.log.info(`setting up ${devices.length} configured device(s)...`);\n }\n for (const device of devices) {\n // Per device, so one failure does not cost the rest of the run: startDevice writes\n // header objects and the disconnected stamp, and a states/objects hiccup on device\n // two used to abort onReady \u2014 devices three and four never came up, the overview was\n // never written and the background search never ran (audit 2026-09-06).\n try {\n await this.startDevice(device, pushReceiver);\n } catch (e) {\n this.log.error(`${device.id}: could not be set up (${errorMessage(e)}) \u2014 the other devices continue`);\n }\n }\n this.writeDeviceOverview();\n // Auto mode with remembered devices: they started WITHOUT waiting for the network\n // search \u2014 it runs behind them, adds newcomers and moves a device that changed address.\n if (this.discovering && devices.length > 0) {\n void this.discoverAdditionalDevices(pushReceiver);\n }\n } catch (e) {\n this.log.error(`onReady failed: ${errorMessage(e)}`);\n }\n }\n\n /**\n * Bring one device under supervision: header objects, disconnected stamp, the\n * per-device caches, and the supervisor that keeps it connected. Factored out of\n * onReady so the background discovery can start a late-found device the same way.\n *\n * @param device the device record\n * @param pushReceiver the shared YXC push receiver\n */\n private async startDevice(device: DeviceRecord, pushReceiver: YxcPushReceiver): Promise<void> {\n // Both callers check `unloading` after their network search resolves (onReady and\n // discoverAdditionalDevices) \u2014 a device handed over after onUnload never gets here.\n this.deviceConnected.set(device.id, false);\n this.deviceRecords.set(device.id, { ...device });\n this.knownDeviceIps.add(device.ip);\n await this.ensureDeviceHeader(device.id, device.ip, device.source ?? \"discovered\");\n // Stamp it disconnected BEFORE the first attempt: ioBroker keeps a state's last value\n // forever, so a crash or a power cut would otherwise leave the device green until it\n // reports again \u2014 and a device that never answers would stay green for good.\n await this.setState(`${device.id}.info.connection`, { val: false, ack: true });\n // The three protocol flags follow the same rule: left at their last value, a crash\n // would show \"YNCA connected\" on the card next to a red connection dot \u2014 for good, if\n // the device never answers again.\n this.setTransports(device.id, []);\n const reachability = new ReachabilityDedup();\n // Held here, not in the controllers: those are rebuilt on every connection attempt;\n // persisted at the device object (one capability profile), so a restart starts from the\n // remembered answers.\n const profile = await this.loadDeviceProfile(device.id);\n const subunitCache = profile.subunitCache;\n const probeMemory = profile.probeMemory;\n const supervisor = new DeviceSupervisor({\n attempt: () =>\n this.attemptDevice(device, pushReceiver, this.knownDeviceIps, reachability, subunitCache, probeMemory),\n schedule: (cb, ms) => this.setTimeout(cb, ms),\n cancel: handle => this.clearTimeout(handle as ioBroker.Timeout | undefined),\n onConnectionChange: connected => this.reportConnection(device.id, connected),\n backoff: new ReconnectStrategy(RECONNECT_BASE_MS, RECONNECT_MAX_MS),\n log: {\n debug: message => this.log.debug(message),\n info: message => this.log.info(message),\n warn: message => this.log.warn(message),\n },\n });\n this.supervisors.push(supervisor);\n this.supervisorById.set(device.id, supervisor);\n supervisor.start();\n }\n\n /**\n * The background half of auto-discovery: search the network, bring devices online that are\n * not supervised yet, and move a device that answers at a NEW address. The remembered devices\n * did not wait for this \u2014 the search window (seconds) used to gate every restart although the\n * devices were already known.\n *\n * The address half matters because the object id \u2014 and with it the whole tree, its history and\n * every visualisation binding \u2014 is fixed to the device, not to where it sits. A receiver that\n * moved by DHCP is the same device at a new address, so its supervisor is rebuilt there instead\n * of retrying an address nobody answers at any more.\n *\n * @param pushReceiver the shared YXC push receiver\n */\n private async discoverAdditionalDevices(pushReceiver: YxcPushReceiver): Promise<void> {\n try {\n const merged = await this.runDiscovery();\n if (this.unloading) {\n return; // the search outlived the adapter \u2014 nothing may start now\n }\n let changed = false;\n for (const device of merged) {\n const running = this.deviceRecords.get(device.id);\n try {\n if (!running) {\n this.log.info(`discovery found ${device.id} \u2014 setting up`);\n await this.startDevice(device, pushReceiver);\n changed = true;\n } else if (running.ip !== device.ip) {\n this.log.info(`${device.id}: address changed from ${running.ip} to ${device.ip} \u2014 reconnecting it there`);\n this.knownDeviceIps.delete(running.ip);\n this.stopDevice(device.id);\n await this.startDevice(device, pushReceiver);\n changed = true;\n }\n } catch (e) {\n // Same rule as the start-up loop: one device must not end the round for the others.\n this.log.error(`${device.id}: could not be set up (${errorMessage(e)}) \u2014 the other devices continue`);\n }\n }\n if (changed) {\n this.writeDeviceOverview();\n }\n } catch (e) {\n this.log.warn(`background discovery failed: ${errorMessage(e)}`);\n }\n }\n\n /**\n * Whether this instance searches the network at all.\n *\n * Three operating modes out of two independent things \u2014 this setting and the device table:\n * `auto` searches while the table is empty (what every installation did before 2.9.0, so an\n * update changes nothing by itself), `always` searches next to a filled table (mixed mode),\n * `never` runs the table alone. The setting is three-valued on purpose: a checkbox would need\n * a default in `io-package.json`, and either value would be wrong for half the existing\n * installations \u2014 `auto` is right for all of them without writing anything.\n *\n * @param manualCount how many devices the instance's table holds\n * @returns whether the network search runs\n */\n private searchesTheNetwork(manualCount: number): boolean {\n const mode = this.config.discovery ?? \"auto\";\n return mode === \"always\" || (mode === \"auto\" && manualCount === 0);\n }\n\n /**\n * Arm a background search because an auto-found device is offline \u2014 the only way back to a\n * receiver that moved to another address, since it answers at the remembered one no more.\n * Throttled: a device that is merely switched off must not turn this into a scan loop, and\n * one timer covers however many devices are down.\n *\n * @param deviceId the device that just went offline\n */\n private scheduleRediscovery(deviceId: string): void {\n // Per device, not per instance: a manual device sits at an address the user typed, so there\n // is nothing to search for \u2014 it is simply off. Only a discovered one can have moved.\n if (this.deviceRecords.get(deviceId)?.source !== \"discovered\") {\n return;\n }\n if (!this.discovering || this.unloading || this.rediscoverTimer !== undefined) {\n return;\n }\n const receiver = this.pushReceiver;\n if (!receiver) {\n return;\n }\n const due = Math.max(0, REDISCOVER_MIN_INTERVAL_MS - (Date.now() - this.lastRediscovery));\n this.rediscoverTimer = this.setTimeout(() => {\n this.rediscoverTimer = undefined;\n this.lastRediscovery = Date.now();\n if (!this.unloading) {\n void this.discoverAdditionalDevices(receiver);\n }\n }, due);\n }\n\n /**\n * Stop supervising one device and release its supervisor. The object tree is untouched \u2014\n * a readdress puts the same device straight back on it.\n *\n * @param deviceId the id-safe device id\n */\n private stopDevice(deviceId: string): void {\n const supervisor = this.supervisorById.get(deviceId);\n if (!supervisor) {\n return;\n }\n supervisor.close();\n this.supervisorById.delete(deviceId);\n const index = this.supervisors.indexOf(supervisor);\n if (index >= 0) {\n this.supervisors.splice(index, 1);\n }\n }\n\n /**\n * Drop every entry of a state-id-keyed cache that belongs to one device.\n *\n * The caches are keyed by namespace-relative state ids (`stripNamespace` in\n * {@link snapshotExistingDatapoints}), so a device owns exactly the keys under its own prefix.\n *\n * @param cache a map or set keyed by namespace-relative state ids\n * @param deviceId the id-safe device id\n */\n private forgetUnder(cache: StateKeyedCache, deviceId: string): void {\n const prefix = `${deviceId}.`;\n for (const key of [...cache.keys()]) {\n if (key.startsWith(prefix)) {\n cache.delete(key);\n }\n }\n }\n\n /**\n * Remove one device for good: stop talking to it and delete its object tree.\n *\n * Driven by the device manager's delete action. Deleting a discovered device used to only\n * empty the remembered list \u2014 the supervisor kept the connection, the tree stayed, and the card\n * came back on the next start. Now the delete is what it says; the id is additionally kept in\n * the ignored list (device manager) so a later search does not put the device back.\n *\n * @param deviceId the id-safe device id\n */\n public async removeDevice(deviceId: string): Promise<void> {\n this.stopDevice(deviceId);\n // A native patch still inside its coalescing window would fire AFTER the delete below and\n // recreate the device object as a bare orphan \u2014 cancel it before anything else.\n const pendingNative = this.pendingNative.get(deviceId);\n if (pendingNative) {\n this.clearTimeout(pendingNative.timer);\n this.pendingNative.delete(deviceId);\n }\n const record = this.deviceRecords.get(deviceId);\n if (record) {\n this.knownDeviceIps.delete(record.ip);\n }\n this.deviceRecords.delete(deviceId);\n this.deviceConnected.delete(deviceId);\n this.readyDevices.delete(deviceId);\n // Everything else this device left behind goes with it. A cache that survives makes the\n // adapter believe it already did the work: re-adding the SAME id finds the icon cache\n // intact, `updateDeviceIcon` bails on the identity check, and the card keeps the default\n // silhouette `ensureDeviceHeader` seeds \u2014 a soundbar shows a receiver until the next start.\n this.deviceIcons.delete(deviceId);\n this.deviceLabels.delete(deviceId);\n this.profiles.delete(deviceId);\n this.forgetUnder(this.knownDatapoints, deviceId);\n this.forgetUnder(this.storedStates, deviceId);\n this.forgetUnder(this.storedBounds, deviceId);\n this.forgetUnder(this.touchedThisRun, deviceId);\n this.forgetUnder(this.volumeScales, deviceId);\n this.forgetUnder(this.volumeDefs, deviceId);\n this.volumePercent.delete(deviceId);\n try {\n await this.delObjectAsync(deviceId, { recursive: true });\n } catch (e) {\n this.log.warn(`could not remove the object tree of \"${deviceId}\" (${errorMessage(e)})`);\n }\n this.writeState(\"info.connection\", [...this.deviceConnected.values()].some(Boolean));\n this.writeDeviceOverview();\n }\n\n /**\n * Subscribe to the adapter's own states \u2014 OBSERVED, like every other database call.\n *\n * `subscribeStates` without a callback returns a promise, and its wildcard branch reads the\n * matching objects first: any failure there ends in `maybeCallbackWithError`, which rejects\n * for everything except the plain \"database closed\" case (js-controller-common-db source).\n * Left unawaited that is an unhandled rejection \u2014 and js-controller turns those into an\n * adapter stop, the same trap the nine bare state writes carried.\n *\n * A failure is loud but not fatal: without the subscription the tree still fills from the\n * devices, only user writes stop being applied. Saying so beats a silent half-working\n * instance, and beats losing the whole start over it.\n */\n private async subscribeToStates(): Promise<void> {\n try {\n await this.subscribeStatesAsync(\"*\");\n } catch (e) {\n this.log.error(\n `could not subscribe to state changes (${errorMessage(e)}) \u2014 the tree still updates, ` +\n `but writes to datapoints will not reach the device until the instance is restarted`,\n );\n }\n }\n\n /**\n * Aggregate one device's connection state into the adapter's `info.connection`\n * (true while at least one device is connected).\n *\n * @param deviceId the device reporting\n * @param connected whether that device is currently connected\n */\n private reportConnection(deviceId: string, connected: boolean): void {\n if (connected && !this.readyDevices.has(deviceId)) {\n this.readyDevices.add(deviceId);\n // Arm the settle pass even when the connect created nothing new \u2014 the once-per-\n // version orphan purge rides the same settled moment as the balance line.\n this.scheduleDatapointBalance();\n }\n this.deviceConnected.set(deviceId, connected);\n this.writeState(`${deviceId}.info.connection`, connected);\n // A drop clears the per-transport flags; a (re)connect sets them again via onTransports.\n if (!connected) {\n this.setTransports(deviceId, []);\n // A discovered device may simply have moved \u2014 only a search can find it again.\n this.scheduleRediscovery(deviceId);\n }\n const anyConnected = [...this.deviceConnected.values()].some(Boolean);\n this.writeState(\"info.connection\", anyConnected);\n this.writeDeviceOverview();\n }\n\n /**\n * The three overview datapoints: how many devices this instance runs, how many are\n * connected right now, and whether that is all of them. Derived from the SAME map that\n * feeds the per-device markers and written in the same round \u2014 computed separately they\n * would drift away from what the single devices say.\n *\n * `devicesAllOnline` needs at least one device: zero of zero is not \"everything is fine\".\n */\n private writeDeviceOverview(): void {\n const total = this.deviceConnected.size;\n const online = [...this.deviceConnected.values()].filter(Boolean).length;\n this.writeState(\"info.devicesTotal\", total);\n this.writeState(\"info.devicesOnline\", online);\n this.writeState(\"info.devicesAllOnline\", total > 0 && online === total);\n }\n\n /**\n * Reflect the live transport set into a device's `info.transports.*` flags so the\n * device-manager card shows which protocols (YNCA/YXC/XML) are connected right now.\n *\n * @param deviceId the id-safe device id\n * @param names the transports live now (empty on a drop)\n */\n private setTransports(deviceId: string, names: string[]): void {\n const live = new Set(names);\n for (const proto of TRANSPORT_IDS) {\n this.writeState(`${deviceId}.info.transports.${proto}`, live.has(proto));\n }\n }\n\n /**\n * Write a state with ack \u2014 and OBSERVE the promise. js-controller turns an unhandled\n * promise rejection into an adapter stop (`_exceptionHandler` \u2192 exit code\n * UNCAUGHT_EXCEPTION, read in the controller source), and `setState` rejects whenever\n * the states database is not reachable for a moment (`ERROR_DB_CLOSED`, or a pending\n * command cancelled by a reconnecting Redis). Nine fire-and-forget writes used to run\n * bare: one hiccup while a device pushed a value would have restarted the whole\n * instance. The failure lands in the log instead \u2014 once per outage at warn, then at\n * debug until a write succeeds again; during teardown it is expected and stays silent.\n *\n * @param id the state id (namespace-relative)\n * @param value the value to write\n */\n private writeState(id: string, value: ioBroker.StateValue): void {\n this.setState(id, { val: value, ack: true }).then(\n () => {\n this.stateWritesFailing = false;\n },\n (e: unknown) => this.noteWriteFailure(`state ${id}`, e),\n );\n }\n\n /**\n * Persist a device object's `native` part \u2014 observed like {@link writeState}. The two\n * per-device caches (YNCA subunit probe, probe memory) persist through it.\n *\n * @param deviceId the device object id\n * @param native the native fields to merge into the object\n */\n private persistDeviceNative(deviceId: string, native: Record<string, unknown>): void {\n // Coalesced per device: the probe memory persists on EVERY change, and a first connect\n // changes it dozens of times within a second (every observed enum value, every declared\n // list) \u2014 each was one extendObject on the device object. Latest wins, one write per window.\n const pending = this.pendingNative.get(deviceId);\n if (pending) {\n Object.assign(pending.native, native);\n return;\n }\n const entry: PendingNative = { native: { ...native } };\n this.pendingNative.set(deviceId, entry);\n // this.setTimeout refuses during shutdown (returns undefined) \u2014 then write at once.\n entry.timer = this.setTimeout(() => this.flushDeviceNative(deviceId), NATIVE_PERSIST_WINDOW_MS);\n if (!entry.timer) {\n void this.flushDeviceNative(deviceId);\n }\n }\n\n /**\n * Write a device's pending native patch now (the coalescing window ended, or the adapter is\n * unloading).\n *\n * @param deviceId the id-safe device id\n * @returns the write, for the unload path to wait on\n */\n private flushDeviceNative(deviceId: string): Promise<void> {\n const pending = this.pendingNative.get(deviceId);\n if (!pending) {\n return Promise.resolve();\n }\n this.pendingNative.delete(deviceId);\n this.clearTimeout(pending.timer);\n return this.extendObject(deviceId, { native: pending.native }).then(\n () => {\n this.stateWritesFailing = false;\n },\n (e: unknown) => this.noteWriteFailure(`device object ${deviceId}`, e),\n );\n }\n\n /**\n * Log a failed database write: warn on the first failure of an outage, debug for the\n * repeats, silence while unloading (the database is going down with us).\n *\n * @param what which write failed, for the log line\n * @param e the rejection reason\n */\n private noteWriteFailure(what: string, e: unknown): void {\n if (this.unloading) {\n return;\n }\n const message = `could not write ${what} (${errorMessage(e)})`;\n if (this.stateWritesFailing) {\n this.log.debug(message);\n return;\n }\n this.stateWritesFailing = true;\n this.log.warn(`${message} \u2014 repeats stay at debug until a write succeeds`);\n }\n\n /**\n * One-shot startup cleanup: delete every object that does not belong to a\n * configured device (the previous adapter's whole tree, and any device dropped\n * from the config). Runs before the devices connect; a configured device's\n * subtree is kept whether or not it has connected yet.\n *\n * @param deviceIds the ids of the currently configured devices\n */\n private async cleanupStaleObjects(deviceIds: Set<string>): Promise<void> {\n const allObjects = await this.getAdapterObjectsAsync();\n const existing = Object.keys(allObjects);\n const stale = staleObjects(existing, deviceIds, this.namespace);\n // Old states this version renamed/moved (e.g. system.model -> info.model): delete the\n // old object so it does not linger orphaned beside the new one under a kept device.\n const renamed = renamedObjectIds(existing, deviceIds, this.namespace);\n // Objects whose datapoint group the user switched off \u2014 remove them so turning a group from\n // on to off cleans up its whole subtree (a toggle change restarts the instance, so this runs).\n const config = this.config as unknown as Record<string, unknown>;\n const disabled = existing.filter(full => {\n for (const deviceId of deviceIds) {\n const base = `${this.namespace}.${deviceId}.`;\n if (full.startsWith(base) && !isGroupEnabled(full.slice(base.length), config)) {\n return true;\n }\n }\n return false;\n });\n for (const fullId of [...stale, ...renamed, ...disabled]) {\n try {\n await this.delObjectAsync(stripNamespace(fullId, this.namespace));\n } catch {\n // already removed together with its parent\n }\n }\n // The reasons stay available for diagnosis; the user sees ONE balance line instead of\n // three counts they have to add up themselves.\n if (stale.length > 0) {\n this.log.debug(`removed ${stale.length} object(s) from a previous configuration`);\n }\n if (renamed.length > 0) {\n this.log.debug(`removed ${renamed.length} renamed object(s) from an earlier version`);\n }\n if (disabled.length > 0) {\n this.log.debug(`removed ${disabled.length} object(s) from switched-off datapoint groups`);\n }\n // Channels and device nodes go with them, but only datapoints are counted \u2014 that is what\n // the user switched on or off, and what they look for in the object tree.\n this.noteDatapointsRemoved(\n [...stale, ...renamed, ...disabled].filter(fullId => allObjects[fullId]?.type === \"state\"),\n );\n }\n\n /**\n * Remove read-capable states under a CONNECTED device that never carried a value and were not\n * (re)created by this run's transports \u2014 over-declarations of an earlier adapter version that\n * today's claim-with-proof creation no longer makes. Deleting them is lossless (no value, no\n * history). Runs after the tree settled, so a device that has not connected in this run keeps\n * its tree untouched \u2014 its sweep happens on the first start that reaches it.\n *\n * TWO starts decide, not one (2.7.0): a receiver in standby answers many functions\n * `@RESTRICTED`, so one run seeing a datapoint untouched is no proof the device lost it. The\n * first run RECORDS the candidates in the device's capability profile (`pendingPurge`), the\n * next run deletes those still untouched and still never filled, and forgets the rest. A device\n * is examined once per adapter version (`purgeVersion`) OR whenever it carries a recorded\n * candidate \u2014 the confirmation has to reach its second start even without a new version.\n */\n private async purgeNeverFilled(): Promise<void> {\n const candidates: string[] = [];\n for (const deviceId of this.readyDevices) {\n const profile = this.profiles.get(deviceId);\n if (profile?.purgeVersion !== this.version || (profile?.pendingPurge.length ?? 0) > 0) {\n candidates.push(deviceId);\n }\n }\n if (candidates.length === 0) {\n return;\n }\n const allObjects = await this.getAdapterObjectsAsync();\n const states = await this.getStatesAsync(\"*\");\n const untouched = neverWrittenStateIds(allObjects, states, new Set(candidates), this.namespace).filter(\n fullId => !this.touchedThisRun.has(stripNamespace(fullId, this.namespace)),\n );\n const purged: string[] = [];\n for (const deviceId of candidates) {\n const profile = this.profiles.get(deviceId);\n const seenNow = untouched\n .filter(fullId => fullId.startsWith(`${this.namespace}.${deviceId}.`))\n .map(fullId => stripNamespace(fullId, this.namespace));\n const recorded = new Set(profile?.pendingPurge ?? []);\n const confirmed = seenNow.filter(id => recorded.has(id));\n for (const id of confirmed) {\n try {\n await this.delObjectAsync(id);\n purged.push(`${this.namespace}.${id}`);\n } catch {\n // already gone\n }\n }\n // Whatever is untouched THIS run and was not just deleted waits for the next run.\n profile?.setPendingPurge(seenNow.filter(id => !confirmed.includes(id)));\n profile?.markPurged(this.version ?? \"\");\n }\n if (purged.length > 0) {\n this.log.debug(`removed ${purged.length} never-filled object(s), confirmed over two starts`);\n this.noteDatapointsRemoved(purged);\n }\n }\n\n /**\n * Remove folders that hold no datapoint any more, under the devices that connected this run.\n *\n * The two sweeps above only ever delete datapoints, so a folder emptied by a tree rework stays\n * behind and promises content it can never get \u2014 `player.server` is the live case: the v2.0.0\n * migration deletes the SERVER source's playback copies, and the new tree gives that source no\n * datapoint of its own. Runs on every start, not once per version: an empty folder is wrong\n * whenever it is found, and re-reading the objects after the orphan purge catches the ones that\n * purge just emptied. Not counted in the datapoint balance \u2014 a folder is not a datapoint.\n */\n private async purgeChildlessChannels(): Promise<void> {\n if (this.readyDevices.size === 0) {\n return;\n }\n const empty = childlessChannelIds(await this.getAdapterObjectsAsync(), this.readyDevices, this.namespace);\n for (const fullId of empty) {\n try {\n await this.delObjectAsync(stripNamespace(fullId, this.namespace));\n } catch {\n // already removed together with its parent\n }\n }\n if (empty.length > 0) {\n this.log.debug(`removed ${empty.length} empty folder(s) left over from an earlier object tree`);\n }\n }\n\n /**\n * Empty an object's stored `common.states` when it holds a key the new map lacks, so the\n * following merge-write results in exactly the new map (memory\n * `reference_iobroker_objekt_aendern_ohne_loeschen`: never delete an object to change it,\n * write `null` for the key instead). Judged against the start-up snapshot, then against what\n * this run last wrote \u2014 never a database read per state.\n *\n * @param id the object id (namespace-relative)\n * @param next the map about to be written\n */\n private async clearStaleStates(id: string, next: Record<string, string>): Promise<void> {\n const stored = this.storedStates.get(id);\n if (stored && Object.keys(stored).some(key => !(key in next))) {\n await this.extendObject(id, { common: { states: null } });\n }\n this.storedStates.set(id, next);\n }\n\n /**\n * Clear a bound the new definition no longer declares, so the following merge-write leaves\n * exactly the new bounds behind \u2014 the same rule {@link clearStaleStates} applies to a\n * shrinking dropdown (`reference_iobroker_objekt_aendern_ohne_loeschen`: write `null` for the\n * key, never delete the object). Judged against the start-up snapshot and then against what\n * this run wrote, so it costs no read per state.\n *\n * @param id the object id (namespace-relative)\n * @param next the common part about to be written\n */\n private async clearStaleBounds(id: string, next: ObjectDef[\"common\"]): Promise<void> {\n const stored = this.storedBounds.get(id);\n const gone = BOUND_FIELDS.filter(field => stored?.[field] !== undefined && next[field] === undefined);\n this.storedBounds.set(id, { min: next.min, max: next.max, step: next.step });\n if (gone.length === 0) {\n return;\n }\n // \u26A0\uFE0F NOT the `null` write `clearStaleStates` uses. A dropdown has a neutral value \u2014 an empty\n // map \u2014 and `null` reaches it. A bound has none: the merge writes `common.max = null`, and\n // js-controller's range check then compares against it numerically, where `null` counts as 0\n // and every reading is \"greater than max\" (`reference_attribut_entfernen_ohne_setobject`, and\n // the merge semantics measured in `reference_iobroker_objekt_aendern_ohne_loeschen`). The key\n // has to GO, which is read \u2192 delete \u2192 re-create; `setObject` is the checker's S5054.\n const object = await this.getObjectAsync(id);\n if (object?.type !== \"state\") {\n return;\n }\n // The READ common rides along, so nothing the object already carries is lost in the rewrite.\n const common = { ...object.common } as ioBroker.StateCommon & Record<string, unknown>;\n for (const field of gone) {\n delete common[field];\n }\n try {\n // Explicitly non-recursive: a state has no children, and this must never take a tree with it.\n await this.delObjectAsync(id, { recursive: false });\n } catch (e) {\n // The merge that follows would only put the object back as it was \u2014 nothing is lost, but the\n // stale bound stays, so it belongs in the log rather than passing silently.\n this.log.debug(`${id}: could not drop the stale bound(s) ${gone.join(\", \")} (${errorMessage(e)})`);\n return;\n }\n await this.extendObject(id, { type: \"state\", common, native: object.native });\n }\n\n /**\n * Remember every datapoint that already exists, ONCE per adapter run.\n *\n * @see knownDatapoints for why the create path alone cannot answer \"is this new?\"\n */\n private async snapshotExistingDatapoints(): Promise<void> {\n try {\n for (const [fullId, object] of Object.entries(await this.getAdapterObjectsAsync())) {\n if (object?.type === \"state\") {\n const id = stripNamespace(fullId, this.namespace);\n this.knownDatapoints.add(id);\n const common = object.common as\n { states?: unknown; min?: unknown; max?: unknown; step?: unknown } | undefined;\n const states = common?.states;\n if (states !== null && typeof states === \"object\") {\n this.storedStates.set(id, states as Record<string, string>);\n }\n this.storedBounds.set(id, boundsOfCommon(common));\n }\n }\n } catch (e) {\n // Without the snapshot the balance would call every datapoint new; better to stay\n // silent about it than to log a wrong number.\n this.log.debug(`could not read the existing datapoints (${errorMessage(e)}); balance line disabled`);\n this.balanceDisabled = true;\n }\n }\n\n /**\n * Count a datapoint the device tree just created \u2014 new ones only.\n *\n * @param id the state id relative to the namespace\n */\n private noteDatapointCreated(id: string): void {\n if (this.knownDatapoints.has(id)) {\n return;\n }\n this.knownDatapoints.add(id);\n this.createdDatapoints++;\n this.scheduleDatapointBalance();\n }\n\n /**\n * Count removed datapoints, and let them count again should they ever come back.\n *\n * @param fullIds the removed ids, namespace included\n */\n private noteDatapointsRemoved(fullIds: readonly string[]): void {\n for (const fullId of fullIds) {\n this.knownDatapoints.delete(stripNamespace(fullId, this.namespace));\n this.removedDatapoints++;\n }\n if (fullIds.length > 0) {\n this.scheduleDatapointBalance();\n }\n }\n\n /**\n * Log the balance once the tree has settled. A device connects asynchronously and several\n * devices connect at once, so the line waits for quiet instead of firing per device \u2014 the\n * user made ONE change and reads ONE result.\n */\n private scheduleDatapointBalance(): void {\n if (this.balanceDisabled || this.balanceSettling) {\n return;\n }\n this.clearTimeout(this.balanceTimer);\n this.balanceTimer = this.setTimeout(() => {\n this.balanceTimer = undefined;\n void (async () => {\n this.balanceSettling = true;\n // The tree has settled: sweep the never-filled orphans FIRST, so their\n // removals land in the same balance line the user is about to read.\n try {\n await this.purgeNeverFilled();\n } catch (e) {\n this.log.debug(`orphan purge failed (${errorMessage(e)}); skipped for this run`);\n }\n // Then the folders those removals (or an earlier version's tree rework) left empty.\n try {\n await this.purgeChildlessChannels();\n } catch (e) {\n this.log.debug(`empty-folder purge failed (${errorMessage(e)}); skipped for this run`);\n }\n const parts: string[] = [];\n if (this.createdDatapoints > 0) {\n parts.push(`created ${this.createdDatapoints} datapoint(s)`);\n }\n if (this.removedDatapoints > 0) {\n parts.push(`removed ${this.removedDatapoints} datapoint(s)`);\n }\n this.createdDatapoints = 0;\n this.removedDatapoints = 0;\n // Silent when nothing changed: a plain restart must not write a line.\n if (parts.length > 0) {\n this.log.info(`Object tree updated: ${parts.join(\", \")}`);\n }\n this.balanceSettling = false;\n })();\n }, DATAPOINT_BALANCE_SETTLE_MS);\n }\n\n /**\n * Refresh the adapter's OWN `info.*` objects.\n *\n * js-controller creates them from `io-package.json` `instanceObjects` when the instance is\n * added, and leaves an existing object's `common` alone on every later upgrade \u2014 so an\n * instance that predates a change keeps whatever the old version wrote. Measured after the\n * name translation went live: five of them still carried a plain-string name while the whole\n * rest of the tree was translated. Writing them here every start closes that half; extendObject\n * merges, so a recording setting or anything else a user attached survives.\n */\n private async ensureInstanceInfoObjects(): Promise<void> {\n // Spelled out with LITERAL ids on purpose. A loop over a table reads more compactly, but\n // then neither a reader nor the consistency gate can see which manifest objects are\n // actually refreshed \u2014 and \"the call exists\" is not the same question as \"the call runs\n // for THIS object\". This is the one place where that distinction cost a release (2.1.1).\n await this.extendObject(\"info\", {\n type: \"channel\",\n common: { name: tName(\"information\") },\n native: {},\n });\n await this.extendObject(\"info.connection\", {\n type: \"state\",\n common: {\n name: tName(\"deviceOrServiceConnected\"),\n desc: tName(\"descDeviceOrServiceConnected\"),\n type: \"boolean\",\n role: \"indicator.connected\",\n read: true,\n write: false,\n },\n native: {},\n });\n await this.extendObject(\"info.devicesTotal\", {\n type: \"state\",\n common: { name: tName(\"devicesTotal\"), type: \"number\", role: \"value\", read: true, write: false },\n native: {},\n });\n await this.extendObject(\"info.devicesOnline\", {\n type: \"state\",\n common: { name: tName(\"devicesOnline\"), type: \"number\", role: \"value\", read: true, write: false },\n native: {},\n });\n await this.extendObject(\"info.devicesAllOnline\", {\n type: \"state\",\n common: { name: tName(\"allDevicesOnline\"), type: \"boolean\", role: \"indicator\", read: true, write: false },\n native: {},\n });\n }\n\n /**\n * Create AND refresh a device's header objects (the device node, its info channel and a\n * per-device connection indicator) so its state is visible even while offline.\n *\n * Written with `extendObject` on every start, not created once: an object that already exists\n * is otherwise never touched again, so an instance upgraded from an older version keeps\n * whatever that version wrote \u2014 measured live after the name translation, where these were the\n * only device datapoints left with a plain-string name (`info.model`/`info.firmware` came out\n * right only because a catalog entry upserts them on top). extendObject merges, so a recording\n * setting a user attached survives.\n *\n * @param deviceId the id-safe device id\n * @param ip the device's current address (from config or discovery)\n * @param source where the address came from \u2014 kept at the device object so the adapter, the\n * card and the edit path all know it without re-deriving it from which table happens to be\n * filled (which said the same thing about every device on the instance)\n */\n private async ensureDeviceHeader(deviceId: string, ip: string, source: DeviceSource): Promise<void> {\n // statusStates.onlineId lets the admin paint a green/red reachability symbol on the\n // device object itself (as govee does), fed by the per-device connection state.\n // extendObject with preserve:name so an upgrade adds the symbol without overwriting\n // a name the user changed.\n // A device that has not reported its model yet would sit in the tree without any\n // symbol \u2014 an upgraded instance shows that on every start before the first report,\n // and a device that never answers shows it for good. Seed the default silhouette,\n // but only when there is none: overwriting would flip a soundbar back to the\n // receiver default for the seconds until its model arrives.\n let icon: string | undefined;\n // Percent is a DEVICE setting since 2.9.0. A device that carries no answer yet inherits the\n // instance-wide switch 2.8.0 had, so an upgrade keeps every receiver exactly as it was, and\n // the answer is written down here so it never has to be inherited again.\n let percent = this.legacyVolumePercent;\n try {\n const existing = await this.getObjectAsync(deviceId);\n icon = existing?.common?.icon ? undefined : iconForModel(undefined);\n const own = (existing?.native as { volumeAsPercent?: unknown } | undefined)?.volumeAsPercent;\n if (typeof own === \"boolean\") {\n percent = own;\n }\n } catch {\n icon = undefined;\n }\n this.volumePercent.set(deviceId, percent);\n await this.extendObject(\n deviceId,\n {\n type: \"device\",\n common: {\n name: deviceId,\n ...(icon ? { icon } : {}),\n statusStates: { onlineId: `${this.namespace}.${deviceId}.info.connection` },\n },\n native: { source, volumeAsPercent: percent },\n },\n { preserve: { common: [\"name\"] } },\n );\n await this.extendObject(`${deviceId}.info`, {\n type: \"channel\",\n common: { name: tName(\"info\") },\n native: {},\n });\n await this.extendObject(`${deviceId}.info.connection`, {\n type: \"state\",\n common: {\n name: tName(\"connected\"),\n // Its own explanation key: the name key `connected` is shared with the Bluetooth\n // source's \"Connected\", which means something else entirely.\n desc: tName(\"descDeviceConnected\"),\n type: \"boolean\",\n role: \"indicator.reachable\",\n read: true,\n write: false,\n def: false,\n },\n native: {},\n });\n // Model name shown on the device-manager card. Filled by whichever transport reports it\n // (YNCA MODELNAME, YXC/XML model); created here so the card's model line binds even for an\n // offline device or a transport that does not report a model.\n await this.extendObject(`${deviceId}.info.model`, {\n type: \"state\",\n common: { name: tName(\"model\"), type: \"string\", role: \"text\", read: true, write: false, def: \"\" },\n native: {},\n });\n // The device's address \u2014 for a discovered device it lived only in the adapter's\n // internals, so no diagnosis (log capture, browser access to the device's own pages)\n // could name it without a network search. Refreshed every start: DHCP may move it.\n await this.extendObject(`${deviceId}.info.ip`, {\n type: \"state\",\n common: { name: tName(\"ipAddress\"), type: \"string\", role: \"info.ip\", read: true, write: false, def: \"\" },\n native: {},\n });\n await this.setState(`${deviceId}.info.ip`, { val: ip, ack: true });\n // Per-transport connection flags, fed by the live set from connectTransports and read live\n // by the device-manager card indicators. Created here so an offline device's card still\n // renders all three (false) instead of nothing.\n await this.extendObject(`${deviceId}.info.transports`, {\n type: \"channel\",\n common: { name: tName(\"transports\"), desc: tName(\"descTransports\") },\n native: {},\n });\n for (const proto of TRANSPORT_IDS) {\n await this.extendObject(`${deviceId}.info.transports.${proto}`, {\n type: \"state\",\n common: {\n name: tName(\"transportConnected\", proto.toUpperCase()),\n type: \"boolean\",\n role: \"indicator.reachable\",\n read: true,\n write: false,\n def: false,\n },\n native: {},\n });\n }\n }\n\n /** The icon last written per device, so repeated model reports do not re-write the object. */\n private readonly deviceIcons = new Map<string, string>();\n\n /** The label this adapter wrote per device, with the rank of the source behind it. */\n private readonly deviceLabels = new Map<string, { name: string; rank: LabelRank }>();\n\n /**\n * Give the device node a name a user recognises, once the device reports one.\n *\n * An instance upgraded from the previous adapter carries the receiver's ip as its\n * device name \u2014 that adapter knew nothing but an ip, so the migration had nothing\n * else to call it. The object id stays that ip for good (history and visualisation\n * bindings hang off it), but the displayed name does not have to.\n *\n * A name the user typed is never touched, and the model never replaces a name the\n * device reported for itself \u2014 see {@link nextDeviceLabel}.\n *\n * @param deviceId the id-safe device id\n * @param candidate the reported name (a MusicCast zone name, or the model)\n * @param rank how trustworthy the candidate is\n */\n private async updateDeviceLabel(deviceId: string, candidate: string, rank: LabelRank): Promise<void> {\n const own = this.deviceLabels.get(deviceId);\n try {\n const current = (await this.getObjectAsync(deviceId))?.common?.name;\n const label = nextDeviceLabel(\n typeof current === \"string\" ? current : undefined,\n deviceId,\n candidate,\n rank,\n own?.name,\n own?.rank,\n );\n if (label === undefined) {\n return;\n }\n // Deliberately without `preserve: { common: [\"name\"] }`: nextDeviceLabel has just\n // established that the present name is the adapter's own placeholder, not a user's.\n await this.extendObject(deviceId, { common: { name: label } });\n this.deviceLabels.set(deviceId, { name: label, rank });\n this.log.debug(`${deviceId}: device name set to \"${label}\"`);\n } catch (e) {\n this.log.debug(`${deviceId}: setting the device name failed (${errorMessage(e)})`);\n }\n }\n\n /**\n * Paint the device-class silhouette on the device node once the model is known \u2014\n * detected from the reported model name, written only when it actually changes.\n *\n * @param deviceId the id-safe device id\n * @param model the reported model name\n */\n private async updateDeviceIcon(deviceId: string, model: string): Promise<void> {\n const icon = iconForModel(model);\n if (this.deviceIcons.get(deviceId) === icon) {\n return;\n }\n this.deviceIcons.set(deviceId, icon);\n try {\n await this.extendObject(deviceId, { common: { icon } });\n } catch (e) {\n this.log.debug(`${deviceId}: setting device icon failed (${errorMessage(e)})`);\n }\n }\n\n /**\n * Carry over the previous adapter's single-device config into the device table.\n * The old yamaha stored one receiver as `config.ip` (older installs: `config.IP`);\n * the new adapter uses a `devices` table, so an upgraded instance would otherwise\n * start with an empty table and lose its receiver. Persists the row so the admin\n * table shows it, and fills `this.config` in memory so this run already drives it.\n */\n private async migrateLegacyDevice(): Promise<void> {\n const config = this.config as unknown as Record<string, unknown>;\n const row = legacyDeviceRow(config);\n if (!row) {\n return;\n }\n // Fill the in-memory config first, so this run already drives the device even\n // if persisting the table below fails \u2014 persistence is a convenience for the\n // admin view, not a precondition for running.\n config.devices = [row];\n try {\n await this.extendForeignObjectAsync(`system.adapter.${this.namespace}`, { native: { devices: [row] } });\n this.log.info(`carried the previous single-device config (${row.ip}) over into the device table`);\n } catch (e) {\n this.log.warn(\n `could not persist the migrated device table (${errorMessage(e)}); ` + `running with the in-memory value`,\n );\n }\n }\n\n /**\n * Fold the removed `group_zones` toggle into `group_multiroom` \u2014 zone 2/3/4 now\n * belong to the multiroom group. Existing installs that had zones on but multiroom\n * off would otherwise lose their zone datapoints after the update.\n */\n private async migrateGroupZones(): Promise<void> {\n const config = this.config as unknown as Record<string, unknown>;\n if (!(\"group_zones\" in config)) {\n return;\n }\n if (config.group_zones) {\n config.group_multiroom = true;\n }\n delete config.group_zones;\n try {\n const obj = await this.getForeignObjectAsync(`system.adapter.${this.namespace}`);\n if (obj?.native) {\n if (obj.native.group_zones) {\n obj.native.group_multiroom = true;\n }\n delete obj.native.group_zones;\n await this.setForeignObjectAsync(`system.adapter.${this.namespace}`, obj);\n this.log.info(\"migrated group_zones setting into group_multiroom\");\n }\n } catch (e) {\n this.log.warn(`could not persist group_zones migration (${errorMessage(e)})`);\n }\n }\n\n /**\n * Bring one device online across ALL its transports: every protocol that answers\n * \u2014 YNCA (amp control over a held TCP connection), YXC (MusicCast), XML/YNC\n * (pre-2010) \u2014 connects in parallel on one object tree. Returns a connection handle\n * the supervisor keeps, or null when no transport answers this attempt. Each\n * datapoint is owned by exactly one transport (owner-policy), so the mappers never\n * collide on a shared id.\n *\n * @param device the configured device record\n * @param pushReceiver the shared YXC push receiver\n * @param knownDeviceIps IPs of all configured devices, for resolving a multiroom client\n * @param reachability dedup for the \"no reachable transport\" warning (one instance per device,\n * held by the caller across retries \u2014 see {@link ReachabilityDedup})\n * @param yncaSubunitCache per-device cache of the YNCA AVAIL probe (skips the probe on reconnects)\n * @param probeMemory per-device memory for constant device answers (skips re-asking on reconnects)\n * @returns a connection handle, or null when no transport connected\n */\n private attemptDevice(\n device: DeviceRecord,\n pushReceiver: YxcPushReceiver,\n knownDeviceIps: Set<string>,\n reachability: ReachabilityDedup,\n yncaSubunitCache: YncaSubunitCache,\n probeMemory: ProbeMemory,\n ): Promise<ConnectionHandle | null> {\n return attemptDevice(device, {\n reachability,\n yncaSubunitCache,\n probeMemory,\n // Group gate for the YNCA sweep: a disabled group's functions are never even fetched.\n isEntryEnabled: id => isGroupEnabled(id, this.config as unknown as Record<string, unknown>),\n log: {\n debug: message => this.log.debug(message),\n info: message => this.log.info(message),\n warn: message => this.log.warn(message),\n },\n upsertObject: async (id, def) => {\n // Gate on the datapoint group: a switched-off group's objects are not created. The id is\n // \"<deviceId>.<relativeId>\"; groupOf reads the relative part.\n if (!isGroupEnabled(id.slice(id.indexOf(\".\") + 1), this.config as unknown as Record<string, unknown>)) {\n return;\n }\n // A SHRINKING dropdown needs a clearing write first: extendObject merges `common.states`\n // key by key, so the old entries would survive every update (#619 \u2014 the reporter would\n // have seen no change at all). Only when the stored map carries a key the new one lacks;\n // an unchanged or growing map is one write, as before.\n if (def.type === \"state\" && def.common.states) {\n await this.clearStaleStates(id, def.common.states);\n }\n await this.writePresented(id, def);\n if (def.type === \"state\") {\n this.noteDatapointCreated(id);\n this.touchedThisRun.add(id);\n }\n },\n setStateAck: (id, value) => {\n // Same group gate as upsertObject, so a switched-off group seeds no orphan value either.\n if (!isGroupEnabled(id.slice(id.indexOf(\".\") + 1), this.config as unknown as Record<string, unknown>)) {\n return;\n }\n this.writeState(id, this.volumeAsShown(id, value));\n // A model report also decides the device-class icon on the device node \u2014 and, for a\n // device still carrying the ip it was migrated with, its readable name.\n if (id.endsWith(\".info.model\") && typeof value === \"string\" && value.length > 0) {\n const reporting = id.slice(0, id.indexOf(\".\"));\n void this.updateDeviceIcon(reporting, value);\n void this.updateDeviceLabel(reporting, value, LABEL_RANK.model);\n }\n },\n onDeviceName: name => void this.updateDeviceLabel(device.id, name, LABEL_RANK.deviceName),\n timers: {\n schedule: (handler, ms) => (this.unloading ? undefined : this.setTimeout(handler, ms)),\n cancel: handle => this.clearTimeout(handle),\n },\n registerPush: (ip, onPush) => pushReceiver.register(ip, onPush),\n pushActive: () => pushReceiver.isListening(),\n scheduleKeepalive: (handler, ms) => {\n if (this.unloading) {\n return () => {};\n }\n const timer = this.setInterval(handler, ms);\n return () => {\n if (timer) {\n this.clearInterval(timer);\n }\n };\n },\n xmlPollIntervalMs: this.xmlPollIntervalMs(),\n onTransports: names => this.setTransports(device.id, names),\n knownDeviceIps,\n });\n }\n\n /**\n * Route a state change to every device's supervisor (each forwards to its\n * active controller, which ignores ids outside its subtree and its acked echoes).\n *\n * @param id the full state id\n * @param state the new state (null when deleted)\n */\n private onStateChange(id: string, state: ioBroker.State | null | undefined): void {\n if (!state) {\n return;\n }\n const relative = stripNamespace(id, this.namespace);\n // The adapter subscribes to its whole namespace, so every one of its own acked writes\n // comes back here too \u2014 during a sweep that is hundreds of events. Route by the id's\n // first segment instead of offering each one to every device in turn.\n const deviceId = relative.slice(0, relative.indexOf(\".\"));\n const value = state.ack ? state.val : this.volumeAsDeviceScale(relative, state.val);\n this.supervisorById.get(deviceId)?.handleStateChange(relative, state.ack, value);\n }\n\n /**\n * Synchronous teardown \u2014 no await, call the callback immediately (SIGKILL otherwise).\n *\n * @param callback function to invoke once teardown is complete\n */\n private onUnload(callback: () => void): void {\n try {\n this.unloading = true;\n this.clearTimeout(this.balanceTimer);\n this.clearTimeout(this.rediscoverTimer);\n this.pushReceiver?.close();\n for (const supervisor of this.supervisors) {\n supervisor.close();\n }\n // A stopped adapter talks to nothing, so no device may keep claiming to be connected \u2014\n // that state paints the symbol on the device object (statusStates.onlineId), and the\n // instance-wide info.connection alone would leave every device green. The protocol\n // flags and the overview go with them; devicesTotal stays, how many devices there are\n // did not change.\n //\n // The callback goes LAST, after the writes: reporting \"done\" straight away loses them,\n // the host tears the process down as soon as it is told.\n const writes: Promise<unknown>[] = [this.setState(\"info.connection\", { val: false, ack: true })];\n for (const deviceId of this.deviceConnected.keys()) {\n this.deviceConnected.set(deviceId, false);\n writes.push(this.setState(`${deviceId}.info.connection`, { val: false, ack: true }));\n // The protocol flags on the card go down with the connection \u2014 a stopped adapter\n // is connected over no protocol.\n for (const proto of TRANSPORT_IDS) {\n writes.push(this.setState(`${deviceId}.info.transports.${proto}`, { val: false, ack: true }));\n }\n }\n writes.push(this.setState(\"info.devicesOnline\", { val: 0, ack: true }));\n writes.push(this.setState(\"info.devicesAllOnline\", { val: false, ack: true }));\n // A device memory still inside its coalescing window is written now \u2014 a timer on a\n // stopped adapter never fires, and the memory is what the next start rests on.\n for (const deviceId of [...this.pendingNative.keys()]) {\n writes.push(this.flushDeviceNative(deviceId));\n }\n void Promise.all(writes)\n .catch(() => {\n /* states DB already going down \u2014 nothing left to report to */\n })\n .finally(callback);\n return;\n } catch {\n // fall through\n }\n callback();\n }\n\n /**\n * Auto-discovery for an empty device table: scan the network, merge the finds with\n * the devices remembered from earlier runs (standby protection), persist the merged\n * set and return it. XML/pre-2010 receivers do not answer SSDP and never appear here.\n *\n * @returns the device records to run this session\n */\n private async autoDiscover(): Promise<DeviceRecord[]> {\n const store = discoveredStoreDeps(this);\n const known = await readDiscovered(store);\n if (known.length > 0) {\n // Remembered devices start NOW \u2014 the network search used to gate every restart\n // by its collect window although the devices were already known. It still runs,\n // in the background, to pick up newcomers (see discoverAdditionalDevices).\n this.log.info(`setting up ${known.length} remembered device(s); the network search runs in the background`);\n return known;\n }\n this.log.info(\"auto-discovery via SSDP (older XML-only devices must be added manually)\");\n const merged = await this.runDiscovery();\n this.log.info(`setting up ${merged.length} discovered device(s)...`);\n return merged;\n }\n\n /**\n * Search the network, merge with the remembered devices, and persist the result.\n * Shared by the blocking first-setup path and the background search.\n *\n * @returns the merged device records\n */\n private async runDiscovery(): Promise<DeviceRecord[]> {\n // Every search counts against the throttle, whoever asked for it \u2014 otherwise the first\n // offline device would fire another one right behind the start-up search.\n this.lastRediscovery = Date.now();\n const store = discoveredStoreDeps(this);\n const known = await readDiscovered(store);\n let found: Array<{ ip: string; name: string }> = [];\n try {\n found = await discoverYamaha({\n search: (target, ms) => this.ssdpSearch(target, ms),\n fetch: url => this.fetchUrl(url),\n log: { debug: message => this.log.debug(message), warn: message => this.log.warn(message) },\n });\n } catch (e) {\n this.log.warn(`auto-discovery scan failed, using the remembered devices: ${errorMessage(e)}`);\n }\n const merged = mergeDiscovered(known, found, (dropped, takenId) =>\n this.log.warn(`discovered device \"${dropped}\" skipped \u2014 its object id \"${takenId}\" is already taken`),\n );\n // Devices the user deleted from the card list stay out \u2014 otherwise the next search simply\n // undoes the delete. So do the ones that live in the device table: a receiver the user gave\n // a fixed address and entered by hand would otherwise come back as a SECOND card, and the\n // store would carry the found address back over the typed one (`mergeDiscovered` updates a\n // known id's address). Both the id and the address are matched \u2014 the search reads the name\n // off the device, the user typed their own, so the same receiver can carry two ids.\n const ignored = new Set(await readIgnored(ignoredStoreDeps(this)));\n const manual = parseDevices(this.config.devices);\n const manualIds = new Set(manual.map(device => device.id));\n const manualIps = new Set(manual.map(device => device.ip));\n const kept = merged.filter(\n device => !ignored.has(device.id) && !manualIds.has(device.id) && !manualIps.has(device.ip),\n );\n await writeDiscovered(store, kept);\n return kept;\n }\n\n /**\n * Load a device's capability profile \u2014 the one persisted memory of what the device told us\n * (probe memory, YNCA subunit snapshot, purge marker) \u2014 from its device object's native\n * part, wrapped so every change persists back there through the coalescing writer. The\n * device object is the right home: writing an instance object's native restarts the\n * adapter, a device object's does not. Legacy keys of 2.5.2/2.6.0 are converted at load.\n *\n * @param deviceId the id-safe device id\n * @returns the per-device profile store\n */\n private async loadDeviceProfile(deviceId: string): Promise<DeviceProfileStore> {\n let native: Record<string, unknown> | undefined;\n try {\n native = (await this.getObjectAsync(deviceId))?.native;\n } catch {\n native = undefined;\n }\n const store = new DeviceProfileStore(deviceId, native, {\n adapterVersion: this.version ?? \"\",\n now: () => new Date().toISOString(),\n persist: patch => this.persistDeviceNative(deviceId, patch),\n log: message => this.log.debug(message),\n });\n this.profiles.set(deviceId, store);\n return store;\n }\n\n /**\n * Whether the device that owns a datapoint presents its volume in percent.\n *\n * @param id a `<deviceId>.<relativeId>` state or object id\n * @returns true when that device's volume datapoints read 0\u2026100 %\n */\n private percentFor(id: string): boolean {\n return this.volumePercent.get(id.slice(0, id.indexOf(\".\"))) === true;\n }\n\n /**\n * Turn percent presentation on or off for ONE device, at once.\n *\n * Called from the device manager, which runs inside this process. The datapoint is rebuilt\n * before its value follows \u2014 the same order `reshapeVolume` keeps when a receiver changes the\n * scale it displays, and for the same reason: a value written against the old definition is\n * out of range and the js-controller logs it on every refresh.\n *\n * @param deviceId the id-safe device id\n * @param on whether its volume datapoints should read 0\u2026100 %\n */\n public async setVolumePercent(deviceId: string, on: boolean): Promise<void> {\n if (this.volumePercent.get(deviceId) === on) {\n return;\n }\n this.volumePercent.set(deviceId, on);\n await this.extendObject(deviceId, { native: { volumeAsPercent: on } });\n for (const [id, def] of [...this.volumeDefs]) {\n if (!id.startsWith(`${deviceId}.`)) {\n continue;\n }\n const bounds = this.volumeScales.get(id);\n await this.writePresented(id, def);\n if (!bounds) {\n continue;\n }\n const state = await this.getStateAsync(id);\n if (typeof state?.val === \"number\") {\n this.writeState(id, on ? toPercent(state.val, bounds) : fromPercent(state.val, bounds));\n }\n }\n }\n\n /**\n * Write one object definition through the percent presentation \u2014 shared by the upsert funnel\n * and the live switch, so both produce exactly the same object.\n *\n * @param id the full object id\n * @param def the definition the coordinator produced\n */\n private async writePresented(id: string, def: ObjectDef): Promise<void> {\n const written = this.presentVolume(id, def);\n if (written.type === \"state\") {\n await this.clearStaleBounds(id, written.common);\n }\n await this.extendObject(id, { type: written.type, common: written.common, native: {} });\n }\n\n /**\n * The object definition to write for a datapoint, once percent mode has had its say.\n *\n * Applied to the FINISHED definition, after the coordinator picked the owner, so one rule covers\n * all three transports and every zone: the decibels YNCA and XML declare in their catalogs and\n * the display scale MusicCast reports are all just \"the device's own scale\" here. The bounds it\n * replaces are remembered, because they are what the two value directions convert against.\n *\n * @param id the full object id\n * @param def the definition the coordinator produced\n * @returns the definition to write\n */\n private presentVolume(id: string, def: ObjectDef): ObjectDef {\n if (def.type !== \"state\" || !isAmpVolumeId(id.slice(id.indexOf(\".\") + 1))) {\n return def;\n }\n const bounds = volumeBoundsOf(def);\n if (!bounds) {\n // Nothing declared to convert against. Percent would be a number with no meaning, so the\n // datapoint keeps the device's own scale even with the switch on, and says so once.\n this.volumeScales.delete(id);\n this.volumeDefs.delete(id);\n if (this.percentFor(id)) {\n this.log.debug(`${id}: no declared range \u2014 keeping the device's own scale instead of percent`);\n }\n return def;\n }\n this.volumeScales.set(id, bounds);\n this.volumeDefs.set(id, def);\n return this.percentFor(id) ? asPercentObject(def) : def;\n }\n\n /**\n * A device value on its way into a datapoint, converted when that datapoint is in percent.\n *\n * @param id the full state id\n * @param value the value the transport reported, on the device's own scale\n * @returns the value to store\n */\n private volumeAsShown(id: string, value: boolean | number | string): boolean | number | string {\n const bounds = this.percentFor(id) ? this.volumeScales.get(id) : undefined;\n return bounds && typeof value === \"number\" ? toPercent(value, bounds) : value;\n }\n\n /**\n * A user's write on its way out, converted back to the scale the device expects.\n *\n * Only unacked writes reach here: an acked one is the adapter's own echo, already in percent,\n * and converting it a second time would walk the value down on every poll.\n *\n * @param relativeId the state id without the namespace\n * @param value the value the user wrote\n * @returns the value to hand to the device's supervisor\n */\n private volumeAsDeviceScale(relativeId: string, value: ioBroker.StateValue): ioBroker.StateValue {\n const bounds = this.percentFor(relativeId) ? this.volumeScales.get(relativeId) : undefined;\n return bounds && typeof value === \"number\" ? fromPercent(value, bounds) : value;\n }\n\n /**\n * The XML/YNC poll interval in milliseconds, from `config.xmlPollInterval`\n * (seconds, default 60).\n *\n * @returns the interval in ms\n */\n private xmlPollIntervalMs(): number {\n const seconds = Number((this.config as unknown as Record<string, unknown>).xmlPollInterval);\n return (Number.isFinite(seconds) && seconds > 0 ? seconds : 60) * 1000;\n }\n\n /**\n * Run an SSDP M-SEARCH and collect the responders' description URL and address.\n *\n * With a configured network interface the search leaves exactly that one; left empty it\n * leaves EVERY non-internal IPv4 interface at once (one socket each), because multicast\n * egress otherwise follows only the host's default route \u2014 on a multi-homed host whose\n * default route is not the AV network that means the receiver is never reached and nothing\n * is found. Responders from all interfaces are merged into one list; the caller\n * de-duplicates by address.\n *\n * @param target the search target (device type)\n * @param timeoutMs how long to collect responses\n * @returns the responders\n */\n private ssdpSearch(target: string, timeoutMs: number): Promise<Array<{ location: string; address: string }>> {\n return new Promise(resolve => {\n const bindAddrs = searchInterfaces(this.config.networkInterface, networkInterfaces());\n const responders: Array<{ location: string; address: string }> = [];\n const sockets: ReturnType<typeof createSocket>[] = [];\n let settled = false;\n const finish = (): void => {\n if (settled) {\n return;\n }\n settled = true;\n for (const socket of sockets) {\n try {\n socket.close();\n } catch {\n // already closed\n }\n }\n resolve(responders);\n };\n // Open one search socket bound to a single interface (or the default route when bindAddr\n // is undefined). Every socket shares the responders list and the one settle timeout.\n const searchFrom = (bindAddr: string | undefined): void => {\n const socket = createSocket(\"udp4\");\n sockets.push(socket);\n socket.on(\"message\", (msg, rinfo) => {\n const location = /LOCATION:\\s*(\\S+)/i.exec(msg.toString());\n if (location) {\n responders.push({ location: location[1], address: rinfo.address });\n }\n });\n socket.on(\"error\", err => {\n // One interface failing (typically a stale selected IP after a DHCP change) must not\n // kill the search on the others \u2014 warn and drop just this socket; the timeout still\n // resolves whatever the rest found.\n this.log.warn(\n `discovery socket failed${bindAddr ? ` on interface ${bindAddr}` : \"\"}: ${errorMessage(err)}${\n bindAddr ? \" \u2014 check the Network Interface setting\" : \"\"\n }`,\n );\n try {\n socket.close();\n } catch {\n // already closed\n }\n });\n const sendSearch = (): void => {\n if (settled) {\n return;\n }\n const msearch = `M-SEARCH * HTTP/1.1\\r\\nHOST: 239.255.255.250:1900\\r\\nMAN: \"ssdp:discover\"\\r\\nMX: 3\\r\\nST: ${target}\\r\\n\\r\\n`;\n try {\n socket.send(msearch, 1900, \"239.255.255.250\");\n } catch {\n // socket already closed by an error above\n }\n };\n socket.bind(0, bindAddr, () => {\n // Pin OUTGOING multicast to this interface. bind() only sets the source address; the\n // egress interface is IP_MULTICAST_IF \u2014 without it the OS uses its default route, so\n // the search can leave the wrong NIC on a multi-homed host (Node dgram docs).\n if (bindAddr) {\n try {\n socket.setMulticastInterface(bindAddr);\n } catch {\n this.log.info(`discovery: could not pin multicast egress to ${bindAddr} \u2014 using the default interface`);\n }\n }\n // Multicast is lossy and a single request can be dropped \u2014 repeat the M-SEARCH a few\n // times inside the collect window so one lost packet does not hide a receiver.\n for (let i = 0; i < SSDP_SEARCH_BURST; i++) {\n this.setTimeout(sendSearch, i * SSDP_SEARCH_INTERVAL_MS);\n }\n });\n };\n // Configured \u2192 that one interface; empty \u2192 every non-internal IPv4; none usable \u2192 default route.\n if (bindAddrs.length === 0) {\n searchFrom(undefined);\n } else {\n for (const bindAddr of bindAddrs) {\n searchFrom(bindAddr);\n }\n }\n this.setTimeout(finish, timeoutMs);\n });\n }\n\n /**\n * Fetch a URL over HTTP and resolve its body.\n *\n * @param url the URL to fetch\n * @returns the response body\n */\n private fetchUrl(url: string): Promise<string> {\n return new Promise((resolve, reject) => {\n const req = httpGet(url, res => {\n let data = \"\";\n let bytes = 0;\n res.on(\"data\", chunk => {\n bytes += (chunk as Buffer).length;\n if (bytes > MAX_HTTP_BODY_BYTES) {\n // A description document is a few KB \u2014 whatever streams past the cap is not one.\n res.destroy(new Error(`description too large: ${url}`));\n return;\n }\n data += String(chunk);\n });\n // A connection dropped mid-body emits on the RESPONSE stream, not the request \u2014\n // without this handler that is an unhandled error event instead of a rejection.\n res.on(\"error\", reject);\n res.on(\"end\", () => resolve(data));\n });\n req.on(\"error\", reject);\n req.setTimeout(FETCH_TIMEOUT_MS, () => req.destroy(new Error(`fetch timed out: ${url}`)));\n });\n }\n}\n\nif (require.main !== module) {\n // Export the constructor in compact mode\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) => new Yamaha(options);\n} else {\n // Start the instance directly\n (() => new Yamaha())();\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,YAAuB;AACvB,wBAA6B;AAC7B,uBAA+B;AAC/B,qBAAkC;AAClC,4BAA8B;AAC9B,gCAAiC;AACjC,oBAA+B;AAE/B,4BAOO;AACP,yBAA6B;AAC7B,0BAgBO;AACP,kBAAkD;AAClD,kBAAsB;AACtB,uBAA+B;AAC/B,8BAA6D;AAC7D,mCAAsD;AACtD,2BAAgC;AAChC,+BAAuC;AAEvC,+BAAwD;AACxD,gCAAkC;AAClC,gCAAkC;AAGlC,gCAAmC;AAGnC,MAAM,oBAAoB;AAC1B,MAAM,mBAAmB;AAGzB,MAAM,mBAAmB;AAGzB,MAAM,oBAAoB;AAE1B,MAAM,0BAA0B;AAGhC,MAAM,gBAAgB,CAAC,QAAQ,OAAO,KAAK;AAM3C,MAAM,8BAA8B;AAOpC,MAAM,6BAA6B;AAMnC,MAAM,2BAA2B;AA6B1B,MAAM,eAAe,MAAM,QAAQ;AAAA,EACvB,cAAkC,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQnC,eAAe,oBAAI,IAA0B;AAAA;AAAA;AAAA;AAAA;AAAA,EAK7C,aAAa,oBAAI,IAAuB;AAAA;AAAA;AAAA;AAAA;AAAA,EAKxC,gBAAgB,oBAAI,IAAqB;AAAA;AAAA;AAAA;AAAA;AAAA,EAKlD,sBAAsB;AAAA,EAEb,iBAAiB,oBAAI,IAA8B;AAAA,EACnD,kBAAkB,oBAAI,IAAqB;AAAA;AAAA,EAE3C,gBAAgB,oBAAI,IAA0B;AAAA;AAAA,EAE9C,iBAAiB,oBAAI,IAAY;AAAA;AAAA,EAE1C,cAAc;AAAA;AAAA,EAEd;AAAA;AAAA,EAEA,kBAAkB;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,YAAY;AAAA;AAAA,EAEH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,kBAAkB,oBAAI,IAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMlC,eAAe,oBAAI,IAAoC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWvD,eAAe,oBAAI,IAAyB;AAAA;AAAA,EAE5C,iBAAiB,oBAAI,IAAY;AAAA;AAAA,EAEjC,eAAe,oBAAI,IAAY;AAAA,EACxC,oBAAoB;AAAA,EACpB,oBAAoB;AAAA;AAAA,EAEpB;AAAA;AAAA,EAEA,kBAAkB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMlB,kBAAkB;AAAA;AAAA;AAAA;AAAA;AAAA,EAKlB,qBAAqB;AAAA;AAAA,EAEZ,WAAW,oBAAI,IAAgC;AAAA;AAAA,EAE/C,gBAAgB,oBAAI,IAA2B;AAAA;AAAA;AAAA;AAAA,EAKzD,YAAY,UAAyC,CAAC,GAAG;AAC9D,UAAM;AAAA,MACJ,GAAG;AAAA,MACH,MAAM;AAAA,IACR,CAAC;AAED,SAAK,GAAG,SAAS,KAAK,QAAQ,KAAK,IAAI,CAAC;AACxC,SAAK,GAAG,eAAe,KAAK,cAAc,KAAK,IAAI,CAAC;AACpD,SAAK,GAAG,UAAU,KAAK,SAAS,KAAK,IAAI,CAAC;AAC1C,SAAK,mBAAmB,IAAI,gDAAuB,IAAI;AAAA,EACzD;AAAA;AAAA,EAGA,MAAc,UAAyB;AAjOzC;AAkOI,QAAI;AACF,WAAK,IAAI,KAAK,+DAA0D;AACxE,YAAM,KAAK,SAAS,mBAAmB,EAAE,KAAK,OAAO,KAAK,KAAK,CAAC;AAChE,YAAM,KAAK,oBAAoB;AAC/B,YAAM,KAAK,kBAAkB;AAO7B,YAAM,iBAAa;AAAA,QAAa,KAAK,OAAO;AAAA,QAAS,CAAC,SAAS,YAC7D,KAAK,IAAI,KAAK,WAAW,OAAO,mCAA8B,OAAO,qCAAqC;AAAA,MAC5G;AAIA,YAAM,WAAW,MAAM,KAAK,sBAAsB,kBAAkB,KAAK,SAAS,EAAE;AACpF,WAAK,wBACF,0CAAU,WAAV,mBAAgE,qBAAoB;AACvF,WAAK,cAAc,KAAK,mBAAmB,WAAW,MAAM;AAC5D,YAAM,cAAU,kCAAa,YAAY,KAAK,cAAc,MAAM,KAAK,aAAa,IAAI,CAAC,CAAC;AAC1F,UAAI,KAAK,WAAW;AAIlB;AAAA,MACF;AAGA,WAAK,kBAAkB,KAAK,IAAI;AAChC,iBAAW,UAAU,SAAS;AAC5B,aAAK,eAAe,IAAI,OAAO,EAAE;AAAA,MACnC;AAEA,YAAM,KAAK,2BAA2B;AACtC,YAAM,KAAK,oBAAoB,IAAI,IAAI,QAAQ,IAAI,YAAU,OAAO,EAAE,CAAC,CAAC;AACxE,YAAM,KAAK,0BAA0B;AACrC,YAAM,KAAK,kBAAkB;AAC7B,YAAM,eAAe,IAAI,qCAAgB;AAAA,QACvC,KAAK,EAAE,OAAO,aAAW,KAAK,IAAI,MAAM,OAAO,GAAG,MAAM,aAAW,KAAK,IAAI,KAAK,OAAO,EAAE;AAAA,QAC1F,UAAU,CAAC,IAAI,OAAO,KAAK,WAAW,IAAI,EAAE;AAAA,QAC5C,QAAQ,YAAU,KAAK,aAAa,MAAM;AAAA,MAC5C,CAAC;AACD,mBAAa,MAAM;AACnB,WAAK,eAAe;AACpB,UAAI,WAAW,SAAS,GAAG;AACzB,aAAK,IAAI,KAAK,cAAc,QAAQ,MAAM,0BAA0B;AAAA,MACtE;AACA,iBAAW,UAAU,SAAS;AAK5B,YAAI;AACF,gBAAM,KAAK,YAAY,QAAQ,YAAY;AAAA,QAC7C,SAAS,GAAG;AACV,eAAK,IAAI,MAAM,GAAG,OAAO,EAAE,8BAA0B,0BAAa,CAAC,CAAC,qCAAgC;AAAA,QACtG;AAAA,MACF;AACA,WAAK,oBAAoB;AAGzB,UAAI,KAAK,eAAe,QAAQ,SAAS,GAAG;AAC1C,aAAK,KAAK,0BAA0B,YAAY;AAAA,MAClD;AAAA,IACF,SAAS,GAAG;AACV,WAAK,IAAI,MAAM,uBAAmB,0BAAa,CAAC,CAAC,EAAE;AAAA,IACrD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAc,YAAY,QAAsB,cAA8C;AAjThG;AAoTI,SAAK,gBAAgB,IAAI,OAAO,IAAI,KAAK;AACzC,SAAK,cAAc,IAAI,OAAO,IAAI,EAAE,GAAG,OAAO,CAAC;AAC/C,SAAK,eAAe,IAAI,OAAO,EAAE;AACjC,UAAM,KAAK,mBAAmB,OAAO,IAAI,OAAO,KAAI,YAAO,WAAP,YAAiB,YAAY;AAIjF,UAAM,KAAK,SAAS,GAAG,OAAO,EAAE,oBAAoB,EAAE,KAAK,OAAO,KAAK,KAAK,CAAC;AAI7E,SAAK,cAAc,OAAO,IAAI,CAAC,CAAC;AAChC,UAAM,eAAe,IAAI,4CAAkB;AAI3C,UAAM,UAAU,MAAM,KAAK,kBAAkB,OAAO,EAAE;AACtD,UAAM,eAAe,QAAQ;AAC7B,UAAM,cAAc,QAAQ;AAC5B,UAAM,aAAa,IAAI,0CAAiB;AAAA,MACtC,SAAS,MACP,KAAK,cAAc,QAAQ,cAAc,KAAK,gBAAgB,cAAc,cAAc,WAAW;AAAA,MACvG,UAAU,CAAC,IAAI,OAAO,KAAK,WAAW,IAAI,EAAE;AAAA,MAC5C,QAAQ,YAAU,KAAK,aAAa,MAAsC;AAAA,MAC1E,oBAAoB,eAAa,KAAK,iBAAiB,OAAO,IAAI,SAAS;AAAA,MAC3E,SAAS,IAAI,4CAAkB,mBAAmB,gBAAgB;AAAA,MAClE,KAAK;AAAA,QACH,OAAO,aAAW,KAAK,IAAI,MAAM,OAAO;AAAA,QACxC,MAAM,aAAW,KAAK,IAAI,KAAK,OAAO;AAAA,QACtC,MAAM,aAAW,KAAK,IAAI,KAAK,OAAO;AAAA,MACxC;AAAA,IACF,CAAC;AACD,SAAK,YAAY,KAAK,UAAU;AAChC,SAAK,eAAe,IAAI,OAAO,IAAI,UAAU;AAC7C,eAAW,MAAM;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeA,MAAc,0BAA0B,cAA8C;AACpF,QAAI;AACF,YAAM,SAAS,MAAM,KAAK,aAAa;AACvC,UAAI,KAAK,WAAW;AAClB;AAAA,MACF;AACA,UAAI,UAAU;AACd,iBAAW,UAAU,QAAQ;AAC3B,cAAM,UAAU,KAAK,cAAc,IAAI,OAAO,EAAE;AAChD,YAAI;AACF,cAAI,CAAC,SAAS;AACZ,iBAAK,IAAI,KAAK,mBAAmB,OAAO,EAAE,oBAAe;AACzD,kBAAM,KAAK,YAAY,QAAQ,YAAY;AAC3C,sBAAU;AAAA,UACZ,WAAW,QAAQ,OAAO,OAAO,IAAI;AACnC,iBAAK,IAAI,KAAK,GAAG,OAAO,EAAE,0BAA0B,QAAQ,EAAE,OAAO,OAAO,EAAE,+BAA0B;AACxG,iBAAK,eAAe,OAAO,QAAQ,EAAE;AACrC,iBAAK,WAAW,OAAO,EAAE;AACzB,kBAAM,KAAK,YAAY,QAAQ,YAAY;AAC3C,sBAAU;AAAA,UACZ;AAAA,QACF,SAAS,GAAG;AAEV,eAAK,IAAI,MAAM,GAAG,OAAO,EAAE,8BAA0B,0BAAa,CAAC,CAAC,qCAAgC;AAAA,QACtG;AAAA,MACF;AACA,UAAI,SAAS;AACX,aAAK,oBAAoB;AAAA,MAC3B;AAAA,IACF,SAAS,GAAG;AACV,WAAK,IAAI,KAAK,oCAAgC,0BAAa,CAAC,CAAC,EAAE;AAAA,IACjE;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeQ,mBAAmB,aAA8B;AArZ3D;AAsZI,UAAM,QAAO,UAAK,OAAO,cAAZ,YAAyB;AACtC,WAAO,SAAS,YAAa,SAAS,UAAU,gBAAgB;AAAA,EAClE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUQ,oBAAoB,UAAwB;AAlatD;AAqaI,UAAI,UAAK,cAAc,IAAI,QAAQ,MAA/B,mBAAkC,YAAW,cAAc;AAC7D;AAAA,IACF;AACA,QAAI,CAAC,KAAK,eAAe,KAAK,aAAa,KAAK,oBAAoB,QAAW;AAC7E;AAAA,IACF;AACA,UAAM,WAAW,KAAK;AACtB,QAAI,CAAC,UAAU;AACb;AAAA,IACF;AACA,UAAM,MAAM,KAAK,IAAI,GAAG,8BAA8B,KAAK,IAAI,IAAI,KAAK,gBAAgB;AACxF,SAAK,kBAAkB,KAAK,WAAW,MAAM;AAC3C,WAAK,kBAAkB;AACvB,WAAK,kBAAkB,KAAK,IAAI;AAChC,UAAI,CAAC,KAAK,WAAW;AACnB,aAAK,KAAK,0BAA0B,QAAQ;AAAA,MAC9C;AAAA,IACF,GAAG,GAAG;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,WAAW,UAAwB;AACzC,UAAM,aAAa,KAAK,eAAe,IAAI,QAAQ;AACnD,QAAI,CAAC,YAAY;AACf;AAAA,IACF;AACA,eAAW,MAAM;AACjB,SAAK,eAAe,OAAO,QAAQ;AACnC,UAAM,QAAQ,KAAK,YAAY,QAAQ,UAAU;AACjD,QAAI,SAAS,GAAG;AACd,WAAK,YAAY,OAAO,OAAO,CAAC;AAAA,IAClC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWQ,YAAY,OAAwB,UAAwB;AAClE,UAAM,SAAS,GAAG,QAAQ;AAC1B,eAAW,OAAO,CAAC,GAAG,MAAM,KAAK,CAAC,GAAG;AACnC,UAAI,IAAI,WAAW,MAAM,GAAG;AAC1B,cAAM,OAAO,GAAG;AAAA,MAClB;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAa,aAAa,UAAiC;AACzD,SAAK,WAAW,QAAQ;AAGxB,UAAM,gBAAgB,KAAK,cAAc,IAAI,QAAQ;AACrD,QAAI,eAAe;AACjB,WAAK,aAAa,cAAc,KAAK;AACrC,WAAK,cAAc,OAAO,QAAQ;AAAA,IACpC;AACA,UAAM,SAAS,KAAK,cAAc,IAAI,QAAQ;AAC9C,QAAI,QAAQ;AACV,WAAK,eAAe,OAAO,OAAO,EAAE;AAAA,IACtC;AACA,SAAK,cAAc,OAAO,QAAQ;AAClC,SAAK,gBAAgB,OAAO,QAAQ;AACpC,SAAK,aAAa,OAAO,QAAQ;AAKjC,SAAK,YAAY,OAAO,QAAQ;AAChC,SAAK,aAAa,OAAO,QAAQ;AACjC,SAAK,SAAS,OAAO,QAAQ;AAC7B,SAAK,YAAY,KAAK,iBAAiB,QAAQ;AAC/C,SAAK,YAAY,KAAK,cAAc,QAAQ;AAC5C,SAAK,YAAY,KAAK,cAAc,QAAQ;AAC5C,SAAK,YAAY,KAAK,gBAAgB,QAAQ;AAC9C,SAAK,YAAY,KAAK,cAAc,QAAQ;AAC5C,SAAK,YAAY,KAAK,YAAY,QAAQ;AAC1C,SAAK,cAAc,OAAO,QAAQ;AAClC,QAAI;AACF,YAAM,KAAK,eAAe,UAAU,EAAE,WAAW,KAAK,CAAC;AAAA,IACzD,SAAS,GAAG;AACV,WAAK,IAAI,KAAK,wCAAwC,QAAQ,UAAM,0BAAa,CAAC,CAAC,GAAG;AAAA,IACxF;AACA,SAAK,WAAW,mBAAmB,CAAC,GAAG,KAAK,gBAAgB,OAAO,CAAC,EAAE,KAAK,OAAO,CAAC;AACnF,SAAK,oBAAoB;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeA,MAAc,oBAAmC;AAC/C,QAAI;AACF,YAAM,KAAK,qBAAqB,GAAG;AAAA,IACrC,SAAS,GAAG;AACV,WAAK,IAAI;AAAA,QACP,6CAAyC,0BAAa,CAAC,CAAC;AAAA,MAE1D;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASQ,iBAAiB,UAAkB,WAA0B;AACnE,QAAI,aAAa,CAAC,KAAK,aAAa,IAAI,QAAQ,GAAG;AACjD,WAAK,aAAa,IAAI,QAAQ;AAG9B,WAAK,yBAAyB;AAAA,IAChC;AACA,SAAK,gBAAgB,IAAI,UAAU,SAAS;AAC5C,SAAK,WAAW,GAAG,QAAQ,oBAAoB,SAAS;AAExD,QAAI,CAAC,WAAW;AACd,WAAK,cAAc,UAAU,CAAC,CAAC;AAE/B,WAAK,oBAAoB,QAAQ;AAAA,IACnC;AACA,UAAM,eAAe,CAAC,GAAG,KAAK,gBAAgB,OAAO,CAAC,EAAE,KAAK,OAAO;AACpE,SAAK,WAAW,mBAAmB,YAAY;AAC/C,SAAK,oBAAoB;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUQ,sBAA4B;AAClC,UAAM,QAAQ,KAAK,gBAAgB;AACnC,UAAM,SAAS,CAAC,GAAG,KAAK,gBAAgB,OAAO,CAAC,EAAE,OAAO,OAAO,EAAE;AAClE,SAAK,WAAW,qBAAqB,KAAK;AAC1C,SAAK,WAAW,sBAAsB,MAAM;AAC5C,SAAK,WAAW,yBAAyB,QAAQ,KAAK,WAAW,KAAK;AAAA,EACxE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASQ,cAAc,UAAkB,OAAuB;AAC7D,UAAM,OAAO,IAAI,IAAI,KAAK;AAC1B,eAAW,SAAS,eAAe;AACjC,WAAK,WAAW,GAAG,QAAQ,oBAAoB,KAAK,IAAI,KAAK,IAAI,KAAK,CAAC;AAAA,IACzE;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeQ,WAAW,IAAY,OAAkC;AAC/D,SAAK,SAAS,IAAI,EAAE,KAAK,OAAO,KAAK,KAAK,CAAC,EAAE;AAAA,MAC3C,MAAM;AACJ,aAAK,qBAAqB;AAAA,MAC5B;AAAA,MACA,CAAC,MAAe,KAAK,iBAAiB,SAAS,EAAE,IAAI,CAAC;AAAA,IACxD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASQ,oBAAoB,UAAkB,QAAuC;AAInF,UAAM,UAAU,KAAK,cAAc,IAAI,QAAQ;AAC/C,QAAI,SAAS;AACX,aAAO,OAAO,QAAQ,QAAQ,MAAM;AACpC;AAAA,IACF;AACA,UAAM,QAAuB,EAAE,QAAQ,EAAE,GAAG,OAAO,EAAE;AACrD,SAAK,cAAc,IAAI,UAAU,KAAK;AAEtC,UAAM,QAAQ,KAAK,WAAW,MAAM,KAAK,kBAAkB,QAAQ,GAAG,wBAAwB;AAC9F,QAAI,CAAC,MAAM,OAAO;AAChB,WAAK,KAAK,kBAAkB,QAAQ;AAAA,IACtC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASQ,kBAAkB,UAAiC;AACzD,UAAM,UAAU,KAAK,cAAc,IAAI,QAAQ;AAC/C,QAAI,CAAC,SAAS;AACZ,aAAO,QAAQ,QAAQ;AAAA,IACzB;AACA,SAAK,cAAc,OAAO,QAAQ;AAClC,SAAK,aAAa,QAAQ,KAAK;AAC/B,WAAO,KAAK,aAAa,UAAU,EAAE,QAAQ,QAAQ,OAAO,CAAC,EAAE;AAAA,MAC7D,MAAM;AACJ,aAAK,qBAAqB;AAAA,MAC5B;AAAA,MACA,CAAC,MAAe,KAAK,iBAAiB,iBAAiB,QAAQ,IAAI,CAAC;AAAA,IACtE;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASQ,iBAAiB,MAAc,GAAkB;AACvD,QAAI,KAAK,WAAW;AAClB;AAAA,IACF;AACA,UAAM,UAAU,mBAAmB,IAAI,SAAK,0BAAa,CAAC,CAAC;AAC3D,QAAI,KAAK,oBAAoB;AAC3B,WAAK,IAAI,MAAM,OAAO;AACtB;AAAA,IACF;AACA,SAAK,qBAAqB;AAC1B,SAAK,IAAI,KAAK,GAAG,OAAO,sDAAiD;AAAA,EAC3E;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAc,oBAAoB,WAAuC;AACvE,UAAM,aAAa,MAAM,KAAK,uBAAuB;AACrD,UAAM,WAAW,OAAO,KAAK,UAAU;AACvC,UAAM,YAAQ,kCAAa,UAAU,WAAW,KAAK,SAAS;AAG9D,UAAM,cAAU,sCAAiB,UAAU,WAAW,KAAK,SAAS;AAGpE,UAAM,SAAS,KAAK;AACpB,UAAM,WAAW,SAAS,OAAO,UAAQ;AACvC,iBAAW,YAAY,WAAW;AAChC,cAAM,OAAO,GAAG,KAAK,SAAS,IAAI,QAAQ;AAC1C,YAAI,KAAK,WAAW,IAAI,KAAK,KAAC,8BAAe,KAAK,MAAM,KAAK,MAAM,GAAG,MAAM,GAAG;AAC7E,iBAAO;AAAA,QACT;AAAA,MACF;AACA,aAAO;AAAA,IACT,CAAC;AACD,eAAW,UAAU,CAAC,GAAG,OAAO,GAAG,SAAS,GAAG,QAAQ,GAAG;AACxD,UAAI;AACF,cAAM,KAAK,mBAAe,oCAAe,QAAQ,KAAK,SAAS,CAAC;AAAA,MAClE,QAAQ;AAAA,MAER;AAAA,IACF;AAGA,QAAI,MAAM,SAAS,GAAG;AACpB,WAAK,IAAI,MAAM,WAAW,MAAM,MAAM,0CAA0C;AAAA,IAClF;AACA,QAAI,QAAQ,SAAS,GAAG;AACtB,WAAK,IAAI,MAAM,WAAW,QAAQ,MAAM,4CAA4C;AAAA,IACtF;AACA,QAAI,SAAS,SAAS,GAAG;AACvB,WAAK,IAAI,MAAM,WAAW,SAAS,MAAM,+CAA+C;AAAA,IAC1F;AAGA,SAAK;AAAA,MACH,CAAC,GAAG,OAAO,GAAG,SAAS,GAAG,QAAQ,EAAE,OAAO,YAAO;AAzuBxD;AAyuB2D,iCAAW,MAAM,MAAjB,mBAAoB,UAAS;AAAA,OAAO;AAAA,IAC3F;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgBA,MAAc,mBAAkC;AA3vBlD;AA4vBI,UAAM,aAAuB,CAAC;AAC9B,eAAW,YAAY,KAAK,cAAc;AACxC,YAAM,UAAU,KAAK,SAAS,IAAI,QAAQ;AAC1C,WAAI,mCAAS,kBAAiB,KAAK,aAAY,wCAAS,aAAa,WAAtB,YAAgC,KAAK,GAAG;AACrF,mBAAW,KAAK,QAAQ;AAAA,MAC1B;AAAA,IACF;AACA,QAAI,WAAW,WAAW,GAAG;AAC3B;AAAA,IACF;AACA,UAAM,aAAa,MAAM,KAAK,uBAAuB;AACrD,UAAM,SAAS,MAAM,KAAK,eAAe,GAAG;AAC5C,UAAM,gBAAY,0CAAqB,YAAY,QAAQ,IAAI,IAAI,UAAU,GAAG,KAAK,SAAS,EAAE;AAAA,MAC9F,YAAU,CAAC,KAAK,eAAe,QAAI,oCAAe,QAAQ,KAAK,SAAS,CAAC;AAAA,IAC3E;AACA,UAAM,SAAmB,CAAC;AAC1B,eAAW,YAAY,YAAY;AACjC,YAAM,UAAU,KAAK,SAAS,IAAI,QAAQ;AAC1C,YAAM,UAAU,UACb,OAAO,YAAU,OAAO,WAAW,GAAG,KAAK,SAAS,IAAI,QAAQ,GAAG,CAAC,EACpE,IAAI,gBAAU,oCAAe,QAAQ,KAAK,SAAS,CAAC;AACvD,YAAM,WAAW,IAAI,KAAI,wCAAS,iBAAT,YAAyB,CAAC,CAAC;AACpD,YAAM,YAAY,QAAQ,OAAO,QAAM,SAAS,IAAI,EAAE,CAAC;AACvD,iBAAW,MAAM,WAAW;AAC1B,YAAI;AACF,gBAAM,KAAK,eAAe,EAAE;AAC5B,iBAAO,KAAK,GAAG,KAAK,SAAS,IAAI,EAAE,EAAE;AAAA,QACvC,QAAQ;AAAA,QAER;AAAA,MACF;AAEA,yCAAS,gBAAgB,QAAQ,OAAO,QAAM,CAAC,UAAU,SAAS,EAAE,CAAC;AACrE,yCAAS,YAAW,UAAK,YAAL,YAAgB;AAAA,IACtC;AACA,QAAI,OAAO,SAAS,GAAG;AACrB,WAAK,IAAI,MAAM,WAAW,OAAO,MAAM,oDAAoD;AAC3F,WAAK,sBAAsB,MAAM;AAAA,IACnC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAc,yBAAwC;AACpD,QAAI,KAAK,aAAa,SAAS,GAAG;AAChC;AAAA,IACF;AACA,UAAM,YAAQ,yCAAoB,MAAM,KAAK,uBAAuB,GAAG,KAAK,cAAc,KAAK,SAAS;AACxG,eAAW,UAAU,OAAO;AAC1B,UAAI;AACF,cAAM,KAAK,mBAAe,oCAAe,QAAQ,KAAK,SAAS,CAAC;AAAA,MAClE,QAAQ;AAAA,MAER;AAAA,IACF;AACA,QAAI,MAAM,SAAS,GAAG;AACpB,WAAK,IAAI,MAAM,WAAW,MAAM,MAAM,wDAAwD;AAAA,IAChG;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAc,iBAAiB,IAAY,MAA6C;AACtF,UAAM,SAAS,KAAK,aAAa,IAAI,EAAE;AACvC,QAAI,UAAU,OAAO,KAAK,MAAM,EAAE,KAAK,SAAO,EAAE,OAAO,KAAK,GAAG;AAC7D,YAAM,KAAK,aAAa,IAAI,EAAE,QAAQ,EAAE,QAAQ,KAAK,EAAE,CAAC;AAAA,IAC1D;AACA,SAAK,aAAa,IAAI,IAAI,IAAI;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAc,iBAAiB,IAAY,MAA0C;AACnF,UAAM,SAAS,KAAK,aAAa,IAAI,EAAE;AACvC,UAAM,OAAO,iCAAa,OAAO,YAAS,iCAAS,YAAW,UAAa,KAAK,KAAK,MAAM,MAAS;AACpG,SAAK,aAAa,IAAI,IAAI,EAAE,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,MAAM,KAAK,KAAK,CAAC;AAC3E,QAAI,KAAK,WAAW,GAAG;AACrB;AAAA,IACF;AAOA,UAAM,SAAS,MAAM,KAAK,eAAe,EAAE;AAC3C,SAAI,iCAAQ,UAAS,SAAS;AAC5B;AAAA,IACF;AAEA,UAAM,SAAS,EAAE,GAAG,OAAO,OAAO;AAClC,eAAW,SAAS,MAAM;AACxB,aAAO,OAAO,KAAK;AAAA,IACrB;AACA,QAAI;AAEF,YAAM,KAAK,eAAe,IAAI,EAAE,WAAW,MAAM,CAAC;AAAA,IACpD,SAAS,GAAG;AAGV,WAAK,IAAI,MAAM,GAAG,EAAE,uCAAuC,KAAK,KAAK,IAAI,CAAC,SAAK,0BAAa,CAAC,CAAC,GAAG;AACjG;AAAA,IACF;AACA,UAAM,KAAK,aAAa,IAAI,EAAE,MAAM,SAAS,QAAQ,QAAQ,OAAO,OAAO,CAAC;AAAA,EAC9E;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAc,6BAA4C;AACxD,QAAI;AACF,iBAAW,CAAC,QAAQ,MAAM,KAAK,OAAO,QAAQ,MAAM,KAAK,uBAAuB,CAAC,GAAG;AAClF,aAAI,iCAAQ,UAAS,SAAS;AAC5B,gBAAM,SAAK,oCAAe,QAAQ,KAAK,SAAS;AAChD,eAAK,gBAAgB,IAAI,EAAE;AAC3B,gBAAM,SAAS,OAAO;AAEtB,gBAAM,SAAS,iCAAQ;AACvB,cAAI,WAAW,QAAQ,OAAO,WAAW,UAAU;AACjD,iBAAK,aAAa,IAAI,IAAI,MAAgC;AAAA,UAC5D;AACA,eAAK,aAAa,IAAI,QAAI,oCAAe,MAAM,CAAC;AAAA,QAClD;AAAA,MACF;AAAA,IACF,SAAS,GAAG;AAGV,WAAK,IAAI,MAAM,+CAA2C,0BAAa,CAAC,CAAC,0BAA0B;AACnG,WAAK,kBAAkB;AAAA,IACzB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,qBAAqB,IAAkB;AAC7C,QAAI,KAAK,gBAAgB,IAAI,EAAE,GAAG;AAChC;AAAA,IACF;AACA,SAAK,gBAAgB,IAAI,EAAE;AAC3B,SAAK;AACL,SAAK,yBAAyB;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,sBAAsB,SAAkC;AAC9D,eAAW,UAAU,SAAS;AAC5B,WAAK,gBAAgB,WAAO,oCAAe,QAAQ,KAAK,SAAS,CAAC;AAClE,WAAK;AAAA,IACP;AACA,QAAI,QAAQ,SAAS,GAAG;AACtB,WAAK,yBAAyB;AAAA,IAChC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,2BAAiC;AACvC,QAAI,KAAK,mBAAmB,KAAK,iBAAiB;AAChD;AAAA,IACF;AACA,SAAK,aAAa,KAAK,YAAY;AACnC,SAAK,eAAe,KAAK,WAAW,MAAM;AACxC,WAAK,eAAe;AACpB,YAAM,YAAY;AAChB,aAAK,kBAAkB;AAGvB,YAAI;AACF,gBAAM,KAAK,iBAAiB;AAAA,QAC9B,SAAS,GAAG;AACV,eAAK,IAAI,MAAM,4BAAwB,0BAAa,CAAC,CAAC,yBAAyB;AAAA,QACjF;AAEA,YAAI;AACF,gBAAM,KAAK,uBAAuB;AAAA,QACpC,SAAS,GAAG;AACV,eAAK,IAAI,MAAM,kCAA8B,0BAAa,CAAC,CAAC,yBAAyB;AAAA,QACvF;AACA,cAAM,QAAkB,CAAC;AACzB,YAAI,KAAK,oBAAoB,GAAG;AAC9B,gBAAM,KAAK,WAAW,KAAK,iBAAiB,eAAe;AAAA,QAC7D;AACA,YAAI,KAAK,oBAAoB,GAAG;AAC9B,gBAAM,KAAK,WAAW,KAAK,iBAAiB,eAAe;AAAA,QAC7D;AACA,aAAK,oBAAoB;AACzB,aAAK,oBAAoB;AAEzB,YAAI,MAAM,SAAS,GAAG;AACpB,eAAK,IAAI,KAAK,wBAAwB,MAAM,KAAK,IAAI,CAAC,EAAE;AAAA,QAC1D;AACA,aAAK,kBAAkB;AAAA,MACzB,GAAG;AAAA,IACL,GAAG,2BAA2B;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAc,4BAA2C;AAKvD,UAAM,KAAK,aAAa,QAAQ;AAAA,MAC9B,MAAM;AAAA,MACN,QAAQ,EAAE,UAAM,mBAAM,aAAa,EAAE;AAAA,MACrC,QAAQ,CAAC;AAAA,IACX,CAAC;AACD,UAAM,KAAK,aAAa,mBAAmB;AAAA,MACzC,MAAM;AAAA,MACN,QAAQ;AAAA,QACN,UAAM,mBAAM,0BAA0B;AAAA,QACtC,UAAM,mBAAM,8BAA8B;AAAA,QAC1C,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,MACT;AAAA,MACA,QAAQ,CAAC;AAAA,IACX,CAAC;AACD,UAAM,KAAK,aAAa,qBAAqB;AAAA,MAC3C,MAAM;AAAA,MACN,QAAQ,EAAE,UAAM,mBAAM,cAAc,GAAG,MAAM,UAAU,MAAM,SAAS,MAAM,MAAM,OAAO,MAAM;AAAA,MAC/F,QAAQ,CAAC;AAAA,IACX,CAAC;AACD,UAAM,KAAK,aAAa,sBAAsB;AAAA,MAC5C,MAAM;AAAA,MACN,QAAQ,EAAE,UAAM,mBAAM,eAAe,GAAG,MAAM,UAAU,MAAM,SAAS,MAAM,MAAM,OAAO,MAAM;AAAA,MAChG,QAAQ,CAAC;AAAA,IACX,CAAC;AACD,UAAM,KAAK,aAAa,yBAAyB;AAAA,MAC/C,MAAM;AAAA,MACN,QAAQ,EAAE,UAAM,mBAAM,kBAAkB,GAAG,MAAM,WAAW,MAAM,aAAa,MAAM,MAAM,OAAO,MAAM;AAAA,MACxG,QAAQ,CAAC;AAAA,IACX,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmBA,MAAc,mBAAmB,UAAkB,IAAY,QAAqC;AAtiCtG;AAgjCI,QAAI;AAIJ,QAAI,UAAU,KAAK;AACnB,QAAI;AACF,YAAM,WAAW,MAAM,KAAK,eAAe,QAAQ;AACnD,eAAO,0CAAU,WAAV,mBAAkB,QAAO,aAAY,iCAAa,MAAS;AAClE,YAAM,OAAO,0CAAU,WAAV,mBAAgE;AAC7E,UAAI,OAAO,QAAQ,WAAW;AAC5B,kBAAU;AAAA,MACZ;AAAA,IACF,QAAQ;AACN,aAAO;AAAA,IACT;AACA,SAAK,cAAc,IAAI,UAAU,OAAO;AACxC,UAAM,KAAK;AAAA,MACT;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,QAAQ;AAAA,UACN,MAAM;AAAA,UACN,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,UACvB,cAAc,EAAE,UAAU,GAAG,KAAK,SAAS,IAAI,QAAQ,mBAAmB;AAAA,QAC5E;AAAA,QACA,QAAQ,EAAE,QAAQ,iBAAiB,QAAQ;AAAA,MAC7C;AAAA,MACA,EAAE,UAAU,EAAE,QAAQ,CAAC,MAAM,EAAE,EAAE;AAAA,IACnC;AACA,UAAM,KAAK,aAAa,GAAG,QAAQ,SAAS;AAAA,MAC1C,MAAM;AAAA,MACN,QAAQ,EAAE,UAAM,mBAAM,MAAM,EAAE;AAAA,MAC9B,QAAQ,CAAC;AAAA,IACX,CAAC;AACD,UAAM,KAAK,aAAa,GAAG,QAAQ,oBAAoB;AAAA,MACrD,MAAM;AAAA,MACN,QAAQ;AAAA,QACN,UAAM,mBAAM,WAAW;AAAA;AAAA;AAAA,QAGvB,UAAM,mBAAM,qBAAqB;AAAA,QACjC,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,QACP,KAAK;AAAA,MACP;AAAA,MACA,QAAQ,CAAC;AAAA,IACX,CAAC;AAID,UAAM,KAAK,aAAa,GAAG,QAAQ,eAAe;AAAA,MAChD,MAAM;AAAA,MACN,QAAQ,EAAE,UAAM,mBAAM,OAAO,GAAG,MAAM,UAAU,MAAM,QAAQ,MAAM,MAAM,OAAO,OAAO,KAAK,GAAG;AAAA,MAChG,QAAQ,CAAC;AAAA,IACX,CAAC;AAID,UAAM,KAAK,aAAa,GAAG,QAAQ,YAAY;AAAA,MAC7C,MAAM;AAAA,MACN,QAAQ,EAAE,UAAM,mBAAM,WAAW,GAAG,MAAM,UAAU,MAAM,WAAW,MAAM,MAAM,OAAO,OAAO,KAAK,GAAG;AAAA,MACvG,QAAQ,CAAC;AAAA,IACX,CAAC;AACD,UAAM,KAAK,SAAS,GAAG,QAAQ,YAAY,EAAE,KAAK,IAAI,KAAK,KAAK,CAAC;AAIjE,UAAM,KAAK,aAAa,GAAG,QAAQ,oBAAoB;AAAA,MACrD,MAAM;AAAA,MACN,QAAQ,EAAE,UAAM,mBAAM,YAAY,GAAG,UAAM,mBAAM,gBAAgB,EAAE;AAAA,MACnE,QAAQ,CAAC;AAAA,IACX,CAAC;AACD,eAAW,SAAS,eAAe;AACjC,YAAM,KAAK,aAAa,GAAG,QAAQ,oBAAoB,KAAK,IAAI;AAAA,QAC9D,MAAM;AAAA,QACN,QAAQ;AAAA,UACN,UAAM,mBAAM,sBAAsB,MAAM,YAAY,CAAC;AAAA,UACrD,MAAM;AAAA,UACN,MAAM;AAAA,UACN,MAAM;AAAA,UACN,OAAO;AAAA,UACP,KAAK;AAAA,QACP;AAAA,QACA,QAAQ,CAAC;AAAA,MACX,CAAC;AAAA,IACH;AAAA,EACF;AAAA;AAAA,EAGiB,cAAc,oBAAI,IAAoB;AAAA;AAAA,EAGtC,eAAe,oBAAI,IAA+C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBnF,MAAc,kBAAkB,UAAkB,WAAmB,MAAgC;AA/pCvG;AAgqCI,UAAM,MAAM,KAAK,aAAa,IAAI,QAAQ;AAC1C,QAAI;AACF,YAAM,WAAW,iBAAM,KAAK,eAAe,QAAQ,MAAlC,mBAAsC,WAAtC,mBAA8C;AAC/D,YAAM,YAAQ;AAAA,QACZ,OAAO,YAAY,WAAW,UAAU;AAAA,QACxC;AAAA,QACA;AAAA,QACA;AAAA,QACA,2BAAK;AAAA,QACL,2BAAK;AAAA,MACP;AACA,UAAI,UAAU,QAAW;AACvB;AAAA,MACF;AAGA,YAAM,KAAK,aAAa,UAAU,EAAE,QAAQ,EAAE,MAAM,MAAM,EAAE,CAAC;AAC7D,WAAK,aAAa,IAAI,UAAU,EAAE,MAAM,OAAO,KAAK,CAAC;AACrD,WAAK,IAAI,MAAM,GAAG,QAAQ,yBAAyB,KAAK,GAAG;AAAA,IAC7D,SAAS,GAAG;AACV,WAAK,IAAI,MAAM,GAAG,QAAQ,yCAAqC,0BAAa,CAAC,CAAC,GAAG;AAAA,IACnF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAc,iBAAiB,UAAkB,OAA8B;AAC7E,UAAM,WAAO,iCAAa,KAAK;AAC/B,QAAI,KAAK,YAAY,IAAI,QAAQ,MAAM,MAAM;AAC3C;AAAA,IACF;AACA,SAAK,YAAY,IAAI,UAAU,IAAI;AACnC,QAAI;AACF,YAAM,KAAK,aAAa,UAAU,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;AAAA,IACxD,SAAS,GAAG;AACV,WAAK,IAAI,MAAM,GAAG,QAAQ,qCAAiC,0BAAa,CAAC,CAAC,GAAG;AAAA,IAC/E;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAc,sBAAqC;AACjD,UAAM,SAAS,KAAK;AACpB,UAAM,UAAM,qCAAgB,MAAM;AAClC,QAAI,CAAC,KAAK;AACR;AAAA,IACF;AAIA,WAAO,UAAU,CAAC,GAAG;AACrB,QAAI;AACF,YAAM,KAAK,yBAAyB,kBAAkB,KAAK,SAAS,IAAI,EAAE,QAAQ,EAAE,SAAS,CAAC,GAAG,EAAE,EAAE,CAAC;AACtG,WAAK,IAAI,KAAK,8CAA8C,IAAI,EAAE,8BAA8B;AAAA,IAClG,SAAS,GAAG;AACV,WAAK,IAAI;AAAA,QACP,oDAAgD,0BAAa,CAAC,CAAC;AAAA,MACjE;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAc,oBAAmC;AAC/C,UAAM,SAAS,KAAK;AACpB,QAAI,EAAE,iBAAiB,SAAS;AAC9B;AAAA,IACF;AACA,QAAI,OAAO,aAAa;AACtB,aAAO,kBAAkB;AAAA,IAC3B;AACA,WAAO,OAAO;AACd,QAAI;AACF,YAAM,MAAM,MAAM,KAAK,sBAAsB,kBAAkB,KAAK,SAAS,EAAE;AAC/E,UAAI,2BAAK,QAAQ;AACf,YAAI,IAAI,OAAO,aAAa;AAC1B,cAAI,OAAO,kBAAkB;AAAA,QAC/B;AACA,eAAO,IAAI,OAAO;AAClB,cAAM,KAAK,sBAAsB,kBAAkB,KAAK,SAAS,IAAI,GAAG;AACxE,aAAK,IAAI,KAAK,mDAAmD;AAAA,MACnE;AAAA,IACF,SAAS,GAAG;AACV,WAAK,IAAI,KAAK,gDAA4C,0BAAa,CAAC,CAAC,GAAG;AAAA,IAC9E;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmBQ,cACN,QACA,cACA,gBACA,cACA,kBACA,aACkC;AAClC,eAAO,qCAAc,QAAQ;AAAA,MAC3B;AAAA,MACA;AAAA,MACA;AAAA;AAAA,MAEA,gBAAgB,YAAM,8BAAe,IAAI,KAAK,MAA4C;AAAA,MAC1F,KAAK;AAAA,QACH,OAAO,aAAW,KAAK,IAAI,MAAM,OAAO;AAAA,QACxC,MAAM,aAAW,KAAK,IAAI,KAAK,OAAO;AAAA,QACtC,MAAM,aAAW,KAAK,IAAI,KAAK,OAAO;AAAA,MACxC;AAAA,MACA,cAAc,OAAO,IAAI,QAAQ;AAG/B,YAAI,KAAC,8BAAe,GAAG,MAAM,GAAG,QAAQ,GAAG,IAAI,CAAC,GAAG,KAAK,MAA4C,GAAG;AACrG;AAAA,QACF;AAKA,YAAI,IAAI,SAAS,WAAW,IAAI,OAAO,QAAQ;AAC7C,gBAAM,KAAK,iBAAiB,IAAI,IAAI,OAAO,MAAM;AAAA,QACnD;AACA,cAAM,KAAK,eAAe,IAAI,GAAG;AACjC,YAAI,IAAI,SAAS,SAAS;AACxB,eAAK,qBAAqB,EAAE;AAC5B,eAAK,eAAe,IAAI,EAAE;AAAA,QAC5B;AAAA,MACF;AAAA,MACA,aAAa,CAAC,IAAI,UAAU;AAE1B,YAAI,KAAC,8BAAe,GAAG,MAAM,GAAG,QAAQ,GAAG,IAAI,CAAC,GAAG,KAAK,MAA4C,GAAG;AACrG;AAAA,QACF;AACA,aAAK,WAAW,IAAI,KAAK,cAAc,IAAI,KAAK,CAAC;AAGjD,YAAI,GAAG,SAAS,aAAa,KAAK,OAAO,UAAU,YAAY,MAAM,SAAS,GAAG;AAC/E,gBAAM,YAAY,GAAG,MAAM,GAAG,GAAG,QAAQ,GAAG,CAAC;AAC7C,eAAK,KAAK,iBAAiB,WAAW,KAAK;AAC3C,eAAK,KAAK,kBAAkB,WAAW,OAAO,+BAAW,KAAK;AAAA,QAChE;AAAA,MACF;AAAA,MACA,cAAc,UAAQ,KAAK,KAAK,kBAAkB,OAAO,IAAI,MAAM,+BAAW,UAAU;AAAA,MACxF,QAAQ;AAAA,QACN,UAAU,CAAC,SAAS,OAAQ,KAAK,YAAY,SAAY,KAAK,WAAW,SAAS,EAAE;AAAA,QACpF,QAAQ,YAAU,KAAK,aAAa,MAAM;AAAA,MAC5C;AAAA,MACA,cAAc,CAAC,IAAI,WAAW,aAAa,SAAS,IAAI,MAAM;AAAA,MAC9D,YAAY,MAAM,aAAa,YAAY;AAAA,MAC3C,mBAAmB,CAAC,SAAS,OAAO;AAClC,YAAI,KAAK,WAAW;AAClB,iBAAO,MAAM;AAAA,UAAC;AAAA,QAChB;AACA,cAAM,QAAQ,KAAK,YAAY,SAAS,EAAE;AAC1C,eAAO,MAAM;AACX,cAAI,OAAO;AACT,iBAAK,cAAc,KAAK;AAAA,UAC1B;AAAA,QACF;AAAA,MACF;AAAA,MACA,mBAAmB,KAAK,kBAAkB;AAAA,MAC1C,cAAc,WAAS,KAAK,cAAc,OAAO,IAAI,KAAK;AAAA,MAC1D;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASQ,cAAc,IAAY,OAAgD;AAx2CpF;AAy2CI,QAAI,CAAC,OAAO;AACV;AAAA,IACF;AACA,UAAM,eAAW,oCAAe,IAAI,KAAK,SAAS;AAIlD,UAAM,WAAW,SAAS,MAAM,GAAG,SAAS,QAAQ,GAAG,CAAC;AACxD,UAAM,QAAQ,MAAM,MAAM,MAAM,MAAM,KAAK,oBAAoB,UAAU,MAAM,GAAG;AAClF,eAAK,eAAe,IAAI,QAAQ,MAAhC,mBAAmC,kBAAkB,UAAU,MAAM,KAAK;AAAA,EAC5E;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,SAAS,UAA4B;AA13C/C;AA23CI,QAAI;AACF,WAAK,YAAY;AACjB,WAAK,aAAa,KAAK,YAAY;AACnC,WAAK,aAAa,KAAK,eAAe;AACtC,iBAAK,iBAAL,mBAAmB;AACnB,iBAAW,cAAc,KAAK,aAAa;AACzC,mBAAW,MAAM;AAAA,MACnB;AASA,YAAM,SAA6B,CAAC,KAAK,SAAS,mBAAmB,EAAE,KAAK,OAAO,KAAK,KAAK,CAAC,CAAC;AAC/F,iBAAW,YAAY,KAAK,gBAAgB,KAAK,GAAG;AAClD,aAAK,gBAAgB,IAAI,UAAU,KAAK;AACxC,eAAO,KAAK,KAAK,SAAS,GAAG,QAAQ,oBAAoB,EAAE,KAAK,OAAO,KAAK,KAAK,CAAC,CAAC;AAGnF,mBAAW,SAAS,eAAe;AACjC,iBAAO,KAAK,KAAK,SAAS,GAAG,QAAQ,oBAAoB,KAAK,IAAI,EAAE,KAAK,OAAO,KAAK,KAAK,CAAC,CAAC;AAAA,QAC9F;AAAA,MACF;AACA,aAAO,KAAK,KAAK,SAAS,sBAAsB,EAAE,KAAK,GAAG,KAAK,KAAK,CAAC,CAAC;AACtE,aAAO,KAAK,KAAK,SAAS,yBAAyB,EAAE,KAAK,OAAO,KAAK,KAAK,CAAC,CAAC;AAG7E,iBAAW,YAAY,CAAC,GAAG,KAAK,cAAc,KAAK,CAAC,GAAG;AACrD,eAAO,KAAK,KAAK,kBAAkB,QAAQ,CAAC;AAAA,MAC9C;AACA,WAAK,QAAQ,IAAI,MAAM,EACpB,MAAM,MAAM;AAAA,MAEb,CAAC,EACA,QAAQ,QAAQ;AACnB;AAAA,IACF,QAAQ;AAAA,IAER;AACA,aAAS;AAAA,EACX;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAc,eAAwC;AACpD,UAAM,YAAQ,kDAAoB,IAAI;AACtC,UAAM,QAAQ,UAAM,wCAAe,KAAK;AACxC,QAAI,MAAM,SAAS,GAAG;AAIpB,WAAK,IAAI,KAAK,cAAc,MAAM,MAAM,kEAAkE;AAC1G,aAAO;AAAA,IACT;AACA,SAAK,IAAI,KAAK,yEAAyE;AACvF,UAAM,SAAS,MAAM,KAAK,aAAa;AACvC,SAAK,IAAI,KAAK,cAAc,OAAO,MAAM,0BAA0B;AACnE,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAc,eAAwC;AAGpD,SAAK,kBAAkB,KAAK,IAAI;AAChC,UAAM,YAAQ,kDAAoB,IAAI;AACtC,UAAM,QAAQ,UAAM,wCAAe,KAAK;AACxC,QAAI,QAA6C,CAAC;AAClD,QAAI;AACF,cAAQ,UAAM,iCAAe;AAAA,QAC3B,QAAQ,CAAC,QAAQ,OAAO,KAAK,WAAW,QAAQ,EAAE;AAAA,QAClD,OAAO,SAAO,KAAK,SAAS,GAAG;AAAA,QAC/B,KAAK,EAAE,OAAO,aAAW,KAAK,IAAI,MAAM,OAAO,GAAG,MAAM,aAAW,KAAK,IAAI,KAAK,OAAO,EAAE;AAAA,MAC5F,CAAC;AAAA,IACH,SAAS,GAAG;AACV,WAAK,IAAI,KAAK,iEAA6D,0BAAa,CAAC,CAAC,EAAE;AAAA,IAC9F;AACA,UAAM,aAAS;AAAA,MAAgB;AAAA,MAAO;AAAA,MAAO,CAAC,SAAS,YACrD,KAAK,IAAI,KAAK,sBAAsB,OAAO,mCAA8B,OAAO,oBAAoB;AAAA,IACtG;AAOA,UAAM,UAAU,IAAI,IAAI,UAAM,yCAAY,+CAAiB,IAAI,CAAC,CAAC;AACjE,UAAM,aAAS,kCAAa,KAAK,OAAO,OAAO;AAC/C,UAAM,YAAY,IAAI,IAAI,OAAO,IAAI,YAAU,OAAO,EAAE,CAAC;AACzD,UAAM,YAAY,IAAI,IAAI,OAAO,IAAI,YAAU,OAAO,EAAE,CAAC;AACzD,UAAM,OAAO,OAAO;AAAA,MAClB,YAAU,CAAC,QAAQ,IAAI,OAAO,EAAE,KAAK,CAAC,UAAU,IAAI,OAAO,EAAE,KAAK,CAAC,UAAU,IAAI,OAAO,EAAE;AAAA,IAC5F;AACA,cAAM,yCAAgB,OAAO,IAAI;AACjC,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAc,kBAAkB,UAA+C;AAn/CjF;AAo/CI,QAAI;AACJ,QAAI;AACF,gBAAU,WAAM,KAAK,eAAe,QAAQ,MAAlC,mBAAsC;AAAA,IAClD,QAAQ;AACN,eAAS;AAAA,IACX;AACA,UAAM,QAAQ,IAAI,6CAAmB,UAAU,QAAQ;AAAA,MACrD,iBAAgB,UAAK,YAAL,YAAgB;AAAA,MAChC,KAAK,OAAM,oBAAI,KAAK,GAAE,YAAY;AAAA,MAClC,SAAS,WAAS,KAAK,oBAAoB,UAAU,KAAK;AAAA,MAC1D,KAAK,aAAW,KAAK,IAAI,MAAM,OAAO;AAAA,IACxC,CAAC;AACD,SAAK,SAAS,IAAI,UAAU,KAAK;AACjC,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,WAAW,IAAqB;AACtC,WAAO,KAAK,cAAc,IAAI,GAAG,MAAM,GAAG,GAAG,QAAQ,GAAG,CAAC,CAAC,MAAM;AAAA,EAClE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAa,iBAAiB,UAAkB,IAA4B;AAC1E,QAAI,KAAK,cAAc,IAAI,QAAQ,MAAM,IAAI;AAC3C;AAAA,IACF;AACA,SAAK,cAAc,IAAI,UAAU,EAAE;AACnC,UAAM,KAAK,aAAa,UAAU,EAAE,QAAQ,EAAE,iBAAiB,GAAG,EAAE,CAAC;AACrE,eAAW,CAAC,IAAI,GAAG,KAAK,CAAC,GAAG,KAAK,UAAU,GAAG;AAC5C,UAAI,CAAC,GAAG,WAAW,GAAG,QAAQ,GAAG,GAAG;AAClC;AAAA,MACF;AACA,YAAM,SAAS,KAAK,aAAa,IAAI,EAAE;AACvC,YAAM,KAAK,eAAe,IAAI,GAAG;AACjC,UAAI,CAAC,QAAQ;AACX;AAAA,MACF;AACA,YAAM,QAAQ,MAAM,KAAK,cAAc,EAAE;AACzC,UAAI,QAAO,+BAAO,SAAQ,UAAU;AAClC,aAAK,WAAW,IAAI,SAAK,iCAAU,MAAM,KAAK,MAAM,QAAI,mCAAY,MAAM,KAAK,MAAM,CAAC;AAAA,MACxF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAc,eAAe,IAAY,KAA+B;AACtE,UAAM,UAAU,KAAK,cAAc,IAAI,GAAG;AAC1C,QAAI,QAAQ,SAAS,SAAS;AAC5B,YAAM,KAAK,iBAAiB,IAAI,QAAQ,MAAM;AAAA,IAChD;AACA,UAAM,KAAK,aAAa,IAAI,EAAE,MAAM,QAAQ,MAAM,QAAQ,QAAQ,QAAQ,QAAQ,CAAC,EAAE,CAAC;AAAA,EACxF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcQ,cAAc,IAAY,KAA2B;AAC3D,QAAI,IAAI,SAAS,WAAW,KAAC,qCAAc,GAAG,MAAM,GAAG,QAAQ,GAAG,IAAI,CAAC,CAAC,GAAG;AACzE,aAAO;AAAA,IACT;AACA,UAAM,aAAS,sCAAe,GAAG;AACjC,QAAI,CAAC,QAAQ;AAGX,WAAK,aAAa,OAAO,EAAE;AAC3B,WAAK,WAAW,OAAO,EAAE;AACzB,UAAI,KAAK,WAAW,EAAE,GAAG;AACvB,aAAK,IAAI,MAAM,GAAG,EAAE,8EAAyE;AAAA,MAC/F;AACA,aAAO;AAAA,IACT;AACA,SAAK,aAAa,IAAI,IAAI,MAAM;AAChC,SAAK,WAAW,IAAI,IAAI,GAAG;AAC3B,WAAO,KAAK,WAAW,EAAE,QAAI,uCAAgB,GAAG,IAAI;AAAA,EACtD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASQ,cAAc,IAAY,OAA6D;AAC7F,UAAM,SAAS,KAAK,WAAW,EAAE,IAAI,KAAK,aAAa,IAAI,EAAE,IAAI;AACjE,WAAO,UAAU,OAAO,UAAU,eAAW,iCAAU,OAAO,MAAM,IAAI;AAAA,EAC1E;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYQ,oBAAoB,YAAoB,OAAiD;AAC/F,UAAM,SAAS,KAAK,WAAW,UAAU,IAAI,KAAK,aAAa,IAAI,UAAU,IAAI;AACjF,WAAO,UAAU,OAAO,UAAU,eAAW,mCAAY,OAAO,MAAM,IAAI;AAAA,EAC5E;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,oBAA4B;AAClC,UAAM,UAAU,OAAQ,KAAK,OAA8C,eAAe;AAC1F,YAAQ,OAAO,SAAS,OAAO,KAAK,UAAU,IAAI,UAAU,MAAM;AAAA,EACpE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgBQ,WAAW,QAAgB,WAA0E;AAC3G,WAAO,IAAI,QAAQ,aAAW;AAC5B,YAAM,gBAAY,4CAAiB,KAAK,OAAO,sBAAkB,kCAAkB,CAAC;AACpF,YAAM,aAA2D,CAAC;AAClE,YAAM,UAA6C,CAAC;AACpD,UAAI,UAAU;AACd,YAAM,SAAS,MAAY;AACzB,YAAI,SAAS;AACX;AAAA,QACF;AACA,kBAAU;AACV,mBAAW,UAAU,SAAS;AAC5B,cAAI;AACF,mBAAO,MAAM;AAAA,UACf,QAAQ;AAAA,UAER;AAAA,QACF;AACA,gBAAQ,UAAU;AAAA,MACpB;AAGA,YAAM,aAAa,CAAC,aAAuC;AACzD,cAAM,aAAS,gCAAa,MAAM;AAClC,gBAAQ,KAAK,MAAM;AACnB,eAAO,GAAG,WAAW,CAAC,KAAK,UAAU;AACnC,gBAAM,WAAW,qBAAqB,KAAK,IAAI,SAAS,CAAC;AACzD,cAAI,UAAU;AACZ,uBAAW,KAAK,EAAE,UAAU,SAAS,CAAC,GAAG,SAAS,MAAM,QAAQ,CAAC;AAAA,UACnE;AAAA,QACF,CAAC;AACD,eAAO,GAAG,SAAS,SAAO;AAIxB,eAAK,IAAI;AAAA,YACP,0BAA0B,WAAW,iBAAiB,QAAQ,KAAK,EAAE,SAAK,0BAAa,GAAG,CAAC,GACzF,WAAW,gDAA2C,EACxD;AAAA,UACF;AACA,cAAI;AACF,mBAAO,MAAM;AAAA,UACf,QAAQ;AAAA,UAER;AAAA,QACF,CAAC;AACD,cAAM,aAAa,MAAY;AAC7B,cAAI,SAAS;AACX;AAAA,UACF;AACA,gBAAM,UAAU;AAAA;AAAA;AAAA;AAAA,MAA6F,MAAM;AAAA;AAAA;AACnH,cAAI;AACF,mBAAO,KAAK,SAAS,MAAM,iBAAiB;AAAA,UAC9C,QAAQ;AAAA,UAER;AAAA,QACF;AACA,eAAO,KAAK,GAAG,UAAU,MAAM;AAI7B,cAAI,UAAU;AACZ,gBAAI;AACF,qBAAO,sBAAsB,QAAQ;AAAA,YACvC,QAAQ;AACN,mBAAK,IAAI,KAAK,gDAAgD,QAAQ,qCAAgC;AAAA,YACxG;AAAA,UACF;AAGA,mBAAS,IAAI,GAAG,IAAI,mBAAmB,KAAK;AAC1C,iBAAK,WAAW,YAAY,IAAI,uBAAuB;AAAA,UACzD;AAAA,QACF,CAAC;AAAA,MACH;AAEA,UAAI,UAAU,WAAW,GAAG;AAC1B,mBAAW,MAAS;AAAA,MACtB,OAAO;AACL,mBAAW,YAAY,WAAW;AAChC,qBAAW,QAAQ;AAAA,QACrB;AAAA,MACF;AACA,WAAK,WAAW,QAAQ,SAAS;AAAA,IACnC,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,SAAS,KAA8B;AAC7C,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,UAAM,iBAAAA,KAAQ,KAAK,SAAO;AAC9B,YAAI,OAAO;AACX,YAAI,QAAQ;AACZ,YAAI,GAAG,QAAQ,WAAS;AACtB,mBAAU,MAAiB;AAC3B,cAAI,QAAQ,iCAAqB;AAE/B,gBAAI,QAAQ,IAAI,MAAM,0BAA0B,GAAG,EAAE,CAAC;AACtD;AAAA,UACF;AACA,kBAAQ,OAAO,KAAK;AAAA,QACtB,CAAC;AAGD,YAAI,GAAG,SAAS,MAAM;AACtB,YAAI,GAAG,OAAO,MAAM,QAAQ,IAAI,CAAC;AAAA,MACnC,CAAC;AACD,UAAI,GAAG,SAAS,MAAM;AACtB,UAAI,WAAW,kBAAkB,MAAM,IAAI,QAAQ,IAAI,MAAM,oBAAoB,GAAG,EAAE,CAAC,CAAC;AAAA,IAC1F,CAAC;AAAA,EACH;AACF;AAEA,IAAI,QAAQ,SAAS,QAAQ;AAE3B,SAAO,UAAU,CAAC,YAAuD,IAAI,OAAO,OAAO;AAC7F,OAAO;AAEL,GAAC,MAAM,IAAI,OAAO,GAAG;AACvB;",
4
+ "sourcesContent": ["import * as utils from \"@iobroker/adapter-core\";\nimport { createSocket } from \"node:dgram\";\nimport { get as httpGet } from \"node:http\";\nimport { networkInterfaces } from \"node:os\";\nimport { attemptDevice } from \"./lib/attempt-device\";\nimport { searchInterfaces } from \"./lib/network-interfaces\";\nimport { isGroupEnabled } from \"./lib/catalog/groups\";\nimport type { ObjectDef } from \"./lib/catalog/types\";\nimport {\n asPercentObject,\n fromPercent,\n isAmpVolumeId,\n toPercent,\n volumeBoundsOf,\n type VolumeBounds,\n} from \"./lib/catalog/volume-percent\";\nimport { iconForModel } from \"./lib/device-type\";\nimport {\n BOUND_FIELDS,\n type BoundFields,\n boundsOfCommon,\n childlessChannelIds,\n LABEL_RANK,\n type LabelRank,\n legacyDeviceRow,\n mergeDiscovered,\n neverWrittenStateIds,\n nextDeviceLabel,\n parseDevices,\n unionDevices,\n renamedObjectIds,\n staleObjects,\n stripNamespace,\n} from \"./lib/pure-helpers\";\nimport { errorMessage, MAX_HTTP_BODY_BYTES } from \"./lib/util\";\nimport { tName } from \"./lib/i18n\";\nimport { discoverYamaha } from \"./lib/discovery\";\nimport { readDiscovered, readIgnored, writeDiscovered } from \"./lib/discovered-store\";\nimport { discoveredStoreDeps, ignoredStoreDeps } from \"./lib/discovered-store-deps\";\nimport { YxcPushReceiver } from \"./lib/yxc/push-receiver\";\nimport { YamahaDeviceManagement } from \"./device-management\";\nimport type { DeviceSource, DeviceRecord } from \"./lib/types\";\nimport { DeviceSupervisor, type ConnectionHandle } from \"./lib/lifecycle/device-supervisor\";\nimport { ReconnectStrategy } from \"./lib/lifecycle/reconnect-strategy\";\nimport { ReachabilityDedup } from \"./lib/lifecycle/reachability-dedup\";\nimport type { YncaSubunitCache } from \"./lib/ynca/subunit-cache\";\nimport type { ProbeMemory } from \"./lib/lifecycle/probe-memory\";\nimport { DeviceProfileStore } from \"./lib/lifecycle/capability-profile\";\n\n/** Supervisor reconnect backoff bounds (exponential: 1s, 2s \u2026 capped at 60s). */\nconst RECONNECT_BASE_MS = 1000;\nconst RECONNECT_MAX_MS = 60000;\n\n/** Abort a discovery description fetch after this long, so a dead device cannot hang it. */\nconst FETCH_TIMEOUT_MS = 4000;\n\n/** How often the discovery M-SEARCH is repeated \u2014 multicast is lossy, one dropped packet must not hide a receiver. */\nconst SSDP_SEARCH_BURST = 3;\n/** Spacing between the repeated M-SEARCH sends, inside the collect window. */\nconst SSDP_SEARCH_INTERVAL_MS = 1000;\n\n/** The three transports in attempt order \u2014 also the per-transport `info.transports.*` state ids. */\nconst TRANSPORT_IDS = [\"ynca\", \"yxc\", \"xml\"] as const;\n\n/**\n * How long the datapoint balance waits for quiet before it logs. Devices connect\n * asynchronously and in parallel, so the line has to outlast the slowest of them.\n */\nconst DATAPOINT_BALANCE_SETTLE_MS = 5000;\n\n/**\n * Shortest gap between two network searches triggered by an offline auto-found device. A\n * receiver that moved to another address answers nowhere else, so the search is the only way\n * back to it \u2014 but a device that is simply switched off must not turn that into a scan loop.\n */\nconst REDISCOVER_MIN_INTERVAL_MS = 300000;\n\n/**\n * How long a device object's native writes are collected before ONE extendObject carries them\n * (the probe memory persists on every change \u2014 dozens within a first connect's first second).\n */\nconst NATIVE_PERSIST_WINDOW_MS = 250;\n\n/**\n * A map or set keyed by namespace-relative state ids \u2014 the shape {@link YamahaAdapter.forgetUnder}\n * prunes when a device goes. Structural on purpose: `Map<string, T>` and `Set<string>` both fit.\n */\ninterface StateKeyedCache {\n keys(): IterableIterator<string>;\n delete(key: string): boolean;\n}\n\n/** A device's native patch waiting for its coalescing window to end. */\ninterface PendingNative {\n /** The merged patch (latest value per key wins). */\n native: Record<string, unknown>;\n /** The window timer; undefined when the adapter refused one (shutdown) and the write ran at once. */\n timer?: ioBroker.Timeout;\n}\n\n/**\n * ioBroker.yamaha \u2014 controls Yamaha AV receivers and MusicCast devices.\n *\n * Each configured device is driven by a supervisor that keeps a multi-transport\n * handle online: every protocol the device answers \u2014 YNCA (amp control over a held\n * TCP connection, event-pushed), YXC (MusicCast, push + poll), XML/YNC (pre-2010,\n * polled over HTTP) \u2014 connects in parallel on one object tree, each datapoint owned\n * by the best-fitting transport. All YXC devices share one UDP push receiver, keyed\n * by source IP.\n */\nexport class Yamaha extends utils.Adapter {\n private readonly supervisors: DeviceSupervisor[] = [];\n /** deviceId \u2192 its supervisor, so a state change goes to ONE device, not to all of them. */\n /**\n * What each volume datapoint declares on the DEVICE'S OWN scale, while percent mode replaces\n * that declaration with 0\u2026100 %. Filled by `upsertObject` \u2014 the object is always written before\n * any value for it \u2014 and read by both value directions, so the conversion has exactly one\n * source. Keyed by the full state id, so zones and devices never mix.\n */\n private readonly volumeScales = new Map<string, VolumeBounds>();\n /**\n * Per volume datapoint, the definition the coordinator produced BEFORE percent had its say \u2014\n * what the live switch rebuilds from, so turning it changes the object without a restart.\n */\n private readonly volumeDefs = new Map<string, ObjectDef>();\n /**\n * Per device, whether its volume datapoints read 0\u2026100 %. A device setting, not an instance\n * one: the adapter serves several receivers and 2.8.0's single checkbox hit all of them.\n */\n private readonly volumePercent = new Map<string, boolean>();\n /**\n * The instance-wide percent switch of 2.8.0, read once per start. It decides what a device\n * that has not been asked yet inherits \u2014 see {@link ensureDeviceHeader}.\n */\n private legacyVolumePercent = false;\n\n private readonly supervisorById = new Map<string, DeviceSupervisor>();\n private readonly deviceConnected = new Map<string, boolean>();\n /** deviceId \u2192 the record it is currently running with, so an address change is visible. */\n private readonly deviceRecords = new Map<string, DeviceRecord>();\n /** The addresses of all supervised devices \u2014 a multiroom group resolves its clients through it. */\n private readonly knownDeviceIps = new Set<string>();\n /** Whether the network search runs in this instance \u2014 see {@link searchesTheNetwork}. */\n private discovering = false;\n /** Armed while an auto-found device is offline: the search that can bring it back. */\n private rediscoverTimer: ioBroker.Timeout | undefined;\n /** When the last background search ran, so the retry cannot become a scan loop. */\n private lastRediscovery = 0;\n private pushReceiver: YxcPushReceiver | undefined;\n /**\n * Set the moment teardown begins: a connect attempt still in flight then resolves into\n * a closing adapter and must not arm keepalives/timers any more \u2014 the framework would\n * refuse them anyway, but with a warn line per attempt (\"setInterval called, but\n * adapter is shutting down\", seen live on the 1.7.0 upgrade restart).\n */\n private unloading = false;\n /** Device-manager backend: the receivers as cards with add/edit/delete. */\n private readonly deviceManagement: YamahaDeviceManagement;\n /**\n * Every datapoint that existed when this run started, filled ONCE before the cleanup and\n * before any device connects. Without it the balance below would report the whole tree as\n * new on every restart: `upsertObject` runs `extendObject` on every state it touches (the\n * role/unit retrofit), so \"did the create path run?\" is not the same question as \"is this\n * datapoint new?\".\n */\n private readonly knownDatapoints = new Set<string>();\n /**\n * The `common.states` map every existing datapoint carried when this run started, then the\n * map last written by this run \u2014 what a clearing write has to be judged against (#619).\n * Filled from the same start-up read as {@link knownDatapoints}; no per-state database read.\n */\n private readonly storedStates = new Map<string, Record<string, string>>();\n /**\n * The numeric bounds every existing datapoint carried when this run started, then the ones\n * last written by this run \u2014 judged the same way {@link storedStates} is, from the one\n * start-up read, never a database read per state.\n *\n * A bound the new definition DROPS has to be cleared explicitly: `extendObject` merges, so an\n * old `min`/`max` outlives the definition that put it there forever. Measured on\n * `tuner.frequency`, whose FM-only envelope had to go once a DAB receiver reported 180064 kHz\n * into it \u2014 without a clearing write exactly the installations with the problem would keep it.\n */\n private readonly storedBounds = new Map<string, BoundFields>();\n /** State ids (namespace-relative) some transport upserted in THIS run \u2014 live claims. */\n private readonly touchedThisRun = new Set<string>();\n /** Devices that reported connected at least once in this run (gates the orphan purge). */\n private readonly readyDevices = new Set<string>();\n private createdDatapoints = 0;\n private removedDatapoints = 0;\n /** Debounce for the balance line, so one config change produces ONE line, not one per device. */\n private balanceTimer: ioBroker.Timeout | undefined;\n /** Set when the start-up snapshot failed \u2014 a balance without it would be wrong, so none is written. */\n private balanceDisabled = false;\n /**\n * True while the settle pass runs. Its own purges report their removals, which used to\n * re-arm the timer and run the whole pass a second time five seconds later \u2014 two more full\n * reads of the object tree for a round that could only ever remove nothing (audit 2026-09-06).\n */\n private balanceSettling = false;\n /**\n * Latched after the first failed database write, so an outage warns once and the\n * repeats stay at debug until a write goes through again (nut2 `failedUps` pattern).\n */\n private stateWritesFailing = false;\n /** Per device, its capability profile (probe memory, YNCA snapshot, purge marker) \u2014 see loadDeviceProfile. */\n private readonly profiles = new Map<string, DeviceProfileStore>();\n /** Per device, the native patch inside its coalescing window (see persistDeviceNative). */\n private readonly pendingNative = new Map<string, PendingNative>();\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: \"yamaha\",\n });\n\n this.on(\"ready\", this.onReady.bind(this));\n this.on(\"stateChange\", this.onStateChange.bind(this));\n this.on(\"unload\", this.onUnload.bind(this));\n this.deviceManagement = new YamahaDeviceManagement(this);\n }\n\n /** Start a supervisor for each configured device, then subscribe to state changes. */\n private async onReady(): Promise<void> {\n try {\n this.log.info('starting \u2014 a \"ready\" message will follow for each device');\n await this.setState(\"info.connection\", { val: false, ack: true });\n await this.migrateLegacyDevice();\n await this.migrateGroupZones();\n // The running set is the UNION of the device table and the discovery store, and whether\n // the network search runs at all is its own setting. Until 2.9.0 the table WAS the switch\n // \u2014 filled meant manual, empty meant auto \u2014 so the two could never be combined, and\n // turning a single discovered device into a manual one dropped every other one from the\n // run (their trees went with them). XML/pre-2010 devices never answer SSDP, so they are\n // always added by hand; that is exactly the case the mixed mode exists for.\n const configured = parseDevices(this.config.devices, (dropped, takenId) =>\n this.log.warn(`device \"${dropped}\" skipped \u2014 its object id \"${takenId}\" is already used by another device`),\n );\n // The 2.8.0 instance-wide percent switch: read from the instance OBJECT, not from\n // `this.config`. The key is out of the config schema now, so `this.config` is no longer a\n // source to build on \u2014 the object still carries what the user chose.\n const instance = await this.getForeignObjectAsync(`system.adapter.${this.namespace}`);\n this.legacyVolumePercent =\n (instance?.native as { volumeAsPercent?: unknown } | undefined)?.volumeAsPercent === true;\n this.discovering = this.searchesTheNetwork(configured.length);\n const devices = unionDevices(configured, this.discovering ? await this.autoDiscover() : []);\n if (this.unloading) {\n // Stopped during the initial network search: it resolves on its own clock, and\n // everything below \u2014 push socket, subscriptions, device sockets and timers \u2014\n // would come up on an instance that is already gone, with nothing left to close it.\n return;\n }\n // The start-up search (blocking first setup, or the background one below) is the first\n // search of this run \u2014 an offline device must not fire another one right behind it.\n this.lastRediscovery = Date.now();\n for (const device of devices) {\n this.knownDeviceIps.add(device.ip);\n }\n // Devices the discovery store still remembers while the search is off. They do not run\n // this time, but the user never deleted them \u2014 so their trees stay and they are stamped\n // offline instead. Until 2.9.1 the first hand-entered receiver silently took every found\n // one's object tree with it, recordings and VIS bindings included.\n const idle = this.discovering ? [] : await this.rememberedButIdle(devices);\n // Before the cleanup and before any device connects \u2014 see knownDatapoints.\n await this.snapshotExistingDatapoints();\n await this.cleanupStaleObjects(new Set(devices.map(device => device.id)), new Set(idle.map(device => device.id)));\n await this.ensureInstanceInfoObjects();\n await this.markIdleDevicesOffline(idle);\n await this.subscribeToStates();\n const pushReceiver = new YxcPushReceiver({\n log: { debug: message => this.log.debug(message), warn: message => this.log.warn(message) },\n schedule: (cb, ms) => this.setTimeout(cb, ms),\n cancel: handle => this.clearTimeout(handle),\n });\n pushReceiver.start();\n this.pushReceiver = pushReceiver;\n if (configured.length > 0) {\n this.log.info(`setting up ${devices.length} configured device(s)...`);\n }\n for (const device of devices) {\n // Per device, so one failure does not cost the rest of the run: startDevice writes\n // header objects and the disconnected stamp, and a states/objects hiccup on device\n // two used to abort onReady \u2014 devices three and four never came up, the overview was\n // never written and the background search never ran (audit 2026-09-06).\n try {\n await this.startDevice(device, pushReceiver);\n } catch (e) {\n this.log.error(`${device.id}: could not be set up (${errorMessage(e)}) \u2014 the other devices continue`);\n }\n }\n this.writeDeviceOverview();\n // Auto mode with remembered devices: they started WITHOUT waiting for the network\n // search \u2014 it runs behind them, adds newcomers and moves a device that changed address.\n if (this.discovering && devices.length > 0) {\n void this.discoverAdditionalDevices(pushReceiver);\n }\n } catch (e) {\n this.log.error(`onReady failed: ${errorMessage(e)}`);\n }\n }\n\n /**\n * Bring one device under supervision: header objects, disconnected stamp, the\n * per-device caches, and the supervisor that keeps it connected. Factored out of\n * onReady so the background discovery can start a late-found device the same way.\n *\n * @param device the device record\n * @param pushReceiver the shared YXC push receiver\n */\n private async startDevice(device: DeviceRecord, pushReceiver: YxcPushReceiver): Promise<void> {\n // Both callers check `unloading` after their network search resolves (onReady and\n // discoverAdditionalDevices) \u2014 a device handed over after onUnload never gets here.\n this.deviceConnected.set(device.id, false);\n this.deviceRecords.set(device.id, { ...device });\n this.knownDeviceIps.add(device.ip);\n await this.ensureDeviceHeader(device.id, device.ip, device.source ?? \"discovered\");\n // Stamp it disconnected BEFORE the first attempt: ioBroker keeps a state's last value\n // forever, so a crash or a power cut would otherwise leave the device green until it\n // reports again \u2014 and a device that never answers would stay green for good.\n await this.setState(`${device.id}.info.connection`, { val: false, ack: true });\n // The three protocol flags follow the same rule: left at their last value, a crash\n // would show \"YNCA connected\" on the card next to a red connection dot \u2014 for good, if\n // the device never answers again.\n this.setTransports(device.id, []);\n const reachability = new ReachabilityDedup();\n // Held here, not in the controllers: those are rebuilt on every connection attempt;\n // persisted at the device object (one capability profile), so a restart starts from the\n // remembered answers.\n const profile = await this.loadDeviceProfile(device.id);\n const subunitCache = profile.subunitCache;\n const probeMemory = profile.probeMemory;\n const supervisor = new DeviceSupervisor({\n attempt: () =>\n this.attemptDevice(device, pushReceiver, this.knownDeviceIps, reachability, subunitCache, probeMemory),\n schedule: (cb, ms) => this.setTimeout(cb, ms),\n cancel: handle => this.clearTimeout(handle as ioBroker.Timeout | undefined),\n onConnectionChange: connected => this.reportConnection(device.id, connected),\n backoff: new ReconnectStrategy(RECONNECT_BASE_MS, RECONNECT_MAX_MS),\n log: {\n debug: message => this.log.debug(message),\n info: message => this.log.info(message),\n warn: message => this.log.warn(message),\n },\n });\n this.supervisors.push(supervisor);\n this.supervisorById.set(device.id, supervisor);\n supervisor.start();\n }\n\n /**\n * The background half of auto-discovery: search the network, bring devices online that are\n * not supervised yet, and move a device that answers at a NEW address. The remembered devices\n * did not wait for this \u2014 the search window (seconds) used to gate every restart although the\n * devices were already known.\n *\n * The address half matters because the object id \u2014 and with it the whole tree, its history and\n * every visualisation binding \u2014 is fixed to the device, not to where it sits. A receiver that\n * moved by DHCP is the same device at a new address, so its supervisor is rebuilt there instead\n * of retrying an address nobody answers at any more.\n *\n * @param pushReceiver the shared YXC push receiver\n */\n private async discoverAdditionalDevices(pushReceiver: YxcPushReceiver): Promise<void> {\n try {\n const merged = await this.runDiscovery();\n if (this.unloading) {\n return; // the search outlived the adapter \u2014 nothing may start now\n }\n let changed = false;\n for (const device of merged) {\n const running = this.deviceRecords.get(device.id);\n try {\n if (!running) {\n this.log.info(`discovery found ${device.id} \u2014 setting up`);\n await this.startDevice(device, pushReceiver);\n changed = true;\n } else if (running.ip !== device.ip) {\n this.log.info(`${device.id}: address changed from ${running.ip} to ${device.ip} \u2014 reconnecting it there`);\n this.knownDeviceIps.delete(running.ip);\n this.stopDevice(device.id);\n await this.startDevice(device, pushReceiver);\n changed = true;\n }\n } catch (e) {\n // Same rule as the start-up loop: one device must not end the round for the others.\n this.log.error(`${device.id}: could not be set up (${errorMessage(e)}) \u2014 the other devices continue`);\n }\n }\n if (changed) {\n this.writeDeviceOverview();\n }\n } catch (e) {\n this.log.warn(`background discovery failed: ${errorMessage(e)}`);\n }\n }\n\n /**\n * Whether this instance searches the network at all.\n *\n * Three operating modes out of two independent things \u2014 this setting and the device table:\n * `auto` searches while the table is empty (what every installation did before 2.9.0, so an\n * update changes nothing by itself), `always` searches next to a filled table (mixed mode),\n * `never` runs the table alone. The setting is three-valued on purpose: a checkbox would need\n * a default in `io-package.json`, and either value would be wrong for half the existing\n * installations \u2014 `auto` is right for all of them without writing anything.\n *\n * @param manualCount how many devices the instance's table holds\n * @returns whether the network search runs\n */\n private searchesTheNetwork(manualCount: number): boolean {\n const mode = this.config.discovery ?? \"auto\";\n return mode === \"always\" || (mode === \"auto\" && manualCount === 0);\n }\n\n /**\n * The devices the discovery store still remembers that are NOT part of this run: the network\n * search is off, so nothing looked for them. They keep their objects \u2014 switching the search\n * off is a configuration change, not a delete, and only the card's delete button removes a\n * device (it takes the record out of the store in the same step).\n *\n * @param running the devices this run does start\n * @returns the remembered records that stay idle\n */\n private async rememberedButIdle(running: readonly DeviceRecord[]): Promise<DeviceRecord[]> {\n const runningIds = new Set(running.map(device => device.id));\n const remembered = await readDiscovered(discoveredStoreDeps(this));\n return remembered.filter(device => !runningIds.has(device.id));\n }\n\n /**\n * Stamp the idle devices disconnected and say once why they are idle. ioBroker keeps a\n * state's last value forever, so a kept tree would otherwise still claim \"connected,\n * YNCA \u2713\" while nothing is talking to the device \u2014 the same lie the disconnected stamp in\n * {@link startDevice} prevents for a device that does run.\n *\n * Every write is guarded on the object being there: nothing starts these devices, so\n * nothing creates their header either, and a blind write would leave bare orphan states\n * behind for a device whose tree is already gone.\n *\n * @param idle the remembered devices that do not run this time\n */\n private async markIdleDevicesOffline(idle: readonly DeviceRecord[]): Promise<void> {\n if (idle.length === 0) {\n return;\n }\n for (const device of idle) {\n const ids = [\n `${device.id}.info.connection`,\n ...TRANSPORT_IDS.map(protocol => `${device.id}.info.transports.${protocol}`),\n ];\n for (const id of ids) {\n if (await this.getObjectAsync(id)) {\n await this.setState(id, { val: false, ack: true });\n }\n }\n }\n const names = idle.map(device => device.id).join(\", \");\n // Two levels for two situations. \"Never\" is what the user asked for, so it is a fact, not\n // a problem. With \"Automatic\" the SETTING stopped the search the moment the first device\n // was typed in \u2014 the user did not choose that, and without this line nothing tells them\n // why half their receivers went quiet.\n if ((this.config.discovery ?? \"auto\") === \"never\") {\n this.log.info(\n `${idle.length} remembered device(s) stay idle \u2014 the network search is off (${names}); their objects are kept, use the delete button on a card to remove one`,\n );\n } else {\n this.log.warn(\n `${idle.length} remembered device(s) are not running: the device list is filled and the network search is set to Automatic (${names}) \u2014 their objects are kept; set the search to Always to run them next to the devices you entered`,\n );\n }\n }\n\n /**\n * Arm a background search because an auto-found device is offline \u2014 the only way back to a\n * receiver that moved to another address, since it answers at the remembered one no more.\n * Throttled: a device that is merely switched off must not turn this into a scan loop, and\n * one timer covers however many devices are down.\n *\n * @param deviceId the device that just went offline\n */\n private scheduleRediscovery(deviceId: string): void {\n // Per device, not per instance: a manual device sits at an address the user typed, so there\n // is nothing to search for \u2014 it is simply off. Only a discovered one can have moved.\n if (this.deviceRecords.get(deviceId)?.source !== \"discovered\") {\n return;\n }\n if (!this.discovering || this.unloading || this.rediscoverTimer !== undefined) {\n return;\n }\n const receiver = this.pushReceiver;\n if (!receiver) {\n return;\n }\n const due = Math.max(0, REDISCOVER_MIN_INTERVAL_MS - (Date.now() - this.lastRediscovery));\n this.rediscoverTimer = this.setTimeout(() => {\n this.rediscoverTimer = undefined;\n this.lastRediscovery = Date.now();\n if (!this.unloading) {\n void this.discoverAdditionalDevices(receiver);\n }\n }, due);\n }\n\n /**\n * Stop supervising one device and release its supervisor. The object tree is untouched \u2014\n * a readdress puts the same device straight back on it.\n *\n * @param deviceId the id-safe device id\n */\n private stopDevice(deviceId: string): void {\n const supervisor = this.supervisorById.get(deviceId);\n if (!supervisor) {\n return;\n }\n supervisor.close();\n this.supervisorById.delete(deviceId);\n const index = this.supervisors.indexOf(supervisor);\n if (index >= 0) {\n this.supervisors.splice(index, 1);\n }\n }\n\n /**\n * Drop every entry of a state-id-keyed cache that belongs to one device.\n *\n * The caches are keyed by namespace-relative state ids (`stripNamespace` in\n * {@link snapshotExistingDatapoints}), so a device owns exactly the keys under its own prefix.\n *\n * @param cache a map or set keyed by namespace-relative state ids\n * @param deviceId the id-safe device id\n */\n private forgetUnder(cache: StateKeyedCache, deviceId: string): void {\n const prefix = `${deviceId}.`;\n for (const key of [...cache.keys()]) {\n if (key.startsWith(prefix)) {\n cache.delete(key);\n }\n }\n }\n\n /**\n * Remove one device for good: stop talking to it and delete its object tree.\n *\n * Driven by the device manager's delete action. Deleting a discovered device used to only\n * empty the remembered list \u2014 the supervisor kept the connection, the tree stayed, and the card\n * came back on the next start. Now the delete is what it says; the id is additionally kept in\n * the ignored list (device manager) so a later search does not put the device back.\n *\n * @param deviceId the id-safe device id\n */\n public async removeDevice(deviceId: string): Promise<void> {\n this.stopDevice(deviceId);\n // A native patch still inside its coalescing window would fire AFTER the delete below and\n // recreate the device object as a bare orphan \u2014 cancel it before anything else.\n const pendingNative = this.pendingNative.get(deviceId);\n if (pendingNative) {\n this.clearTimeout(pendingNative.timer);\n this.pendingNative.delete(deviceId);\n }\n const record = this.deviceRecords.get(deviceId);\n if (record) {\n this.knownDeviceIps.delete(record.ip);\n }\n this.deviceRecords.delete(deviceId);\n this.deviceConnected.delete(deviceId);\n this.readyDevices.delete(deviceId);\n // Everything else this device left behind goes with it. A cache that survives makes the\n // adapter believe it already did the work: re-adding the SAME id finds the icon cache\n // intact, `updateDeviceIcon` bails on the identity check, and the card keeps the default\n // silhouette `ensureDeviceHeader` seeds \u2014 a soundbar shows a receiver until the next start.\n this.deviceIcons.delete(deviceId);\n this.deviceLabels.delete(deviceId);\n this.profiles.delete(deviceId);\n this.forgetUnder(this.knownDatapoints, deviceId);\n this.forgetUnder(this.storedStates, deviceId);\n this.forgetUnder(this.storedBounds, deviceId);\n this.forgetUnder(this.touchedThisRun, deviceId);\n this.forgetUnder(this.volumeScales, deviceId);\n this.forgetUnder(this.volumeDefs, deviceId);\n this.volumePercent.delete(deviceId);\n try {\n await this.delObjectAsync(deviceId, { recursive: true });\n } catch (e) {\n this.log.warn(`could not remove the object tree of \"${deviceId}\" (${errorMessage(e)})`);\n }\n this.writeState(\"info.connection\", [...this.deviceConnected.values()].some(Boolean));\n this.writeDeviceOverview();\n }\n\n /**\n * Subscribe to the adapter's own states \u2014 OBSERVED, like every other database call.\n *\n * `subscribeStates` without a callback returns a promise, and its wildcard branch reads the\n * matching objects first: any failure there ends in `maybeCallbackWithError`, which rejects\n * for everything except the plain \"database closed\" case (js-controller-common-db source).\n * Left unawaited that is an unhandled rejection \u2014 and js-controller turns those into an\n * adapter stop, the same trap the nine bare state writes carried.\n *\n * A failure is loud but not fatal: without the subscription the tree still fills from the\n * devices, only user writes stop being applied. Saying so beats a silent half-working\n * instance, and beats losing the whole start over it.\n */\n private async subscribeToStates(): Promise<void> {\n try {\n await this.subscribeStatesAsync(\"*\");\n } catch (e) {\n this.log.error(\n `could not subscribe to state changes (${errorMessage(e)}) \u2014 the tree still updates, ` +\n `but writes to datapoints will not reach the device until the instance is restarted`,\n );\n }\n }\n\n /**\n * Aggregate one device's connection state into the adapter's `info.connection`\n * (true while at least one device is connected).\n *\n * @param deviceId the device reporting\n * @param connected whether that device is currently connected\n */\n private reportConnection(deviceId: string, connected: boolean): void {\n if (connected && !this.readyDevices.has(deviceId)) {\n this.readyDevices.add(deviceId);\n // Arm the settle pass even when the connect created nothing new \u2014 the once-per-\n // version orphan purge rides the same settled moment as the balance line.\n this.scheduleDatapointBalance();\n }\n this.deviceConnected.set(deviceId, connected);\n this.writeState(`${deviceId}.info.connection`, connected);\n // A drop clears the per-transport flags; a (re)connect sets them again via onTransports.\n if (!connected) {\n this.setTransports(deviceId, []);\n // A discovered device may simply have moved \u2014 only a search can find it again.\n this.scheduleRediscovery(deviceId);\n }\n const anyConnected = [...this.deviceConnected.values()].some(Boolean);\n this.writeState(\"info.connection\", anyConnected);\n this.writeDeviceOverview();\n }\n\n /**\n * The three overview datapoints: how many devices this instance runs, how many are\n * connected right now, and whether that is all of them. Derived from the SAME map that\n * feeds the per-device markers and written in the same round \u2014 computed separately they\n * would drift away from what the single devices say.\n *\n * `devicesAllOnline` needs at least one device: zero of zero is not \"everything is fine\".\n */\n private writeDeviceOverview(): void {\n const total = this.deviceConnected.size;\n const online = [...this.deviceConnected.values()].filter(Boolean).length;\n this.writeState(\"info.devicesTotal\", total);\n this.writeState(\"info.devicesOnline\", online);\n this.writeState(\"info.devicesAllOnline\", total > 0 && online === total);\n }\n\n /**\n * Reflect the live transport set into a device's `info.transports.*` flags so the\n * device-manager card shows which protocols (YNCA/YXC/XML) are connected right now.\n *\n * @param deviceId the id-safe device id\n * @param names the transports live now (empty on a drop)\n */\n private setTransports(deviceId: string, names: string[]): void {\n const live = new Set(names);\n for (const proto of TRANSPORT_IDS) {\n this.writeState(`${deviceId}.info.transports.${proto}`, live.has(proto));\n }\n }\n\n /**\n * Write a state with ack \u2014 and OBSERVE the promise. js-controller turns an unhandled\n * promise rejection into an adapter stop (`_exceptionHandler` \u2192 exit code\n * UNCAUGHT_EXCEPTION, read in the controller source), and `setState` rejects whenever\n * the states database is not reachable for a moment (`ERROR_DB_CLOSED`, or a pending\n * command cancelled by a reconnecting Redis). Nine fire-and-forget writes used to run\n * bare: one hiccup while a device pushed a value would have restarted the whole\n * instance. The failure lands in the log instead \u2014 once per outage at warn, then at\n * debug until a write succeeds again; during teardown it is expected and stays silent.\n *\n * @param id the state id (namespace-relative)\n * @param value the value to write\n */\n private writeState(id: string, value: ioBroker.StateValue): void {\n this.setState(id, { val: value, ack: true }).then(\n () => {\n this.stateWritesFailing = false;\n },\n (e: unknown) => this.noteWriteFailure(`state ${id}`, e),\n );\n }\n\n /**\n * Persist a device object's `native` part \u2014 observed like {@link writeState}. The two\n * per-device caches (YNCA subunit probe, probe memory) persist through it.\n *\n * @param deviceId the device object id\n * @param native the native fields to merge into the object\n */\n private persistDeviceNative(deviceId: string, native: Record<string, unknown>): void {\n // Coalesced per device: the probe memory persists on EVERY change, and a first connect\n // changes it dozens of times within a second (every observed enum value, every declared\n // list) \u2014 each was one extendObject on the device object. Latest wins, one write per window.\n const pending = this.pendingNative.get(deviceId);\n if (pending) {\n Object.assign(pending.native, native);\n return;\n }\n const entry: PendingNative = { native: { ...native } };\n this.pendingNative.set(deviceId, entry);\n // this.setTimeout refuses during shutdown (returns undefined) \u2014 then write at once.\n entry.timer = this.setTimeout(() => this.flushDeviceNative(deviceId), NATIVE_PERSIST_WINDOW_MS);\n if (!entry.timer) {\n void this.flushDeviceNative(deviceId);\n }\n }\n\n /**\n * Write a device's pending native patch now (the coalescing window ended, or the adapter is\n * unloading).\n *\n * @param deviceId the id-safe device id\n * @returns the write, for the unload path to wait on\n */\n private flushDeviceNative(deviceId: string): Promise<void> {\n const pending = this.pendingNative.get(deviceId);\n if (!pending) {\n return Promise.resolve();\n }\n this.pendingNative.delete(deviceId);\n this.clearTimeout(pending.timer);\n return this.extendObject(deviceId, { native: pending.native }).then(\n () => {\n this.stateWritesFailing = false;\n },\n (e: unknown) => this.noteWriteFailure(`device object ${deviceId}`, e),\n );\n }\n\n /**\n * Log a failed database write: warn on the first failure of an outage, debug for the\n * repeats, silence while unloading (the database is going down with us).\n *\n * @param what which write failed, for the log line\n * @param e the rejection reason\n */\n private noteWriteFailure(what: string, e: unknown): void {\n if (this.unloading) {\n return;\n }\n const message = `could not write ${what} (${errorMessage(e)})`;\n if (this.stateWritesFailing) {\n this.log.debug(message);\n return;\n }\n this.stateWritesFailing = true;\n this.log.warn(`${message} \u2014 repeats stay at debug until a write succeeds`);\n }\n\n /**\n * One-shot startup cleanup: delete every object that does not belong to a\n * configured device (the previous adapter's whole tree, and any device dropped\n * from the config). Runs before the devices connect; a configured device's\n * subtree is kept whether or not it has connected yet.\n *\n * @param deviceIds the ids of the currently configured devices\n * @param remembered ids the discovery store still holds that are idle this run\n */\n private async cleanupStaleObjects(deviceIds: Set<string>, remembered: ReadonlySet<string>): Promise<void> {\n const allObjects = await this.getAdapterObjectsAsync();\n const existing = Object.keys(allObjects);\n // Only the DELETION widens to the remembered ids. The two passes below stay on the\n // running set on purpose: an idle device is not being written to at all this run, so\n // neither a rename nor a switched-off group has any business reaching into its tree.\n const stale = staleObjects(existing, deviceIds, this.namespace, remembered);\n // Old states this version renamed/moved (e.g. system.model -> info.model): delete the\n // old object so it does not linger orphaned beside the new one under a kept device.\n const renamed = renamedObjectIds(existing, deviceIds, this.namespace);\n // Objects whose datapoint group the user switched off \u2014 remove them so turning a group from\n // on to off cleans up its whole subtree (a toggle change restarts the instance, so this runs).\n const config = this.config as unknown as Record<string, unknown>;\n const disabled = existing.filter(full => {\n for (const deviceId of deviceIds) {\n const base = `${this.namespace}.${deviceId}.`;\n if (full.startsWith(base) && !isGroupEnabled(full.slice(base.length), config)) {\n return true;\n }\n }\n return false;\n });\n for (const fullId of [...stale, ...renamed, ...disabled]) {\n try {\n await this.delObjectAsync(stripNamespace(fullId, this.namespace));\n } catch {\n // already removed together with its parent\n }\n }\n // The reasons stay available for diagnosis; the user sees ONE balance line instead of\n // three counts they have to add up themselves.\n if (stale.length > 0) {\n this.log.debug(`removed ${stale.length} object(s) from a previous configuration`);\n }\n if (renamed.length > 0) {\n this.log.debug(`removed ${renamed.length} renamed object(s) from an earlier version`);\n }\n if (disabled.length > 0) {\n this.log.debug(`removed ${disabled.length} object(s) from switched-off datapoint groups`);\n }\n // Channels and device nodes go with them, but only datapoints are counted \u2014 that is what\n // the user switched on or off, and what they look for in the object tree.\n this.noteDatapointsRemoved(\n [...stale, ...renamed, ...disabled].filter(fullId => allObjects[fullId]?.type === \"state\"),\n );\n }\n\n /**\n * Remove read-capable states under a CONNECTED device that never carried a value and were not\n * (re)created by this run's transports \u2014 over-declarations of an earlier adapter version that\n * today's claim-with-proof creation no longer makes. Deleting them is lossless (no value, no\n * history). Runs after the tree settled, so a device that has not connected in this run keeps\n * its tree untouched \u2014 its sweep happens on the first start that reaches it.\n *\n * TWO starts decide, not one (2.7.0): a receiver in standby answers many functions\n * `@RESTRICTED`, so one run seeing a datapoint untouched is no proof the device lost it. The\n * first run RECORDS the candidates in the device's capability profile (`pendingPurge`), the\n * next run deletes those still untouched and still never filled, and forgets the rest. A device\n * is examined once per adapter version (`purgeVersion`) OR whenever it carries a recorded\n * candidate \u2014 the confirmation has to reach its second start even without a new version.\n */\n private async purgeNeverFilled(): Promise<void> {\n const candidates: string[] = [];\n for (const deviceId of this.readyDevices) {\n const profile = this.profiles.get(deviceId);\n if (profile?.purgeVersion !== this.version || (profile?.pendingPurge.length ?? 0) > 0) {\n candidates.push(deviceId);\n }\n }\n if (candidates.length === 0) {\n return;\n }\n const allObjects = await this.getAdapterObjectsAsync();\n const states = await this.getStatesAsync(\"*\");\n const untouched = neverWrittenStateIds(allObjects, states, new Set(candidates), this.namespace).filter(\n fullId => !this.touchedThisRun.has(stripNamespace(fullId, this.namespace)),\n );\n const purged: string[] = [];\n for (const deviceId of candidates) {\n const profile = this.profiles.get(deviceId);\n const seenNow = untouched\n .filter(fullId => fullId.startsWith(`${this.namespace}.${deviceId}.`))\n .map(fullId => stripNamespace(fullId, this.namespace));\n const recorded = new Set(profile?.pendingPurge ?? []);\n const confirmed = seenNow.filter(id => recorded.has(id));\n for (const id of confirmed) {\n try {\n await this.delObjectAsync(id);\n purged.push(`${this.namespace}.${id}`);\n } catch {\n // already gone\n }\n }\n // Whatever is untouched THIS run and was not just deleted waits for the next run.\n profile?.setPendingPurge(seenNow.filter(id => !confirmed.includes(id)));\n profile?.markPurged(this.version ?? \"\");\n }\n if (purged.length > 0) {\n this.log.debug(`removed ${purged.length} never-filled object(s), confirmed over two starts`);\n this.noteDatapointsRemoved(purged);\n }\n }\n\n /**\n * Remove folders that hold no datapoint any more, under the devices that connected this run.\n *\n * The two sweeps above only ever delete datapoints, so a folder emptied by a tree rework stays\n * behind and promises content it can never get \u2014 `player.server` is the live case: the v2.0.0\n * migration deletes the SERVER source's playback copies, and the new tree gives that source no\n * datapoint of its own. Runs on every start, not once per version: an empty folder is wrong\n * whenever it is found, and re-reading the objects after the orphan purge catches the ones that\n * purge just emptied. Not counted in the datapoint balance \u2014 a folder is not a datapoint.\n */\n private async purgeChildlessChannels(): Promise<void> {\n if (this.readyDevices.size === 0) {\n return;\n }\n const empty = childlessChannelIds(await this.getAdapterObjectsAsync(), this.readyDevices, this.namespace);\n for (const fullId of empty) {\n try {\n await this.delObjectAsync(stripNamespace(fullId, this.namespace));\n } catch {\n // already removed together with its parent\n }\n }\n if (empty.length > 0) {\n this.log.debug(`removed ${empty.length} empty folder(s) left over from an earlier object tree`);\n }\n }\n\n /**\n * Empty an object's stored `common.states` when it holds a key the new map lacks, so the\n * following merge-write results in exactly the new map (memory\n * `reference_iobroker_objekt_aendern_ohne_loeschen`: never delete an object to change it,\n * write `null` for the key instead). Judged against the start-up snapshot, then against what\n * this run last wrote \u2014 never a database read per state.\n *\n * @param id the object id (namespace-relative)\n * @param next the map about to be written\n */\n private async clearStaleStates(id: string, next: Record<string, string>): Promise<void> {\n const stored = this.storedStates.get(id);\n if (stored && Object.keys(stored).some(key => !(key in next))) {\n await this.extendObject(id, { common: { states: null } });\n }\n this.storedStates.set(id, next);\n }\n\n /**\n * Clear a bound the new definition no longer declares, so the following merge-write leaves\n * exactly the new bounds behind \u2014 the same rule {@link clearStaleStates} applies to a\n * shrinking dropdown (`reference_iobroker_objekt_aendern_ohne_loeschen`: write `null` for the\n * key, never delete the object). Judged against the start-up snapshot and then against what\n * this run wrote, so it costs no read per state.\n *\n * @param id the object id (namespace-relative)\n * @param next the common part about to be written\n */\n private async clearStaleBounds(id: string, next: ObjectDef[\"common\"]): Promise<void> {\n const stored = this.storedBounds.get(id);\n const gone = BOUND_FIELDS.filter(field => stored?.[field] !== undefined && next[field] === undefined);\n this.storedBounds.set(id, { min: next.min, max: next.max, step: next.step });\n if (gone.length === 0) {\n return;\n }\n // \u26A0\uFE0F NOT the `null` write `clearStaleStates` uses. A dropdown has a neutral value \u2014 an empty\n // map \u2014 and `null` reaches it. A bound has none: the merge writes `common.max = null`, and\n // js-controller's range check then compares against it numerically, where `null` counts as 0\n // and every reading is \"greater than max\" (`reference_attribut_entfernen_ohne_setobject`, and\n // the merge semantics measured in `reference_iobroker_objekt_aendern_ohne_loeschen`). The key\n // has to GO, which is read \u2192 delete \u2192 re-create; `setObject` is the checker's S5054.\n const object = await this.getObjectAsync(id);\n if (object?.type !== \"state\") {\n return;\n }\n // The READ common rides along, so nothing the object already carries is lost in the rewrite.\n const common = { ...object.common } as ioBroker.StateCommon & Record<string, unknown>;\n for (const field of gone) {\n delete common[field];\n }\n try {\n // Explicitly non-recursive: a state has no children, and this must never take a tree with it.\n await this.delObjectAsync(id, { recursive: false });\n } catch (e) {\n // The merge that follows would only put the object back as it was \u2014 nothing is lost, but the\n // stale bound stays, so it belongs in the log rather than passing silently.\n this.log.debug(`${id}: could not drop the stale bound(s) ${gone.join(\", \")} (${errorMessage(e)})`);\n return;\n }\n await this.extendObject(id, { type: \"state\", common, native: object.native });\n }\n\n /**\n * Remember every datapoint that already exists, ONCE per adapter run.\n *\n * @see knownDatapoints for why the create path alone cannot answer \"is this new?\"\n */\n private async snapshotExistingDatapoints(): Promise<void> {\n try {\n for (const [fullId, object] of Object.entries(await this.getAdapterObjectsAsync())) {\n if (object?.type === \"state\") {\n const id = stripNamespace(fullId, this.namespace);\n this.knownDatapoints.add(id);\n const common = object.common as\n { states?: unknown; min?: unknown; max?: unknown; step?: unknown } | undefined;\n const states = common?.states;\n if (states !== null && typeof states === \"object\") {\n this.storedStates.set(id, states as Record<string, string>);\n }\n this.storedBounds.set(id, boundsOfCommon(common));\n }\n }\n } catch (e) {\n // Without the snapshot the balance would call every datapoint new; better to stay\n // silent about it than to log a wrong number.\n this.log.debug(`could not read the existing datapoints (${errorMessage(e)}); balance line disabled`);\n this.balanceDisabled = true;\n }\n }\n\n /**\n * Count a datapoint the device tree just created \u2014 new ones only.\n *\n * @param id the state id relative to the namespace\n */\n private noteDatapointCreated(id: string): void {\n if (this.knownDatapoints.has(id)) {\n return;\n }\n this.knownDatapoints.add(id);\n this.createdDatapoints++;\n this.scheduleDatapointBalance();\n }\n\n /**\n * Count removed datapoints, and let them count again should they ever come back.\n *\n * @param fullIds the removed ids, namespace included\n */\n private noteDatapointsRemoved(fullIds: readonly string[]): void {\n for (const fullId of fullIds) {\n this.knownDatapoints.delete(stripNamespace(fullId, this.namespace));\n this.removedDatapoints++;\n }\n if (fullIds.length > 0) {\n this.scheduleDatapointBalance();\n }\n }\n\n /**\n * Log the balance once the tree has settled. A device connects asynchronously and several\n * devices connect at once, so the line waits for quiet instead of firing per device \u2014 the\n * user made ONE change and reads ONE result.\n */\n private scheduleDatapointBalance(): void {\n if (this.balanceDisabled || this.balanceSettling) {\n return;\n }\n this.clearTimeout(this.balanceTimer);\n this.balanceTimer = this.setTimeout(() => {\n this.balanceTimer = undefined;\n void (async () => {\n this.balanceSettling = true;\n // The tree has settled: sweep the never-filled orphans FIRST, so their\n // removals land in the same balance line the user is about to read.\n try {\n await this.purgeNeverFilled();\n } catch (e) {\n this.log.debug(`orphan purge failed (${errorMessage(e)}); skipped for this run`);\n }\n // Then the folders those removals (or an earlier version's tree rework) left empty.\n try {\n await this.purgeChildlessChannels();\n } catch (e) {\n this.log.debug(`empty-folder purge failed (${errorMessage(e)}); skipped for this run`);\n }\n const parts: string[] = [];\n if (this.createdDatapoints > 0) {\n parts.push(`created ${this.createdDatapoints} datapoint(s)`);\n }\n if (this.removedDatapoints > 0) {\n parts.push(`removed ${this.removedDatapoints} datapoint(s)`);\n }\n this.createdDatapoints = 0;\n this.removedDatapoints = 0;\n // Silent when nothing changed: a plain restart must not write a line.\n if (parts.length > 0) {\n this.log.info(`Object tree updated: ${parts.join(\", \")}`);\n }\n this.balanceSettling = false;\n })();\n }, DATAPOINT_BALANCE_SETTLE_MS);\n }\n\n /**\n * Refresh the adapter's OWN `info.*` objects.\n *\n * js-controller creates them from `io-package.json` `instanceObjects` when the instance is\n * added, and leaves an existing object's `common` alone on every later upgrade \u2014 so an\n * instance that predates a change keeps whatever the old version wrote. Measured after the\n * name translation went live: five of them still carried a plain-string name while the whole\n * rest of the tree was translated. Writing them here every start closes that half; extendObject\n * merges, so a recording setting or anything else a user attached survives.\n */\n private async ensureInstanceInfoObjects(): Promise<void> {\n // Spelled out with LITERAL ids on purpose. A loop over a table reads more compactly, but\n // then neither a reader nor the consistency gate can see which manifest objects are\n // actually refreshed \u2014 and \"the call exists\" is not the same question as \"the call runs\n // for THIS object\". This is the one place where that distinction cost a release (2.1.1).\n await this.extendObject(\"info\", {\n type: \"channel\",\n common: { name: tName(\"information\") },\n native: {},\n });\n await this.extendObject(\"info.connection\", {\n type: \"state\",\n common: {\n name: tName(\"deviceOrServiceConnected\"),\n desc: tName(\"descDeviceOrServiceConnected\"),\n type: \"boolean\",\n role: \"indicator.connected\",\n read: true,\n write: false,\n },\n native: {},\n });\n await this.extendObject(\"info.devicesTotal\", {\n type: \"state\",\n common: { name: tName(\"devicesTotal\"), type: \"number\", role: \"value\", read: true, write: false },\n native: {},\n });\n await this.extendObject(\"info.devicesOnline\", {\n type: \"state\",\n common: { name: tName(\"devicesOnline\"), type: \"number\", role: \"value\", read: true, write: false },\n native: {},\n });\n await this.extendObject(\"info.devicesAllOnline\", {\n type: \"state\",\n common: { name: tName(\"allDevicesOnline\"), type: \"boolean\", role: \"indicator\", read: true, write: false },\n native: {},\n });\n }\n\n /**\n * Create AND refresh a device's header objects (the device node, its info channel and a\n * per-device connection indicator) so its state is visible even while offline.\n *\n * Written with `extendObject` on every start, not created once: an object that already exists\n * is otherwise never touched again, so an instance upgraded from an older version keeps\n * whatever that version wrote \u2014 measured live after the name translation, where these were the\n * only device datapoints left with a plain-string name (`info.model`/`info.firmware` came out\n * right only because a catalog entry upserts them on top). extendObject merges, so a recording\n * setting a user attached survives.\n *\n * @param deviceId the id-safe device id\n * @param ip the device's current address (from config or discovery)\n * @param source where the address came from \u2014 kept at the device object so the adapter, the\n * card and the edit path all know it without re-deriving it from which table happens to be\n * filled (which said the same thing about every device on the instance)\n */\n private async ensureDeviceHeader(deviceId: string, ip: string, source: DeviceSource): Promise<void> {\n // statusStates.onlineId lets the admin paint a green/red reachability symbol on the\n // device object itself (as govee does), fed by the per-device connection state.\n // extendObject with preserve:name so an upgrade adds the symbol without overwriting\n // a name the user changed.\n // A device that has not reported its model yet would sit in the tree without any\n // symbol \u2014 an upgraded instance shows that on every start before the first report,\n // and a device that never answers shows it for good. Seed the default silhouette,\n // but only when there is none: overwriting would flip a soundbar back to the\n // receiver default for the seconds until its model arrives.\n let icon: string | undefined;\n // Percent is a DEVICE setting since 2.9.0. A device that carries no answer yet inherits the\n // instance-wide switch 2.8.0 had, so an upgrade keeps every receiver exactly as it was, and\n // the answer is written down here so it never has to be inherited again.\n let percent = this.legacyVolumePercent;\n try {\n const existing = await this.getObjectAsync(deviceId);\n icon = existing?.common?.icon ? undefined : iconForModel(undefined);\n const own = (existing?.native as { volumeAsPercent?: unknown } | undefined)?.volumeAsPercent;\n if (typeof own === \"boolean\") {\n percent = own;\n }\n } catch {\n icon = undefined;\n }\n this.volumePercent.set(deviceId, percent);\n await this.extendObject(\n deviceId,\n {\n type: \"device\",\n common: {\n name: deviceId,\n ...(icon ? { icon } : {}),\n statusStates: { onlineId: `${this.namespace}.${deviceId}.info.connection` },\n },\n native: { source, volumeAsPercent: percent },\n },\n { preserve: { common: [\"name\"] } },\n );\n await this.extendObject(`${deviceId}.info`, {\n type: \"channel\",\n common: { name: tName(\"info\") },\n native: {},\n });\n await this.extendObject(`${deviceId}.info.connection`, {\n type: \"state\",\n common: {\n name: tName(\"connected\"),\n // Its own explanation key: the name key `connected` is shared with the Bluetooth\n // source's \"Connected\", which means something else entirely.\n desc: tName(\"descDeviceConnected\"),\n type: \"boolean\",\n role: \"indicator.reachable\",\n read: true,\n write: false,\n def: false,\n },\n native: {},\n });\n // Model name shown on the device-manager card. Filled by whichever transport reports it\n // (YNCA MODELNAME, YXC/XML model); created here so the card's model line binds even for an\n // offline device or a transport that does not report a model.\n await this.extendObject(`${deviceId}.info.model`, {\n type: \"state\",\n common: { name: tName(\"model\"), type: \"string\", role: \"text\", read: true, write: false, def: \"\" },\n native: {},\n });\n // The device's address \u2014 for a discovered device it lived only in the adapter's\n // internals, so no diagnosis (log capture, browser access to the device's own pages)\n // could name it without a network search. Refreshed every start: DHCP may move it.\n await this.extendObject(`${deviceId}.info.ip`, {\n type: \"state\",\n common: { name: tName(\"ipAddress\"), type: \"string\", role: \"info.ip\", read: true, write: false, def: \"\" },\n native: {},\n });\n await this.setState(`${deviceId}.info.ip`, { val: ip, ack: true });\n // Per-transport connection flags, fed by the live set from connectTransports and read live\n // by the device-manager card indicators. Created here so an offline device's card still\n // renders all three (false) instead of nothing.\n await this.extendObject(`${deviceId}.info.transports`, {\n type: \"channel\",\n common: { name: tName(\"transports\"), desc: tName(\"descTransports\") },\n native: {},\n });\n for (const proto of TRANSPORT_IDS) {\n await this.extendObject(`${deviceId}.info.transports.${proto}`, {\n type: \"state\",\n common: {\n name: tName(\"transportConnected\", proto.toUpperCase()),\n type: \"boolean\",\n role: \"indicator.reachable\",\n read: true,\n write: false,\n def: false,\n },\n native: {},\n });\n }\n }\n\n /** The icon last written per device, so repeated model reports do not re-write the object. */\n private readonly deviceIcons = new Map<string, string>();\n\n /** The label this adapter wrote per device, with the rank of the source behind it. */\n private readonly deviceLabels = new Map<string, { name: string; rank: LabelRank }>();\n\n /**\n * Give the device node a name a user recognises, once the device reports one.\n *\n * An instance upgraded from the previous adapter carries the receiver's ip as its\n * device name \u2014 that adapter knew nothing but an ip, so the migration had nothing\n * else to call it. The object id stays that ip for good (history and visualisation\n * bindings hang off it), but the displayed name does not have to.\n *\n * A name the user typed is never touched, and the model never replaces a name the\n * device reported for itself \u2014 see {@link nextDeviceLabel}.\n *\n * @param deviceId the id-safe device id\n * @param candidate the reported name (a MusicCast zone name, or the model)\n * @param rank how trustworthy the candidate is\n */\n private async updateDeviceLabel(deviceId: string, candidate: string, rank: LabelRank): Promise<void> {\n const own = this.deviceLabels.get(deviceId);\n try {\n const current = (await this.getObjectAsync(deviceId))?.common?.name;\n const label = nextDeviceLabel(\n typeof current === \"string\" ? current : undefined,\n deviceId,\n candidate,\n rank,\n own?.name,\n own?.rank,\n );\n if (label === undefined) {\n return;\n }\n // Deliberately without `preserve: { common: [\"name\"] }`: nextDeviceLabel has just\n // established that the present name is the adapter's own placeholder, not a user's.\n await this.extendObject(deviceId, { common: { name: label } });\n this.deviceLabels.set(deviceId, { name: label, rank });\n this.log.debug(`${deviceId}: device name set to \"${label}\"`);\n } catch (e) {\n this.log.debug(`${deviceId}: setting the device name failed (${errorMessage(e)})`);\n }\n }\n\n /**\n * Paint the device-class silhouette on the device node once the model is known \u2014\n * detected from the reported model name, written only when it actually changes.\n *\n * @param deviceId the id-safe device id\n * @param model the reported model name\n */\n private async updateDeviceIcon(deviceId: string, model: string): Promise<void> {\n const icon = iconForModel(model);\n if (this.deviceIcons.get(deviceId) === icon) {\n return;\n }\n this.deviceIcons.set(deviceId, icon);\n try {\n await this.extendObject(deviceId, { common: { icon } });\n } catch (e) {\n this.log.debug(`${deviceId}: setting device icon failed (${errorMessage(e)})`);\n }\n }\n\n /**\n * Carry over the previous adapter's single-device config into the device table.\n * The old yamaha stored one receiver as `config.ip` (older installs: `config.IP`);\n * the new adapter uses a `devices` table, so an upgraded instance would otherwise\n * start with an empty table and lose its receiver. Persists the row so the admin\n * table shows it, and fills `this.config` in memory so this run already drives it.\n */\n private async migrateLegacyDevice(): Promise<void> {\n const config = this.config as unknown as Record<string, unknown>;\n const row = legacyDeviceRow(config);\n if (!row) {\n return;\n }\n // Fill the in-memory config first, so this run already drives the device even\n // if persisting the table below fails \u2014 persistence is a convenience for the\n // admin view, not a precondition for running.\n config.devices = [row];\n try {\n await this.extendForeignObjectAsync(`system.adapter.${this.namespace}`, { native: { devices: [row] } });\n this.log.info(`carried the previous single-device config (${row.ip}) over into the device table`);\n } catch (e) {\n this.log.warn(\n `could not persist the migrated device table (${errorMessage(e)}); ` + `running with the in-memory value`,\n );\n }\n }\n\n /**\n * Fold the removed `group_zones` toggle into `group_multiroom` \u2014 zone 2/3/4 now\n * belong to the multiroom group. Existing installs that had zones on but multiroom\n * off would otherwise lose their zone datapoints after the update.\n */\n private async migrateGroupZones(): Promise<void> {\n const config = this.config as unknown as Record<string, unknown>;\n if (!(\"group_zones\" in config)) {\n return;\n }\n if (config.group_zones) {\n config.group_multiroom = true;\n }\n delete config.group_zones;\n try {\n const obj = await this.getForeignObjectAsync(`system.adapter.${this.namespace}`);\n if (obj?.native) {\n if (obj.native.group_zones) {\n obj.native.group_multiroom = true;\n }\n delete obj.native.group_zones;\n await this.setForeignObjectAsync(`system.adapter.${this.namespace}`, obj);\n this.log.info(\"migrated group_zones setting into group_multiroom\");\n }\n } catch (e) {\n this.log.warn(`could not persist group_zones migration (${errorMessage(e)})`);\n }\n }\n\n /**\n * Bring one device online across ALL its transports: every protocol that answers\n * \u2014 YNCA (amp control over a held TCP connection), YXC (MusicCast), XML/YNC\n * (pre-2010) \u2014 connects in parallel on one object tree. Returns a connection handle\n * the supervisor keeps, or null when no transport answers this attempt. Each\n * datapoint is owned by exactly one transport (owner-policy), so the mappers never\n * collide on a shared id.\n *\n * @param device the configured device record\n * @param pushReceiver the shared YXC push receiver\n * @param knownDeviceIps IPs of all configured devices, for resolving a multiroom client\n * @param reachability dedup for the \"no reachable transport\" warning (one instance per device,\n * held by the caller across retries \u2014 see {@link ReachabilityDedup})\n * @param yncaSubunitCache per-device cache of the YNCA AVAIL probe (skips the probe on reconnects)\n * @param probeMemory per-device memory for constant device answers (skips re-asking on reconnects)\n * @returns a connection handle, or null when no transport connected\n */\n private attemptDevice(\n device: DeviceRecord,\n pushReceiver: YxcPushReceiver,\n knownDeviceIps: Set<string>,\n reachability: ReachabilityDedup,\n yncaSubunitCache: YncaSubunitCache,\n probeMemory: ProbeMemory,\n ): Promise<ConnectionHandle | null> {\n return attemptDevice(device, {\n reachability,\n yncaSubunitCache,\n probeMemory,\n // Group gate for the YNCA sweep: a disabled group's functions are never even fetched.\n isEntryEnabled: id => isGroupEnabled(id, this.config as unknown as Record<string, unknown>),\n log: {\n debug: message => this.log.debug(message),\n info: message => this.log.info(message),\n warn: message => this.log.warn(message),\n },\n upsertObject: async (id, def) => {\n // Gate on the datapoint group: a switched-off group's objects are not created. The id is\n // \"<deviceId>.<relativeId>\"; groupOf reads the relative part.\n if (!isGroupEnabled(id.slice(id.indexOf(\".\") + 1), this.config as unknown as Record<string, unknown>)) {\n return;\n }\n // A SHRINKING dropdown needs a clearing write first: extendObject merges `common.states`\n // key by key, so the old entries would survive every update (#619 \u2014 the reporter would\n // have seen no change at all). Only when the stored map carries a key the new one lacks;\n // an unchanged or growing map is one write, as before.\n if (def.type === \"state\" && def.common.states) {\n await this.clearStaleStates(id, def.common.states);\n }\n await this.writePresented(id, def);\n if (def.type === \"state\") {\n this.noteDatapointCreated(id);\n this.touchedThisRun.add(id);\n }\n },\n setStateAck: (id, value) => {\n // Same group gate as upsertObject, so a switched-off group seeds no orphan value either.\n if (!isGroupEnabled(id.slice(id.indexOf(\".\") + 1), this.config as unknown as Record<string, unknown>)) {\n return;\n }\n this.writeState(id, this.volumeAsShown(id, value));\n // A model report also decides the device-class icon on the device node \u2014 and, for a\n // device still carrying the ip it was migrated with, its readable name.\n if (id.endsWith(\".info.model\") && typeof value === \"string\" && value.length > 0) {\n const reporting = id.slice(0, id.indexOf(\".\"));\n void this.updateDeviceIcon(reporting, value);\n void this.updateDeviceLabel(reporting, value, LABEL_RANK.model);\n }\n },\n onDeviceName: name => void this.updateDeviceLabel(device.id, name, LABEL_RANK.deviceName),\n timers: {\n schedule: (handler, ms) => (this.unloading ? undefined : this.setTimeout(handler, ms)),\n cancel: handle => this.clearTimeout(handle),\n },\n registerPush: (ip, onPush) => pushReceiver.register(ip, onPush),\n pushActive: () => pushReceiver.isListening(),\n scheduleKeepalive: (handler, ms) => {\n if (this.unloading) {\n return () => {};\n }\n const timer = this.setInterval(handler, ms);\n return () => {\n if (timer) {\n this.clearInterval(timer);\n }\n };\n },\n xmlPollIntervalMs: this.xmlPollIntervalMs(),\n onTransports: names => this.setTransports(device.id, names),\n knownDeviceIps,\n });\n }\n\n /**\n * Route a state change to every device's supervisor (each forwards to its\n * active controller, which ignores ids outside its subtree and its acked echoes).\n *\n * @param id the full state id\n * @param state the new state (null when deleted)\n */\n private onStateChange(id: string, state: ioBroker.State | null | undefined): void {\n if (!state) {\n return;\n }\n const relative = stripNamespace(id, this.namespace);\n // The adapter subscribes to its whole namespace, so every one of its own acked writes\n // comes back here too \u2014 during a sweep that is hundreds of events. Route by the id's\n // first segment instead of offering each one to every device in turn.\n const deviceId = relative.slice(0, relative.indexOf(\".\"));\n const value = state.ack ? state.val : this.volumeAsDeviceScale(relative, state.val);\n this.supervisorById.get(deviceId)?.handleStateChange(relative, state.ack, value);\n }\n\n /**\n * Synchronous teardown \u2014 no await, call the callback immediately (SIGKILL otherwise).\n *\n * @param callback function to invoke once teardown is complete\n */\n private onUnload(callback: () => void): void {\n try {\n this.unloading = true;\n this.clearTimeout(this.balanceTimer);\n this.clearTimeout(this.rediscoverTimer);\n this.pushReceiver?.close();\n for (const supervisor of this.supervisors) {\n supervisor.close();\n }\n // A stopped adapter talks to nothing, so no device may keep claiming to be connected \u2014\n // that state paints the symbol on the device object (statusStates.onlineId), and the\n // instance-wide info.connection alone would leave every device green. The protocol\n // flags and the overview go with them; devicesTotal stays, how many devices there are\n // did not change.\n //\n // The callback goes LAST, after the writes: reporting \"done\" straight away loses them,\n // the host tears the process down as soon as it is told.\n const writes: Promise<unknown>[] = [this.setState(\"info.connection\", { val: false, ack: true })];\n for (const deviceId of this.deviceConnected.keys()) {\n this.deviceConnected.set(deviceId, false);\n writes.push(this.setState(`${deviceId}.info.connection`, { val: false, ack: true }));\n // The protocol flags on the card go down with the connection \u2014 a stopped adapter\n // is connected over no protocol.\n for (const proto of TRANSPORT_IDS) {\n writes.push(this.setState(`${deviceId}.info.transports.${proto}`, { val: false, ack: true }));\n }\n }\n writes.push(this.setState(\"info.devicesOnline\", { val: 0, ack: true }));\n writes.push(this.setState(\"info.devicesAllOnline\", { val: false, ack: true }));\n // A device memory still inside its coalescing window is written now \u2014 a timer on a\n // stopped adapter never fires, and the memory is what the next start rests on.\n for (const deviceId of [...this.pendingNative.keys()]) {\n writes.push(this.flushDeviceNative(deviceId));\n }\n void Promise.all(writes)\n .catch(() => {\n /* states DB already going down \u2014 nothing left to report to */\n })\n .finally(callback);\n return;\n } catch {\n // fall through\n }\n callback();\n }\n\n /**\n * Auto-discovery for an empty device table: scan the network, merge the finds with\n * the devices remembered from earlier runs (standby protection), persist the merged\n * set and return it. XML/pre-2010 receivers do not answer SSDP and never appear here.\n *\n * @returns the device records to run this session\n */\n private async autoDiscover(): Promise<DeviceRecord[]> {\n const store = discoveredStoreDeps(this);\n const known = await readDiscovered(store);\n if (known.length > 0) {\n // Remembered devices start NOW \u2014 the network search used to gate every restart\n // by its collect window although the devices were already known. It still runs,\n // in the background, to pick up newcomers (see discoverAdditionalDevices).\n this.log.info(`setting up ${known.length} remembered device(s); the network search runs in the background`);\n return known;\n }\n this.log.info(\"auto-discovery via SSDP (older XML-only devices must be added manually)\");\n const merged = await this.runDiscovery();\n this.log.info(`setting up ${merged.length} discovered device(s)...`);\n return merged;\n }\n\n /**\n * Search the network, merge with the remembered devices, and persist the result.\n * Shared by the blocking first-setup path and the background search.\n *\n * @returns the merged device records\n */\n private async runDiscovery(): Promise<DeviceRecord[]> {\n // Every search counts against the throttle, whoever asked for it \u2014 otherwise the first\n // offline device would fire another one right behind the start-up search.\n this.lastRediscovery = Date.now();\n const store = discoveredStoreDeps(this);\n const known = await readDiscovered(store);\n let found: Array<{ ip: string; name: string }> = [];\n try {\n found = await discoverYamaha({\n search: (target, ms) => this.ssdpSearch(target, ms),\n fetch: url => this.fetchUrl(url),\n log: { debug: message => this.log.debug(message), warn: message => this.log.warn(message) },\n });\n } catch (e) {\n this.log.warn(`auto-discovery scan failed, using the remembered devices: ${errorMessage(e)}`);\n }\n const merged = mergeDiscovered(known, found, (dropped, takenId) =>\n this.log.warn(`discovered device \"${dropped}\" skipped \u2014 its object id \"${takenId}\" is already taken`),\n );\n // Devices the user deleted from the card list stay out \u2014 otherwise the next search simply\n // undoes the delete. So do the ones that live in the device table: a receiver the user gave\n // a fixed address and entered by hand would otherwise come back as a SECOND card, and the\n // store would carry the found address back over the typed one (`mergeDiscovered` updates a\n // known id's address). Both the id and the address are matched \u2014 the search reads the name\n // off the device, the user typed their own, so the same receiver can carry two ids.\n const ignored = new Set(await readIgnored(ignoredStoreDeps(this)));\n const manual = parseDevices(this.config.devices);\n const manualIds = new Set(manual.map(device => device.id));\n const manualIps = new Set(manual.map(device => device.ip));\n const kept = merged.filter(\n device => !ignored.has(device.id) && !manualIds.has(device.id) && !manualIps.has(device.ip),\n );\n await writeDiscovered(store, kept);\n return kept;\n }\n\n /**\n * Load a device's capability profile \u2014 the one persisted memory of what the device told us\n * (probe memory, YNCA subunit snapshot, purge marker) \u2014 from its device object's native\n * part, wrapped so every change persists back there through the coalescing writer. The\n * device object is the right home: writing an instance object's native restarts the\n * adapter, a device object's does not. Legacy keys of 2.5.2/2.6.0 are converted at load.\n *\n * @param deviceId the id-safe device id\n * @returns the per-device profile store\n */\n private async loadDeviceProfile(deviceId: string): Promise<DeviceProfileStore> {\n let native: Record<string, unknown> | undefined;\n try {\n native = (await this.getObjectAsync(deviceId))?.native;\n } catch {\n native = undefined;\n }\n const store = new DeviceProfileStore(deviceId, native, {\n adapterVersion: this.version ?? \"\",\n now: () => new Date().toISOString(),\n persist: patch => this.persistDeviceNative(deviceId, patch),\n log: message => this.log.debug(message),\n });\n this.profiles.set(deviceId, store);\n return store;\n }\n\n /**\n * Whether the device that owns a datapoint presents its volume in percent.\n *\n * @param id a `<deviceId>.<relativeId>` state or object id\n * @returns true when that device's volume datapoints read 0\u2026100 %\n */\n private percentFor(id: string): boolean {\n return this.volumePercent.get(id.slice(0, id.indexOf(\".\"))) === true;\n }\n\n /**\n * Turn percent presentation on or off for ONE device, at once.\n *\n * Called from the device manager, which runs inside this process. The datapoint is rebuilt\n * before its value follows \u2014 the same order `reshapeVolume` keeps when a receiver changes the\n * scale it displays, and for the same reason: a value written against the old definition is\n * out of range and the js-controller logs it on every refresh.\n *\n * @param deviceId the id-safe device id\n * @param on whether its volume datapoints should read 0\u2026100 %\n */\n public async setVolumePercent(deviceId: string, on: boolean): Promise<void> {\n if (this.volumePercent.get(deviceId) === on) {\n return;\n }\n this.volumePercent.set(deviceId, on);\n await this.extendObject(deviceId, { native: { volumeAsPercent: on } });\n for (const [id, def] of [...this.volumeDefs]) {\n if (!id.startsWith(`${deviceId}.`)) {\n continue;\n }\n const bounds = this.volumeScales.get(id);\n await this.writePresented(id, def);\n if (!bounds) {\n continue;\n }\n const state = await this.getStateAsync(id);\n if (typeof state?.val === \"number\") {\n this.writeState(id, on ? toPercent(state.val, bounds) : fromPercent(state.val, bounds));\n }\n }\n }\n\n /**\n * Write one object definition through the percent presentation \u2014 shared by the upsert funnel\n * and the live switch, so both produce exactly the same object.\n *\n * @param id the full object id\n * @param def the definition the coordinator produced\n */\n private async writePresented(id: string, def: ObjectDef): Promise<void> {\n const written = this.presentVolume(id, def);\n if (written.type === \"state\") {\n await this.clearStaleBounds(id, written.common);\n }\n await this.extendObject(id, { type: written.type, common: written.common, native: {} });\n }\n\n /**\n * The object definition to write for a datapoint, once percent mode has had its say.\n *\n * Applied to the FINISHED definition, after the coordinator picked the owner, so one rule covers\n * all three transports and every zone: the decibels YNCA and XML declare in their catalogs and\n * the display scale MusicCast reports are all just \"the device's own scale\" here. The bounds it\n * replaces are remembered, because they are what the two value directions convert against.\n *\n * @param id the full object id\n * @param def the definition the coordinator produced\n * @returns the definition to write\n */\n private presentVolume(id: string, def: ObjectDef): ObjectDef {\n if (def.type !== \"state\" || !isAmpVolumeId(id.slice(id.indexOf(\".\") + 1))) {\n return def;\n }\n const bounds = volumeBoundsOf(def);\n if (!bounds) {\n // Nothing declared to convert against. Percent would be a number with no meaning, so the\n // datapoint keeps the device's own scale even with the switch on, and says so once.\n this.volumeScales.delete(id);\n this.volumeDefs.delete(id);\n if (this.percentFor(id)) {\n this.log.debug(`${id}: no declared range \u2014 keeping the device's own scale instead of percent`);\n }\n return def;\n }\n this.volumeScales.set(id, bounds);\n this.volumeDefs.set(id, def);\n return this.percentFor(id) ? asPercentObject(def) : def;\n }\n\n /**\n * A device value on its way into a datapoint, converted when that datapoint is in percent.\n *\n * @param id the full state id\n * @param value the value the transport reported, on the device's own scale\n * @returns the value to store\n */\n private volumeAsShown(id: string, value: boolean | number | string): boolean | number | string {\n const bounds = this.percentFor(id) ? this.volumeScales.get(id) : undefined;\n return bounds && typeof value === \"number\" ? toPercent(value, bounds) : value;\n }\n\n /**\n * A user's write on its way out, converted back to the scale the device expects.\n *\n * Only unacked writes reach here: an acked one is the adapter's own echo, already in percent,\n * and converting it a second time would walk the value down on every poll.\n *\n * @param relativeId the state id without the namespace\n * @param value the value the user wrote\n * @returns the value to hand to the device's supervisor\n */\n private volumeAsDeviceScale(relativeId: string, value: ioBroker.StateValue): ioBroker.StateValue {\n const bounds = this.percentFor(relativeId) ? this.volumeScales.get(relativeId) : undefined;\n return bounds && typeof value === \"number\" ? fromPercent(value, bounds) : value;\n }\n\n /**\n * The XML/YNC poll interval in milliseconds, from `config.xmlPollInterval`\n * (seconds, default 60).\n *\n * @returns the interval in ms\n */\n private xmlPollIntervalMs(): number {\n const seconds = Number((this.config as unknown as Record<string, unknown>).xmlPollInterval);\n return (Number.isFinite(seconds) && seconds > 0 ? seconds : 60) * 1000;\n }\n\n /**\n * Run an SSDP M-SEARCH and collect the responders' description URL and address.\n *\n * With a configured network interface the search leaves exactly that one; left empty it\n * leaves EVERY non-internal IPv4 interface at once (one socket each), because multicast\n * egress otherwise follows only the host's default route \u2014 on a multi-homed host whose\n * default route is not the AV network that means the receiver is never reached and nothing\n * is found. Responders from all interfaces are merged into one list; the caller\n * de-duplicates by address.\n *\n * @param target the search target (device type)\n * @param timeoutMs how long to collect responses\n * @returns the responders\n */\n private ssdpSearch(target: string, timeoutMs: number): Promise<Array<{ location: string; address: string }>> {\n return new Promise(resolve => {\n const bindAddrs = searchInterfaces(this.config.networkInterface, networkInterfaces());\n const responders: Array<{ location: string; address: string }> = [];\n const sockets: ReturnType<typeof createSocket>[] = [];\n let settled = false;\n const finish = (): void => {\n if (settled) {\n return;\n }\n settled = true;\n for (const socket of sockets) {\n try {\n socket.close();\n } catch {\n // already closed\n }\n }\n resolve(responders);\n };\n // Open one search socket bound to a single interface (or the default route when bindAddr\n // is undefined). Every socket shares the responders list and the one settle timeout.\n const searchFrom = (bindAddr: string | undefined): void => {\n const socket = createSocket(\"udp4\");\n sockets.push(socket);\n socket.on(\"message\", (msg, rinfo) => {\n const location = /LOCATION:\\s*(\\S+)/i.exec(msg.toString());\n if (location) {\n responders.push({ location: location[1], address: rinfo.address });\n }\n });\n socket.on(\"error\", err => {\n // One interface failing (typically a stale selected IP after a DHCP change) must not\n // kill the search on the others \u2014 warn and drop just this socket; the timeout still\n // resolves whatever the rest found.\n this.log.warn(\n `discovery socket failed${bindAddr ? ` on interface ${bindAddr}` : \"\"}: ${errorMessage(err)}${\n bindAddr ? \" \u2014 check the Network Interface setting\" : \"\"\n }`,\n );\n try {\n socket.close();\n } catch {\n // already closed\n }\n });\n const sendSearch = (): void => {\n if (settled) {\n return;\n }\n const msearch = `M-SEARCH * HTTP/1.1\\r\\nHOST: 239.255.255.250:1900\\r\\nMAN: \"ssdp:discover\"\\r\\nMX: 3\\r\\nST: ${target}\\r\\n\\r\\n`;\n try {\n socket.send(msearch, 1900, \"239.255.255.250\");\n } catch {\n // socket already closed by an error above\n }\n };\n socket.bind(0, bindAddr, () => {\n // Pin OUTGOING multicast to this interface. bind() only sets the source address; the\n // egress interface is IP_MULTICAST_IF \u2014 without it the OS uses its default route, so\n // the search can leave the wrong NIC on a multi-homed host (Node dgram docs).\n if (bindAddr) {\n try {\n socket.setMulticastInterface(bindAddr);\n } catch {\n this.log.info(`discovery: could not pin multicast egress to ${bindAddr} \u2014 using the default interface`);\n }\n }\n // Multicast is lossy and a single request can be dropped \u2014 repeat the M-SEARCH a few\n // times inside the collect window so one lost packet does not hide a receiver.\n for (let i = 0; i < SSDP_SEARCH_BURST; i++) {\n this.setTimeout(sendSearch, i * SSDP_SEARCH_INTERVAL_MS);\n }\n });\n };\n // Configured \u2192 that one interface; empty \u2192 every non-internal IPv4; none usable \u2192 default route.\n if (bindAddrs.length === 0) {\n searchFrom(undefined);\n } else {\n for (const bindAddr of bindAddrs) {\n searchFrom(bindAddr);\n }\n }\n this.setTimeout(finish, timeoutMs);\n });\n }\n\n /**\n * Fetch a URL over HTTP and resolve its body.\n *\n * @param url the URL to fetch\n * @returns the response body\n */\n private fetchUrl(url: string): Promise<string> {\n return new Promise((resolve, reject) => {\n const req = httpGet(url, res => {\n let data = \"\";\n let bytes = 0;\n res.on(\"data\", chunk => {\n bytes += (chunk as Buffer).length;\n if (bytes > MAX_HTTP_BODY_BYTES) {\n // A description document is a few KB \u2014 whatever streams past the cap is not one.\n res.destroy(new Error(`description too large: ${url}`));\n return;\n }\n data += String(chunk);\n });\n // A connection dropped mid-body emits on the RESPONSE stream, not the request \u2014\n // without this handler that is an unhandled error event instead of a rejection.\n res.on(\"error\", reject);\n res.on(\"end\", () => resolve(data));\n });\n req.on(\"error\", reject);\n req.setTimeout(FETCH_TIMEOUT_MS, () => req.destroy(new Error(`fetch timed out: ${url}`)));\n });\n }\n}\n\nif (require.main !== module) {\n // Export the constructor in compact mode\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) => new Yamaha(options);\n} else {\n // Start the instance directly\n (() => new Yamaha())();\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["httpGet"]
7
7
  }
package/io-package.json CHANGED
@@ -1,9 +1,22 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "yamaha",
4
- "version": "2.9.0",
4
+ "version": "2.9.1",
5
5
  "nogit": true,
6
6
  "news": {
7
+ "2.9.1": {
8
+ "en": "A receiver the network search found keeps its datapoints when you add a device by hand: they stay with their history and are marked offline instead of being deleted.",
9
+ "de": "Ein per Netzsuche gefundener Receiver behält seine Datenpunkte, wenn Sie ein Gerät von Hand eintragen: Sie bleiben samt Historie und werden nur als offline gekennzeichnet.",
10
+ "ru": "Ресивер, найденный поиском в сети, сохраняет свои точки данных при добавлении устройства вручную: они остаются вместе с историей и просто помечаются как офлайн.",
11
+ "pt": "Um recetor encontrado pela pesquisa de rede mantém os seus pontos de dados quando adiciona um aparelho à mão: ficam com o histórico e são apenas marcados como offline.",
12
+ "nl": "Een receiver die de netwerkzoekactie vond behoudt zijn datapunten als u een apparaat handmatig toevoegt: ze blijven met hun historie en worden alleen offline gemarkeerd.",
13
+ "fr": "Un ampli trouvé par la recherche réseau conserve ses points de données quand vous ajoutez un appareil à la main : ils restent avec leur historique et sont marqués hors ligne.",
14
+ "it": "Un ricevitore trovato dalla ricerca di rete mantiene i suoi datapoint quando aggiungete un dispositivo a mano: restano con la loro cronologia e vengono solo segnati offline.",
15
+ "es": "Un receptor encontrado por la búsqueda de red conserva sus puntos de datos al añadir un equipo a mano: permanecen con su histórico y solo se marcan como desconectados.",
16
+ "pl": "Amplituner znaleziony przez wyszukiwanie w sieci zachowuje swoje punkty danych po ręcznym dodaniu urządzenia: zostają wraz z historią i są tylko oznaczane jako offline.",
17
+ "uk": "Ресивер, знайдений мережевим пошуком, зберігає свої точки даних, коли ви додаєте пристрій вручну: вони лишаються з історією і лише позначаються як офлайн.",
18
+ "zh-cn": "网络搜索找到的功放在您手动添加设备时会保留其数据点:它们连同历史记录一起保留,只是被标记为离线,而不再被删除。"
19
+ },
7
20
  "2.9.0": {
8
21
  "en": "Entered and found devices now run side by side, every card can be edited, and \"Volume as 0-100 %\" is set per device.",
9
22
  "de": "Eingetragene und gefundene Geräte laufen jetzt nebeneinander, jede Karte ist bearbeitbar, und \"Lautstärke als 0-100 %\" gilt pro Gerät.",
@@ -81,19 +94,6 @@
81
94
  "pl": "Kolejne 174 punkty danych same się objaśniają: głośność i barwa podają swoją skalę, zapisane listy mówią, co zawierają, a Połączono znaczy osiągalny, nie włączony.",
82
95
  "uk": "Ще 174 датапоінти тепер пояснюють себе: гучність і тембр називають свою шкалу, збережені списки кажуть, що всередині, а «З'єднано» означає доступний, а не ввімкнений.",
83
96
  "zh-cn": "另有 174 个数据点现在会自我说明:音量和音调标明所用刻度,已存列表说明其内容,“已连接”表示可达,而非已开机。"
84
- },
85
- "2.5.1": {
86
- "en": "Installing straight from GitHub is no longer offered: the adapter is built before publishing, so it is installed from the ioBroker repository instead.",
87
- "de": "Die Installation direkt von GitHub wird nicht mehr angeboten: Der Adapter wird vor der Veröffentlichung gebaut und kommt daher aus dem ioBroker-Repository.",
88
- "ru": "Установка напрямую с GitHub больше не предлагается: адаптер собирается перед публикацией и поэтому устанавливается из репозитория ioBroker.",
89
- "pt": "A instalação diretamente do GitHub deixou de ser oferecida: o adaptador é compilado antes da publicação e por isso é instalado a partir do repositório ioBroker.",
90
- "nl": "Installeren rechtstreeks vanaf GitHub wordt niet meer aangeboden: de adapter wordt vóór publicatie gebouwd en komt daarom uit de ioBroker-repository.",
91
- "fr": "L’installation directe depuis GitHub n’est plus proposée : l’adaptateur est compilé avant publication et s’installe donc depuis le dépôt ioBroker.",
92
- "it": "L’installazione diretta da GitHub non è più proposta: l’adattatore viene compilato prima della pubblicazione e quindi si installa dal repository ioBroker.",
93
- "es": "La instalación directa desde GitHub ya no se ofrece: el adaptador se compila antes de publicarse y por eso se instala desde el repositorio de ioBroker.",
94
- "pl": "Instalacja bezpośrednio z GitHuba nie jest już oferowana: adapter jest budowany przed publikacją, dlatego instaluje się go z repozytorium ioBroker.",
95
- "uk": "Встановлення безпосередньо з GitHub більше не пропонується: адаптер збирається перед публікацією, тому встановлюється з репозиторію ioBroker.",
96
- "zh-cn": "不再提供直接从 GitHub 安装:适配器在发布前完成构建,因此请从 ioBroker 仓库安装。"
97
97
  }
98
98
  },
99
99
  "titleLang": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.yamaha",
3
- "version": "2.9.0",
3
+ "version": "2.9.1",
4
4
  "description": "ioBroker adapter for Yamaha AV receivers and MusicCast devices",
5
5
  "author": {
6
6
  "name": "krobi",