@pellebot/homebridge-fritz-platform-hb2 1.1.1 → 1.2.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.
@@ -0,0 +1,46 @@
1
+ ## Wizard Edit-Mode (Phase G) — Re-Runs überschreiben deine Einstellungen nicht mehr
2
+
3
+ Der Setup-Wizard hat eine echte **Aktualisierungs-Routine** bekommen. Wenn du den Wizard ein zweites Mal aufrufst — zum Beispiel um ein neues Smart-Home-Gerät zu integrieren — werden deine bestehenden Anpassungen jetzt aus der `config.json` vorgeladen statt überschrieben.
4
+
5
+ ### Was das konkret fixt
6
+
7
+ In v1.1.x sind beim Wizard-Re-Run folgende User-Anpassungen verloren gegangen:
8
+
9
+ - **Tile-Namen** wie „PV Einspeisung Hausanschluss" wurden auf „Aktuelle Einspeisung" zurückgesetzt
10
+ - **`accType`-Wahl** zwischen `energy-temperature` (°C-Tile) und `energy-light` (lx-Sensor) wurde immer auf `energy-temperature` zurückgesetzt
11
+ - **Custom-Namen** für DECT-Geräte wurden durch den AVM-Auto-Discovery-Namen ersetzt
12
+ - **Nur Router-Switches** waren korrekt vorausgefüllt
13
+
14
+ Phase G dehnt diesen Hydration-Pattern auf alle Schritte aus.
15
+
16
+ ### Highlights
17
+
18
+ - 🔁 **Schritt 1 (Verbindung):** Host/User/PW/SSL/TR064 wie bisher vorausgefüllt — jetzt über einen zentralen Loader
19
+ - 🏷️ **Schritt 2 (Geräte):** für jedes AIN, das bereits in `config.json` steht, wird der bestehende `accType` und Custom-Name übernommen statt Auto-Detection-Default
20
+ - ⚡ **Schritt 3 (Energy 250):** Tile-Namen aus existierender Config übernommen, °C/lx-Radio aus den vorhandenen Einträgen vorausgewählt
21
+ - 🔘 **Schritt 4 (Router-Switches):** unverändert (war schon korrekt)
22
+ - 📝 **Schritt 5 (Speichern):** **Neue Diff-Vorschau** zeigt vor dem Speichern, was sich gegenüber der aktuellen Config ändert — keine bösen Überraschungen mehr
23
+
24
+ ### Breaking Changes
25
+
26
+ Keine. Bestehende Configs werden ohne Migration übernommen.
27
+
28
+ ### Migration für bestehende Nutzer
29
+
30
+ Keine Action nötig. Wenn du den Wizard das nächste Mal öffnest, sind deine Einstellungen einfach da. Beim Speichern siehst du in der neuen Diff-Vorschau exakt, was sich ändern wird — falls die Liste leer ist, war alles schon korrekt.
31
+
32
+ ### Installation
33
+
34
+ ```bash
35
+ sudo npm install -g @pellebot/homebridge-fritz-platform-hb2@1.2.0
36
+ sudo hb-service restart
37
+ ```
38
+
39
+ ### Bekannte Limitationen
40
+
41
+ - **Globaler statt per-Tile `energyAccType`:** Die Wizard-UI behandelt °C/lx als globale Einstellung für alle OBIS-Tiles. Mix-Setups (einige Tiles `energy-temperature`, andere `energy-light`) werden beim Wizard-Save auf eine einheitliche Wahl reduziert (light gewinnt). Wer einen Mix braucht: `config.json` direkt editieren, nicht den Wizard zum Speichern nutzen.
42
+ - **Diff zeigt nur Änderungen gegenüber der gespeicherten Config**, nicht gegenüber dem letzten Wizard-Lauf. By design — aber bedeutet: wenn du den Wizard mehrmals öffnest und schließt ohne zu speichern, siehst du jedes Mal denselben Diff zur Live-Version.
43
+
44
+ ### Vollständiger Changelog
45
+
46
+ Siehe [CHANGELOG.md](./CHANGELOG.md)
package/CHANGELOG.md CHANGED
@@ -2,6 +2,35 @@
2
2
 
3
3
  > **Hinweis zum Format:** Releases ab `v1.0.0` werden unter dem npm-Paket `@pellebot/homebridge-fritz-platform-hb2` veröffentlicht. Die `6.1.0-hb2patch.x` Pre-Releases sind Vorstufen unter dem alten Paketnamen `@pellebot/homebridge-fritz-platform` (deprecated). Die `registerPlatform`-Kennung bleibt durchgängig `homebridge-fritz-platform`, sodass cached Accessories über den Rename hinweg matchen — kein Re-Pairing nötig.
4
4
 
