iobroker.alias-workbench 0.9.12 → 0.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.
@@ -2,7 +2,7 @@
2
2
  Muster (passt / n von m belegt) und der Knopf, der freie Plaetze
3
3
  vorschlaegt. Gebaut wird beides hier, eingehaengt woanders: der Block
4
4
  je nach Ansicht im Vorlagenkasten oder in der Leiste ueber der Liste,
5
- der Knopf immer in der Leiste.
5
+ der Knopf mit ihm (seit 19.09.2026; vorher immer in der Leiste).
6
6
 
7
7
  Herausgeloest aus zeichneErgebnis - der Abschnitt hing an `e`, dem
8
8
  Erkennungsergebnis und sonst an nichts. */
@@ -10,11 +10,84 @@
10
10
  import { S } from './zustand.js';
11
11
  import { D, el, $ } from './basis.js';
12
12
  import { tr, sprachtext } from './sprache.js';
13
- import { VORSCHAU, musterVon, erkenneEntwurf, platzFuerRolle, typenFuerRolle, typKonflikte } from './erkennung.js';
13
+ import { VORSCHAU, musterVon, erkenneEntwurf, platzFuerRolle, typenFuerRolle, typKonflikte, fehlendePflicht } from './erkennung.js';
14
14
  import { anMusterAnpassen } from './vorlagen.js';
15
15
  import { ratePlaetze, rateAnzahl } from './vorschlagen.js';
16
16
  import { zeichneErgebnis, entwurfAngefasst, knopfFrisch } from './ergebnis.js';
17
17
  import { musterName, musterZeile } from './musternamen.js';
18
+ import { nurWasInsMusterPasst } from './zustandsliste.js';
19
+ import { kindZustaende } from './werte.js';
20
+
21
+ /* Musterwechsel ohne Vorlage: die Haken folgen dem neuen Muster (C54).
22
+
23
+ Vorher lief die Vorbelegung nur einmal, beim Aufbau des Entwurfs; wer
24
+ danach das Muster wechselte, behielt die Haken des alten. Jetzt gilt
25
+ dieselbe Regel wie beim Aufbau — angehakt ist, was im gewaehlten
26
+ Muster einen Platz hat —, dazu jede Zeile, an der schon etwas von Hand
27
+ eingestellt wurde (Ricardo, 19.09.2026).
28
+
29
+ Nicht mit Vorlage: die bestimmt selbst, was an ist. Nicht an einem
30
+ Alias, den es schon gibt: dort ist jeder Haken eine getroffene
31
+ Entscheidung, abwaehlen hiesse loeschen — dieselben Ausnahmen wie bei
32
+ der Vorbelegung in `berechnePlaetze`. */
33
+ function hakenNachMuster(e) {
34
+ if (e.vorlage) { return; }
35
+ if (S.current && S.current.indexOf('alias.') === 0) { return; }
36
+ if (e.ziel && kindZustaende(e.ziel).length > 0) { return; }
37
+ /* Greift das neue Muster gar nicht — auch nicht mit allen Zeilen an —,
38
+ gibt es nichts, dem die Haken folgen koennten. Vorher fielen dann
39
+ alle bis auf die geaenderten: an der Klimaanlage verschwanden mit
40
+ dem Wechsel auf airCondition (MODE noch nicht da) ausgerechnet
41
+ Sollwert und Schalter, und nach dem Raten fehlte der Sollwert, den
42
+ das Muster verlangt (U13, 19.09.2026). Die Haken bleiben also, wie
43
+ sie sind; das Raten kann darauf aufbauen. */
44
+ var probe = JSON.parse(JSON.stringify({ kanal: e.kanal, states: e.states }));
45
+ probe.states.forEach(function (s) { s.on = true; });
46
+ if (!erkenneEntwurf(probe, e.want).length) { return; }
47
+ nurWasInsMusterPasst(e, true);
48
+ /* Wie beim Aufbau: der Schalter jedes weiteren Kanals kommt mit, damit
49
+ „als ein Geraet" keinen Ausgang heimlich verliert. */
50
+ (e.kanalGeraete || []).forEach(function (k) {
51
+ if (k.typ === 'info') { return; } /* Wartungskanal: kein Ausgang */
52
+ (k.pflicht || []).forEach(function (pid) {
53
+ var kurz = pid.slice(S.current.length + 1).replace(/\./g, '_');
54
+ e.states.forEach(function (st) { if (st.n === kurz) { st.on = true; } });
55
+ });
56
+ });
57
+ }
58
+
59
+ /* Die Meldung, wenn der Vorschlag nichts belegt hat - Begruendung dazu
60
+ in `baueMusterwahl`. Auf Modulebene, seit auch die Legende vorschlagen
61
+ kann (19.09.2026). */
62
+ function plaetzeText(frei) {
63
+ var kurz = frei.slice(0, 6).join(', ');
64
+ return frei.length > 6 ? tr('guess.slotsMore', kurz, frei.length - 6) : kurz;
65
+ }
66
+ function meldungZu(p) {
67
+ if (p.anzahl > 0) { return null; }
68
+ var satz = p.grund === 'voll' ? tr('guess.noneFull')
69
+ : p.grund === 'vergeben' ? tr('guess.noneUsed', plaetzeText(p.frei))
70
+ : p.grund === 'keinTreffer' ? tr('guess.noneFit', plaetzeText(p.frei))
71
+ : null;
72
+ /* Der Zusatz zur Handentscheidung (U15): Steht ein Platz nur deshalb
73
+ leer, weil die Zeile dazu von Hand entschieden ist, muss genau das
74
+ dastehen - sonst sieht es aus wie ein Fehler des Ratens. */
75
+ /* Mit Namen und in der richtigen Zahl — „1 Zeilen sind …" sagte weder
76
+ richtig Deutsch noch welche (Ricardo, 19.09.2026). */
77
+ if (satz && p.handWeg) {
78
+ var hn = (p.handNamen || []).slice(0, 4).join(', ') +
79
+ ((p.handNamen || []).length > 4 ? ' …' : '');
80
+ satz += ' ' + (p.handWeg === 1 ? tr('guess.noneHandOne', hn) : tr('guess.noneHand', p.handWeg, hn));
81
+ }
82
+ return satz;
83
+ }
84
+
85
+ /* Vorschlagen und die Meldung dazu in einem Schritt. Gebraucht vom
86
+ Knopf, vom Pfeil hinter „… wuerde evtl. besser passen" und von der
87
+ orangen Legende, wenn das Muster erst nach dem Vorschlag greift. */
88
+ export function rateUndMelde(e) {
89
+ e.rateMeldung = meldungZu(ratePlaetze(e));
90
+ }
18
91
 
