iobroker.yamaha 2.9.1 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +21 -12
  2. package/admin/i18n/de.json +3 -2
  3. package/admin/i18n/en.json +3 -2
  4. package/admin/i18n/es.json +3 -2
  5. package/admin/i18n/fr.json +3 -2
  6. package/admin/i18n/it.json +3 -2
  7. package/admin/i18n/nl.json +3 -2
  8. package/admin/i18n/pl.json +3 -2
  9. package/admin/i18n/pt.json +3 -2
  10. package/admin/i18n/ru.json +3 -2
  11. package/admin/i18n/uk.json +3 -2
  12. package/admin/i18n/zh-cn.json +3 -2
  13. package/admin/jsonConfig.json +13 -0
  14. package/admin/yamaha.svg +5 -4
  15. package/build/device-management-helpers.js +3 -2
  16. package/build/device-management-helpers.js.map +2 -2
  17. package/build/device-management.js +44 -43
  18. package/build/device-management.js.map +2 -2
  19. package/build/lib/attempt-device.js +8 -6
  20. package/build/lib/attempt-device.js.map +2 -2
  21. package/build/lib/catalog/value-coerce.js +15 -0
  22. package/build/lib/catalog/value-coerce.js.map +2 -2
  23. package/build/lib/device-controller.js +6 -5
  24. package/build/lib/device-controller.js.map +2 -2
  25. package/build/lib/device-type.js +47 -11
  26. package/build/lib/device-type.js.map +2 -2
  27. package/build/lib/lifecycle/device-supervisor.js +25 -2
  28. package/build/lib/lifecycle/device-supervisor.js.map +2 -2
  29. package/build/lib/types.js.map +1 -1
  30. package/build/lib/xml/command-mapper.js +5 -1
  31. package/build/lib/xml/command-mapper.js.map +2 -2
  32. package/build/lib/xml/device-controller.js +31 -5
  33. package/build/lib/xml/device-controller.js.map +2 -2
  34. package/build/lib/ynca/catalog.js +5 -1
  35. package/build/lib/ynca/catalog.js.map +2 -2
  36. package/build/lib/ynca/ynca-client.js +19 -1
  37. package/build/lib/ynca/ynca-client.js.map +2 -2
  38. package/build/lib/yxc/command-mapper.js +5 -1
  39. package/build/lib/yxc/command-mapper.js.map +2 -2
  40. package/build/lib/yxc/device-controller.js +90 -1
  41. package/build/lib/yxc/device-controller.js.map +2 -2
  42. package/build/lib/yxc/http-client.js +15 -2
  43. package/build/lib/yxc/http-client.js.map +2 -2
  44. package/build/lib/yxc/push-receiver.js +15 -3
  45. package/build/lib/yxc/push-receiver.js.map +2 -2
  46. package/build/lib/yxc/push.js +26 -1
  47. package/build/lib/yxc/push.js.map +2 -2
  48. package/build/main.js +95 -22
  49. package/build/main.js.map +2 -2
  50. package/io-package.json +29 -27
  51. package/package.json +2 -2
package/README.md CHANGED
@@ -74,6 +74,27 @@ 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.10.0 (2026-09-15)
78
+
79
+ - (krobipd) Fixed: A receiver the search found is searched for again after it moved to another address — until now that only worked for receivers found at start-up
80
+ - (krobipd) Fixed: A receiver that is unplugged or switched off at the mains now shows as disconnected within about 90 seconds instead of staying green for many minutes
81
+ - (krobipd) Fixed: A MusicCast device that stops answering a command is checked right away and shown as disconnected — until now that took up to 15 minutes
82
+ - (krobipd) Fixed: On receivers without live updates, a value you write is confirmed as soon as the receiver took it, instead of up to five minutes later
83
+ - (krobipd) Fixed: A zone name you changed on an older receiver stays after a reconnect — until now the previous name came back
84
+ - (krobipd) Fixed: Deleting a device from its card while it is still connecting no longer leaves parts of its object tree behind
85
+ - (krobipd) Fixed: Writing false, off or 0 to a switch datapoint now switches it off — until now any text, even the word false, switched it on
86
+ - (krobipd) Improved: The history of a datapoint only records values the receiver actually changed — a restart or a lost connection no longer adds identical entries
87
+ - (krobipd) Improved: MusicCast live updates now start on their own once a port another program held at start-up becomes free — before, only a restart helped
88
+ - (krobipd) New: Device pictograms in the object tree and on the device cards — receiver, stereo receiver, speaker, soundbar or CD system, readable in every theme, also for a device that is off
89
+ - (krobipd) Changed: The device card shows a speaker symbol; with the percent switch on it also shows the current volume as a percentage. The pencil and magnifier markers are gone
90
+ - (krobipd) Fixed: The adapter logo is readable in the Admin's dark themes as well — until now its dark strokes vanished on a dark background
91
+ - (krobipd) Changed: The instance settings show the fixed MusicCast event port, so the Admin warns when a second instance on the same host would take it
92
+
93
+ ### 2.9.2 (2026-09-12)
94
+
95
+ - (krobipd) New: The device card shows a 0–100 % badge while that receiver's volume is in percent, so you can tell the two scales apart at a glance
96
+ - (krobipd) Fixed: The percent setting is made in one place again — the device's edit dialog; the extra switch on the card showed the wrong position and is gone
97
+
77
98
  ### 2.9.1 (2026-09-12)
78
99
 