5
+ # v1.2.0 — 2026-05-14 (Wizard Edit-Mode / Phase G)
6
+
7
+ **Setup-Wizard erhält Aktualisierungs-Routine.** Beim erneuten Aufruf des Wizards (z.B. um ein weiteres Gerät hinzuzufügen) werden die in `config.json` gespeicherten Einstellungen jetzt vorgeladen statt überschrieben. Damit ist der Hauptschmerz aus v1.1.x adressiert: manuell vergebene Tile-Namen, gewählter `accType` (z.B. `energy-light` statt `energy-temperature`) und Router-Switch-Zustände überleben einen Wizard-Re-Run unverändert.
8
+
9
+ ## Hintergrund
10
+
11
+ Der Wizard in v1.1.x baute den `smarthome[]`-Array beim Speichern (Schritt 5) **immer aus dem In-Memory-State neu auf**. Dieser State wurde aber nur teilweise (Schritt 4 / Router-Switches) aus der bestehenden Config hydratisiert — Schritte 1–3 fielen jedes Mal auf Auto-Detection-Defaults zurück. Die sichtbare Folge: nach einem Wizard-Re-Run zeigte Apple Home wieder °C statt lx, und Tile-Namen wie „PV Einspeisung Hausanschluss" wurden auf „Aktuelle Einspeisung" zurückgesetzt.
12
+
13
+ ## Neuerungen
14
+
15
+ - **`loadExistingConfig()`** — zentrale Lade-Funktion, die beim Wizard-Start einmal `homebridge.getPluginConfig()` aufruft und Master-Device + `smarthome[]` in `wizardState.existingConfig` ablegt. Single source of truth für alle Step-Hydrations.
16
+ - **Schritt 1 (Verbindung):** Connection-Felder wie bisher vorausgefüllt, jetzt aber über `loadExistingConfig()` statt eines lokalen IIFE — konsistenter Pattern.
17
+ - **Schritt 2 (Geräte):** Für jedes durch Discovery gefundene AIN wird geprüft, ob bereits ein `smarthome[]`-Eintrag existiert. Wenn ja: User-`accType` und Custom-Name übernehmen statt Auto-Detection-Default. `energy-light`-Einträge werden auf Wizard-State-Ebene zu `energy-temperature` normalisiert, damit Schritt 3 die AINs weiterhin erkennt (die °C/lx-Wahl lebt nun in `energyAccType`).
18
+ - **Schritt 3 (Energy 250 / OBIS-Tiles):** Tile-Namen aus existierender Config gruppiert nach Sub-AIN übernehmen — fehlende Channels (`current_power`, `total_energy`) werden mit Defaults aufgefüllt. Der `energyAccType`-Radio (°C vs lx) wird global aus der Existing-Config vorausgewählt: wenn irgendein OBIS-Tile `energy-light` nutzt, ist der Light-Radio vorausgewählt.
19
+ - **Schritt 4 (Router-Switches):** unverändert, war schon korrekt — fungierte als Pattern-Vorlage.
20
+ - **Schritt 5 (Speichern):** Neue **Diff-Vorschau** zwischen alter und neuer Config: zeigt Tile-Umbenennungen, accType-Wechsel, neue/entfernte Geräte und geänderte Router-Switches als Liste. Wenn nichts sich ändert: „Keine Änderungen"-Hinweis. Macht klar was passieren wird, bevor man auf Speichern klickt.
21
+ - **Bonus:** Summary-Counter in Schritt 5 zählen jetzt `energy-temperature` UND `energy-light` als „Energy-Tiles" (vorher wurde `energy-light` als „normales Gerät" gezählt).
22
+
23
+ ## Breaking Changes
24
+
25
+ Keine. Bestehende Configs werden ohne Migration übernommen. First-Run-Verhalten (keine FritzPlatform-Sektion in `config.json`) ist unverändert: alle Defaults wie in v1.1.x.
26
+
27
+ ## Bekannte Limitationen
28
+
29
+ - **Globaler vs. per-Tile-`energyAccType`:** Die Wizard-UI behandelt °C/lx als globale Einstellung für alle OBIS-Tiles. Wenn jemand manuell in der `config.json` gemischte Tiles erstellt hat (einige `energy-temperature`, einige `energy-light`), zeigt der Wizard nur den Wert „energy-light" an (sobald ein Tile light ist, gilt global light). Beim Speichern werden dann alle Tiles auf `energy-light` vereinheitlicht. Workaround für Mix-Setups: nicht den Wizard zum Speichern benutzen, sondern `config.json` direkt bearbeiten.
30
+ - **Diff zeigt nur das was sich gegenüber der gespeicherten Config unterscheidet** — nicht das was sich gegenüber dem letzten Wizard-Lauf unterscheidet. Das ist by design, aber bedeutet: wenn man den Wizard mehrmals öffnet und schließt, ohne zu speichern, sieht man trotzdem nur den Diff zur letzten gespeicherten Version.
31
+
32
+ ---
33
+
5
34
  # v1.1.1 — 2026-05-14 (LightSensor variant for energy tiles)
6
35
 
7
36
  **Neuer accType `energy-light` als Alternative zu `energy-temperature`.** Beide nutzen dieselbe OBIS-Datenflow-Logik (Fritz!Smart Energy 250 sub-AINs), aber `energy-light` rendert die Werte als **LightSensor** (Einheit `lx`) statt **TemperatureSensor** (Einheit `°C`).
@@ -1,12 +1,16 @@
1
1
  <!--
2
2
  Custom UI for @pellebot/homebridge-fritz-platform-hb2
3
3
 
4
- Phases A+B+C implemented:
4
+ Phases A–G implemented:
5
5
  A. Foundation (server.js, public/index.html, customUi flag)
6
6
  B. Connection test — Step 1 with form + /test-connection endpoint
7
7
  C. Device discovery — Step 2 with auto-detected accType per device
8
-
9
- Future phases: Energy 250, Router-Switches, Save, Edit mode.
8
+ D. Energy 250 OBIS-Reader auto-setup — Step 3 (4 tiles per reader)
9
+ E. Router-Switches — Step 4 (master.options pre-hydrated from config)
10
+ F. Save + Apply — Step 5 (writes config.json, restarts Homebridge)
11
+ G. Edit-Mode — re-run preserves user edits (tile names, accType, energy
12
+ display type, router-switches) by hydrating wizardState from the
13
+ existing config.json. Step 5 shows a diff of pending changes.
10
14
  -->
11
15
  <style>
