iobroker.alias-workbench 0.9.10 → 0.9.11

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.
@@ -12,7 +12,7 @@ import { D, el, $ } from './basis.js';
12
12
  import { tr, sprachtext } from './sprache.js';
13
13
  import { VORSCHAU, musterVon, erkenneEntwurf, platzFuerRolle, typenFuerRolle, typKonflikte } from './erkennung.js';
14
14
  import { anMusterAnpassen } from './vorlagen.js';
15
- import { ratePlaetze, rateZurueck, rateAnzahl, rateMoeglich } from './vorschlagen.js';
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
18
 
@@ -52,51 +52,93 @@ export function baueMusterwahl(e, haupt, pflichtFehlt) {
52
52
  Er wird hier gebaut, weil er zum Muster gehoert - haengt aber unten
53
53
  in der Leiste ueber der Zustandsliste. Neben dem Musterfeld stand er
54
54
  am Rand; die Liste, die er veraendert, faengt darunter an. */
55
+ /* Der Trockenlauf, der beides beantwortet: ob der Knopf etwas
56
+ anzubieten haette, und - wenn nicht - warum nicht. Frueher lief er
57
+ zweimal (einmal fuer `rateMoeglich`, einmal fuer den Klick); einmal
58
+ reicht, das Ergebnis traegt den Grund gleich mit. */
59
+ var probe = ratePlaetze(e, true);
60
+
61
+ /* Was frueher ein Sammelsatz war („entweder alle Plaetze besetzt, oder
62
+ kein Datenpunkt passt"), steht jetzt als der Fall da, der wirklich
63
+ vorliegt - mit den Namen der Plaetze, die frei bleiben. Ohne die
64
+ Namen war die Meldung nicht nachpruefbar.
65
+
66
+ Gezeigt wird sie, solange keine Vermutung offen ist: dann fuehrt die
67
+ blaue Leiste das Wort, und zwei Meldungen uebereinander widersprechen
68
+ 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
+ /* Ungefragt gezeigt wird sie nur, wo sie etwas erklaert: dass kein
86
+ Knopf dasteht, obwohl Plaetze frei sind. Der Fall „alle Plaetze
87
+ besetzt" bleibt stumm - er sagt „alles gut, nichts zu tun", und das
88
+ stuende dann an fast jedem Geraet (dieselbe Ueberlegung wie C27).
89
+ Ueber den Klick ist er weiter erreichbar. */
90
+ e.rateMeldung = (rateAnzahl(e) || probe.grund === 'voll')
91
+ ? null : meldungZu(probe);
92
+
55
93
  /* Und nur, wenn er etwas anzubieten haette: ohne freie Plaetze oder
56
94
  freie Zeilen ist der Knopf ein leeres Versprechen — dann faellt er
57
- weg, statt beim Klick „nichts gefunden" zu melden. Zuruecknehmen
58
- bleibt immer moeglich, solange Vermutungen im Entwurf stehen. */
95
+ weg, statt beim Klick „nichts gefunden" zu melden. Das Zuruecknehmen
96
+ steht seit dem 15.09.2026 nicht mehr auch hier: derselbe Knopf hiess
97
+ je nach Lage „Freie Plaetze vorschlagen" oder „Vorschlaege
98
+ verwerfen", und daneben stand in der blauen Leiste ein zweiter mit
99
+ demselben Wort. Verworfen wird dort - bei dem, was es betrifft. */
100
+ /* Der Rueckfrage-Dialog, wenn eine Vorlage greift (U27). Er sitzt hier
101
+ als eigene Funktion, weil ihn zwei Knoepfe brauchen: der in der
102
+ Leiste und der kleine neben dem Hinweis, der Muster wechselt und
103
+ gleich vorschlaegt. */
104
+ function mitVorlagenfrage(dann) {
105
+ var vq = null;
106
+ S.VORLAGEN.forEach(function (v) { if (v.id === e.vorlage) { vq = v; } });
107
+ var body = $('#guess-body');
108
+ body.textContent = '';
109
+ body.appendChild(el('div', 'aside w',
110
+ tr('guess.tplAsk', vq ? sprachtext(vq.name) : e.vorlage)));
111
+ /* onclick statt addEventListener: der Dialog ist einer fuer alle
112
+ Geraete, der Griff muss beim Oeffnen den alten ersetzen. */
113
+ $('#btn-guess-go').onclick = function () {
114
+ $('#dlg-guess').close();
115
+ dann();
116
+ };
117
+ $('#dlg-guess').showModal();
118
+ }
119
+
59
120
  var rateKnopf = null;
60
- if (rateAnzahl(e) || rateMoeglich(e)) {
121
+ if (probe.anzahl > 0) {
61
122
  /* Mit passender Vorlage ist Raten nicht der normale Weg - die
62
123
  Zuordnung steht ja schon. Der Knopf bleibt, tritt aber leise auf
63
124
  und fragt vor dem Lauf nach (Ricardo, 25.08.2026). */
64
125
  var beilaeufig = !!e.vorlage && !rateAnzahl(e);
65
- rateKnopf = el('button', 'btn mini' + (rateAnzahl(e) ? ' wichtig' : '')
126
+ rateKnopf = el('button', 'btn mini'
66
127
  + (beilaeufig ? ' leise' : '')
67
128
  /* Ein leiser Knopf pocht nicht - das Pochen wuerde genau die
68
129
  Aufmerksamkeit holen, die er nicht verdient. */
69
- + (!beilaeufig && knopfFrisch('raten:' + S.current + ':' + (rateAnzahl(e) ? 'zurueck' : 'vor')) ? ' frisch' : ''),
70
- rateAnzahl(e) ? tr('guess.undo') : tr('guess.button'));
130
+ + (!beilaeufig && knopfFrisch('raten:' + S.current + ':vor') ? ' frisch' : ''),
131
+ tr('guess.button'));
71
132
  rateKnopf.title = beilaeufig ? tr('guess.tplHint') : tr('guess.hint');
72
133
  var rateLos = function () {
73
- e.rateMeldung = ratePlaetze(e) ? null : tr('guess.none');
134
+ e.rateMeldung = meldungZu(ratePlaetze(e));
74
135
  entwurfAngefasst();
75
136
  S.openRow = null;
76
137
  zeichneErgebnis();
77
138
  };
78
139
  rateKnopf.addEventListener('click', function () {
79
- if (rateAnzahl(e)) {
80
- rateZurueck(e); e.rateMeldung = null;
81
- entwurfAngefasst();
82
- S.openRow = null;
83
- zeichneErgebnis();
84
- return;
85
- }
86
140
  if (!beilaeufig) { rateLos(); return; }
87
- var vq = null;
88
- S.VORLAGEN.forEach(function (v) { if (v.id === e.vorlage) { vq = v; } });
89
- var body = $('#guess-body');
90
- body.textContent = '';
91
- body.appendChild(el('div', 'aside w',
92
- tr('guess.tplAsk', vq ? sprachtext(vq.name) : e.vorlage)));
93
- /* onclick statt addEventListener: der Dialog ist einer fuer alle
94
- Geraete, der Griff muss beim Oeffnen den alten ersetzen. */
95
- $('#btn-guess-go').onclick = function () {
96
- $('#dlg-guess').close();
97
- rateLos();
98
- };
99
- $('#dlg-guess').showModal();
141
+ mitVorlagenfrage(rateLos);
100
142
  });
101
143
  }
102
144
 
@@ -184,6 +226,58 @@ export function baueMusterwahl(e, haupt, pflichtFehlt) {
184
226
  }
185
227
  function markeFuer(m) { return bewerte(m).mark; }
186
228
 
229
+ /* ---- Was nach dem Raten passen wuerde ----------------------------
230
+
231
+ Ricardos Klimaanlage ist der Fall: `thermostat` greift und belegt 3
232
+ von 24 Plaetzen, `airCondition` faellt durch, weil sein einziger
233
+ Pflichtplatz MODE die Rolle nicht traegt. Nach dem Raten stuenden
234
+ dort 9 von 29 statt 8 von 24 - nur sieht man das nirgends, weil
235
+ „passt nicht" das letzte Wort behaelt. Wer nicht weiss, dass es den
236
+ Knopf gibt, waehlt `airCondition` nie aus.
237
+
238
+ Also wird einmal durchgerechnet, was jedes Muster nach dem Raten
239
+ ergaebe. Gerechnet wird auf einer Kopie - am Entwurf aendert das
240
+ nichts. Das Raten liest nur Namen, Werteliste, Einheit und Typ,
241
+ keine Messwerte; das Ergebnis haengt also allein an den Zeilen und
242
+ wird beim blossen Neuzeichnen nicht ungueltig. Gemerkt wird es
243
+ deshalb an `e` und faellt erst, wenn eine Zeile sich aendert
244
+ (`entwurfAngefasst`) oder ein anderes Muster gewaehlt ist.
245
+
246
+ Gemessen am `midea.AC` (43 Punkte): ein Trockenlauf 1,2 ms, alle 51
247
+ Muster 35 ms - einmal je Geraeteauswahl, gegen rund 200 ms fuers
248
+ Auswaehlen selbst. */
249
+ function probeMuster(m) {
250
+ if (!musterVon(m)) { return null; }
251
+ var kopie = JSON.parse(JSON.stringify({ kanal: e.kanal, states: e.states, want: m }));
252
+ /* Dieselbe Vorbereitung wie in `bewerte`: beim Vorschlag entscheidet
253
+ nicht das Muster, sondern die Rollen, die das Auswaehlen setzen
254
+ wuerde. */
255
+ if (e.vorschlag && !e.roh && m !== e.want) { anMusterAnpassen(kopie, m); }
256
+ ratePlaetze(kopie);
257
+ var r = erkenneEntwurf(kopie, m);
258
+ if (!r.length) { return { passt: false }; }
259
+ return { passt: true, gesamt: r[0].states.length,
260
+ belegt: r[0].states.filter(function (x) { return x.id; }).length };
261
+ }
262
+
263
+ function aussicht() {
264
+ if (e.rateAussicht && e.rateAussicht.want === e.want) { return e.rateAussicht; }
265
+ var proM = {}, neuPassend = [];
266
+ if (e.states && e.states.length) {
267
+ alleTypen.forEach(function (m) {
268
+ var p = probeMuster(m);
269
+ if (!p) { return; }
270
+ proM[m] = p;
271
+ /* „Neu" heisst: passt jetzt nicht, nach dem Raten schon. Das
272
+ gewaehlte Muster gehoert nicht dazu - es ist kein anderes. */
273
+ if (p.passt && m !== e.want && !bewerte(m).passt) { neuPassend.push(m); }
274
+ });
275
+ }
276
+ e.rateAussicht = { want: e.want, proM: proM, neu: neuPassend,
277
+ selbst: proM[e.want] || null };
278
+ return e.rateAussicht;
279
+ }
280
+
187
281
  /* Welche Muster ohne Suchtext angeboten werden. Das erkannte und das
188
282
  gerade gewaehlte gehoeren immer dazu - sonst fiel rgbSingle heraus,
189
283
  sobald man einmal auf light gewechselt hatte, und der Weg zurueck
@@ -262,6 +356,7 @@ export function baueMusterwahl(e, haupt, pflichtFehlt) {
262
356
  return a.rang - b.rang;
263
357
  });
264
358
  mEintraege = treffer;
359
+ var auL = aussicht();
265
360
 
266
361
  mlist.textContent = '';
267
362
  if (!treffer.length) {
@@ -273,8 +368,13 @@ export function baueMusterwahl(e, haupt, pflichtFehlt) {
273
368
  var z = el('div', 'vz' + (n === mMarkiert ? ' an' : '')
274
369
  + (t0.m === e.want ? ' gewaehlt' : ''));
275
370
  z.appendChild(el('span', null, musterZeile(t0.m)));
371
+ /* Hinter „✕ MODE fehlt" steht, ob das Muster nach dem Raten doch
372
+ greift - sonst liest man nur, dass es nicht passt, und haelt es
373
+ fuer erledigt. */
374
+ var nachRaten = (!bewerte(t0.m).passt && auL.proM[t0.m] && auL.proM[t0.m].passt)
375
+ ? ' ' + tr('guess.alsoRow') : '';
276
376
  z.appendChild(el('span', 'tiefer',
277
- (t0.via ? tr('pattern.viaRole', t0.via) + ' ' : '') + markeFuer(t0.m)));
377
+ (t0.via ? tr('pattern.viaRole', t0.via) + ' ' : '') + markeFuer(t0.m) + nachRaten));
278
378
  /* mousedown statt click: sonst greift der Fokusverlust zuerst und
279
379
  die Liste ist weg, bevor der Klick ankommt. */
