iobroker.yamaha 2.0.2 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -5
- package/build/lib/pure-helpers.js +0 -3
- package/build/lib/pure-helpers.js.map +2 -2
- package/io-package.json +14 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,6 +103,10 @@ On the very first contact the adapter asks the receiver which functions it suppo
|
|
|
103
103
|
Placeholder for the next version (at the beginning of the line):
|
|
104
104
|
### **WORK IN PROGRESS**
|
|
105
105
|
-->
|
|
106
|
+
### 2.0.3 (2026-09-01)
|
|
107
|
+
|
|
108
|
+
- (krobipd) Fixed: the update cleanup now removes every never-filled leftover datapoint — a history recording setting no longer shields it, because nothing was ever recorded there and nothing is lost
|
|
109
|
+
|
|
106
110
|
### 2.0.2 (2026-09-01)
|
|
107
111
|
|
|
108
112
|
- (krobipd) Fixed: a restart while the receiver stands by no longer forgets abilities the device proved while awake — the remembered capability map only ever grows for the same device and firmware
|
|
@@ -137,11 +141,6 @@ On the very first contact the adapter asks the receiver which functions it suppo
|
|
|
137
141
|
- (krobipd) Improved: restarts are fast — the adapter remembers what each device can do, brings it online in seconds and refreshes values in the background; only the first contact asks everything
|
|
138
142
|
- (krobipd) Improved: known devices no longer wait for the network search at startup — it runs in the background and only adds newcomers
|
|
139
143
|
|
|
140
|
-
### 1.6.0 (2026-08-27)
|
|
141
|
-
|
|
142
|
-
- (krobipd) New: three states show how many receivers are set up, how many are connected right now and whether that is all of them — one line to watch instead of every device
|
|
143
|
-
- (krobipd) Fixed: a receiver kept showing as connected while the adapter was stopped, and after a crash it stayed that way until it answered again — both now show the truth
|
|
144
|
-
|
|
145
144
|
[Older changelogs can be found there](CHANGELOG_OLD.md)
|
|
146
145
|
|
|
147
146
|
## History
|
|
@@ -285,9 +285,6 @@ function neverWrittenStateIds(objects, states, deviceIds, namespace) {
|
|
|
285
285
|
if ((object == null ? void 0 : object.type) !== "state" || (common == null ? void 0 : common.read) === false) {
|
|
286
286
|
continue;
|
|
287
287
|
}
|
|
288
|
-
if ((common == null ? void 0 : common.custom) && Object.keys(common.custom).length > 0) {
|
|
289
|
-
continue;
|
|
290
|
-
}
|
|
291
288
|
const relative = stripNamespace(fullId, namespace);
|
|
292
289
|
const top = relative.split(".")[0];
|
|
293
290
|
if (!deviceIds.has(top) || relative.slice(top.length + 1).startsWith("info.")) {
|
|
@@ -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 });\n }\n return records;\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. New addresses are added; a discovered device is turned\n * into a record via its friendly name (or its ip when it advertises none), and one\n * whose id would collide with an already-kept device is skipped (reported via\n * `onCollision`, so the missing device is explainable). De-duplicated by ip.\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, de-duplicated by ip\n */\nexport function mergeDiscovered(\n known: DeviceRecord[],\n found: DiscoveredDevice[],\n onCollision?: (dropped: string, takenId: string) => void,\n): DeviceRecord[] {\n const byIp = new Map<string, DeviceRecord>();\n const takenIds = new Set<string>([\"info\"]); // reserved: the adapter's own info channel\n for (const device of known) {\n if (byIp.has(device.ip) || takenIds.has(device.id)) {\n continue;\n }\n byIp.set(device.ip, device);\n takenIds.add(device.id);\n }\n for (const device of found) {\n if (byIp.has(device.ip)) {\n continue;\n }\n const id = sanitizeId(device.name || device.ip);\n if (takenIds.has(id)) {\n onCollision?.(device.name || device.ip, id);\n continue;\n }\n takenIds.add(id);\n byIp.set(device.ip, { id, ip: device.ip });\n }\n return [...byIp.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];\n\nexport const RENAMED_STATE_IDS = [\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.spotify\",\n \"player.deezer\",\n \"player.tidal\",\n \"player.ipod\",\n \"player.ipodUsb\",\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), states carrying per-state user\n * settings (`common.custom`, e.g. a history binding \u2014 a deliberate user link is never\n * deleted behind their back), and the `info.` header the adapter itself maintains for\n * every device, connected or not.\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; custom?: Record<string, unknown> } | undefined;\n if (object?.type !== \"state\" || common?.read === false) {\n continue;\n }\n if (common?.custom && Object.keys(common.custom).length > 0) {\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 * 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"],
|
|
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 });\n }\n return records;\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. New addresses are added; a discovered device is turned\n * into a record via its friendly name (or its ip when it advertises none), and one\n * whose id would collide with an already-kept device is skipped (reported via\n * `onCollision`, so the missing device is explainable). De-duplicated by ip.\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, de-duplicated by ip\n */\nexport function mergeDiscovered(\n known: DeviceRecord[],\n found: DiscoveredDevice[],\n onCollision?: (dropped: string, takenId: string) => void,\n): DeviceRecord[] {\n const byIp = new Map<string, DeviceRecord>();\n const takenIds = new Set<string>([\"info\"]); // reserved: the adapter's own info channel\n for (const device of known) {\n if (byIp.has(device.ip) || takenIds.has(device.id)) {\n continue;\n }\n byIp.set(device.ip, device);\n takenIds.add(device.id);\n }\n for (const device of found) {\n if (byIp.has(device.ip)) {\n continue;\n }\n const id = sanitizeId(device.name || device.ip);\n if (takenIds.has(id)) {\n onCollision?.(device.name || device.ip, id);\n continue;\n }\n takenIds.add(id);\n byIp.set(device.ip, { id, ip: device.ip });\n }\n return [...byIp.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];\n\nexport const RENAMED_STATE_IDS = [\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.spotify\",\n \"player.deezer\",\n \"player.tidal\",\n \"player.ipod\",\n \"player.ipodUsb\",\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 * 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"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "yamaha",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.3",
|
|
5
5
|
"news": {
|
|
6
|
+
"2.0.3": {
|
|
7
|
+
"en": "The update cleanup now removes every never-filled leftover datapoint — a history recording setting no longer shields it, because nothing was ever recorded there and nothing is lost",
|
|
8
|
+
"de": "Das Update-Aufräumen entfernt jetzt jeden nie befüllten Alt-Datenpunkt — eine Verlaufs-Aufzeichnungseinstellung schützt ihn nicht mehr, denn dort wurde nie etwas aufgezeichnet",
|
|
9
|
+
"ru": "Очистка при обновлении теперь удаляет каждую незаполненную устаревшую точку данных — настройка записи истории больше не защищает её, ведь туда никогда ничего не записывалось",
|
|
10
|
+
"pt": "A limpeza de atualização agora remove todos os pontos de dados antigos nunca preenchidos — uma configuração de gravação de histórico não os protege mais, pois nada foi gravado lá",
|
|
11
|
+
"nl": "De update-opschoning verwijdert nu elk nooit gevuld achtergebleven datapunt — een instelling voor historie-opname beschermt het niet meer, want er is daar nooit iets opgenomen",
|
|
12
|
+
"fr": "Le nettoyage de mise à jour supprime désormais chaque point de données jamais rempli — un réglage d'enregistrement d'historique ne le protège plus, car rien n'y a jamais été enregistré",
|
|
13
|
+
"it": "La pulizia dell'aggiornamento ora rimuove ogni datapoint residuo mai riempito — un'impostazione di registrazione della cronologia non lo protegge più, lì non è mai stato registrato nulla",
|
|
14
|
+
"es": "La limpieza de actualización ahora elimina cada punto de datos antiguo nunca llenado — un ajuste de grabación de historial ya no lo protege, pues allí nunca se grabó nada",
|
|
15
|
+
"pl": "Czyszczenie przy aktualizacji usuwa teraz każdy nigdy niewypełniony stary punkt danych — ustawienie zapisu historii już go nie chroni, bo nic tam nigdy nie zapisano",
|
|
16
|
+
"uk": "Очищення під час оновлення тепер видаляє кожну ніколи не заповнену застарілу точку даних — налаштування запису історії більше не захищає її, адже туди ніколи нічого не записувалось",
|
|
17
|
+
"zh-cn": "更新清理现在会删除每个从未填充过的遗留数据点——历史记录设置不再保护它,因为那里从未记录过任何数据,不会有任何损失"
|
|
18
|
+
},
|
|
6
19
|
"2.0.2": {
|
|
7
20
|
"en": "Bug fix: a restart while the receiver stands by no longer forgets abilities the device proved while awake — the remembered capability map only ever grows for the same device and firmware",
|
|
8
21
|
"de": "Fehlerbereinigung: Ein Neustart bei Receiver im Bereitschaftszustand vergisst keine wach nachgewiesenen Fähigkeiten mehr — der gemerkte Fähigkeits-Stand wächst je Gerät und Firmware nur noch",
|
|
@@ -80,19 +93,6 @@
|
|
|
80
93
|
"pl": "Poprawiono: przeglądanie menu w amplitunerach udostępniających je starym protokołem XML (#613)\nPoprawiono: pozbawione zasilania urządzenie MusicCast jest zgłaszane jako offline, a nie nadal połączone\nPo włączeniu lub wyłączeniu grupy punktów danych widzisz teraz, ile punktów przybyło lub zniknęło\nNowość: opcjonalne raportowanie błędów przez Sentry, bez danych osobowych",
|
|
81
94
|
"uk": "Виправлено: навігація меню на ресиверах, які надають меню старим протоколом XML (#613)\nВиправлено: знеструмлений пристрій MusicCast повідомляється як офлайн, а не як підключений\nПісля вмикання чи вимикання групи точок даних тепер видно, скільки точок з'явилося або зникло\nНове: необов'язкові звіти про помилки через Sentry, без персональних даних",
|
|
82
95
|
"zh-cn": "修复:可通过旧版 XML 协议提供菜单的功放现在可以浏览菜单 (#613)\n修复:断电的 MusicCast 设备会报告为离线,而不再显示为已连接\n开启或关闭数据点分组后,现在可以看到新增或消失了多少数据点\n新增:可选的 Sentry 错误报告,不传输个人数据"
|
|
83
|
-
},
|
|
84
|
-
"1.4.0": {
|
|
85
|
-
"en": "Commands sent in one go all arrive, favourites land in the zone that is listening, special characters work again, and startup and reconnects are noticeably faster.",
|
|
86
|
-
"de": "Mehrere Befehle auf einmal kommen alle an, Favoriten landen in der hörenden Zone, Sonderzeichen funktionieren wieder, und Start und Neuverbindung sind spürbar schneller.",
|
|
87
|
-
"ru": "Команды, отправленные подряд, доходят все; избранное попадает в слушающую зону; спецсимволы снова работают; запуск и переподключение заметно быстрее.",
|
|
88
|
-
"pt": "Comandos enviados de uma vez chegam todos, os favoritos vão para a zona que está a ouvir, os caracteres especiais voltam a funcionar e o arranque e a reconexão são bem mais rápidos.",
|
|
89
|
-
"nl": "Meerdere commando's tegelijk komen allemaal aan, favorieten belanden in de luisterende zone, speciale tekens werken weer, en opstarten en opnieuw verbinden gaan merkbaar sneller.",
|
|
90
|
-
"fr": "Les commandes envoyées d'un coup arrivent toutes, les favoris vont dans la zone qui écoute, les caractères spéciaux fonctionnent de nouveau, et le démarrage est nettement plus rapide.",
|
|
91
|
-
"it": "I comandi inviati insieme arrivano tutti, i preferiti finiscono nella zona in ascolto, i caratteri speciali funzionano di nuovo e avvio e riconnessione sono molto più rapidi.",
|
|
92
|
-
"es": "Los comandos enviados a la vez llegan todos, los favoritos van a la zona que está escuchando, los caracteres especiales vuelven a funcionar y el arranque y la reconexión son mucho más rápidos.",
|
|
93
|
-
"pl": "Komendy wysłane naraz docierają wszystkie, ulubione trafiają do słuchającej strefy, znaki specjalne znów działają, a start i ponowne łączenie są wyraźnie szybsze.",
|
|
94
|
-
"uk": "Команди, надіслані разом, доходять усі, обране потрапляє в зону, що слухає, спецсимволи знову працюють, а запуск і перепідключення помітно швидші.",
|
|
95
|
-
"zh-cn": "一次发送的多条指令全部送达,收藏会进入正在收听的分区,特殊字符恢复正常,启动与重连明显更快。"
|
|
96
96
|
}
|
|
97
97
|
},
|
|
98
98
|
"titleLang": {
|