12
16
  .wizard-step-indicator {
@@ -320,6 +324,9 @@
320
324
 
321
325
  <div id="saveSummary" style="margin-top: 1rem;"></div>
322
326
 
327
+ <!-- Phase G: diff against existing config (empty on first run) -->
328
+ <div id="saveDiff" style="margin-top: 1rem;"></div>
329
+
323
330
  <details style="margin-top: 1rem;">
324
331
  <summary style="cursor: pointer;">Generierte Plugin-Config anzeigen (JSON-Vorschau)</summary>
325
332
  <pre id="savePreview" style="background: var(--bs-tertiary-bg, #f8f9fa); padding: 1rem; border-radius: 0.5rem; overflow-x: auto; max-height: 400px; font-size: 0.75rem; margin-top: 0.5rem;"></pre>
@@ -345,6 +352,7 @@
345
352
  energyTiles: {}, // { ain: [{ obisChannel, name }, ...] } after step 3
346
353
  energyAccType: 'energy-temperature', // 'energy-temperature' | 'energy-light' — chosen in step 3
347
354
  routerSwitches: {}, // { wifi_guest: bool, wps: bool, ... } after step 4
355
+ existingConfig: null, // Phase G: { master, smarthome[] } from current config.json (null = first run)
348
356
  };
349
357
 
350
358
  // ============ DOM References ============
@@ -384,23 +392,59 @@
384
392
  });
385
393
  }
386
394
 