79
100
  - (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
@@ -100,18 +121,6 @@ Details on all settings, the object tree and the ports the adapter uses are in t
100
121
  - (krobipd) Changed: After this update every receiver is asked about its abilities once more, so the first start takes a little longer than usual
101
122
  - (krobipd) Fixed: A DAB receiver no longer logs a warning on every tuner poll — the frequency datapoint was limited to the FM band while the receiver reported DAB frequencies
102
123
 
103
- ### 2.7.2 (2026-09-09)
104
-
105
- - (krobipd) Fixed: The volume readout now follows the scale the receiver is actually showing, so a receiver set to numbers no longer reports them as decibels.
106
- - (krobipd) New: Inputs now appear under the names the receiver carries for them, so a socket named “Apple TV” reads that way instead of HDMI1.
107
- - (krobipd) Changed: After this update every receiver is asked about its abilities once more, so the first start takes a little longer than usual.
108
-
109
- ### 2.7.1 (2026-09-09)
110
-
111
- - (krobipd) Improved: The first connection asks a receiver only what its generation can answer, so zones, trigger sockets and per-input settings follow the device, not a catalogue.
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.
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.
114
-
115
124
  [Older changelogs can be found there](CHANGELOG_OLD.md)
116
125
 
117
126
  ## History
@@ -15,6 +15,8 @@
15
15
  "invalidName": "Dieser Name kann nicht verwendet werden",
16
16
  "networkInterfaceLabel": "Netzwerk-Interface",
17
17
  "networkInterfaceTooltip": "Netzwerk-Interface für LAN-Discovery und -Steuerung. Standard (0.0.0.0 = alle Interfaces) beibehalten, außer dieser Host hat mehrere Netzwerk-Interfaces.",
18
+ "pushPortLabel": "MusicCast-Ereignisport",
19
+ "pushPortTooltip": "UDP-Port, an den MusicCast-Geräte ihre Änderungen melden. Vom Protokoll festgelegt; eine zweite Instanz auf diesem Host kann ihn nicht mitbenutzen.",
18
20
  "dmAdd": "Yamaha-Gerät hinzufügen",
19
21
  "dmEdit": "Bearbeiten",
20
22
  "dmDelete": "Löschen",
@@ -608,6 +610,5 @@
608
610
  "discoveryAuto": "Automatisch (solange die Liste leer ist)",
609
611
  "discoveryAlways": "Immer",
610
612
  "discoveryNever": "Nie",
611
- "sourceManual": "Von Hand eingetragen",
612
- "sourceDiscovered": "Im Netzwerk gefunden"
613
+ "volumeDeviceScale": "Lautstärke in der Skala des Receivers"
613
614
  }
@@ -15,6 +15,8 @@
15
15
  "invalidName": "This name cannot be used",
16
16
  "networkInterfaceLabel": "Network Interface",
17
17
  "networkInterfaceTooltip": "Select the network interface for LAN discovery and control. Keep the default (0.0.0.0 = all interfaces) unless this host has multiple network interfaces.",
18
+ "pushPortLabel": "MusicCast event port",
19
+ "pushPortTooltip": "UDP port on which MusicCast devices push their changes. Fixed by the protocol; a second instance on this host cannot share it.",
18
20
  "dmAdd": "Add Yamaha device",
19
21
  "dmEdit": "Edit",
20
22
  "dmDelete": "Delete",
@@ -608,6 +610,5 @@
608
610
  "discoveryAuto": "Automatically (while the list is empty)",
609
611
  "discoveryAlways": "Always",
610
612
  "discoveryNever": "Never",
611
- "sourceManual": "Entered by hand",
612
- "sourceDiscovered": "Found on the network"
613
+ "volumeDeviceScale": "Volume in the receiver's own scale"
613
614
  }
@@ -15,6 +15,8 @@
15
15
  "invalidName": "Este nombre no se puede utilizar",
16
16
  "networkInterfaceLabel": "Interfaz de red",
17
17
  "networkInterfaceTooltip": "Selecciona la interfaz de red para el descubrimiento y control LAN. Mantén el valor predeterminado (0.0.0.0 = todas) salvo que este host tenga varias interfaces.",
18
+ "pushPortLabel": "Puerto de eventos MusicCast",
19
+ "pushPortTooltip": "Puerto UDP en el que los dispositivos MusicCast envían sus cambios. Fijado por el protocolo; una segunda instancia en este host no puede compartirlo.",
18
20
  "dmAdd": "Añadir dispositivo Yamaha",
19
21
  "dmEdit": "Editar",
20
22
  "dmDelete": "Eliminar",
@@ -608,6 +610,5 @@
608
610
  "discoveryAuto": "Automático (mientras la lista esté vacía)",
609
611
  "discoveryAlways": "Siempre",
610
612
  "discoveryNever": "Nunca",
611
- "sourceManual": "Introducido a mano",
612
- "sourceDiscovered": "Encontrado en la red"
613
+ "volumeDeviceScale": "Volumen en la escala propia del receptor"
613
614
  }
@@ -15,6 +15,8 @@
15
15
  "invalidName": "Ce nom ne peut pas être utilisé",
16
16
  "networkInterfaceLabel": "Interface réseau",
17
17
  "networkInterfaceTooltip": "Sélectionnez l'interface réseau pour la découverte et le contrôle LAN. Conservez la valeur par défaut (0.0.0.0 = toutes) sauf si cet hôte possède plusieurs interfaces.",
18
+ "pushPortLabel": "Port d'événements MusicCast",
19
+ "pushPortTooltip": "Port UDP sur lequel les appareils MusicCast envoient leurs changements. Fixé par le protocole ; une seconde instance sur cet hôte ne peut pas le partager.",
18
20
  "dmAdd": "Ajouter un appareil Yamaha",
19
21
  "dmEdit": "Modifier",
20
22
  "dmDelete": "Supprimer",
@@ -608,6 +610,5 @@
608
610
  "discoveryAuto": "Automatique (tant que la liste est vide)",
609
611
  "discoveryAlways": "Toujours",
610
612
  "discoveryNever": "Jamais",
611
- "sourceManual": "Saisi à la main",
612
- "sourceDiscovered": "Trouvé sur le réseau"
613
+ "volumeDeviceScale": "Volume dans l'échelle propre du récepteur"
613
614
  }
@@ -15,6 +15,8 @@
15
15
  "invalidName": "Questo nome non può essere utilizzato",