280
380
  z.addEventListener('mousedown', function (ev) {
@@ -321,6 +421,73 @@ export function baueMusterwahl(e, haupt, pflichtFehlt) {
321
421
  };
322
422
  mBeschriften();
323
423
 
424
+ /* Der Hinweis neben dem Feld: genannt wird EIN Muster - das beste -
425
+ und nur, wenn es wirklich besser ist als das gewaehlte.
426
+
427
+ Der erste Anlauf zaehlte bloss, wie viele Muster nach dem Vorschlag
428
+ dazukaemen, und schrieb bei mehreren „12 weitere Muster" hin. Am
429
+ `midea.AC` sind das tatsaechlich zwoelf - aber elf davon sind
430
+ Beifang: `blind` 4/19, `illuminance` 3/7, `windowTilt` 3/7 an einer
431
+ Klimaanlage. Eine Zahl, die Beifang mitzaehlt, verdeckt genau die
432
+ eine Auskunft, um die es geht (Ricardo, 15.09.2026): `airCondition`
433
+ 9/29 gegen `thermostat` 8/24.
434
+
435
+ Gemessen wird an den belegten Plaetzen, nicht am Anteil. 9 von 29
436
+ ist besser als 8 von 24, weil neun Punkte des Geraets einen Platz
437
+ bekommen statt acht - der Anteil waere hier sogar knapp kleiner.
438
+ Beide Zahlen stehen NACH dem Vorschlag; „3/24" ist der Stand jetzt
439
+ und gehoert nicht daneben.
440
+
441
+ Ist nichts besser, steht hier nichts - der haeufigste Fall bleibt
442
+ damit stumm. */
443
+ var auF = aussicht();
444
+ var bestes = null;
445
+ auF.neu.forEach(function (m) {
446
+ if (!bestes || auF.proM[m].belegt > auF.proM[bestes].belegt) { bestes = m; }
447
+ });
448
+ /* Der Vergleichswert ist der Stand des gewaehlten Musters NACH dem
449
+ Vorschlag. Passt es auch dann nicht, ist jedes andere besser. */
450
+ var jetzt = (auF.selbst && auF.selbst.passt) ? auF.selbst.belegt : -1;
451
+ if (bestes && auF.proM[bestes].belegt > jetzt) {
452
+ var pB = auF.proM[bestes], nB = musterName(bestes) || bestes;
453
+ var hw = el('span', 'ratAussicht');
454
+ var htx = el('span', null, '· ' + tr('guess.better', nB));
455
+ htx.title = jetzt >= 0
456
+ ? tr('guess.betterHint', nB, pB.belegt + '/' + pB.gesamt,
457
+ musterName(e.want) || e.want, auF.selbst.belegt + '/' + auF.selbst.gesamt)
458
+ : tr('guess.betterHintNone', nB, pB.belegt + '/' + pB.gesamt,
459
+ musterName(e.want) || e.want);
460
+ hw.appendChild(htx);
461
+
462
+ /* Der Weg dorthin in einem Griff: wechseln UND vorschlagen.
463
+ Sonst waere der Hinweis eine Ansage ohne Handlung - man muesste
464
+ das Muster in der Liste suchen und danach noch den Knopf in der
465
+ Leiste finden (Ricardo, 16.09.2026).
466
+
467
+ Ein Zeichen statt eines Wortes: neben dem Satz bleiben rund 55 px,
468
+ „wechseln" braucht 58. Dieselbe Bauart wie die Uebernahmeknoepfe
469
+ im Detail (`←`, `⟲`) - kleines Zeichen, ausfuehrlicher Tooltip. */
470
+ var hgo = el('button', 'btn winzig rtgo', '→');
471
+ hgo.title = tr('guess.betterGoHint', nB, tr('guess.button'));
472
+ hgo.addEventListener('click', function (ev) {
473
+ ev.stopPropagation();
474
+ var los = function () {
475
+ /* Beim Vorschlag entscheidet nicht das Muster, sondern die
476
+ Rollen, die das Auswaehlen setzt - genau wie in `mWaehlen`. */
477
+ if (e.vorschlag && !e.roh) { anMusterAnpassen(e, bestes); } else { e.want = bestes; }
478
+ e.rateMeldung = meldungZu(ratePlaetze(e));
479
+ entwurfAngefasst();
480
+ S.openRow = null;
481
+ zeichneErgebnis();
482
+ };
483
+ /* Greift eine Vorlage, fragt auch dieser Weg nach (U27) - er raet
484
+ ja genauso ueber eine fertige Zuordnung hinweg. */
485
+ if (e.vorlage && !rateAnzahl(e)) { mitVorlagenfrage(los); } else { los(); }
486
+ });
487
+ hw.appendChild(hgo);
488
+ vw.appendChild(hw);
489
+ }
490
+
324
491
  sel.addEventListener('focus', function () {
325
492
  sel.value = '';
326
493
  mMarkiert = -1;
@@ -41,6 +41,7 @@ export function ladeObjekte(danach) {
41
41
  if (--offen === 0) {
42
42
  S.keysSorted = Object.keys(S.objects).sort();
43
43
  S.kleinIndex = {};
44
+ kaputteAliaseNeu();
44
45
  ladeEnums();
45
46
  /* Erst die Einstellungen, dann die Vorlagen - sonst holt die
46
47
  Werkbank sie auch dann, wenn sie abgeschaltet sind. */
@@ -206,9 +207,39 @@ export function uebernimmObjekt(id, o) {
206
207
  /* Der Baum liest aus S.keysSorted, nicht aus S.objects. Wer einen Punkt
207
208
  hinzufuegt oder entfernt, muss den Index neu bauen — sonst kennt der
208
209
  Baum ihn nicht, egal wie oft man zeichnet. */
210
+ /* Aliase, bei denen mindestens ein Punkt auf eine Quelle zeigt, die es
211
+ nicht (mehr) gibt.
212
+
213
+ Der teuerste Fehler am Alias: der js-controller merkt sich „Quelle
214
+ fehlt" dauerhaft und korrigiert das nie - nur loeschen und neu anlegen
215
+ hilft (siehe schreiben.js). Bis zum 14.09.2026 stand das nur im
216
+ aufgeklappten Detail und im Pruefungen-Kasten; die Zeile in der Liste
217
+ schwieg, weil sie „ist eine Quelle eingetragen" fragte statt „gibt es
218
+ sie".
219
+
220
+ Einmal gerechnet und gemerkt, nicht bei jedem Zeichnen: den Baum baut
221
+ die Werkbank an sechzehn Stellen neu. Gemessen am Produktivsystem:
222
+ 531 Alias-Punkte in 5,2 ms. */
223
+ export function kaputteAliaseNeu() {
224
+ var raus = {};
225
+ Object.keys(S.objects).forEach(function (id) {
226
+ if (id.indexOf('alias.') !== 0) { return; }
227
+ var ob = S.objects[id];
228
+ if (!ob || ob.type !== 'state') { return; }
229
+ var a = (ob.common || {}).alias;
230
+ if (!a || !a.id) { return; }
231
+ var ziele = (typeof a.id === 'string') ? [a.id] : [a.id.read, a.id.write];
232
+ if (!ziele.some(function (q) { return q && !S.objects[q]; })) { return; }
233
+ var kanal = id.split('.').slice(0, -1).join('.');
234
+ raus[kanal] = (raus[kanal] || 0) + 1;
235
+ });
236
+ S.aliasKaputt = raus;
237
+ }
238
+
209
239
  export function indexNeu() {
210
240
  S.keysSorted = Object.keys(S.objects).sort();
211
241
  S.kleinIndex = {};
242
+ kaputteAliaseNeu();
212
243
  zeigeObjektzahl();
213
244
  }
214
245
 
@@ -1,9 +1,9 @@
1
- /* Die sieben Pruefungen - laufen immer mit, fassen nichts an. */
1
+ /* Die acht Pruefungen - laufen immer mit, fassen nichts an. */
2
2
 
3
3
  import { S } from './zustand.js';
4
4
  import { $, el } from './basis.js';
5
5
  import { tr } from './sprache.js';
6
- import { wertVon } from './werte.js';
6
+ import { wertVon, zielKann } from './werte.js';
7
7
  import './erkennung.js';
8
8
  import { sofortRueckmeldung, mqttEinstellung } from './mqtt.js';
9
9
 
@@ -94,6 +94,31 @@ export function pruefungSenden(e) {
94
94
  }
95
95
 
96
96
 
97
+ /* Kann das Ziel, was der Punkt von ihm verlangt?
98
+
99
+ Drei Fragen an fremde Objekte, die die Werkbank vorher nie gestellt hat:
100
+ Nimmt das Schreibziel den Wert der Formel an? Laesst es sich ueberhaupt
101
+ beschreiben? Meldet die Lesequelle etwas? Was dabei schiefgeht, faellt
102
+ nirgends auf — geschrieben wird trotzdem, gelesen auch, es kommt nur
103
+ nichts dabei heraus (Ricardo, 15.09.2026).
104
+
105
+ Hinweis, nicht Fehler: Ob am Ende wirklich nichts passiert, entscheidet
106
+ der Adapter hinter dem Punkt, und den kennt die Werkbank nicht. */
107
+ export function pruefungZielKann(e) {
108
+ var treffer = [];
109
+ e.states.forEach(function (s) {
110
+ if (!s.on) { return; }
111
+ zielKann(s).forEach(function (x) { treffer.push({ n: s.n, art: x.art, typ: x.typ }); });
112
+ });
113
+ if (!treffer.length) { return { s: 'ok', t: tr('check.targetCan'), d: tr('check.targetCanFine') }; }
114
+ var text = treffer.map(function (x) {
115
+ if (x.art === 'formelBlind') { return tr('check.targetVoidFormula', x.n, x.typ); }
116
+ if (x.art === 'nichtSchreibbar') { return tr('check.targetNoWrite', x.n); }
117
+ return tr('check.targetNoRead', x.n);
118
+ }).join(', ');
119
+ return { s: 'warn', t: tr('check.targetCan'), d: text };
120
+ }
121
+
97
122
  export function baueChecks(e, haupt, pflichtFehlt) {
98
123
  var an = e.states.filter(function (s) { return s.on; });
99
124
  var ohneQuelle = an.filter(function (s) { return s.srcR && !S.objects[s.srcR]; });
@@ -113,6 +138,7 @@ export function baueChecks(e, haupt, pflichtFehlt) {
113
138
  : haupt.type) : tr('check.noPatternFits') },
114
139
  pruefungSenden(e),
115
140
  pruefungRueckmeldung(e),
141
+ pruefungZielKann(e),
116
142
  pruefungDopplung(e)
117
143
  ];
118
144
 
@@ -28,6 +28,7 @@ import { zeichneErgebnis } from './ergebnis.js';
28
28
  import { uebernehmeBestand , enumAenderungen, setzeZiel } from './zuordnung.js';
29
29
 
30
30
  import { mqttEinzelnStill } from './mqtt.js';
31
+ import { sucheInSkripten, sucheInSkriptenMehrere, skriptListe } from './verwendung.js';
31
32
 
32
33
  import { rateBehalten } from './vorschlagen.js';
33
34
 
@@ -270,20 +271,33 @@ export function baueObjekte(e) {
270
271
  } });
271
272
 
272
273
  e.states.forEach(function (s) {
273
- if (!s.on || !s.n || !s.srcR) { return; }
274
+ if (!s.on || !s.n) { return; }
275
+ /* Ohne Lesequelle faellt die Zeile heraus - ausser sie ist ein
276
+ Taster, der nur schreibt (K23). */
277
+ if (!s.srcR && !taster(s)) { return; }
274
278
  var id = ziel + '.' + s.n;
275
279
 
276
280
  var alias = {};
277
- if (s.srcW && s.srcW !== s.srcR) { alias.id = { read: s.srcR, write: s.srcW }; }
281
+ /* ioBroker verlangt immer eine `alias.id`; ein reines `{ write: … }`
282
+ weist der js-controller ab („Alias id is invalid: The id is
283
+ empty!", gemessen 15.09.2026). Beim Taster steht deshalb das
284
+ Schreibziel darin, und `common.read: false` sagt, dass dort nichts
285
+ abzuholen ist. */
286
+ if (!s.srcR) { alias.id = s.srcW; }
287
+ else if (s.srcW && s.srcW !== s.srcR) { alias.id = { read: s.srcR, write: s.srcW }; }
278
288
  else { alias.id = s.srcR; }
279
289
  if (s.f) { alias.read = s.f; }
280
290
  if (s.fw && s.srcW) { alias.write = s.fw; }
281
291
 
292
+ /* Ein Taster meldet nichts — dann sagt der Alias-Punkt das auch.
293
+ Sonst malen vis, Alexa und matter einen Kippschalter, der ewig auf
294
+ „aus" steht, statt eines Knopfes. */
295
+ var istTaster = taster(s);
282
296
  var common = {
283
297
  name: s.caption || s.n,
284
298
  role: s.role,
285
299
  type: s.typ || 'mixed',
286
- read: true,
300
+ read: !istTaster,
287
301
  write: !!s.srcW,
288
302
  alias: alias
289
303
  };
@@ -349,6 +363,19 @@ export function verwaiste(e) {
349
363
  }
350
364
 
351
365
  /* Was dem Schreiben im Weg steht. */
366
+ /* Ein Taster: keine Lesequelle, ein Schreibziel, und das Ziel sagt
367
+ selbst, dass es nichts zu melden hat (`common.read === false`).
368
+
369
+ Nicht ueber die Rolle entschieden: `button.stop` ist Konvention, aber
370
+ nur der Adapter weiss wirklich, ob am Punkt etwas abzulesen ist. An
371
+ `hm-rpc.2.NEQ1795694.1.STOP` steht `read: false` — hingeschrieben von
372
+ hm-rpc, nicht von uns. */
373
+ export function taster(s) {
374
+ if (!s || s.srcR || !s.srcW) { return false; }
375
+ var o = S.objects[s.srcW];
376
+ return !!(o && o.common && o.common.read === false);
377
+ }
378
+
352
379
  export function pruefeSchreiben(e) {
353
380
  var probleme = [];
354
381
  var an = e.states.filter(function (s) { return s.on; });
@@ -361,7 +388,20 @@ export function pruefeSchreiben(e) {
361
388
  namen[s.n] = 1;
362
389
 
363
390
  if (!s.srcR) {
364
- probleme.push(tr('write.noSourceChosen', s.n));
391
+ /* Ein Taster hat keinen Zustand zum Lesen.
392
+
393
+ `STOP` am Rollladen schreibt nur; am Geraet traegt der Punkt
394
+ `read: false`, der hm-rpc-Adapter setzt das selbst. Die Sperre
395
+ hier war deshalb richtig UND falsch zugleich: richtig, weil
396
+ ioBroker ohne `alias.id` keinen Punkt anlegt — falsch, weil sie
397
+ damit jeden Rollladen-Alias mit STOP unmoeglich machte. Kein
398
+ einziger ist je entstanden (gefunden 14.09.2026, K23).
399
+
400
+ Massgeblich ist, was das ZIEL kann, nicht was die Rolle
401
+ nahelegt: `read: false` ist eine Tatsache am Objekt, die der
402
+ Adapter hinschreibt. Beim Schreiben nimmt `alias.id` dann das
403
+ Schreibziel, und der Alias-Punkt bekommt selbst `read: false`. */
404
+ if (!(s.srcW && taster(s))) { probleme.push(tr('write.noSourceChosen', s.n)); }
365
405
  } else if (!S.objects[s.srcR]) {
366
406
  /* Der teuerste Fehler: der js-controller merkt sich „Quelle fehlt"
367
407
  dauerhaft und korrigiert das nie. Nur loeschen und neu anlegen
@@ -1069,6 +1109,32 @@ export function zeichneLoeschen() {
1069
1109
  body.appendChild(ew);
1070
1110
  }
1071
1111
 
1112
+ /* Wer nennt diese Kennungen sonst noch?
1113
+
1114
+ Beim Verlegen steht die Liste seit jeher da, beim Loeschen fehlte
1115
+ sie - dabei ist die Folge hier endgueltig: ein Skript, das den
1116
+ Alias anspricht, laeuft danach ins Leere. Gesucht wird nur im
1117
+ Klartext und nur in den Skripten; der Hinweis darunter sagt das. */
1118
+ var skriptFeld = el('div');
1119
+ skriptFeld.style.marginTop = '11px';
1120
+ body.appendChild(skriptFeld);
1121
+ var wurzeln = [ziel].concat(S.loeschAlleAusgaenge ? geschwisterVon(ziel) : []);
1122
+ sucheInSkriptenMehrere(wurzeln, function (treffer) {
1123
+ if (S.loeschZiel !== ziel) { return; }
1124
+ skriptFeld.textContent = '';
1125
+ if (!treffer.length) {
1126
+ skriptFeld.appendChild(el('div', 'aside', tr('move.noScripts')));
1127
+ return;
1128
+ }
1129
+ var w = el('div', 'aside w');
1130
+ w.appendChild(el('b', null, treffer.length === 1
1131
+ ? tr('del.scriptsHead1')
1132
+ : tr('del.scriptsHead', treffer.length)));
1133
+ w.appendChild(skriptListe(treffer));
1134
+ w.appendChild(el('div', 'hint', tr('del.scriptsHint')));
1135
+ skriptFeld.appendChild(w);
1136
+ });
1137
+
1072
1138
  var b = $('#btn-del-go');
1073
1139
  b.disabled = false;
1074
1140
  b.hidden = false;
@@ -1226,41 +1292,6 @@ function verlegeUmfang(alt) {
1226
1292
  });
1227
1293
  }
1228
1294
 
1229
- /* Wer nennt diese Kennung sonst noch?
1230
-
1231
- Die Skripte des javascript-Adapters lassen sich durchsuchen; sie sind
1232
- der haeufigste Ort, an dem eine Kennung im Klartext steht. vis-
1233
- Ansichten und fremde Adapter kann die Werkbank nicht durchsehen -
1234
- darauf weist der Dialog eigens hin, statt Vollstaendigkeit
1235
- vorzutaeuschen. */
1236
- export function sucheInSkripten(kennung, fertig) {
1237
- var treffer = [];
1238
- var fertigMit = function () { fertig(treffer); };
1239
- try {
1240
- socket.emit('getObjectView', 'script', 'javascript',
1241
- { startkey: 'script.js.', endkey: 'script.js.\u9999' }, function (err, doc) {
1242
- if (err || !doc || !doc.rows) { return fertigMit(); }
1243
- doc.rows.forEach(function (r) {
1244
- var o = r.value;
1245
- var q = o && o.common && o.common.source;
1246
- if (!q || q.indexOf(kennung) === -1) { return; }
1247
- var zeilen = String(q).split('\n');
1248
- var stellen = [];
1249
- zeilen.forEach(function (z, i) {
1250
- if (z.indexOf(kennung) > -1) { stellen.push(i + 1); }
1251
- });
1252
- treffer.push({
1253
- id: r.id,
1254
- name: txt((o.common || {}).name) || r.id.split('.').pop(),
1255
- an: !!(o.common && o.common.enabled),
1256
- zeilen: stellen
1257
- });
1258
- });
1259
- fertigMit();
1260
- });
1261
- } catch { fertigMit(); }
1262
- }
1263
-
1264
1295
  export function zeigeVerlegen(altVorgabe, zielVorgabe) {
1265
1296
  /* Der Knopf steht auch an der Quelle, sobald es den Alias gibt (T1).
1266
1297
  Dort ist S.current die Quelle, nicht der Alias - gemeint ist immer
@@ -1531,14 +1562,7 @@ export function zeigeVerlegen(altVorgabe, zielVorgabe) {
1531
1562
  w3.appendChild(el('b', null, treffer.length === 1
1532
1563
  ? tr('move.scriptsHead1')
1533
1564
  : tr('move.scriptsHead', treffer.length)));
1534
- var ul3 = el('ul');
1535
- treffer.forEach(function (t) {
1536
- ul3.appendChild(el('li', null, t.name + (t.an ? '' : ' (' + tr('move.scriptOff') + ')') +
1537
- ' \u2014 ' + (t.zeilen.length === 1
1538
- ? tr('move.scriptLines', t.zeilen[0])
1539
- : tr('move.scriptLinesN', t.zeilen.join(', ')))));
1540
- });
1541
- w3.appendChild(ul3);
1565
+ w3.appendChild(skriptListe(treffer));
1542
1566
  skriptFeld.appendChild(w3);
1543
1567
  });
1544
1568
  };
@@ -1849,9 +1873,32 @@ export function schreibeObjekte() {
1849
1873
  stehen, bis man den Reiter neu lud, und behauptete damit eine
1850
1874
  Abweichung, die es nicht mehr gibt. Nur bei Erfolg loeschen: ging
1851
1875
  etwas schief, ist die Aenderung tatsaechlich noch nicht drin. */
1852
- if (!fehler.length && S.entwurf && S.entwurf.states) {
1853
- S.entwurf.states.forEach(function (s) { s.geaendert = false; rateBehalten(s); });
1876
+ if (!fehler.length && S.entwurf) {
1877
+ if (S.entwurf.states) {
1878
+ S.entwurf.states.forEach(function (s) { s.geaendert = false; rateBehalten(s); });
1879
+ }
1854
1880
  S.entwurf.rateMeldung = null;
1881
+ /* Und dasselbe eine Ebene hoeher (G51, Ricardo, 15.09.2026).
1882
+
1883
+ `angefasst` sagt „hier steht etwas Ungeschriebenes". Die Marke
1884
+ wurde im ganzen Programm nur gesetzt, nie geloescht - weg war
1885
+ sie allein, wenn der Entwurf ganz wegfiel (Loeschen, Verlegen).
1886
+ Nach einem gelungenen Schreibvorgang blieb sie also stehen, und
1887
+ der naechste Klick im Baum fragte „Ungespeicherte Aenderungen …
1888
+ Wechselst du jetzt, sind sie weg" - ueber einen Alias, der
1889
+ gerade eben geschrieben worden war.
1890
+
1891
+ Die Marke faellt nur bei Erfolg. Ging etwas schief, ist die
1892
+ Aenderung tatsaechlich noch nicht drin, und die Frage ist dann
1893
+ berechtigt.
1894
+
1895
+ Sie hat eine zweite Aufgabe, und die erledigt sich mit: Solange
1896
+ `angefasst` steht, baut `neuZeichnenOderAufbauen` den Entwurf
1897
+ absichtlich NICHT neu auf - es soll ja nichts Ungeschriebenes
1898
+ ueberbuegeln. Das `holeZweig` unten laeuft gleich danach; jetzt
1899
+ baut es den Entwurf am frisch geschriebenen Alias neu auf,
1900
+ statt den alten stehenzulassen. */
1901
+ S.entwurf.angefasst = false;
1855
1902
  }
1856
1903
 
1857
1904
  /* Nur der Aliaszweig, nicht die ganze Datenbank. */