387
- // ============ Step 1 — Connection Test ============
388
- // Prefill from existing plugin config
389
- (async () => {
395
+ // ============ Phase G — Edit-Mode (Existing-Config Hydration) ============
396
+ // Load the existing FritzPlatform config exactly once at wizard start so a
397
+ // re-run of the wizard doesn't blindly overwrite user customizations:
398
+ // - Step 1: prefills connection fields (host/user/pw/ssl/tr064)
399
+ // - Step 2: preserves user-chosen accType + custom names per AIN
400
+ // - Step 3: preserves OBIS tile names + the °C-vs-lx display choice
401
+ // - Step 4: hydrateRouterSwitches() already reads this directly
402
+ // - Step 5: drives the diff preview against the new config
403
+ //
404
+ // Falls back to defaults / auto-detection on first install (no FritzPlatform
405
+ // entry in config.json yet).
406
+ async function loadExistingConfig() {
390
407
  try {
391
408
  const allConfig = await homebridge.getPluginConfig();
392
409
  const fritz = (allConfig || []).find((c) => c && c.platform === 'FritzPlatform');
393
- const master = fritz && fritz.devices && fritz.devices.find((d) => d.master === true);
394
- if (master) {
395
- if (master.host) inputs.host.value = master.host;
396
- if (master.port) inputs.port.value = master.port;
397
- if (typeof master.ssl === 'boolean') inputs.ssl.checked = master.ssl;
398
- if (typeof master.tr064 === 'boolean') inputs.tr064.checked = master.tr064;
399
- if (master.username) inputs.username.value = master.username;
400
- if (master.password) inputs.password.value = master.password;
401
- }
410
+ if (!fritz) return null;
411
+ const master = (fritz.devices || []).find((d) => d && d.master === true) || null;
412
+ const smarthome = Array.isArray(fritz.smarthome) ? fritz.smarthome : [];
413
+ return { master, smarthome };
402
414
  } catch (err) {
403
- // No config yet — keep placeholders.
415
+ return null;
416
+ }
417
+ }
418
+
419
+ // ============ Step 1 — Connection Test ============
420
+ // Prefill from existing plugin config. Also seeds wizardState.existingConfig
421
+ // and wizardState.energyAccType so later steps can hydrate from it.
422
+ (async () => {
423
+ wizardState.existingConfig = await loadExistingConfig();
424
+ const cfg = wizardState.existingConfig;
425
+ if (!cfg) return;
426
+
427
+ // Step 1 — connection fields
428
+ const master = cfg.master;
429
+ if (master) {
430
+ if (master.host) inputs.host.value = master.host;
431
+ if (master.port) inputs.port.value = master.port;
432
+ if (typeof master.ssl === 'boolean') inputs.ssl.checked = master.ssl;
433
+ if (typeof master.tr064 === 'boolean') inputs.tr064.checked = master.tr064;
434
+ if (master.username) inputs.username.value = master.username;
435
+ if (master.password) inputs.password.value = master.password;
436
+ }
437
+
438
+ // Seed energyAccType from existing OBIS entries so the Step 3 radio
439
+ // preselects the right display type (temperature vs light). If ANY
440
+ // existing tile uses energy-light, treat the whole reader as light —
441
+ // the wizard applies one global type to all OBIS tiles on save.
442
+ const obisEntries = cfg.smarthome.filter(
443
+ (e) => e && (e.accType === 'energy-temperature' || e.accType === 'energy-light')
444
+ );
445
+ if (obisEntries.length > 0) {
446
+ const anyLight = obisEntries.some((e) => e.accType === 'energy-light');
447
+ wizardState.energyAccType = anyLight ? 'energy-light' : 'energy-temperature';
404
448
  }
405
449
  })();
406
450
 
@@ -509,15 +553,41 @@
509
553
  { value: 'skip', label: '— ignorieren —' },
510
554
  ];
511
555
 
556
+ // Phase G: index existing smarthome entries by AIN for quick lookup.
557
+ // Multiple OBIS entries may share an AIN (one per obisChannel); we just
558
+ // need the first one to recover the accType + custom name.
559
+ const existingSh = (wizardState.existingConfig && wizardState.existingConfig.smarthome) || [];
560
+ const existingByAin = {};
561
+ for (const e of existingSh) {
562
+ if (e && e.ain && !existingByAin[e.ain]) existingByAin[e.ain] = e;
563
+ }
564
+
512
565
  const html = wizardState.discoveredDevices.map((d) => {
513
566
  // Default-Selection: include if accType detected (not 'unknown' / 'skip')
514
567
  const defaultInclude = d.accType !== 'unknown' && d.accType !== 'skip';
515
568
  if (!wizardState.deviceSelections[d.ain]) {
516
- wizardState.deviceSelections[d.ain] = {
517
- include: defaultInclude,
518
- accType: d.accType,
519
- name: d.name,
520
- };
569
+ const existingEntry = existingByAin[d.ain];
570
+ if (existingEntry) {
571
+ // Phase G: AIN is already configured — preserve the user's chosen
572
+ // accType and name instead of overwriting with auto-detection.
573
+ // energy-light is normalized to energy-temperature at this layer
574
+ // so Step 3 picks the AIN up (energyAccType holds the display
575
+ // choice and is applied at save time).
576
+ const normalizedAccType = (existingEntry.accType === 'energy-light')
577
+ ? 'energy-temperature'
578
+ : existingEntry.accType;
579
+ wizardState.deviceSelections[d.ain] = {
580
+ include: true,
581
+ accType: normalizedAccType,
582
+ name: existingEntry.name || d.name,
583
+ };
584
+ } else {
585
+ wizardState.deviceSelections[d.ain] = {
586
+ include: defaultInclude,
587
+ accType: d.accType,
588
+ name: d.name,
589
+ };
590
+ }
521
591
  }
522
592
  const sel = wizardState.deviceSelections[d.ain];
523
593
 
@@ -727,13 +797,104 @@
727
797
  };
728
798
  }
729
799
 
800
+ /**
801
+ * Phase G: compute a human-readable list of changes the upcoming save will
802
+ * make against the currently persisted config.json. Empty/null on a first
803
+ * install (nothing to compare to).
804
+ *
805
+ * Key by (ain + obisChannel) for OBIS tiles (multiple per AIN), by ain
806
+ * only for everything else.
807
+ */
808
+ function renderConfigDiff(newConfig, existing) {
809
+ if (!existing) return ''; // first run — nothing to diff
810
+
811
+ const lines = [];
812
+
813
+ // -- Master device fields --
814
+ const oldMaster = existing.master || {};
815
+ const newMaster = (newConfig.devices && newConfig.devices[0]) || {};
816
+ if ((oldMaster.host || '') !== (newMaster.host || '')) {
817
+ lines.push(`Host: <code>${escapeHtml(oldMaster.host || '—')}</code> → <code>${escapeHtml(newMaster.host || '—')}</code>`);
818
+ }
819
+ if ((oldMaster.username || '') !== (newMaster.username || '')) {
820
+ lines.push(`Username: <code>${escapeHtml(oldMaster.username || '—')}</code> → <code>${escapeHtml(newMaster.username || '—')}</code>`);
821
+ }
822
+ if (!!oldMaster.ssl !== !!newMaster.ssl) {
823
+ lines.push(`SSL: ${oldMaster.ssl ? 'an' : 'aus'} → ${newMaster.ssl ? 'an' : 'aus'}`);
824
+ }
825
+ if ((oldMaster.port || 49000) !== (newMaster.port || 49000)) {
826
+ lines.push(`Port: <code>${oldMaster.port || 49000}</code> → <code>${newMaster.port || 49000}</code>`);
827
+ }
828
+
829
+ // -- Router-Switches (master.options) --
830
+ const oldOpts = (oldMaster.options) || {};
831
+ const newOpts = (newMaster.options) || {};
832
+ const allOptKeys = new Set([...Object.keys(oldOpts), ...Object.keys(newOpts)]);
833
+ for (const k of allOptKeys) {
834
+ const wasOn = !!oldOpts[k];
835
+ const isOn = !!newOpts[k];
836
+ if (wasOn && !isOn) lines.push(`Router-Switch <code>${escapeHtml(k)}</code> entfernt`);
837
+ else if (!wasOn && isOn) lines.push(`Router-Switch <code>${escapeHtml(k)}</code> hinzugefügt`);
838
+ }
839
+
840
+ // -- Smarthome entries (devices + OBIS tiles) --
841
+ const oldSh = existing.smarthome || [];
842
+ const newSh = newConfig.smarthome || [];
843
+ const keyOf = (e) => e.obisChannel ? `${e.ain}::${e.obisChannel}` : e.ain;
844
+ const oldByKey = {};
845
+ const newByKey = {};
846
+ for (const e of oldSh) if (e && e.ain) oldByKey[keyOf(e)] = e;
847
+ for (const e of newSh) if (e && e.ain) newByKey[keyOf(e)] = e;
848
+
849
+ for (const k of Object.keys(newByKey)) {
850
+ const n = newByKey[k];
851
+ const o = oldByKey[k];
852
+ if (!o) {
853
+ const chan = n.obisChannel ? ` <small>(${escapeHtml(n.obisChannel)})</small>` : '';
854
+ lines.push(`Neu: <code>${escapeHtml(n.ain)}</code>${chan} — <em>${escapeHtml(n.accType)}</em> "${escapeHtml(n.name || '')}"`);
855
+ } else {
856
+ if ((o.accType || '') !== (n.accType || '')) {
857
+ lines.push(`<code>${escapeHtml(n.ain)}</code>${n.obisChannel ? ` <small>(${escapeHtml(n.obisChannel)})</small>` : ''}: accType <em>${escapeHtml(o.accType || '—')}</em> → <em>${escapeHtml(n.accType || '—')}</em>`);
858
+ }
859
+ if ((o.name || '') !== (n.name || '')) {
860
+ lines.push(`<code>${escapeHtml(n.ain)}</code>${n.obisChannel ? ` <small>(${escapeHtml(n.obisChannel)})</small>` : ''}: Name "${escapeHtml(o.name || '—')}" → "${escapeHtml(n.name || '—')}"`);
861
+ }
862
+ }
863
+ }
864
+ for (const k of Object.keys(oldByKey)) {
865
+ if (newByKey[k]) continue;
866
+ const o = oldByKey[k];
867
+ const chan = o.obisChannel ? ` <small>(${escapeHtml(o.obisChannel)})</small>` : '';
868
+ lines.push(`Entfernt: <code>${escapeHtml(o.ain)}</code>${chan} — war <em>${escapeHtml(o.accType)}</em> "${escapeHtml(o.name || '')}"`);
869
+ }
870
+
871
+ if (lines.length === 0) {
872
+ return `
873
+ <div class="alert alert-secondary">
874
+ <strong>Keine Änderungen</strong> gegenüber der aktuellen <code>config.json</code>.
875
+ Du kannst trotzdem speichern (no-op) oder den Wizard verlassen.
876
+ </div>
877
+ `;
878
+ }
879
+
880
+ return `
881
+ <div class="alert alert-warning">
882
+ <div style="font-weight: 600; margin-bottom: 0.5rem;">📝 Änderungen gegenüber der aktuellen Konfiguration (${lines.length}):</div>
883
+ <ul style="margin: 0; padding-left: 1.25rem; font-size: 0.9rem;">
884
+ ${lines.map((l) => `<li>${l}</li>`).join('')}
885
+ </ul>
886
+ </div>
887
+ `;
888
+ }
889
+
730
890
  function renderSavePreview() {
731
891
  const config = buildPluginConfig();
732
892
  const summary = document.getElementById('saveSummary');
733
893
  const preview = document.getElementById('savePreview');
894
+ const diffEl = document.getElementById('saveDiff');
734
895
 
735
- const nDevices = config.smarthome.filter((s) => s.accType !== 'energy-temperature').length;
736
- const nEnergy = config.smarthome.filter((s) => s.accType === 'energy-temperature').length;
896
+ const nDevices = config.smarthome.filter((s) => s.accType !== 'energy-temperature' && s.accType !== 'energy-light').length;
897
+ const nEnergy = config.smarthome.filter((s) => s.accType === 'energy-temperature' || s.accType === 'energy-light').length;
737
898
  const nRouter = Object.keys((config.devices[0] && config.devices[0].options) || {}).length;
738
899
 
739
900
  // Mask password in preview for safety
@@ -755,6 +916,10 @@
755
916
  `;
756
917
 
757
918
  preview.textContent = JSON.stringify(maskedConfig, null, 2);
919
+
920
+ // Phase G: render diff against the config that was on disk when the
921
+ // wizard opened, so the user sees exactly what will change.
922
+ diffEl.innerHTML = renderConfigDiff(config, wizardState.existingConfig);
758
923
  }
759
924
 
760
925
  const saveBtn = document.getElementById('saveBtn');
@@ -911,13 +1076,50 @@
911
1076
  '-2': { current_power: 'Aktuelle Einspeisung', total_energy: 'Gesamt Einspeisung' },
912
1077
  };
913
1078
 
1079
+ // Phase G: index existing OBIS tiles by AIN so we can preserve the
1080
+ // user's custom tile names instead of resetting to "Aktuelle Einspeisung"
1081
+ // etc. on every wizard re-run. Existing config may store one OR two
1082
+ // entries per sub-AIN (current_power, total_energy) — we keep whatever
1083
+ // is there and only fill in the missing channel with the default name.
1084
+ const existingObisByAin = {};
1085
+ const existingShAll = (wizardState.existingConfig && wizardState.existingConfig.smarthome) || [];
1086
+ for (const e of existingShAll) {
1087
+ if (!e || !e.ain) continue;
1088
+ if (e.accType !== 'energy-temperature' && e.accType !== 'energy-light') continue;
1089
+ if (!existingObisByAin[e.ain]) existingObisByAin[e.ain] = [];
1090
+ existingObisByAin[e.ain].push({
1091
+ obisChannel: e.obisChannel || 'current_power',
1092
+ name: e.name,
1093
+ });
1094
+ }
1095
+
914
1096
  // Initialize state for each OBIS AIN if not already there
915
1097
  for (const baseAin of Object.keys(byBase)) {
916
1098
  const group = byBase[baseAin];
917
1099
  for (const dir of ['-1', '-2']) {
918
1100
  const entry = dir === '-1' ? group.ain1 : group.ain2;
919
1101
  if (!entry) continue;
920
- if (!wizardState.energyTiles[entry.ain]) {
1102
+ if (wizardState.energyTiles[entry.ain]) continue;
1103
+
1104
+ const existingTiles = existingObisByAin[entry.ain];
1105
+ if (existingTiles && existingTiles.length > 0) {
1106
+ // Phase G: rebuild from existing config, preserving names.
1107
+ // Ensure both channels exist (wizard UI always renders 2 tiles
1108
+ // per sub-AIN — current_power first, total_energy second).
1109
+ const byChan = {};
1110
+ for (const t of existingTiles) byChan[t.obisChannel] = t;
1111
+ const tiles = [
1112
+ {
1113
+ obisChannel: 'current_power',
1114
+ name: (byChan.current_power && byChan.current_power.name) || defaultNames[dir].current_power,
1115
+ },
1116
+ {
1117
+ obisChannel: 'total_energy',
1118
+ name: (byChan.total_energy && byChan.total_energy.name) || defaultNames[dir].total_energy,
1119
+ },
1120
+ ];
1121
+ wizardState.energyTiles[entry.ain] = tiles;
1122
+ } else {
921
1123
  wizardState.energyTiles[entry.ain] = [
922
1124
  { obisChannel: 'current_power', name: defaultNames[dir].current_power },
923
1125
  { obisChannel: 'total_energy', name: defaultNames[dir].total_energy },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pellebot/homebridge-fritz-platform-hb2",
3
3
  "displayName": "Homebridge FRITZ!Box Platform (HB2)",
4
- "version": "1.1.1",
4
+ "version": "1.2.0",
5
5
  "description": "Homebridge 2.0 plugin for AVM FRITZ!DECT devices (thermostats, lamps, sensors, OBIS smart-meter readers) and FRITZ!Box router switches (Guest WLAN, WPS). Node 22 / 24 ready.",
6
6
  "icon": "https://raw.githubusercontent.com/PelleBot/homebridge-fritz-platform-ng/main/branding/Icon.png",
7
7
  "type": "module",
@@ -58,10 +58,7 @@ class Accessory {
58
58
  'Toggling not allowed for energy-meter (read-only sensor)',
59
59
  `${this.accessory.displayName} (${this.accessory.context.config.subtype})`
60
60
  );
61
- setTimeout(
62
- () => service.getCharacteristic(this.api.hap.Characteristic.On).updateValue(true),
63
- 1000
64
- );
61
+ setTimeout(() => service.getCharacteristic(this.api.hap.Characteristic.On).updateValue(true), 1000);
65
62
  });
66
63
 
67
64
  // OutletInUse always true so the tile renders as active in HomeKit
@@ -1,7 +1,20 @@
1
1
  'use strict';
2
2
 
3
3
  const Config = (smarthomeConfig) => {
4
- const validTypes = ['switch', 'contact', 'thermostat', 'lightbulb', 'temperature', 'window', 'blind', 'button', 'smoke', 'energy-meter', 'energy-temperature', 'energy-light'];
4
+ const validTypes = [
5
+ 'switch',
6
+ 'contact',
7
+ 'thermostat',
8
+ 'lightbulb',
9
+ 'temperature',
10
+ 'window',
11
+ 'blind',
12
+ 'button',
13
+ 'smoke',
14
+ 'energy-meter',
15
+ 'energy-temperature',
16
+ 'energy-light',
17
+ ];
5
18
  const validObisChannels = ['current_power', 'total_energy'];
6
19
  const validTypesGroup = ['switch', 'lightbulb', 'thermostat', 'switch-lightbulb'];
7
20
  const validButtons = [1, 4];
@@ -687,9 +687,7 @@ class Handler {
687
687
  let service = accessory.getService(this.api.hap.Service.LightSensor);
688
688
 
689
689
  try {
690
- let device = this.smarthomeList.devices.find((device) =>
691
- device.ain.includes(accessory.context.config.ain)
692
- );
690
+ let device = this.smarthomeList.devices.find((device) => device.ain.includes(accessory.context.config.ain));
693
691
  logger.debug(device, `${accessory.displayName} (${subtype})`);
694
692
 
695
693
  if (device) {
@@ -746,9 +744,7 @@ class Handler {
746
744
  let service = accessory.getService(this.api.hap.Service.TemperatureSensor);
747
745
 
748
746
  try {
749
- let device = this.smarthomeList.devices.find((device) =>
750
- device.ain.includes(accessory.context.config.ain)
751
- );
747
+ let device = this.smarthomeList.devices.find((device) => device.ain.includes(accessory.context.config.ain));
752
748
  logger.debug(device, `${accessory.displayName} (${subtype})`);
753
749
 
754
750
  if (device) {
@@ -814,9 +810,7 @@ class Handler {
814
810
  let service = accessory.getService(this.api.hap.Service.Outlet);
815
811
 
816
812
  try {
817
- let device = this.smarthomeList.devices.find((device) =>
818
- device.ain.includes(accessory.context.config.ain)
819
- );
813
+ let device = this.smarthomeList.devices.find((device) => device.ain.includes(accessory.context.config.ain));
820
814
  logger.debug(device, `${accessory.displayName} (${subtype})`);
821
815
 
822
816
  if (device) {
@@ -833,13 +827,9 @@ class Handler {
833
827
  const currentPower = Math.abs(device.powermeter.power || 0);
834
828
  const totalEnergy = device.powermeter.energy || 0;
835
829
 
836
- service
837
- .getCharacteristic(this.api.hap.Characteristic.CurrentConsumption)
838
- .updateValue(currentPower);
830
+ service.getCharacteristic(this.api.hap.Characteristic.CurrentConsumption).updateValue(currentPower);
839
831
 
840
- service
841
- .getCharacteristic(this.api.hap.Characteristic.TotalConsumption)
842
- .updateValue(totalEnergy);
832
+ service.getCharacteristic(this.api.hap.Characteristic.TotalConsumption).updateValue(totalEnergy);
843
833
 
844
834
  service.getCharacteristic(this.api.hap.Characteristic.On).updateValue(true);
845
835
  service.getCharacteristic(this.api.hap.Characteristic.OutletInUse).updateValue(true);
package/src/platform.js CHANGED
@@ -76,7 +76,9 @@ function FritzPlatform(log, config, api) {
76
76
  HistoryService.prototype.addEntry = function () {};
77
77
  HistoryService.prototype.cleanPersist = function () {};
78
78
  HistoryService.prototype.setExtraPersistedData = function () {};
79
- HistoryService.prototype.getExtraPersistedData = function () { return null; };
79
+ HistoryService.prototype.getExtraPersistedData = function () {
80
+ return null;
81
+ };
80
82
 
81
83
  this.api = api;
82
84
  this.log = log;
@@ -209,9 +211,9 @@ FritzPlatform.prototype = {
209
211
  else if (device.subtype === 'smarthome-window-switch')
210
212
  new SHWindowSwitchAccessory(this.api, accessory, this.accessories, this.meshMaster);
211
213
  else if (device.subtype === 'smarthome-lightbulb')
212
- new SHLightbulbAccessory(this.api, accessory, this.accessories, this.meshMaster);
214
+ new SHLightbulbAccessory(this.api, accessory, this.accessories, this.meshMaster);
213
215
  else if (device.subtype === 'smarthome-smoke')
214
- new SHSmokeAccessory(this.api, accessory, this.accessories, this.meshMaster, HistoryService);
216
+ new SHSmokeAccessory(this.api, accessory, this.accessories, this.meshMaster, HistoryService);
215
217
  else if (device.subtype === 'smarthome-switch-lightbulb' && device.energy)
216
218
  new SHOutletLightbulbAccessory(this.api, accessory, this.accessories, this.meshMaster, HistoryService);
217
219
  else if (device.subtype === 'smarthome-switch-lightbulb' && !device.energy)
@@ -47,82 +47,105 @@ exports.registerWith = (hap) => {
47
47
 
48
48
  // ---- WiFi toggles ----
49
49
  declare('WifiTwo', 'WIFI 2.4GHZ', '0026e147-5d51-4f42-b157-6aca6050be8e', {
50
- format: 'bool', perms: PR_PW_EV,
50
+ format: 'bool',
51
+ perms: PR_PW_EV,
51
52
  });
52
53
  declare('WifiFive', 'WIFI 5GHZ', 'a72aeeca-c6ce-45ce-b026-5d400aab5fc9', {
53
- format: 'bool', perms: PR_PW_EV,
54
+ format: 'bool',
55
+ perms: PR_PW_EV,
54
56
  });
55
57
  declare('WifiGuest', 'WIFI Guest', 'a87bbf2b-885c-4713-8169-22abdbf0b2a1', {
56
- format: 'bool', perms: PR_PW_EV,
58
+ format: 'bool',
59
+ perms: PR_PW_EV,
57
60
  });
58
61
  declare('WifiWPS', 'WIFI WPS', '991dd58c-7d8c-46b1-acd1-411d8f6902ac', {
59
- format: 'bool', perms: PR_PW_EV,
62
+ format: 'bool',
63
+ perms: PR_PW_EV,
60
64
  });
61
65
 
62
66
  // ---- FritzBox device toggles ----
63
67
  declare('Reconnect', 'Reconnect', 'ce1cb310-d2d8-4cd1-9686-c6f2aba6ad15', {
64
- format: 'bool', perms: PR_PW_EV,
68
+ format: 'bool',
69
+ perms: PR_PW_EV,
65
70
  });
66
71
  declare('DECT', 'DECT Station', '1718fc65-453b-403a-ab81-79a1c96ba195', {
67
- format: 'bool', perms: PR_PW_EV,
72
+ format: 'bool',
73
+ perms: PR_PW_EV,
68
74
  });
69
75
  declare('AnsweringMachine', 'Answering Machine', 'd19ef9d5-3bc6-47ac-b6bb-7bdcf0df20b0', {
70
- format: 'bool', perms: PR_PW_EV,
76
+ format: 'bool',
77
+ perms: PR_PW_EV,
71
78
  });
72
79
  declare('Deflection', 'Deflection', '658086c7-274c-4988-bd5e-3c720fa3054d', {
73
- format: 'bool', perms: PR_PW_EV,
80
+ format: 'bool',
81
+ perms: PR_PW_EV,
74
82
  });
75
83
  declare('WakeUp', 'Wake Up', 'c60a4aa9-cefb-4c61-8185-ad2b3ba52842', {
76
- format: 'bool', perms: PR_PW_EV,
84
+ format: 'bool',
85
+ perms: PR_PW_EV,
77
86
  });
78
87
 
79
88
  // ---- Device-control toggles ----
80
89
  declare('DeviceLED', 'LED', 'fd81f0dc-4324-457e-8164-289743873fb1', {
81
- format: 'bool', perms: PR_PW_EV,
90
+ format: 'bool',
91
+ perms: PR_PW_EV,
82
92
  });
83
93
  declare('DeviceLock', 'Device Lock', '56af4239-46bc-4bae-b55b-dbc5be2d0897', {
84
- format: 'bool', perms: PR_PW_EV,
94
+ format: 'bool',
95
+ perms: PR_PW_EV,
85
96
  });
86
97
  declare('RingLock', 'Ring Lock', 'cab7d43e-422c-4452-bc9a-11c89454332b', {
87
- format: 'bool', perms: PR_PW_EV,
98
+ format: 'bool',
99
+ perms: PR_PW_EV,
88
100
  });
89
101
 
90
102
  // ---- Phone & alarm ----
91
103
  declare('PhoneBook', 'Refresh Phone Book', '8f9aeaa4-092f-4c3a-85f1-dfd064a07c3d', {
92
- format: 'bool', perms: PR_PW_EV,
104
+ format: 'bool',
105
+ perms: PR_PW_EV,
93
106
  });
94
107
  declare('DialAlarm', 'Alarm', '8fe6e841-41e4-479f-b334-8af339ce5b30', {
95
- format: 'bool', perms: PR_PW_EV,
108
+ format: 'bool',
109
+ perms: PR_PW_EV,
96
110
  });
97
111
 
98
112
  // ---- Network ----
99
113
  declare('DNSServer', 'DNS Server', 'c34f1eb0-92bb-44a8-b399-17f2599639f1', {
100
- format: 'bool', perms: PR_PW_EV,
114
+ format: 'bool',
115
+ perms: PR_PW_EV,
101
116
  });
102
117
  declare('FallbackInternet', 'Internet Fallback', 'ef088934-4ec8-4174-b550-e8a8faaed88c', {
103
- format: 'bool', perms: PR_PW_EV,
118
+ format: 'bool',
119
+ perms: PR_PW_EV,
104
120
  });
105
121
 
106
122
  // ---- Read-only display (callmonitor is out of scope but the router
107
123
  // accessory may still reference Caller/Called via Service.ContactSensor
108
124
  // optionals — registering here keeps router/router.accessory.js safe). ----
109
125
  declare('Caller', 'Last Caller', 'eb0b2d83-569b-44aa-989d-190a911b4397', {
110
- format: 'string', perms: PR_EV,
126
+ format: 'string',
127
+ perms: PR_EV,
111
128
  });
112
129
  declare('Called', 'Last Called', 'cf42e4a1-ff61-4aa6-9cc5-55d3c09cfbbd', {
113
- format: 'string', perms: PR_EV,
130
+ format: 'string',
131
+ perms: PR_EV,
114
132
  });
115
133
 
116
134
  // ---- Bandwidth / connection stats ----
117
135
  declare('Download', 'Download', '37574b8e-2d7c-47ee-8b5e-6bfc42f195d9', {
118
- format: 'string', perms: PR_EV,
136
+ format: 'string',
137
+ perms: PR_EV,
119
138
  });
120
139
  declare('Upload', 'Upload', '9b2e94f7-a665-4575-9efd-1b37474d758b', {
121
- format: 'string', perms: PR_EV,
140
+ format: 'string',
141
+ perms: PR_EV,
122
142
  });
123
143
  declare('Ping', 'Ping', 'ce18aaef-1026-4538-943b-026501599dc0', {
124
- format: 'float', perms: PR_EV,
125
- maxValue: 9999, minValue: 0, minStep: 0.1,
144
+ format: 'float',
145
+ perms: PR_EV,
146
+ maxValue: 9999,
147
+ minValue: 0,
148
+ minStep: 0.1,
126
149
  });
127
150
 
128
151
  // NOTE on Service.Switch: original file overrode the built-in HAP Switch
@@ -49,58 +49,83 @@ exports.registerWith = (hap) => {
49
49
  // History-related (used by fakegato-history; our stub no-ops them but
50
50
  // accessory code may still reference these as registered types).
51
51
  declare('ResetTotal', 'Reset Total', 'E863F112-079E-48FF-8F27-9C2605A29F52', {
52
- format: 'uint32', unit: 'seconds', perms: PR_EV_PW,
52
+ format: 'uint32',
53
+ unit: 'seconds',
54
+ perms: PR_EV_PW,
53
55
  });
54
56
  declare('HistoryStatus', 'History Status', 'E863F116-079E-48FF-8F27-9C2605A29F52', {
55
- format: 'data', perms: PR_EV_PW,
57
+ format: 'data',
58
+ perms: PR_EV_PW,
56
59
  });
57
60
  declare('HistoryEntries', 'History Entries', 'E863F117-079E-48FF-8F27-9C2605A29F52', {
58
- format: 'data', perms: PR_EV_PW,
61
+ format: 'data',
62
+ perms: PR_EV_PW,
59
63
  });
60
64
  declare('HistoryRequest', 'History Request', 'E863F11C-079E-48FF-8F27-9C2605A29F52', {
61
- format: 'data', perms: PR_EV_PW,
65
+ format: 'data',
66
+ perms: PR_EV_PW,
62
67
  });
63
68
  declare('SetTime', 'Set Time', 'E863F121-079E-48FF-8F27-9C2605A29F52', {
64
- format: 'data', perms: PR_EV_PW,
69
+ format: 'data',
70
+ perms: PR_EV_PW,
65
71
  });
66
72
 
67
73
  // Window / contact sensor (Eve-extended)
68
74
  declare('LastActivation', 'Last Activation', 'E863F11A-079E-48FF-8F27-9C2605A29F52', {
69
- format: 'uint32', unit: 'seconds', perms: PR_EV,
75
+ format: 'uint32',
76
+ unit: 'seconds',
77
+ perms: PR_EV,
70
78
  });
71
79
  declare('TimesOpened', 'Times Opened', 'E863F129-079E-48FF-8F27-9C2605A29F52', {
72
- format: 'uint32', perms: PR_EV,
80
+ format: 'uint32',
81
+ perms: PR_EV,
73
82
  });
74
83
  declare('OpenDuration', 'Open Duration', 'E863F118-079E-48FF-8F27-9C2605A29F52', {
75
- format: 'uint32', unit: 'seconds', perms: PR_EV_PW,
84
+ format: 'uint32',
85
+ unit: 'seconds',
86
+ perms: PR_EV_PW,
76
87
  });
77
88
  declare('ClosedDuration', 'Closed Duration', 'E863F119-079E-48FF-8F27-9C2605A29F52', {
78
- format: 'uint32', unit: 'seconds', perms: PR_EV_PW,
89
+ format: 'uint32',
90
+ unit: 'seconds',
91
+ perms: PR_EV_PW,
79
92
  });
80
93
 
81
94
  // Outlet / energy (Eve-extended)
82
95
  declare('CurrentConsumption', 'Current Consumption', 'E863F10D-079E-48FF-8F27-9C2605A29F52', {
83
- format: 'float', unit: 'W', perms: PR_EV,
96
+ format: 'float',
97
+ unit: 'W',
98
+ perms: PR_EV,
84
99
  });
85
100
  declare('TotalConsumption', 'Total Consumption', 'E863F10C-079E-48FF-8F27-9C2605A29F52', {
86
- format: 'float', unit: 'kWh', perms: PR_EV,
101
+ format: 'float',
102
+ unit: 'kWh',
103
+ perms: PR_EV,
87
104
  });
88
105
  declare('Volts', 'Volts', 'E863F10A-079E-48FF-8F27-9C2605A29F52', {
89
- format: 'float', unit: 'V', perms: PR_EV,
106
+ format: 'float',
107
+ unit: 'V',
108
+ perms: PR_EV,
90
109
  });
91
110
  declare('Amperes', 'Amperes', 'E863F126-079E-48FF-8F27-9C2605A29F52', {
92
- format: 'float', unit: 'A', perms: PR_EV,
111
+ format: 'float',
112
+ unit: 'A',
113
+ perms: PR_EV,
93
114
  });
94
115
 
95
116
  // Thermostat (Eve-extended)
96
117
  declare('ValvePosition', 'Valve Position', 'E863F12E-079E-48FF-8F27-9C2605A29F52', {
97
- format: 'uint8', unit: 'percentage', perms: PR_EV,
118
+ format: 'uint8',
119
+ unit: 'percentage',
120
+ perms: PR_EV,
98
121
  });
99
122
  declare('ProgramCommand', 'Program Command', 'E863F12C-079E-48FF-8F27-9C2605A29F52', {
100
- format: 'data', perms: ['pw'],
123
+ format: 'data',
124
+ perms: ['pw'],
101
125
  });
102
126
  declare('ProgramData', 'Program Data', 'E863F12F-079E-48FF-8F27-9C2605A29F52', {
103
- format: 'data', perms: PR_EV,
127
+ format: 'data',
128
+ perms: PR_EV,
104
129
  });
105
130
 
106
131
  // NOTE: The original file overrode Service.Outlet, Service.Thermostat,