16
16
  "networkInterfaceLabel": "Interfaccia di rete",
17
17
  "networkInterfaceTooltip": "Seleziona l'interfaccia di rete per il rilevamento e il controllo LAN. Mantieni il valore predefinito (0.0.0.0 = tutte) a meno che questo host non abbia più interfacce.",
18
+ "pushPortLabel": "Porta eventi MusicCast",
19
+ "pushPortTooltip": "Porta UDP su cui i dispositivi MusicCast inviano le loro modifiche. Fissata dal protocollo; una seconda istanza su questo host non può condividerla.",
18
20
  "dmAdd": "Aggiungi dispositivo Yamaha",
19
21
  "dmEdit": "Modifica",
20
22
  "dmDelete": "Elimina",
@@ -608,6 +610,5 @@
608
610
  "discoveryAuto": "Automatico (finché l'elenco è vuoto)",
609
611
  "discoveryAlways": "Sempre",
610
612
  "discoveryNever": "Mai",
611
- "sourceManual": "Inserito a mano",
612
- "sourceDiscovered": "Trovato in rete"
613
+ "volumeDeviceScale": "Volume nella scala propria del ricevitore"
613
614
  }
@@ -15,6 +15,8 @@
15
15
  "invalidName": "Deze naam kan niet worden gebruikt",
16
16
  "networkInterfaceLabel": "Netwerkinterface",
17
17
  "networkInterfaceTooltip": "Selecteer de netwerkinterface voor LAN-detectie en -besturing. Behoud de standaard (0.0.0.0 = alle interfaces), tenzij deze host meerdere netwerkinterfaces heeft.",
18
+ "pushPortLabel": "MusicCast-gebeurtenispoort",
19
+ "pushPortTooltip": "UDP-poort waarop MusicCast-apparaten hun wijzigingen melden. Vastgelegd door het protocol; een tweede instantie op deze host kan hem niet delen.",
18
20
  "dmAdd": "Yamaha-apparaat toevoegen",
19
21
  "dmEdit": "Bewerken",
20
22
  "dmDelete": "Verwijderen",
@@ -608,6 +610,5 @@
608
610
  "discoveryAuto": "Automatisch (zolang de lijst leeg is)",
609
611
  "discoveryAlways": "Altijd",
610
612
  "discoveryNever": "Nooit",
611
- "sourceManual": "Handmatig ingevoerd",
612
- "sourceDiscovered": "Gevonden in het netwerk"
613
+ "volumeDeviceScale": "Volume in de eigen schaal van de receiver"
613
614
  }
@@ -15,6 +15,8 @@
15
15
  "invalidName": "Ta nazwa nie może być użyta",
16
16
  "networkInterfaceLabel": "Interfejs sieciowy",
17
17
  "networkInterfaceTooltip": "Wybierz interfejs sieciowy do wykrywania i sterowania LAN. Pozostaw domyślny (0.0.0.0 = wszystkie), chyba że ten host ma wiele interfejsów.",
18
+ "pushPortLabel": "Port zdarzeń MusicCast",
19
+ "pushPortTooltip": "Port UDP, na który urządzenia MusicCast wysyłają swoje zmiany. Ustalony przez protokół; druga instancja na tym hoście nie może go współdzielić.",
18
20
  "dmAdd": "Dodaj urządzenie Yamaha",
19
21
  "dmEdit": "Edytuj",
20
22
  "dmDelete": "Usuń",
@@ -608,6 +610,5 @@
608
610
  "discoveryAuto": "Automatycznie (dopóki lista jest pusta)",
609
611
  "discoveryAlways": "Zawsze",
610
612
  "discoveryNever": "Nigdy",
611
- "sourceManual": "Wpisane ręcznie",
612
- "sourceDiscovered": "Znalezione w sieci"
613
+ "volumeDeviceScale": "Głośność we własnej skali amplitunera"
613
614
  }
@@ -15,6 +15,8 @@
15
15
  "invalidName": "Este nome não pode ser usado",
16
16
  "networkInterfaceLabel": "Interface de rede",
17
17
  "networkInterfaceTooltip": "Selecione a interface de rede para descoberta e controlo LAN. Mantenha o padrão (0.0.0.0 = todas) a menos que este host tenha várias interfaces.",
18
+ "pushPortLabel": "Porta de eventos MusicCast",
19
+ "pushPortTooltip": "Porta UDP na qual os dispositivos MusicCast enviam as suas alterações. Fixada pelo protocolo; uma segunda instância neste host não a pode partilhar.",
18
20
  "dmAdd": "Adicionar dispositivo Yamaha",
19
21
  "dmEdit": "Editar",
20
22
  "dmDelete": "Eliminar",
@@ -608,6 +610,5 @@
608
610
  "discoveryAuto": "Automático (enquanto a lista estiver vazia)",
609
611
  "discoveryAlways": "Sempre",
610
612
  "discoveryNever": "Nunca",
611
- "sourceManual": "Introduzido manualmente",
612
- "sourceDiscovered": "Encontrado na rede"
613
+ "volumeDeviceScale": "Volume na escala própria do receptor"
613
614
  }
@@ -15,6 +15,8 @@
15
15
  "invalidName": "Это имя нельзя использовать",
16
16
  "networkInterfaceLabel": "Сетевой интерфейс",
17
17
  "networkInterfaceTooltip": "Выберите сетевой интерфейс для обнаружения и управления по LAN. Оставьте значение по умолчанию (0.0.0.0 = все интерфейсы), если у хоста нет нескольких интерфейсов.",
18
+ "pushPortLabel": "Порт событий MusicCast",
19
+ "pushPortTooltip": "UDP-порт, на который устройства MusicCast отправляют свои изменения. Задан протоколом; второй экземпляр на этом хосте не может его использовать.",
18
20
  "dmAdd": "Добавить устройство Yamaha",