19
92
  export function baueMusterwahl(e, haupt, pflichtFehlt) {
20
93
  var vw = el('div', 'verdictwrap' + (haupt && !pflichtFehlt.length ? '' : ' bad'));
@@ -49,9 +122,13 @@ export function baueMusterwahl(e, haupt, pflichtFehlt) {
49
122
  als `thermostat` erkannt, man wechselt also gar nichts, und ohne
50
123
  Knopf bliebe es dabei.
51
124
 
52
- Er wird hier gebaut, weil er zum Muster gehoert - haengt aber unten
53
- in der Leiste ueber der Zustandsliste. Neben dem Musterfeld stand er
54
- am Rand; die Liste, die er veraendert, faengt darunter an. */
125
+ Er wird hier gebaut, weil er zum Muster gehoert. Seit 19.09.2026
126
+ haengt er im blauen Vorlagenkasten, gleich hinter dem Musterfeld -
127
+ dort, wo die Erkennung steht, zu der er gehoert. Vorher stand er in
128
+ der Leiste ueber der Liste, und den Knopf, der den ganzen Bestand
129
+ des Geraets laedt, fand man im Kasten; beide standen am jeweils
130
+ falschen Ort (Ricardo). Nur im Aliasmodus, wo es keinen Kasten gibt,
131
+ bleibt er in der Leiste. */
55
132
  /* Der Trockenlauf, der beides beantwortet: ob der Knopf etwas
56
133
  anzubieten haette, und - wenn nicht - warum nicht. Frueher lief er
57
134
  zweimal (einmal fuer `rateMoeglich`, einmal fuer den Klick); einmal
@@ -66,22 +143,6 @@ export function baueMusterwahl(e, haupt, pflichtFehlt) {
66
143
  Gezeigt wird sie, solange keine Vermutung offen ist: dann fuehrt die
67
144
  blaue Leiste das Wort, und zwei Meldungen uebereinander widersprechen
68
145
  sich nur. */
69
- function plaetzeText(frei) {
70
- var kurz = frei.slice(0, 6).join(', ');
71
- return frei.length > 6 ? tr('guess.slotsMore', kurz, frei.length - 6) : kurz;
72
- }
73
- function meldungZu(p) {
74
- if (p.anzahl > 0) { return null; }
75
- var satz = p.grund === 'voll' ? tr('guess.noneFull')
76
- : p.grund === 'vergeben' ? tr('guess.noneUsed', plaetzeText(p.frei))
77
- : p.grund === 'keinTreffer' ? tr('guess.noneFit', plaetzeText(p.frei))
78
- : null;
79
- /* Der Zusatz zur Handentscheidung (U15): Steht ein Platz nur deshalb
80
- leer, weil die Zeile dazu von Hand entschieden ist, muss genau das
81
- dastehen - sonst sieht es aus wie ein Fehler des Ratens. */
82
- if (satz && p.handWeg) { satz += ' ' + tr('guess.noneHand', p.handWeg); }
83
- return satz;
84
- }
85
146
  /* Ungefragt gezeigt wird sie nur, wo sie etwas erklaert: dass kein
86
147
  Knopf dasteht, obwohl Plaetze frei sind. Der Fall „alle Plaetze
87
148
  besetzt" bleibt stumm - er sagt „alles gut, nichts zu tun", und das
@@ -131,7 +192,7 @@ export function baueMusterwahl(e, haupt, pflichtFehlt) {
131
192
  tr('guess.button'));
132
193
  rateKnopf.title = beilaeufig ? tr('guess.tplHint') : tr('guess.hint');
133
194
  var rateLos = function () {
134
- e.rateMeldung = meldungZu(ratePlaetze(e));
195
+ rateUndMelde(e);
135
196
  entwurfAngefasst();
136
197
  S.openRow = null;
137
198
  zeichneErgebnis();
@@ -178,8 +239,15 @@ export function baueMusterwahl(e, haupt, pflichtFehlt) {
178
239
  }
179
240
  var r = erkenneEntwurf(pruefE, m), mark;
180
241
  if (!r.length) {
181
- var fehlend = musterVon(m).states.filter(function (x) { return x.required; })
182
- .map(function (x) { return x.name; });
242
+ /* Genannt wird, was wirklich fehlt — nicht jeder Pflichtplatz. An
243
+ der Klimaanlage stand „✕ MODE fehlt" neben einem bestaetigten
244
+ MODE; gefehlt hat der Sollwert (U13, 19.09.2026). Findet die
245
+ Grobpruefung nichts, bleibt die alte Auskunft. */
246
+ var fehlend = fehlendePflicht(pruefE.states, m);
247
+ if (!fehlend.length) {
248
+ fehlend = musterVon(m).states.filter(function (x) { return x.required; })
249
+ .map(function (x) { return x.name; });
250
+ }
183
251
  /* Ein Pflichtplatz kann auch deshalb leer sein, weil die Zeile
184
252
  dafuer im falschen Datentyp steht - die Rolle stimmt, der Typ
185
253
  nicht, und der Punkt faellt aus dem Muster. „SET fehlt" ist dann
@@ -211,7 +279,14 @@ export function baueMusterwahl(e, haupt, pflichtFehlt) {
211
279
  }
212
280
  } else {
213
281
  var g = r[0].states.filter(function (x) { return x.id; }).length;
214
- mark = tr('pattern.slotsTaken', g, r[0].states.length);
282
+ /* Ein Platz, der mehrfach vergeben wird (`multiple`, bei `info` das
283
+ ACTUAL), bekommt je Punkt einen Eintrag — die Gesamtzahl wuchs mit
284
+ jedem Haken, und „24/31“ klang, als fehlten sieben. Wo es keine
285
+ Hoechstzahl gibt, steht auch keine: nur die Zahl der Punkte
286
+ (Ricardo, 19.09.2026). Betrifft info, mediaPlayer, weatherForecast. */
287
+ var mehrfach = r[0].states.some(function (x) { return x.multiple; });
288
+ mark = mehrfach ? tr('pattern.pointsTaken', g)
289
+ : tr('pattern.slotsTaken', g, r[0].states.length);
215
290
  /* Hier stand frueher „(Rollen werden angepasst)" hinter der
216
291
  Belegung. Der Vermerk erschien an jeder Zeile ausser der gerade
217
292
  gewaehlten — an einem Fensterkontakt also an sieben von acht.
@@ -316,6 +391,7 @@ export function baueMusterwahl(e, haupt, pflichtFehlt) {
316
391
  } else {
317
392
  e.want = neuTyp;
318
393
  }
394
+ hakenNachMuster(e);
319
395
  S.openRow = null;
320
396
  /* Kein Fokus zurueck ins Feld: der Fokus oeffnete die Liste sofort
321
397
  wieder, und nach der Wahl stand sie erneut offen — man musste
@@ -475,7 +551,8 @@ export function baueMusterwahl(e, haupt, pflichtFehlt) {
475
551
  /* Beim Vorschlag entscheidet nicht das Muster, sondern die
476
552
  Rollen, die das Auswaehlen setzt - genau wie in `mWaehlen`. */
477
553
  if (e.vorschlag && !e.roh) { anMusterAnpassen(e, bestes); } else { e.want = bestes; }
478
- e.rateMeldung = meldungZu(ratePlaetze(e));
554
+ hakenNachMuster(e);
555
+ rateUndMelde(e);
479
556
  entwurfAngefasst();
480
557
  S.openRow = null;
481
558
  zeichneErgebnis();
@@ -170,6 +170,7 @@ export function horcheAufObjekte() {
170
170
 
171
171
  var gabEs = S.objects[id] !== undefined;
172
172
  if (obj) { S.objects[id] = obj; } else { delete S.objects[id]; }
173
+ S.objektStand++;
173
174
  if (gabEs !== (S.objects[id] !== undefined)) { nachziehBaum = true; }
174
175
 
175
176
  var betrifft = S.current && (id === S.current || id.indexOf(S.current + '.') === 0);
@@ -214,6 +215,7 @@ export function horcheAufObjekte() {
214
215
  export function uebernimmObjekt(id, o) {
215
216
  var gabEs = S.objects[id] !== undefined;
216
217
  if (o) { S.objects[id] = o; } else { delete S.objects[id]; }
218
+ S.objektStand++;
217
219
  return gabEs !== (S.objects[id] !== undefined);
218
220
  }
219
221
 
@@ -250,6 +252,7 @@ export function kaputteAliaseNeu() {
250
252
  }
251
253
 
252
254
  export function indexNeu() {
255
+ S.objektStand++;
253
256
  S.keysSorted = Object.keys(S.objects).sort();
254
257
  S.kleinIndex = {};
255
258
  kaputteAliaseNeu();
@@ -116,8 +116,7 @@ export function pruefungZielKann(e) {
116
116
  if (!treffer.length) { return { s: 'ok', t: tr('check.targetCan'), d: tr('check.targetCanFine') }; }
117
117
  var text = treffer.map(function (x) {
118
118
  if (x.art === 'formelBlind') { return tr('check.targetVoidFormula', x.n, x.typ); }
119
- if (x.art === 'nichtSchreibbar') { return tr('check.targetNoWrite', x.n); }
120
- return tr('check.targetNoRead', x.n);
119
+ return tr('check.targetNoWrite', x.n);
121
120
  }).join(', ');
122
121
  return { s: 'warn', t: tr('check.targetCan'), d: text };
123
122
  }
@@ -478,10 +478,18 @@ function zeichneTausch() {
478
478
  });
479
479
 
480
480
  var go = $('#btn-swap-go');
481
- if (!S.tauschNeu) { if (go) { go.disabled = true; } return; }
481
+ /* Der Knopf bleibt ueber das Neuzeichnen hinweg dasselbe Element.
482
+ Wer ihn sperrt, setzt deshalb auch den Grund — sonst traegt er den
483
+ des vorher gewaehlten Geraets weiter („Erst entscheiden — LOWBAT,
484
+ UNREACH, WORKING …" an der bisherigen Quelle; Lauf vom 19.09.2026,
485
+ W10). */
486
+ if (!S.tauschNeu) {
487
+ if (go) { go.disabled = true; go.title = tr('swap.pickFirst'); }
488
+ return;
489
+ }
482
490
  if (S.tauschNeu === alt) {
483
491
  body.appendChild(el('div', 'aside w', tr('swap.same')));
484
- if (go) { go.disabled = true; }
492
+ if (go) { go.disabled = true; go.title = tr('swap.same'); }
485
493
  return;
486
494
  }
487
495
 
@@ -505,7 +513,28 @@ function zeichneTausch() {
505
513
  zum Entfernen an und opfert seinen Schalter, statt den Knoten
506
514
  darueber zu nehmen (Ricardos Frage, 12.09.2026). */
507
515
  var eltern = S.tauschNeu.split('.').slice(0, -1).join('.');
508
- if (eltern.split('.').length > 2 && S.tauschNeu !== alt) {
516
+ /* Nur eine Hilfestellung - lieber zu selten als zu oft (Ricardo,
517
+ 19.09.2026). Deshalb zwei Bedingungen, die beide halten muessen:
518
+
519
+ 1. Die Wahl ist ein Blatt: alle Datenpunkte darunter haengen direkt
520
+ an ihr, es gibt keine Unterordner. Das trifft `…Licht.tele`,
521
+ `…stat`, `…cmnd` und den Homematic-Kanal `1` - genau die Faelle,
522
+ in denen die Ebene darueber das Geraet ist. Ein Geraet mit
523
+ Unterordnern ist selbst schon die richtige Ebene; darueber liegt
524
+ ein Ordner voller anderer Geraete, und der findet fast immer
525
+ irgendwo passende Namen. Gemessen 19.09.2026: ueber `PC_Jonas`
526
+ fand `mqtt-client.0.SmartHome` 7 von 7 - beim 3D-Drucker, am
527
+ Bastelzimmer_Licht und am RGB-Deckenlicht (W24).
528
+ 2. Die bisherige Quelle liegt nicht darunter. Sonst zaehlt die Ebene
529
+ darueber deren eigene Punkte mit: `mqtt.Steckdose` gegen das
530
+ Nachbargeraet `mqtt.Stumm` ergab „7 von 7, hier nur 2", und die 7
531
+ waren die der alten Steckdose (Lauf vom 19.09.2026). */
532
+ var pre = S.tauschNeu + '.';
533
+ var istBlatt = kindZustaende(S.tauschNeu).every(function (id) {
534
+ return id.indexOf(pre) === 0 && id.slice(pre.length).indexOf('.') === -1;
535
+ });
536
+ var altDarunter = !!alt && (alt === eltern || alt.indexOf(eltern + '.') === 0);
537
+ if (eltern.split('.').length > 2 && S.tauschNeu !== alt && istBlatt && !altDarunter) {
509
538
  var trefferHier = plan.filter(function (z) { return z.neuR; }).length;
510
539
  var planOben = tauschPlan(kanal, eltern);
511
540
  var trefferOben = planOben.filter(function (z) { return z.neuR; }).length;
@@ -733,6 +762,31 @@ export function tauscheAus() {
733
762
  holeZweig(neu, function () { waehle(kanal); zeichneErgebnis(); });
734
763
  };
735
764
 
765
+ /* Der Kanal kommt zuletzt, und nur, wenn alles davor durchging.
766
+
767
+ Er traegt `native.quelle`, und daraus liest der Dialog beim naechsten
768
+ Oeffnen, was „heute" die Quelle ist (`alteQuelle`). Bis 19.09.2026
769
+ ging er zusammen mit den Punkten hinaus, jeder fuer sich. Scheiterte
770
+ ein Punkt, stand der Kanal trotzdem auf dem neuen Geraet: nach
771
+ „Schliessen" nannte der Dialog ABC2 als heutige Quelle, obwohl SET
772
+ noch aus ABC1 las, und ABC2 galt als „dieselbe Quelle wie bisher" —
773
+ der Tausch liess sich nicht mehr zu Ende bringen (Lauf vom
774
+ 19.09.2026, W21). Jetzt bleibt der Kanal auf der alten Quelle, bis
775
+ alle Punkte umgeschrieben und alle angehakten geloescht sind; ein
776
+ neuer Tausch auf dasselbe Geraet holt dann nach, was fehlte. */
777
+ var kanalArbeit = arbeit.filter(function (a) { return a.id === kanal; });
778
+ arbeit = arbeit.filter(function (a) { return a.id !== kanal; });
779
+
780
+ var kanalZuletzt = function () {
781
+ if (fehler.length || !kanalArbeit.length) { return fertig(); }
782
+ var a = kanalArbeit[0];
783
+ socket.emit('setObject', a.id, a.obj, function (err) {
784
+ if (err) { fehler.push(a.id + ': ' + err); }
785
+ else { S.objects[a.id] = a.obj; }
786
+ fertig();
787
+ });
788
+ };
789
+
736
790
  var weiter = function () {
737
791
  /* Schlug beim Umschreiben etwas fehl, wird nichts geloescht.
738
792
 
@@ -745,13 +799,13 @@ export function tauscheAus() {
745
799
  umgeschriebenen Punkte stoeren dabei nicht - sie stehen ja bereits
746
800
  richtig (12.09.2026). */
747
801
  if (fehler.length) { return fertig(); }
748
- if (!loeschen.length) { return fertig(); }
802
+ if (!loeschen.length) { return kanalZuletzt(); }
749
803
  var offen2 = loeschen.length;
750
804
  loeschen.forEach(function (id) {
751
805
  socket.emit('delObject', id, function (err) {
752
806
  if (err) { fehler.push(id + ': ' + err); }
753
807
  else { delete S.objects[id]; }
754
- if (--offen2 === 0) { indexNeu(); fertig(); }
808
+ if (--offen2 === 0) { indexNeu(); kanalZuletzt(); }
755
809
  });
756
810
  });
757
811
  };
@@ -766,10 +820,3 @@ export function tauscheAus() {
766
820
  });
767
821
  });
768
822
  }
769
-
770
- /* Der Knopf gilt fuer dasselbe wie Verlegen und Entfernen: einen Alias,
771
- den es gibt. */
772
- export function tauschKnopf(zeigen) {
773
- var b = $('#btn-swap');
774
- if (b) { b.hidden = !zeigen; }
775
- }
@@ -11,7 +11,7 @@ import { enumsVon } from './aufzaehlungen.js';
11
11
  import './katalog.js';
12
12
  import { holeZweig, indexNeu , nachziehenErledigt } from './objekte.js';
13
13
  import { zeichneBaum } from './baum.js';
14
- import './werte.js';
14
+ import { knopfZeile } from './werte.js';
15
15
  import { ausgangName, vorschlag } from './vorlagen.js';
16
16
  import {
17
17
  waehle,
@@ -290,15 +290,14 @@ export function baueObjekte(e) {
290
290
  if (s.f) { alias.read = s.f; }
291
291
  if (s.fw && s.srcW) { alias.write = s.fw; }
292
292
 
293
- /* Ein Taster meldet nichts — dann sagt der Alias-Punkt das auch.
294
- Sonst malen vis, Alexa und matter einen Kippschalter, der ewig auf
295
- „aus" steht, statt eines Knopfes. */
296
- var istTaster = taster(s);
293
+ /* Ein Knopf traegt `read: false` - sonst malen vis, Alexa und matter
294
+ einen Kippschalter statt eines Knopfes. Entschieden wird an der
295
+ Rolle, nicht an der Lesequelle (`knopfZeile`, 19.09.2026). */
297
296
  var common = {
298
297
  name: s.caption || s.n,
299
298
  role: s.role,
300
299
  type: s.typ || 'mixed',
301
- read: !istTaster,
300
+ read: !knopfZeile(s),
302
301
  write: !!s.srcW,
303
302
  alias: alias
304
303
  };
@@ -364,17 +363,17 @@ export function verwaiste(e) {
364
363
  }
365
364
 
366
365
  /* Was dem Schreiben im Weg steht. */
367
- /* Ein Taster: keine Lesequelle, ein Schreibziel, und das Ziel sagt
368
- selbst, dass es nichts zu melden hat (`common.read === false`).
366
+ /* Ein Taster: keine Lesequelle, ein Schreibziel, Rolle `button…`.
369
367
 
370
- Nicht ueber die Rolle entschieden: `button.stop` ist Konvention, aber
371
- nur der Adapter weiss wirklich, ob am Punkt etwas abzulesen ist. An
372
- `hm-rpc.2.NEQ1795694.1.STOP` steht `read: false` — hingeschrieben von
373
- hm-rpc, nicht von uns. */
368
+ Bis 19.09.2026 entschied hier `common.read === false` am Ziel. Das
369
+ Feld setzt aber jeder Adapter anders (hm-rpc an `BOOST_MODE`, einem
370
+ Schalter); die Rollenliste von ioBroker legt den Knopf ueber Rolle und
371
+ Schreiben fest, und so jetzt auch die Werkbank (`knopfZeile`). */
374
372
  export function taster(s) {
375
- if (!s || s.srcR || !s.srcW) { return false; }
376
- var o = S.objects[s.srcW];
377
- return !!(o && o.common && o.common.read === false);
373
+ /* Seit 19.09.2026 an der Rolle wie `read: false` selbst. Die Zeile ohne
374
+ Lesequelle ist beim Knopf erlaubt, aber nicht mehr die Vorgabe - neu
375
+ angelegte Knoepfe lesen aus ihrem Schreibziel mit. */
376
+ return !!(s && !s.srcR && knopfZeile(s));
378
377
  }
379
378
 
380
379
  export function pruefeSchreiben(e) {
@@ -1062,8 +1061,15 @@ export function zeichneLoeschen() {
1062
1061
  sie zwar mit Typ, aber der Hinweistext sprach nur von „Datenpunkten
1063
1062
  darunter", und die Zahl oben sagt nur, wie viele Zeilen es sind.
1064
1063
  Wer sie zaehlt, weiss nicht, dass er drei Geraete mitnimmt. */
1064
+ /* Nur was wirklich UNTER dem Ziel liegt. Mit dem Haken „Alle N
1065
+ Ausgaenge" stehen auch die Geschwister in der Loeschliste; die hat
1066
+ der Nutzer ausdruecklich gewaehlt, und der Haken nennt sie schon.
1067
+ Bis 20.09.2026 zaehlten sie hier mit — die Warnung sprach von
1068
+ „darunter", und weil der Name um die Laenge des Ziels gekuerzt
1069
+ wurde, blieb von `…ZWEIKANAL.Licht_Esstisch` nur „isch" (Lauf vom
1070
+ 19.09.2026, G37). */
1065
1071
  var kanaeleDrunter = alle.filter(function (id) {
1066
- if (id === ziel) { return false; }
1072
+ if (id.indexOf(ziel + '.') !== 0) { return false; }
1067
1073
  var o = S.objects[id];
1068
1074
  return o && (o.type === 'channel' || o.type === 'device');
1069
1075
  });
@@ -1337,7 +1343,6 @@ export function zeigeVerlegen(altVorgabe, zielVorgabe) {
1337
1343
  (T25, 18.09.2026). Verlegt wird ohnehin alles unter dem Pfad; ein
1338
1344
  fehlender Kanal fehlt danach am neuen Ort genauso. */
1339
1345
  if (!alt || alt.indexOf('alias.') !== 0 || !knotenDa(alt)) { return; }
1340
- S.verlegeZiel = alt;
1341
1346
 
1342
1347
  var vorbelegt = (typeof zielVorgabe === 'string' && zielVorgabe &&
1343
1348
  zielVorgabe.indexOf('alias.') === 0) ? zielVorgabe : alt;
@@ -1725,7 +1730,6 @@ export function verlegeAlias(alt, neu) {
1725
1730
  leereOrdnerRaeumen(alt, function () {
1726
1731
  S.current = neu;
1727
1732
  S.entwurf = null;
1728
- S.verlegeZiel = null;
1729
1733
  melden(fehler, neu);
1730
1734
  });
1731
1735
  }
@@ -515,16 +515,35 @@ export function wendeAn(v, kanal, gruende, instanz, auchOhneTreffer) {
515
515
  });
516
516
  }
517
517
 
518
- var quelle = null, feld = null, leseformel = null, gescheitert = [];
518
+ /* `gruende` sagt, WARUM ein Weg nicht ging — es gibt drei Faelle, und
519
+ sie verlangen dreierlei Handgriffe: der Punkt fehlt ganz, der Punkt
520
+ liefert kein JSON, oder das Feld darin fehlt. Bis 20.09.2026 stand
521
+ fuer alle drei derselbe Satz („tele_SENSOR: tele.SENSOR ·
522
+ SML.Total_Summe fehlt"), auch wenn es den Punkt gar nicht gab
523
+ (Ricardo, 20.09.2026; Lauf vom 19.09.2026, C45). */
524
+ var quelle = null, feld = null, leseformel = null, gescheitert = [], gruende = [];
519
525
  for (var wi = 0; wi < wege.length; wi++) {
520
526
  var w = wege[wi];
521
527
  var id0 = finde(w.punkt);
522
528
  var wo = auf(w.punkt) + (w.feld ? ' · ' + auf(w.feld) : '');
523
- if (!id0) { gescheitert.push(wo); continue; }
529
+ if (!id0) {
530
+ gescheitert.push(wo);
531
+ gruende.push(tr('tpl.whyNoPoint', auf(w.punkt)));
532
+ continue;
533
+ }
524
534
  if (w.feld) {
525
535
  var f0 = auf(w.feld);
526
536
  var j0 = jsonVon(id0);
527
- if (!j0 || feldWert(j0, f0) === undefined) { gescheitert.push(wo); continue; }
537
+ if (!j0) {
538
+ gescheitert.push(wo);
539
+ gruende.push(tr('tpl.whyNoJson', auf(w.punkt)));
540
+ continue;
541
+ }
542
+ if (feldWert(j0, f0) === undefined) {
543
+ gescheitert.push(wo);
544
+ gruende.push(tr('tpl.whyNoField', auf(w.punkt), f0));
545
+ continue;
546
+ }
528
547
  }
529
548
  quelle = id0; feld = w.feld; leseformel = w.leseformel;
530
549
  break;
@@ -549,7 +568,9 @@ export function wendeAn(v, kanal, gruende, instanz, auchOhneTreffer) {
549
568
  e.states.push({
550
569
  n: z.name, on: !z.vorgabeAus, role: z.rolle || '', typ: z.typ || '',
551
570
  unit: z.einheit || '', wr: true,
552
- srcR: '', srcW: ziel0,
571
+ /* Ein Knopf liest aus seinem Schreibziel mit (19.09.2026): der
572
+ Alias sieht jeden Druck, `read: false` kommt aus der Rolle. */
573
+ srcR: /^button/.test(String(z.rolle || '')) ? ziel0 : '', srcW: ziel0,
553
574
  f: '', fw: z.schreibformel ? auf(z.schreibformel) : '',
554
575
  caption: z.beschriftung || '',
555
576
  dec: z.nachkommastellen === undefined ? undefined : String(z.nachkommastellen),
@@ -564,7 +585,9 @@ export function wendeAn(v, kanal, gruende, instanz, auchOhneTreffer) {
564
585
  einen Stelle und die Vorlage hatte an zwei geschaut. */
565
586
  e.uebersprungen.push(gescheitert.length > 1
566
587
  ? tr('tpl.skippedNowhere', z.name, gescheitert.join(tr('tpl.nor')))
567
- : tr('tpl.skippedSource', z.name, gescheitert[0] || auf(z.lesen)));
588
+ : (gruende.length
589
+ ? tr('tpl.skippedWhy', z.name, gruende[0])
590
+ : tr('tpl.skippedSource', z.name, gescheitert[0] || auf(z.lesen))));
568
591
  /* Eine Pflichtzeile, die ihre Quelle nicht findet, heisst: die
569
592
  Vorlage passt zu diesem Geraet nicht. Die Erkennung sieht das
570
593
  nicht — sie prueft, ob die genannten *Punkte* da sind, nicht ob
@@ -12,7 +12,7 @@ import { funktionsAuswahl, kennungZuFunktion } from './aufzaehlungen.js';
12
12
  import { musterVon, platzFuerRolle, typenFuerRolle } from './erkennung.js';
13
13
  import { musterZeile, musterName } from './musternamen.js';
14
14
  import { ladeVorlagen, aendereVorlagen, setzeMeta, pruefeVorlage, wendeAn, hinweisTaugt, INSTANZ_ID } from './vorlagen.js';
15
- import { kindZustaende, hatPunkt, feldAusFormel } from './werte.js';
15
+ import { kindZustaende, hatPunkt, feldAusFormel, pfadTeile, pfadAus } from './werte.js';
16
16
  import { opt } from './entwurf.js';
17
17
  import { zeichneErgebnis } from './ergebnis.js';
18
18
  import { delKnopf } from './schreiben.js';
@@ -120,6 +120,20 @@ function vArbeitsstand() {
120
120
  return vorlageMitId(vGewaehlt);
121
121
  }
122
122
 
123
+ /* Den Namen einer Vorlage in einer Sprache setzen.
124
+
125
+ Das Einlesen nimmt eine Datei auch ohne `name` an (die Liste zeigt dann
126
+ die Kennung). Die Namensfelder schrieben aber blind in `x.name.de` —
127
+ ohne `name` warf das, `vAendern` brach ab, und die Eingabe ging
128
+ verloren: den fehlenden Namen konnte man nie nachtragen (Ricardo,
129
+ 19.09.2026). Ein Name als blosser Text wird wie bisher zu beiden
130
+ Sprachen. */
131
+ function setzeName(x, sprache, w) {
132
+ if (typeof x.name === 'string') { x.name = { de: x.name, en: x.name }; }
133
+ if (!x.name || typeof x.name !== 'object' || Array.isArray(x.name)) { x.name = {}; }
134
+ x.name[sprache] = w;
135
+ }
136
+
123
137
  function vAendern(feld, wert) {
124
138
  if (!vEinlesen && !vEntwurf) {
125
139
  var o = vorlageMitId(vGewaehlt);
@@ -269,9 +283,9 @@ export function zeichneVorlagenBlatt(host) {
269
283
  }
270
284
 
271
285
  kb.appendChild(feld(tr('tpls.nameDe'), textIn(v.name, 'de'),
272
- function (x, w) { if (typeof x.name === 'string') { x.name = { de: x.name, en: x.name }; } x.name.de = w; }, '140px'));
286
+ function (x, w) { setzeName(x, 'de', w); }, '140px'));
273
287
  kb.appendChild(feld(tr('tpls.nameEn'), textIn(v.name, 'en'),
274
- function (x, w) { if (typeof x.name === 'string') { x.name = { de: x.name, en: x.name }; } x.name.en = w; }, '140px'));
288
+ function (x, w) { setzeName(x, 'en', w); }, '140px'));
275
289
  kb.appendChild(feld(tr('tpls.rank'), v.rang || 0,
276
290
  function (x, w) { x.rang = Number(w) || 0; }, '60px'));
277
291
  var rh = el('span', 'hint', eigen ? tr('tv.rankOwn') : tr('tv.rankPackage'));
@@ -996,7 +1010,12 @@ function vPruefeEingelesen(o) {
996
1010
  if (!o || typeof o !== 'object' || Array.isArray(o)) { return tr('tv.badNotObject'); }
997
1011
  if (!o.id || typeof o.id !== 'string') { return tr('tv.badNoId'); }
998
1012
  if (!Array.isArray(o.zustaende) || !o.zustaende.length) { return tr('tv.badNoStates'); }
999
- var schlecht = o.zustaende.filter(function (z) { return !z || !z.name || !z.lesen; });
1013
+ /* Lese- ODER Schreibpfad, wie beim Speichern und Anwenden. Eine Zeile
1014
+ nur mit `schreiben` (ein Befehl ohne Rueckmeldung, frueher auch der
1015
+ STOP am Rollladen) ist gueltig; bis 19.09.2026 wies das Einlesen sie
1016
+ ab, und die Werkbank gab Dateien aus, die sie selbst nicht wieder
1017
+ annahm (I7). */
1018
+ var schlecht = o.zustaende.filter(function (z) { return !z || !z.name || !(z.lesen || z.schreiben); });
1000
1019
  if (schlecht.length) { return tr('tv.badState', schlecht.length); }
1001
1020
 
1002
1021
  if (o.name !== undefined &&
@@ -1304,6 +1323,16 @@ export function vorlagenVorbereiten(e) {
1304
1323
  if (quelle && s.ausVorlage) {
1305
1324
  (quelle.zustaende || []).forEach(function (z) { if (z.name === s.ausVorlage) { zv = z; } });
1306
1325
  }
1326
+ /* Steht der Punkt in der Erkennung der Vorlage, muss er da sein -
1327
+ auch wenn seine Zeile optional ist. `hm-rollladen` fuehrt STOP als
1328
+ optionale Zeile, verlangt `1.STOP` aber in der Erkennung: nur er
1329
+ trennt den Rollladen vom Dimmer. Vorgehakt wurde bis 19.09.2026 nur
1330
+ nach `optional`, und wer die Vorlage aktualisierte, ohne selbst
1331
+ nachzuhaken, speicherte eine, die schon bei LEVEL allein greift -
1332
+ im Probelauf 23 Geraete statt der Rolllaeden (I20). */
1333
+ var erfQ = (quelle && quelle.erkennung && quelle.erkennung.erforderlich) || [];
1334
+ var inErkennung = !!(zv && ((zv.lesen && erfQ.indexOf(zv.lesen) > -1) ||
1335
+ (zv.schreiben && erfQ.indexOf(zv.schreiben) > -1)));
1307
1336
  zeilen.push({
1308
1337
  s: s,
1309
1338
  name: s.n,
@@ -1320,7 +1349,11 @@ export function vorlagenVorbereiten(e) {
1320
1349
  einer Vorlage kam, erbt deren Einstufung. Von Hand ergaenzte
1321
1350
  Punkte bleiben freiwillig — jeder Pflichtpunkt macht die
1322
1351
  Vorlage genauer und zugleich sproeder. */
1323
- pflicht: r.abs ? false : (zv ? !zv.optional : !!w)
1352
+ pflicht: r.abs ? false : (zv ? (!zv.optional || inErkennung) : !!w),
1353
+ /* Die Zeile bleibt, was sie in der Vorlage war: optional fuer den
1354
+ Alias, Pflicht nur fuer die Erkennung. Der Haken „muss da sein"
1355
+ legt sonst beides zusammen fest. */
1356
+ optionalBleibt: !!(zv && zv.optional && inErkennung)
1324
1357
  });
1325
1358
  });
1326
1359
 
@@ -1328,7 +1361,7 @@ export function vorlagenVorbereiten(e) {
1328
1361
  zeilen.forEach(function (z) {
1329
1362
  var f = feldAusFormel(z.s.f);
1330
1363
  if (!f || z.lesenAbs) { return; }
1331
- var oben = f.split('.')[0];
1364
+ var oben = pfadAus([pfadTeile(f)[0]]);
1332
1365
  var da = false;
1333
1366
  inhalte.forEach(function (x) { if (x.punkt === z.lesenRoh && x.feld === oben) { da = true; } });
1334
1367
  if (!da) { inhalte.push({ punkt: z.lesenRoh, feld: oben, an: false }); }
@@ -1410,6 +1443,15 @@ export function baueVorlage(z) {
1410
1443
  z.inhalte.forEach(function (x) { if (x.an) { inh[x.punkt] = x.feld; } });
1411
1444
  if (Object.keys(inh).length) { v.erkennung.inhalt = inh; }
1412
1445
  if (k.namenshinweis) { v.erkennung.namenshinweis = k.namenshinweis; }
1446
+ /* Was die Erkennung der Ursprungsvorlage sonst traegt - der Hinweis,
1447
+ warum ein Punkt Pflicht ist, etwa - bleibt erhalten. Der Dialog
1448
+ zeigt es nicht, also darf er es auch nicht verlieren (I20). */
1449
+ if (k.modus === 'update' && k.quelle && k.quelle.erkennung) {
1450
+ Object.keys(k.quelle.erkennung).forEach(function (feld) {
1451
+ if (['erforderlich', 'verboten', 'inhalt', 'namenshinweis'].indexOf(feld) > -1) { return; }
1452
+ v.erkennung[feld] = k.quelle.erkennung[feld];
1453
+ });
1454
+ }
1413
1455
 
1414
1456
  if (k.mehrfach && k.instanz) {
1415
1457
  v.mehrfach = { platzhalter: '%N%' };
@@ -1432,7 +1474,7 @@ export function baueVorlage(z) {
1432
1474
  if (r.schreibenRoh) { zu.schreiben = zPfad(r, k, true, z.zeilen); }
1433
1475
  if (f) { zu.feld = f; } else if (s.f) { zu.leseformel = s.f; }
1434
1476
  if (s.fw && r.schreibenRoh) { zu.schreibformel = s.fw; }
1435
- if (!r.pflicht) { zu.optional = true; }
1477
+ if (!r.pflicht || r.optionalBleibt) { zu.optional = true; }
1436
1478
  if (r.aus) { zu.vorgabeAus = true; }
1437
1479
  if (s.caption) { zu.beschriftung = s.caption; }
1438
1480
  if (s.dec !== undefined && s.dec !== '') { zu.nachkommastellen = Number(s.dec); }