19
21
  "dmEdit": "Изменить",
20
22
  "dmDelete": "Удалить",
@@ -608,6 +610,5 @@
608
610
  "discoveryAuto": "Автоматически (пока список пуст)",
609
611
  "discoveryAlways": "Всегда",
610
612
  "discoveryNever": "Никогда",
611
- "sourceManual": "Добавлено вручную",
612
- "sourceDiscovered": "Найдено в сети"
613
+ "volumeDeviceScale": "Громкость в собственной шкале ресивера"
613
614
  }
@@ -15,6 +15,8 @@
15
15
  "invalidName": "Це ім'я не можна використовувати",
16
16
  "networkInterfaceLabel": "Мережевий інтерфейс",
17
17
  "networkInterfaceTooltip": "Виберіть мережевий інтерфейс для виявлення та керування по LAN. Залиште типове значення (0.0.0.0 = всі інтерфейси), якщо хост не має кількох інтерфейсів.",
18
+ "pushPortLabel": "Порт подій MusicCast",
19
+ "pushPortTooltip": "UDP-порт, на який пристрої MusicCast надсилають свої зміни. Визначений протоколом; другий екземпляр на цьому хості не може його використовувати.",
18
20
  "dmAdd": "Додати пристрій Yamaha",
19
21
  "dmEdit": "Редагувати",
20
22
  "dmDelete": "Видалити",
@@ -608,6 +610,5 @@
608
610
  "discoveryAuto": "Автоматично (доки список порожній)",
609
611
  "discoveryAlways": "Завжди",
610
612
  "discoveryNever": "Ніколи",
611
- "sourceManual": "Додано вручну",
612
- "sourceDiscovered": "Знайдено в мережі"
613
+ "volumeDeviceScale": "Гучність у власній шкалі ресивера"
613
614
  }
@@ -15,6 +15,8 @@
15
15
  "invalidName": "此名称不可用",
16
16
  "networkInterfaceLabel": "网络接口",
17
17
  "networkInterfaceTooltip": "选择用于局域网发现和控制的网络接口。除非此主机有多个网络接口,否则请保留默认值(0.0.0.0 = 所有接口)。",
18
+ "pushPortLabel": "MusicCast 事件端口",
19
+ "pushPortTooltip": "MusicCast 设备向此 UDP 端口推送其变化。由协议固定;同一主机上的第二个实例无法共用。",
18
20
  "dmAdd": "添加 Yamaha 设备",
19
21
  "dmEdit": "编辑",
20
22
  "dmDelete": "删除",
@@ -608,6 +610,5 @@
608
610
  "discoveryAuto": "自动(列表为空时)",
609
611
  "discoveryAlways": "始终",
610
612
  "discoveryNever": "从不",
611
- "sourceManual": "手动添加",
612
- "sourceDiscovered": "网络中发现"
613
+ "volumeDeviceScale": "音量采用接收机自身的刻度"
613
614
  }
@@ -81,6 +81,19 @@
81
81
  "lg": 6,
82
82
  "xl": 6
83
83
  },
84
+ "port": {
85
+ "type": "port",
86
+ "label": "pushPortLabel",
87
+ "tooltip": "pushPortTooltip",
88
+ "disabled": true,
89
+ "min": 41100,
90
+ "max": 41100,
91
+ "xs": 12,
92
+ "sm": 12,
93
+ "md": 6,
94
+ "lg": 6,
95
+ "xl": 6
96
+ },
84
97
  "xmlPollInterval": {
85
98
  "newLine": true,
86
99
  "type": "number",
package/admin/yamaha.svg CHANGED
@@ -1,8 +1,9 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="#3b4046" stroke-width="22" stroke-linecap="round" stroke-linejoin="round">
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="#78869a" stroke-width="22" stroke-linecap="round" stroke-linejoin="round">
2
2
  <title>ioBroker.yamaha</title>
3
- <!-- Fixed theme-aware colours — never currentColor: embedded as <img> that falls back to
4
- black and disappears on dark backgrounds. Dark strokes on light, light strokes on dark. -->
5
- <style>@media (prefers-color-scheme: dark) { svg { stroke: #d6dade; } }</style>
3
+ <!-- ONE fixed colour, never currentColor: the logo is embedded as <img>, which inherits
4
+ nothing. A media query would follow the OPERATING SYSTEM's scheme, not the Admin's
5
+ theme (the two disagree all the time), so the colour has to read on both: #78869a is
6
+ 3.7:1 on white and 3.6–5.1:1 on the Admin's dark backgrounds (#303030 … #121212). -->
6
7
  <circle cx="256" cy="256" r="234" />
7
8
  <path d="M236,104 L236,212 L256,246 L276,212 L276,104" transform="rotate(0 256 256)" />
8
9
  <path d="M236,104 L236,212 L256,246 L276,212 L276,104" transform="rotate(120 256 256)" />
@@ -54,8 +54,9 @@ function buildDeviceForm(usedIps) {
54
54
  md: 6
55
55
  },
56
56
  // Per device, not per instance: the adapter serves several receivers, and one of them
57
- // wanting percent says nothing about the others. Same switch as on the card — one value,
58
- // two places to reach it.
57
+ // wanting percent says nothing about the others. THE place to set it — 2.9.1 also had a
58
+ // switch control on the card itself, and one value reachable from two independently-read
59
+ // places is how the card came to show "off" while this checkbox showed "on".
59
60
  volumeAsPercent: {
60
61
  newLine: true,
61
62
  type: "checkbox",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/device-management-helpers.ts"],
4
- "sourcesContent": ["import type { JsonFormSchema } from \"@iobroker/dm-utils\";\nimport { t } from \"./lib/i18n\";\nimport { sanitizeId } from \"./lib/pure-helpers\";\nimport { TRANSPORT_LABELS } from \"./lib/ready-line\";\n\n/** Object-id segments the adapter reserves for its own tree \u2014 a device may not take them. */\nconst RESERVED_IDS = new Set([\"info\"]);\n/** IPv4 dotted-quad \u2014 the single source for both the frontend validator and the backend check. */\nconst IP_RE = /^(\\d{1,3}\\.){3}\\d{1,3}$/;\n\n/** The transports shown as card indicators \u2014 the single source shared with the ready-log line. */\nexport const TRANSPORTS = TRANSPORT_LABELS;\n\n/** One raw manual device row from `native.devices` (the name is optional). */\nexport interface ManualRow {\n /** Display name, or empty/absent when the user left it blank. */\n name?: string;\n /** The device IP address. */\n ip: string;\n}\n\n/** A running device as shown on a card, plus which source it came from (routes edit/delete). */\nexport interface CardDevice {\n /** The id-safe device id (object-tree path segment). */\n id: string;\n /** The device IP address. */\n ip: string;\n /** The card header name. */\n name: string;\n /** Where it lives: the manual `native.devices` table, or the auto-discovery store. */\n source: \"manual\" | \"discovered\";\n}\n\n/**\n * The id the object tree uses for a manual row: its name, or the ip when the name is blank.\n *\n * @param row the manual device row\n * @returns the id-safe device id\n */\nexport function rowId(row: ManualRow): string {\n return sanitizeId(row.name && row.name.length > 0 ? row.name : row.ip);\n}\n\n/**\n * The add/edit form for one receiver: a display name, the IP address and whether its volume\n * datapoints read 0\u2013100 %. The IP field\n * carries a live validator against a valid dotted-quad that is not already in use (the OK\n * button greys out on a clash). Labels are resolved translation objects so the embedded\n * form is language-correct.\n *\n * @param usedIps the IPs taken by OTHER devices (the edited device excluded)\n * @returns the jsonConfig panel schema for one device\n */\nexport function buildDeviceForm(usedIps: readonly string[]): JsonFormSchema {\n const ipList = JSON.stringify([...usedIps]);\n return {\n type: \"panel\",\n items: {\n name: {\n type: \"text\",\n label: t(\"columnName\"),\n sm: 12,\n md: 6,\n },\n ip: {\n type: \"text\",\n label: t(\"columnIp\"),\n validator: `!!(data.ip && ${IP_RE.toString()}.test(data.ip)) && !${ipList}.includes(data.ip)`,\n validatorErrorText: t(\"invalidIp\"),\n validatorNoSaveOnError: true,\n sm: 12,\n md: 6,\n },\n // Per device, not per instance: the adapter serves several receivers, and one of them\n // wanting percent says nothing about the others. Same switch as on the card \u2014 one value,\n // two places to reach it.\n volumeAsPercent: {\n newLine: true,\n type: \"checkbox\",\n label: t(\"volumeAsPercent\"),\n help: t(\"volumeAsPercent_help\"),\n sm: 12,\n md: 12,\n },\n },\n } as unknown as JsonFormSchema;\n}\n\n/**\n * A duplicate-IP/name or invalid-value clash against the other rows, as a ready-to-show\n * message \u2014 the backend safety net behind the form validator (the dialog validator may not\n * fire in every admin version; this never lets a bad row through). Each failure mode gets\n * its own message: a malformed IP is not the same problem as a name that is reserved or\n * already taken \u2014 reporting \"invalid IP\" for a name clash sends the user hunting in the\n * wrong field.\n *\n * @param rows the current manual rows\n * @param candidate the row being added/edited\n * @param exceptIndex the row position to ignore (the row being edited), or -1\n * @returns a translated clash message, or null when the row is fine\n */\nexport function findClash(\n rows: readonly ManualRow[],\n candidate: ManualRow,\n exceptIndex: number,\n): ioBroker.StringOrTranslated | null {\n if (!IP_RE.test(candidate.ip)) {\n return t(\"invalidIp\");\n }\n const id = rowId(candidate);\n if (id === \"\" || RESERVED_IDS.has(id)) {\n return t(\"invalidName\");\n }\n for (let i = 0; i < rows.length; i++) {\n if (i === exceptIndex) {\n continue;\n }\n if (rows[i].ip === candidate.ip || rowId(rows[i]) === id) {\n return t(\"duplicateDevice\");\n }\n }\n return null;\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,kBAAkB;AAClB,0BAA2B;AAC3B,wBAAiC;AAGjC,MAAM,eAAe,oBAAI,IAAI,CAAC,MAAM,CAAC;AAErC,MAAM,QAAQ;AAGP,MAAM,aAAa;AA4BnB,SAAS,MAAM,KAAwB;AAC5C,aAAO,gCAAW,IAAI,QAAQ,IAAI,KAAK,SAAS,IAAI,IAAI,OAAO,IAAI,EAAE;AACvE;AAYO,SAAS,gBAAgB,SAA4C;AAC1E,QAAM,SAAS,KAAK,UAAU,CAAC,GAAG,OAAO,CAAC;AAC1C,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO;AAAA,MACL,MAAM;AAAA,QACJ,MAAM;AAAA,QACN,WAAO,eAAE,YAAY;AAAA,QACrB,IAAI;AAAA,QACJ,IAAI;AAAA,MACN;AAAA,MACA,IAAI;AAAA,QACF,MAAM;AAAA,QACN,WAAO,eAAE,UAAU;AAAA,QACnB,WAAW,iBAAiB,MAAM,SAAS,CAAC,uBAAuB,MAAM;AAAA,QACzE,wBAAoB,eAAE,WAAW;AAAA,QACjC,wBAAwB;AAAA,QACxB,IAAI;AAAA,QACJ,IAAI;AAAA,MACN;AAAA;AAAA;AAAA;AAAA,MAIA,iBAAiB;AAAA,QACf,SAAS;AAAA,QACT,MAAM;AAAA,QACN,WAAO,eAAE,iBAAiB;AAAA,QAC1B,UAAM,eAAE,sBAAsB;AAAA,QAC9B,IAAI;AAAA,QACJ,IAAI;AAAA,MACN;AAAA,IACF;AAAA,EACF;AACF;AAeO,SAAS,UACd,MACA,WACA,aACoC;AACpC,MAAI,CAAC,MAAM,KAAK,UAAU,EAAE,GAAG;AAC7B,eAAO,eAAE,WAAW;AAAA,EACtB;AACA,QAAM,KAAK,MAAM,SAAS;AAC1B,MAAI,OAAO,MAAM,aAAa,IAAI,EAAE,GAAG;AACrC,eAAO,eAAE,aAAa;AAAA,EACxB;AACA,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,QAAI,MAAM,aAAa;AACrB;AAAA,IACF;AACA,QAAI,KAAK,CAAC,EAAE,OAAO,UAAU,MAAM,MAAM,KAAK,CAAC,CAAC,MAAM,IAAI;AACxD,iBAAO,eAAE,iBAAiB;AAAA,IAC5B;AAAA,EACF;AACA,SAAO;AACT;",
4
+ "sourcesContent": ["import type { JsonFormSchema } from \"@iobroker/dm-utils\";\nimport { t } from \"./lib/i18n\";\nimport { sanitizeId } from \"./lib/pure-helpers\";\nimport { TRANSPORT_LABELS } from \"./lib/ready-line\";\n\n/** Object-id segments the adapter reserves for its own tree \u2014 a device may not take them. */\nconst RESERVED_IDS = new Set([\"info\"]);\n/** IPv4 dotted-quad \u2014 the single source for both the frontend validator and the backend check. */\nconst IP_RE = /^(\\d{1,3}\\.){3}\\d{1,3}$/;\n\n/** The transports shown as card indicators \u2014 the single source shared with the ready-log line. */\nexport const TRANSPORTS = TRANSPORT_LABELS;\n\n/** One raw manual device row from `native.devices` (the name is optional). */\nexport interface ManualRow {\n /** Display name, or empty/absent when the user left it blank. */\n name?: string;\n /** The device IP address. */\n ip: string;\n}\n\n/** A running device as shown on a card, plus which source it came from (routes edit/delete). */\nexport interface CardDevice {\n /** The id-safe device id (object-tree path segment). */\n id: string;\n /** The device IP address. */\n ip: string;\n /** The card header name. */\n name: string;\n /** Where it lives: the manual `native.devices` table, or the auto-discovery store. */\n source: \"manual\" | \"discovered\";\n}\n\n/**\n * The id the object tree uses for a manual row: its name, or the ip when the name is blank.\n *\n * @param row the manual device row\n * @returns the id-safe device id\n */\nexport function rowId(row: ManualRow): string {\n return sanitizeId(row.name && row.name.length > 0 ? row.name : row.ip);\n}\n\n/**\n * The add/edit form for one receiver: a display name, the IP address and whether its volume\n * datapoints read 0\u2013100 %. The IP field\n * carries a live validator against a valid dotted-quad that is not already in use (the OK\n * button greys out on a clash). Labels are resolved translation objects so the embedded\n * form is language-correct.\n *\n * @param usedIps the IPs taken by OTHER devices (the edited device excluded)\n * @returns the jsonConfig panel schema for one device\n */\nexport function buildDeviceForm(usedIps: readonly string[]): JsonFormSchema {\n const ipList = JSON.stringify([...usedIps]);\n return {\n type: \"panel\",\n items: {\n name: {\n type: \"text\",\n label: t(\"columnName\"),\n sm: 12,\n md: 6,\n },\n ip: {\n type: \"text\",\n label: t(\"columnIp\"),\n validator: `!!(data.ip && ${IP_RE.toString()}.test(data.ip)) && !${ipList}.includes(data.ip)`,\n validatorErrorText: t(\"invalidIp\"),\n validatorNoSaveOnError: true,\n sm: 12,\n md: 6,\n },\n // Per device, not per instance: the adapter serves several receivers, and one of them\n // wanting percent says nothing about the others. THE place to set it \u2014 2.9.1 also had a\n // switch control on the card itself, and one value reachable from two independently-read\n // places is how the card came to show \"off\" while this checkbox showed \"on\".\n volumeAsPercent: {\n newLine: true,\n type: \"checkbox\",\n label: t(\"volumeAsPercent\"),\n help: t(\"volumeAsPercent_help\"),\n sm: 12,\n md: 12,\n },\n },\n } as unknown as JsonFormSchema;\n}\n\n/**\n * A duplicate-IP/name or invalid-value clash against the other rows, as a ready-to-show\n * message \u2014 the backend safety net behind the form validator (the dialog validator may not\n * fire in every admin version; this never lets a bad row through). Each failure mode gets\n * its own message: a malformed IP is not the same problem as a name that is reserved or\n * already taken \u2014 reporting \"invalid IP\" for a name clash sends the user hunting in the\n * wrong field.\n *\n * @param rows the current manual rows\n * @param candidate the row being added/edited\n * @param exceptIndex the row position to ignore (the row being edited), or -1\n * @returns a translated clash message, or null when the row is fine\n */\nexport function findClash(\n rows: readonly ManualRow[],\n candidate: ManualRow,\n exceptIndex: number,\n): ioBroker.StringOrTranslated | null {\n if (!IP_RE.test(candidate.ip)) {\n return t(\"invalidIp\");\n }\n const id = rowId(candidate);\n if (id === \"\" || RESERVED_IDS.has(id)) {\n return t(\"invalidName\");\n }\n for (let i = 0; i < rows.length; i++) {\n if (i === exceptIndex) {\n continue;\n }\n if (rows[i].ip === candidate.ip || rowId(rows[i]) === id) {\n return t(\"duplicateDevice\");\n }\n }\n return null;\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,kBAAkB;AAClB,0BAA2B;AAC3B,wBAAiC;AAGjC,MAAM,eAAe,oBAAI,IAAI,CAAC,MAAM,CAAC;AAErC,MAAM,QAAQ;AAGP,MAAM,aAAa;AA4BnB,SAAS,MAAM,KAAwB;AAC5C,aAAO,gCAAW,IAAI,QAAQ,IAAI,KAAK,SAAS,IAAI,IAAI,OAAO,IAAI,EAAE;AACvE;AAYO,SAAS,gBAAgB,SAA4C;AAC1E,QAAM,SAAS,KAAK,UAAU,CAAC,GAAG,OAAO,CAAC;AAC1C,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO;AAAA,MACL,MAAM;AAAA,QACJ,MAAM;AAAA,QACN,WAAO,eAAE,YAAY;AAAA,QACrB,IAAI;AAAA,QACJ,IAAI;AAAA,MACN;AAAA,MACA,IAAI;AAAA,QACF,MAAM;AAAA,QACN,WAAO,eAAE,UAAU;AAAA,QACnB,WAAW,iBAAiB,MAAM,SAAS,CAAC,uBAAuB,MAAM;AAAA,QACzE,wBAAoB,eAAE,WAAW;AAAA,QACjC,wBAAwB;AAAA,QACxB,IAAI;AAAA,QACJ,IAAI;AAAA,MACN;AAAA;AAAA;AAAA;AAAA;AAAA,MAKA,iBAAiB;AAAA,QACf,SAAS;AAAA,QACT,MAAM;AAAA,QACN,WAAO,eAAE,iBAAiB;AAAA,QAC1B,UAAM,eAAE,sBAAsB;AAAA,QAC9B,IAAI;AAAA,QACJ,IAAI;AAAA,MACN;AAAA,IACF;AAAA,EACF;AACF;AAeO,SAAS,UACd,MACA,WACA,aACoC;AACpC,MAAI,CAAC,MAAM,KAAK,UAAU,EAAE,GAAG;AAC7B,eAAO,eAAE,WAAW;AAAA,EACtB;AACA,QAAM,KAAK,MAAM,SAAS;AAC1B,MAAI,OAAO,MAAM,aAAa,IAAI,EAAE,GAAG;AACrC,eAAO,eAAE,aAAa;AAAA,EACxB;AACA,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,QAAI,MAAM,aAAa;AACrB;AAAA,IACF;AACA,QAAI,KAAK,CAAC,EAAE,OAAO,UAAU,MAAM,MAAM,KAAK,CAAC,CAAC,MAAM,IAAI;AACxD,iBAAO,eAAE,iBAAiB;AAAA,IAC5B;AAAA,EACF;AACA,SAAO;AACT;",
6
6
  "names": []
7
7
  }
@@ -59,17 +59,15 @@ class YamahaDeviceManagement extends import_dm_utils.DeviceManagement {
59
59
  await this.adapter.extendForeignObjectAsync(this.objId, { native: { devices: rows } });
60
60
  }
61
61
  /**
62
- * One device's percent switch as a state, for the card control to show.
62
+ * Whether one device's volume datapoints read 0–100 %, for the edit dialog to prefill.
63
63
  *
64
64
  * @param deviceId the id-safe device id
65
- * @returns the switch position as an ioBroker state
65
+ * @returns true when this device is set to percent
66
66
  */
67
- async volumePercentState(deviceId) {
67
+ async volumeAsPercentOf(deviceId) {
68
68
  var _a;
69
69
  const node = await this.adapter.getForeignObjectAsync(`${this.adapter.namespace}.${deviceId}`);
70
- const on = ((_a = node == null ? void 0 : node.native) == null ? void 0 : _a.volumeAsPercent) === true;
71
- const now = Date.now();
72
- return { val: on, ack: true, ts: now, lc: now, from: `system.adapter.${this.adapter.namespace}` };
70
+ return ((_a = node == null ? void 0 : node.native) == null ? void 0 : _a.volumeAsPercent) === true;
73
71
  }
74
72
  /**
75
73
  * Set one device's percent switch. The value lives at the DEVICE object — one place the card,
@@ -127,17 +125,19 @@ class YamahaDeviceManagement extends import_dm_utils.DeviceManagement {
127
125
  * @param context the load context
128
126
  */
129
127
  async loadDevices(context) {
130
- var _a;
128
+ var _a, _b;
131
129
  for (const card of await this.cards()) {
132
130
  const [model, node] = await Promise.all([
133
131
  this.adapter.getForeignStateAsync(`${this.adapter.namespace}.${card.id}.info.model`),
134
132
  this.adapter.getForeignObjectAsync(`${this.adapter.namespace}.${card.id}`)
135
133
  ]);
136
134
  const label = typeof ((_a = node == null ? void 0 : node.common) == null ? void 0 : _a.name) === "string" ? node.common.name : void 0;
135
+ const percent = ((_b = node == null ? void 0 : node.native) == null ? void 0 : _b.volumeAsPercent) === true;
137
136
  context.addDevice(
138
137
  this.toDeviceInfo(
139
138
  label && label !== card.id ? { ...card, name: label } : card,
140
- typeof (model == null ? void 0 : model.val) === "string" ? model.val : void 0
139
+ typeof (model == null ? void 0 : model.val) === "string" ? model.val : void 0,
140
+ percent
141
141
  )
142
142
  );
143
143
  }
@@ -145,14 +145,16 @@ class YamahaDeviceManagement extends import_dm_utils.DeviceManagement {
145
145
  /**
146
146
  * Build one device card: the live model, the IP as the identifier line, a connection
147
147
  * status, and one indicator per connected protocol (hidden while that protocol is not
148
- * connected). All live values read from the device's own `info.*` states. A discovered
149
- * card has no edit action — it is auto-found and re-found; it can only be removed.
148
+ * connected). The live values read from the device's own `info.*` states. Every card can be
149
+ * edited, a discovered one included: that is how a found receiver is given the fixed address
150
+ * the user just assigned it (see {@link editDevice}).
150
151
  *
151
152
  * @param card the running device
152
153
  * @param model the device's reported model name, for the device-class icon
154
+ * @param percent whether this device's volume datapoints read 0–100 %, for the badge
153
155
  * @returns the card descriptor
154
156
  */
155
- toDeviceInfo(card, model) {
157
+ toDeviceInfo(card, model, percent = false) {
156
158
  const base = `${this.adapter.namespace}.${card.id}`;
157
159
  const del = {
158
160
  id: "delete",
@@ -175,47 +177,46 @@ class YamahaDeviceManagement extends import_dm_utils.DeviceManagement {
175
177
  status: {
176
178
  connection: { stateId: `${base}.info.connection`, mapping: { true: "connected", false: "disconnected" } }
177
179
  },
178
- // No icon on the transports: the indicator icon accepts only a reserved/`fa-*`/`data:`/URL
179
- // name, so a plain "wifi" rendered as a "?". The transport label as text plus a green "on"
180
- // colour carries it; `hideIfEmpty` shows only the protocols this device is connected over.
180
+ // No icon on the transports: dm-gui-components renders exactly 17 `fa-*` names (its own
181
+ // whitelist, `getFaIcon`) and inline `data:image/svg+xml` URLs — anything else is a "?".
182
+ // The transport label as text plus a green "on" colour carries it; `hideIfEmpty` shows
183
+ // only the protocols this device is connected over. Own glyphs go in as data URLs, drawn
184
+ // with `currentColor` so they take the indicator's colour (see device-type.ts).
181
185
  indicators: [
182
- // Where the address came from. The adapter treats the two differently — only a device
183
- // the search found is looked for again when it drops off — so the card says which it is.
184
- {
185
- id: "device-source",
186
- value: true,
187
- icon: card.source === "manual" ? "fa-pencil" : "fa-search",
188
- tooltip: (0, import_i18n.t)(card.source === "manual" ? "sourceManual" : "sourceDiscovered"),
189
- color: "primary",
190
- order: 10
191
- },
192
186
  ...import_device_management_helpers.TRANSPORTS.map((tr) => ({
193
187
  id: `transport-${tr.id}`,
194
188
  value: { stateId: `${base}.info.transports.${tr.id}` },
195
189
  text: tr.label,
196
190
  colorOn: "ok",
197
191
  hideIfEmpty: true
198
- }))
192
+ })),
193
+ // The percent setting is SET in the edit dialog, but it has to be READABLE at a glance —
194
+ // otherwise the only way to find out what a receiver's volume datapoints carry is to open
195
+ // a dialog. With percent on, the glyph is a speaker with a percent sign and the main
196
+ // zone's live volume stands under it as "42 %"; in the device's own scale the plain
197
+ // speaker stands alone. Always shown (`hideIfEmpty: false` — 0 % is a value), and given
198
+ // a colour for BOTH states so the glyph does not grey out at the bottom of the scale.
199
+ // Deliberately not clickable: the dialog stays the one place that sets it.
200
+ {
201
+ id: "volume",
202
+ icon: (0, import_device_type.volumeIndicatorIcon)(percent),
203
+ value: percent ? { stateId: `${base}.volume` } : true,
204
+ ...percent ? { showValue: true, unit: "%" } : {},
205
+ hideIfEmpty: false,
206
+ color: "primary",
207
+ colorOn: "primary",
208
+ tooltip: (0, import_i18n.t)(percent ? "volumeAsPercent" : "volumeDeviceScale"),
209
+ order: 20
210
+ }
199
211
  ],
200
212
  // Edit on every card: a device the search found can be given the fixed address the user
201
213
  // just assigned it, which makes it a manual device (see editDevice).
202
- actions: [edit, del],
203
- // The percent switch, per device. It is a decision, not something switched around, so it
204
- // sits in the admin next to the device rather than in the object tree — but it belongs to
205
- // THIS device, which is what 2.8.0's single instance checkbox could not express.
206
- controls: [
207
- {
208
- id: "volumeAsPercent",
209
- type: "switch",
210
- label: (0, import_i18n.t)("volumeAsPercent"),
211
- description: (0, import_i18n.t)("volumeAsPercent_help"),
212
- getStateHandler: async (deviceId) => this.volumePercentState(deviceId),
213
- handler: async (deviceId, _controlId, state) => {
214
- await this.applyVolumePercent(deviceId, state === true);
215
- return this.volumePercentState(deviceId);
216
- }
217
- }
218
- ]
214
+ // The percent switch is NOT a second control on the card: it lives in the edit dialog,
215
+ // next to name and address, because it is a decision about the device and not something
216
+ // flipped in passing. 2.9.1 had it in both places — the card control showed the wrong
217
+ // position while the dialog showed the right one, and two ways to set one value is one
218
+ // too many (krobi 2026-09-12: "für was hast du den das doppelt gemoppelt?").
219
+ actions: [edit, del]
219
220
  };
220
221
  }
221
222
  /**
@@ -290,7 +291,7 @@ class YamahaDeviceManagement extends import_dm_utils.DeviceManagement {
290
291
  }
291
292
  const node = await this.adapter.getForeignObjectAsync(`${this.adapter.namespace}.${cardId}`);
292
293
  const shownName = typeof ((_a = node == null ? void 0 : node.common) == null ? void 0 : _a.name) === "string" && node.common.name !== cardId ? node.common.name : card.name;
293
- const percent = (await this.volumePercentState(cardId)).val === true;
294
+ const percent = await this.volumeAsPercentOf(cardId);
294
295
  const data = await context.showForm(
295
296
  (0, import_device_management_helpers.buildDeviceForm)(cards.filter((entry) => entry.id !== cardId).map((entry) => entry.ip)),
296
297
  { title: (0, import_i18n.t)("dmEditTitle"), data: { name: shownName, ip: card.ip, volumeAsPercent: percent } }