@pellebot/homebridge-fritz-platform-hb2 1.1.0 → 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,42 @@
1
+ ## Setup-Wizard Release
2
+
3
+ Beim Klick auf das Plugin-Settings-Icon in Config UI X startet ab dieser Version ein geführter 5-Schritt-Wizard — kein Raten mehr bei Feldern wie `accType`, `obisChannel` oder AIN-Format.
4
+
5
+ ### Highlights
6
+
7
+ - 🧙 **5-Schritt-Wizard:** FritzBox-Verbindung → Geräte-Auswahl → Smart Energy 250 → Router-Switches → Speichern
8
+ - 🔍 **Auto-Discovery** aller DECT-Geräte mit `accType`-Vorschlag pro Gerät anhand der Geräte-Eigenschaften
9
+ - ⚡ **Smart Energy 250 OBIS-Reader Auto-Setup:** erkennt Sub-AIN-Paare (-1/-2) und legt 4 HomeKit-Tiles auto-an (Aktueller/Gesamt Verbrauch + Aktuelle/Gesamt Einspeisung)
10
+ - 🔘 **Router-Switches Section:** Gast-WLAN, WPS, WLAN 2.4/5 GHz, DECT-Basis, AB, Rufumleitung, Reconnect — alles als Checkboxen
11
+ - 💾 **Save + Apply:** Zusammenfassung mit JSON-Vorschau (Passwort maskiert), direkter Homebridge-Restart-Button
12
+ - ✏️ **Auto-Pre-fill** aus bestehender Config im Verbindungs-Schritt und bei Router-Switches
13
+
14
+ ### Bug-Fixes
15
+
16
+ - AIN-Suffix-Detection korrekt priorisiert: DECT 500 Lampen (`-1` als HAN-FUN-Sub-Unit) werden nicht mehr fälschlich als OBIS-Reader interpretiert
17
+ - Korrekte `@seydx/fritzbox`-Feldnamen verwendet: `.thermostat` (nicht `.hkr`), `.light` (nicht `.colorcontrol`/`.levelcontrol`), `.alert` (boolean)
18
+
19
+ ### Installation
20
+
21
+ ```bash
22
+ sudo npm install -g @pellebot/homebridge-fritz-platform-hb2@1.1.0
23
+ sudo hb-service restart
24
+ ```
25
+
26
+ Bei npm-Cache-Permission-Fehler vorab:
27
+ ```bash
28
+ sudo chown -R 501:20 ~/.npm
29
+ ```
30
+
31
+ ### Breaking Changes
32
+
33
+ Keine. Bestehende Configs werden unverändert übernommen. Das alte JSON-Schema-Form ist über den "Save"-Button (bottom-right) im Plugin-Settings-Modal weiterhin erreichbar als Fallback.
34
+
35
+ ### Bekannte Limitationen
36
+
37
+ - Standalone Config UI X: `homebridge.savePluginConfig()` kann in Standalone-Modus die WebSocket-ACK nicht empfangen → Save mit 3-Sek-Timeout gerennt. Datei wird trotzdem geschrieben.
38
+ - Custom-UI-Icon: erscheint im Config UI X Plugin-Tile erst nach Homebridge-Verified-Plugin-Approval (frühestens 13. Juni 2026, siehe Roadmap).
39
+
40
+ ### Vollständiger Changelog
41
+
42
+ Siehe [CHANGELOG.md](./CHANGELOG.md)
@@ -0,0 +1,54 @@
1
+ ## LightSensor variant for energy tiles
2
+
3
+ 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`).
4
+
5
+ ### Hintergrund
6
+
7
+ Apple Home rendert beide Sensor-Typen unterschiedlich:
8
+
9
+ - **TemperatureSensor** (`energy-temperature`): prominentes Tile im Raum-View, `°C`-Einheit
10
+ - **LightSensor** (`energy-light`): "passiver Sensor" in der Status-Zeile oben, `lx`-Einheit — per Long-Press → "Im Zuhause anzeigen" als Favorit ins Home-Tab holbar
11
+
12
+ Beide Einheiten (°C, lx) sind semantisch falsch für Energie-Werte. Manche Nutzer empfinden `lx` als abstrakter und weniger irreführend als `°C` (das auf Temperaturmessung schließen lässt).
13
+
14
+ ### Highlights
15
+
16
+ - 🆕 Neuer `accType: energy-light` parallel zu `energy-temperature` (beide bleiben verfügbar)
17
+ - 🧙 Setup-Wizard Schritt 3 (Smart Energy 250) bekommt **Display-Typ-Auswahl** (Radio: Temperatur / Lichtsensor)
18
+ - 🔄 Auto-Migration: wenn man via Wizard von Temperatur auf Lichtsensor wechselt, entfernt das Plugin den alten TemperatureSensor-Service sauber
19
+
20
+ ### Breaking Changes
21
+
22
+ Keine. Bestehende Configs mit `energy-temperature` laufen unverändert weiter.
23
+
24
+ ### Migration für bestehende Nutzer
25
+
26
+ Pro Smart-Home-Eintrag den `accType` von `"energy-temperature"` auf `"energy-light"` ändern, Homebridge neu starten:
27
+
28
+ ```json
29
+ {
30
+ "active": true,
31
+ "name": "Aktueller Verbrauch",
32
+ "ain": "...-1",
33
+ "accType": "energy-light",
34
+ "obisChannel": "current_power"
35
+ }
36
+ ```
37
+
38
+ Oder den Setup-Wizard noch einmal durchlaufen und in Schritt 3 die LightSensor-Option wählen.
39
+
40
+ ### Installation
41
+
42
+ ```bash
43
+ sudo npm install -g @pellebot/homebridge-fritz-platform-hb2@1.1.1
44
+ sudo hb-service restart
45
+ ```
46
+
47
+ ### Bekannte Limitationen
48
+
49
+ - **HAP minValue für AmbientLightLevel:** 0.0001 (nicht 0 möglich). Bei aktuellem Wert = 0 W zeigt das Tile `0.0001 lx`. Funktional kein Unterschied, kosmetisch leicht verwirrend.
50
+ - **LightSensor in Apple Home Status-Bereich:** das Tile erscheint nicht direkt im Raum sondern im Status-Bereich oben. Per Long-Press als Favorit ins Home-Tab holen (siehe README).
51
+
52
+ ### Vollständiger Changelog
53
+
54
+ Siehe [CHANGELOG.md](./CHANGELOG.md)
@@ -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,65 @@
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
+
34
+ # v1.1.1 — 2026-05-14 (LightSensor variant for energy tiles)
35
+
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`).
37
+
38
+ ## Hintergrund
39
+
40
+ Apple Home rendert die zwei Sensor-Typen unterschiedlich:
41
+ - **TemperatureSensor:** prominentes Tile im Raum-View, °C-Einheit
42
+ - **LightSensor:** "passive sensor" im Status-Bereich oben, lx-Einheit — per Long-Press als Favorit ins Home-Tab holbar
43
+
44
+ Beide Einheiten (°C, lx) sind semantisch falsch für Energie-Werte. Manche Nutzer empfinden `lx` als abstrakter und weniger irreführend als `°C` (das auf Temperaturmessung schließen lässt). Außerdem kollidiert LightSensor weniger mit echten Temperatur-Sensoren wenn man viele davon hat (Thermostate etc.).
45
+
46
+ ## Neuerungen
47
+
48
+ - **Neuer accType `energy-light`** (siehe `src/accessories/smarthome/energy-light/energy-light.accessory.js`). Eigenständiger Service, parallel zu `energy-temperature`. Beide bleiben dauerhaft verfügbar.
49
+ - **`config.schema.json`** mit neuer Option in der accType-Auswahl: "Energy Light (LightSensor, lx unit)".
50
+ - **Wizard Schritt 3** ergänzt um **Display-Auswahl** (Radio-Buttons): "Temperatur-Sensor (°C)" oder "Licht-Sensor (lx)". Pro Tile gilt die Auswahl beim Save.
51
+ - **Auto-Migration:** wenn ein Accessory den accType von `energy-temperature` zu `energy-light` wechselt (z.B. via Wizard-Re-Run), entfernt das Plugin den alten TemperatureSensor-Service sauber vor dem Anlegen des LightSensors.
52
+
53
+ ## Breaking Changes
54
+
55
+ Keine. Bestehende `energy-temperature`-Configs laufen unverändert weiter. Wer auf LightSensor wechseln möchte: pro Smart-Home-Eintrag den `accType`-String von `"energy-temperature"` auf `"energy-light"` ändern, HB neu starten.
56
+
57
+ ## Bekannte Limitationen
58
+
59
+ - **HAP minValue für AmbientLightLevel:** 0.0001 (nicht 0 möglich). Bei aktuellem Wert = 0 W (keine Last oder keine Einspeisung) zeigt das Tile `0.0001 lx` statt `0 lx`. Funktional kein Unterschied, kosmetisch leicht verwirrend für die Sekunde wo nix fließt.
60
+ - **LightSensor in Apple Home Status-Bereich:** standardmäßig wird das Tile nicht direkt im Raum angezeigt sondern in der Status-Zeile oben. Per Long-Press → "Im Zuhause anzeigen" als Favorit setzbar (siehe README).
61
+
62
+ ---
63
+
5
64
  # v1.1.0 — 2026-05-13 (Setup-Wizard Release)
6
65
 
7
66
  **Neue Custom-UI mit Setup-Wizard.** Config UI X öffnet beim Klick auf das Plugin-Settings-Icon ab dieser Version unseren geführten 5-Schritt-Wizard statt des bisherigen JSON-Schema-Formulars. Das macht das Erstsetup deutlich freundlicher — kein Raten mehr bei Feldern wie `accType`, `obisChannel` oder AIN-Format.
package/README.md CHANGED
@@ -25,7 +25,7 @@ Production: läuft produktiv beim Maintainer mit 12 DECT-Thermostaten, 1 DECT-La
25
25
  | FRITZ!Smart Light 500 / FRITZ!DECT 500 (DECT-Lampe) | `lightbulb` | An/Aus, Helligkeit, Farbe |
26
26
  | FRITZ!Smart Control 440 (Temperatursensor) | `temperature` | Temperatur, Batterie |
27
27
  | FRITZ!DECT 200 / 210 (schaltbare Steckdose) | `switch` | An/Aus + optional Energiemessung |
28
- | FRITZ!Smart Energy 250 (OBIS-Reader, an ISKRA o.ä.) | `energy-temperature` | Aktuelle Leistung in W, kumulierte Energie in kWh — als TemperatureSensor exposed, von Apple Home nativ als Zahlen-Tile dargestellt |
28
+ | FRITZ!Smart Energy 250 (OBIS-Reader, an ISKRA o.ä.) | `energy-temperature` oder `energy-light` | Aktuelle Leistung in W, kumulierte Energie in kWh — entweder als TemperatureSensor (°C-Tile, prominent im Raum) oder LightSensor (lx-Tile, passive Sensor-Anzeige, per Favorit ins Home-Tab holbar) |
29
29
  | FRITZ!Box Router (Master) | (via `options`) | Gast-WLAN-Switch, WPS-Switch, optional weitere Toggles |
30
30
 
31
31
  ## Architektur
@@ -182,7 +182,12 @@ Für ein typisches PV-Setup mit Bezug + Einspeisung legst du **vier `smarthome[]
182
182
  }
183
183
  ```
184
184
 
185
- **Wieso TemperatureSensor?** HomeKit hat keinen nativen Service für Leistung oder Energie. TemperatureSensor wird hier als Anzeige-Vehikel "missbraucht": Apple Home rendert den Wert nativ als Zahl, plottet Verlaufsgraphen, und ist auf allen HomeKit-Clients ohne Eve-App-Abhängigkeit sichtbar. **Trade-off:** die Einheit zeigt `°C` an, gemeint sind aber `W` oder `kWh`. In der Home-App das Tile umbenennen ("Aktueller Verbrauch (W)", "Gesamt Einspeisung (kWh)") macht's eindeutig.
185
+ **Wieso TemperatureSensor (oder LightSensor)?** HomeKit hat keinen nativen Service für Leistung oder Energie. Es wird einer der beiden Sensor-Typen als Anzeige-Vehikel "missbraucht":
186
+
187
+ - **`energy-temperature`** (Standard) — TemperatureSensor mit `°C`-Einheit. Apple Home zeigt's prominent als eigenes Tile im Raum-View.
188
+ - **`energy-light`** — LightSensor mit `lx`-Einheit. Apple Home zeigt's als passiven Sensor (Status-Zeile oben). Per Long-Press → "Im Zuhause anzeigen" als Favorit-Tile aktivierbar. Die `lx`-Einheit ist semantisch genauso falsch wie `°C` — viele empfinden sie aber als weniger irreführend, weil seltener im Alltag verwendet.
189
+
190
+ Beide werden seit v1.1.1 unterstützt. Standard ist `energy-temperature`. Empfehlung: probier beide aus, behalte was dir besser gefällt. In der Home-App das Tile umbenennen ("Aktueller Verbrauch (W)", "Gesamt Einspeisung (kWh)") macht den Werte-Kontext eindeutig.
186
191
 
187
192
  **Richtungs-Trennung:** AVM spiegelt den NET-Power-Wert auf beiden Sub-AINs (gleiche Zahl mit Vorzeichen). Das Plugin trennt das anhand des AIN-Suffixes — `-1` zeigt den Wert nur wenn `power > 0` (Bezug), `-2` zeigt `|power|` nur wenn `power < 0` (Einspeisung). Im Ruhe-/Inverse-Zustand zeigt die jeweils andere Kachel `0`.
188
193
 
@@ -667,14 +667,20 @@
667
667
  ]
668
668
  },
669
669
  {
670
- "title": "Energy Temperature (TemperatureSensor, native Home App display)",
670
+ "title": "Energy Temperature (TemperatureSensor, °C unit)",
671
671
  "enum": [
672
672
  "energy-temperature"
673
673
  ]
674
+ },
675
+ {
676
+ "title": "Energy Light (LightSensor, lx unit)",
677
+ "enum": [
678
+ "energy-light"
679
+ ]
674
680
  }
675
681
  ],
676
682
  "required": true,
677
- "description": "The accessory type of the device. Use 'energy-temperature' for Fritz!Smart Energy 250 OBIS sub-AINs to get a numeric value tile in Apple Home (the displayed °C unit must be mentally remapped to W or kWh)."
683
+ "description": "The accessory type of the device. Use 'energy-temperature' or 'energy-light' for Fritz!Smart Energy 250 OBIS sub-AINs — both expose the value as a numeric sensor tile in Apple Home. 'energy-light' uses LightSensor (lx unit) which is a passive sensor and can be added to favorites; 'energy-temperature' uses TemperatureSensor (°C unit) which gets its own tile by default."
678
684
  },
679
685
  "obisChannel": {
680
686
  "title": "OBIS Channel",
@@ -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 {
@@ -198,6 +202,22 @@
198
202
  kumulierte Energie (kWh) je Richtung (Bezug + Einspeisung) anlegen.
199
203
  </div>
200
204
 
205
+ <div id="energy250DisplaySelector" style="margin-top: 1rem; display: none;">
206
+ <label style="font-weight: 600;">Anzeige-Typ in Apple Home:</label>
207
+ <div class="form-check" style="margin-top: 0.5rem;">
208
+ <input type="radio" name="energyDisplay" id="energyDisplay-temp" class="form-check-input" value="energy-temperature" checked>
209
+ <label class="form-check-label" for="energyDisplay-temp">
210
+ <strong>Temperatur-Sensor (°C)</strong> — bekommt automatisch eigene Tiles im Raum-View. Einheit °C ist semantisch falsch aber prominenter dargestellt.
211
+ </label>
212
+ </div>
213
+ <div class="form-check" style="margin-top: 0.5rem;">
214
+ <input type="radio" name="energyDisplay" id="energyDisplay-light" class="form-check-input" value="energy-light">
215
+ <label class="form-check-label" for="energyDisplay-light">
216
+ <strong>Licht-Sensor (lx)</strong> — Apple Home behandelt sie als passive Sensoren (oben im Status-Bereich). Per Long-Press kannst du sie als Favorit ins Home-Tab holen. Einheit lx ist abstrakter als °C, viele User finden's weniger irreführend.
217
+ </label>
218
+ </div>
219
+ </div>
220
+
201
221
  <div id="energy250Content" style="margin-top: 1rem;">
202
222
  <!-- Generated dynamically from wizardState.deviceSelections -->
203
223
  </div>
@@ -304,6 +324,9 @@
304
324
 
305
325
  <div id="saveSummary" style="margin-top: 1rem;"></div>
306
326
 
327
+ <!-- Phase G: diff against existing config (empty on first run) -->
328
+ <div id="saveDiff" style="margin-top: 1rem;"></div>
329
+
307
330
  <details style="margin-top: 1rem;">
308
331
  <summary style="cursor: pointer;">Generierte Plugin-Config anzeigen (JSON-Vorschau)</summary>
309
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>
@@ -327,7 +350,9 @@
327
350
  discoveredDevices: [], // raw discovery output from step 2 load
328
351
  deviceSelections: {}, // { ain: { include: bool, accType: string } } after step 2
329
352
  energyTiles: {}, // { ain: [{ obisChannel, name }, ...] } after step 3
353
+ energyAccType: 'energy-temperature', // 'energy-temperature' | 'energy-light' — chosen in step 3
330
354
  routerSwitches: {}, // { wifi_guest: bool, wps: bool, ... } after step 4
355
+ existingConfig: null, // Phase G: { master, smarthome[] } from current config.json (null = first run)
331
356
  };
332
357
 
333
358
  // ============ DOM References ============
@@ -367,23 +392,59 @@
367
392
  });
368
393
  }
369
394
 
370
- // ============ Step 1 — Connection Test ============
371
- // Prefill from existing plugin config
372
- (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() {
373
407
  try {
374
408
  const allConfig = await homebridge.getPluginConfig();
375
409
  const fritz = (allConfig || []).find((c) => c && c.platform === 'FritzPlatform');
376
- const master = fritz && fritz.devices && fritz.devices.find((d) => d.master === true);
377
- if (master) {
378
- if (master.host) inputs.host.value = master.host;
379
- if (master.port) inputs.port.value = master.port;
380
- if (typeof master.ssl === 'boolean') inputs.ssl.checked = master.ssl;
381
- if (typeof master.tr064 === 'boolean') inputs.tr064.checked = master.tr064;
382
- if (master.username) inputs.username.value = master.username;
383
- if (master.password) inputs.password.value = master.password;
384
- }
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 };
385
414
  } catch (err) {
386
- // 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';
387
448
  }
388
449
  })();
389
450
 
@@ -492,15 +553,41 @@
492
553
  { value: 'skip', label: '— ignorieren —' },
493
554
  ];
494
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
+
495
565
  const html = wizardState.discoveredDevices.map((d) => {
496
566
  // Default-Selection: include if accType detected (not 'unknown' / 'skip')
497
567
  const defaultInclude = d.accType !== 'unknown' && d.accType !== 'skip';
498
568
  if (!wizardState.deviceSelections[d.ain]) {
499
- wizardState.deviceSelections[d.ain] = {
500
- include: defaultInclude,
501
- accType: d.accType,
502
- name: d.name,
503
- };
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
+ }
504
591
  }
505
592
  const sel = wizardState.deviceSelections[d.ain];
506
593
 
@@ -678,8 +765,13 @@
678
765
  }
679
766
 
680
767
  // 2. OBIS Energy 250 tiles from Phase 3 (one entry per (sub-AIN x obisChannel))
768
+ // The accType is the one selected via the Step-3 display-type radio:
769
+ // 'energy-temperature' (default, °C tile) or 'energy-light' (lx sensor)
770
+ const energyAccType = wizardState.energyAccType || 'energy-temperature';
681
771
  for (const [ain, tiles] of Object.entries(wizardState.energyTiles || {})) {
682
- // Only include if user kept this sub-AIN in Phase 2 (in case they went back)
772
+ // Only include if user kept this sub-AIN in Phase 2 as energy-temperature
773
+ // (the wizard always uses energy-temperature in Phase 2 detection — display
774
+ // type is selected in Phase 3 and applied here at save time)
683
775
  const sel = wizardState.deviceSelections[ain];
684
776
  if (!sel || !sel.include || sel.accType !== 'energy-temperature') continue;
685
777
 
@@ -688,7 +780,7 @@
688
780
  active: true,
689
781
  name: tile.name,
690
782
  ain,
691
- accType: 'energy-temperature',
783
+ accType: energyAccType,
692
784
  obisChannel: tile.obisChannel,
693
785
  });
694
786
  }
@@ -705,13 +797,104 @@
705
797
  };
706
798
  }
707
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
+
708
890
  function renderSavePreview() {
709
891
  const config = buildPluginConfig();
710
892
  const summary = document.getElementById('saveSummary');
711
893
  const preview = document.getElementById('savePreview');
894
+ const diffEl = document.getElementById('saveDiff');
712
895
 
713
- const nDevices = config.smarthome.filter((s) => s.accType !== 'energy-temperature').length;
714
- 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;
715
898
  const nRouter = Object.keys((config.devices[0] && config.devices[0].options) || {}).length;
716
899
 
717
900
  // Mask password in preview for safety
@@ -733,6 +916,10 @@
733
916
  `;
734
917
 
735
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);
736
923
  }
737
924
 
738
925
  const saveBtn = document.getElementById('saveBtn');
@@ -843,7 +1030,10 @@
843
1030
  const obisSelections = Object.entries(wizardState.deviceSelections)
844
1031
  .filter(([ain, sel]) => sel.include && sel.accType === 'energy-temperature' && /-[12]$/.test(ain));
845
1032
 
1033
+ const displaySelector = document.getElementById('energy250DisplaySelector');
1034
+
846
1035
  if (obisSelections.length === 0) {
1036
+ displaySelector.style.display = 'none';
847
1037
  container.innerHTML =
848
1038
  '<div class="alert alert-secondary">' +
849
1039
  'Keine OBIS-Sub-AINs in der Geräte-Auswahl gefunden. ' +
@@ -855,6 +1045,21 @@
855
1045
  return;
856
1046
  }
857
1047
 
1048
+ // Show display-type selector when OBIS tiles are present
1049
+ displaySelector.style.display = 'block';
1050
+
1051
+ // Preselect from wizardState (in case user navigated back)
1052
+ const desiredRadio = wizardState.energyAccType === 'energy-light' ? 'energyDisplay-light' : 'energyDisplay-temp';
1053
+ const radioEl = document.getElementById(desiredRadio);
1054
+ if (radioEl) radioEl.checked = true;
1055
+
1056
+ // Wire change handler
1057
+ document.querySelectorAll('input[name="energyDisplay"]').forEach((r) => {
1058
+ r.addEventListener('change', () => {
1059
+ if (r.checked) wizardState.energyAccType = r.value;
1060
+ });
1061
+ });
1062
+
858
1063
  // Group OBIS sub-AINs by their base AIN
859
1064
  const byBase = {};
860
1065
  for (const [ain, sel] of obisSelections) {
@@ -871,13 +1076,50 @@
871
1076
  '-2': { current_power: 'Aktuelle Einspeisung', total_energy: 'Gesamt Einspeisung' },
872
1077
  };
873
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
+
874
1096
  // Initialize state for each OBIS AIN if not already there
875
1097
  for (const baseAin of Object.keys(byBase)) {
876
1098
  const group = byBase[baseAin];
877
1099
  for (const dir of ['-1', '-2']) {
878
1100
  const entry = dir === '-1' ? group.ain1 : group.ain2;
879
1101
  if (!entry) continue;
880
- 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 {
881
1123
  wizardState.energyTiles[entry.ain] = [
882
1124
  { obisChannel: 'current_power', name: defaultNames[dir].current_power },
883
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.0",
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",
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+
3
+ const logger = require('../../../utils/logger');
4
+ const Handler = require('../smarthome.handler');
5
+
6
+ /**
7
+ * Energy-light accessory — same data flow as energy-temperature, but exposes
8
+ * the value as a LightSensor service (CurrentAmbientLightLevel in lx) instead
9
+ * of a TemperatureSensor.
10
+ *
11
+ * Why two service types for the same data? Apple Home renders sensor types
12
+ * differently:
13
+ * - TemperatureSensor: prominent tile in room view, °C unit, history graph
14
+ * - LightSensor: passive sensor (status bar), lx unit, can be made
15
+ * favorite to get a dedicated tile
16
+ *
17
+ * For users who don't want the °C-mislabeling of energy values, this accType
18
+ * uses LightSensor as a more abstract numeric display vehicle. The lx-unit
19
+ * is also incorrect for energy but psychologically less confusing than °C
20
+ * because it is uncommon to interpret it.
21
+ *
22
+ * Designed for AVM Fritz!Smart Energy 250 in OBIS-reader mode — see
23
+ * energy-temperature.accessory.js for full data-flow rationale; this file
24
+ * is a near-clone with the service swapped.
25
+ *
26
+ * Config:
27
+ * accType: "energy-light"
28
+ * ain: "<base-AIN>-1" (Bezug / OBIS 1.8) or "<base-AIN>-2" (Einspeisung / OBIS 2.8)
29
+ * obisChannel:
30
+ * "current_power" -> Math.abs(powermeter.power) in W (default)
31
+ * "total_energy" -> powermeter.energy in kWh
32
+ *
33
+ * AmbientLightLevel value semantics:
34
+ * - lx range: 0.0001 (HAP minimum) up to 100000 by default — we override to
35
+ * 999999 max so multi-year kWh totals fit. minValue stays at 0.0001 (HAP
36
+ * does not allow 0); a value of 0 in the handler maps to 0.0001 at the
37
+ * characteristic level to satisfy validation.
38
+ */
39
+ class Accessory {
40
+ constructor(api, accessory, accessories, meshMaster, HistoryService) {
41
+ this.api = api;
42
+ this.accessory = accessory;
43
+ this.HistoryService = HistoryService;
44
+
45
+ this.handler = Handler.configure(api, accessories, accessory.context.config.polling, meshMaster);
46
+ this.getService();
47
+ }
48
+
49
+ async getService() {
50
+ // Remove any leftover TemperatureSensor service from a previous accType
51
+ // configuration (e.g. user switched from energy-temperature to energy-light).
52
+ const oldTemp = this.accessory.getService(this.api.hap.Service.TemperatureSensor);
53
+ if (oldTemp) {
54
+ logger.info(
55
+ 'Removing legacy TemperatureSensor service (accType switched to energy-light)',
56
+ `${this.accessory.displayName} (${this.accessory.context.config.subtype})`
57
+ );
58
+ this.accessory.removeService(oldTemp);
59
+ }
60
+
61
+ let service = this.accessory.getService(this.api.hap.Service.LightSensor);
62
+
63
+ if (!service) {
64
+ logger.info(
65
+ 'Adding LightSensor service (energy-light, read-only)',
66
+ `${this.accessory.displayName} (${this.accessory.context.config.subtype})`
67
+ );
68
+ service = this.accessory.addService(
69
+ this.api.hap.Service.LightSensor,
70
+ this.accessory.displayName,
71
+ this.accessory.context.config.subtype
72
+ );
73
+ }
74
+
75
+ // Expand the upper bound so high kWh totals (multi-year cumulative) don't
76
+ // get clipped. HAP minValue must stay positive (lx is non-negative).
77
+ service.getCharacteristic(this.api.hap.Characteristic.CurrentAmbientLightLevel).setProps({
78
+ minValue: 0.0001,
79
+ maxValue: 999999,
80
+ minStep: 0.0001,
81
+ });
82
+ }
83
+ }
84
+
85
+ module.exports = Accessory;
@@ -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'];
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];
@@ -680,6 +680,63 @@ class Handler {
680
680
 
681
681
  return state;
682
682
  }
683
+ case 'smarthome-energy-light': {
684
+ // Energy-light: LightSensor variant — same data flow as energy-temperature
685
+ // but uses CurrentAmbientLightLevel (lx) instead of CurrentTemperature (°C)
686
+ // as the display vehicle. HAP requires lx >= 0.0001, so we clamp 0 -> 0.0001.
687
+ let service = accessory.getService(this.api.hap.Service.LightSensor);
688
+
689
+ try {
690
+ let device = this.smarthomeList.devices.find((device) => device.ain.includes(accessory.context.config.ain));
691
+ logger.debug(device, `${accessory.displayName} (${subtype})`);
692
+
693
+ if (device) {
694
+ accessory.context.config.ain = device.ain;
695
+
696
+ if (device.online) {
697
+ if (device.powermeter) {
698
+ const channel = accessory.context.config.obisChannel || 'current_power';
699
+ let value = 0;
700
+
701
+ if (channel === 'current_power') {
702
+ const rawPower = device.powermeter.power || 0;
703
+ const ain = device.ain || accessory.context.config.ain || '';
704
+ if (ain.endsWith('-1')) {
705
+ value = rawPower > 0 ? rawPower : 0;
706
+ } else if (ain.endsWith('-2')) {
707
+ value = rawPower < 0 ? -rawPower : 0;
708
+ } else {
709
+ value = Math.abs(rawPower);
710
+ }
711
+ } else if (channel === 'total_energy') {
712
+ value = device.powermeter.energy || 0;
713
+ }
714
+
715
+ // HAP minimum for CurrentAmbientLightLevel is 0.0001 — clamp 0 to that
716
+ if (value <= 0) value = 0.0001;
717
+ service.getCharacteristic(this.api.hap.Characteristic.CurrentAmbientLightLevel).updateValue(value);
718
+ } else {
719
+ logger.warn(
720
+ 'Can not find powermeter data — sub-AIN (-1 / -2) correct?',
721
+ `${accessory.displayName} (${subtype})`
722
+ );
723
+ }
724
+ } else {
725
+ logger.warn('Device offline!', `${accessory.displayName} (${subtype})`);
726
+ }
727
+ } else {
728
+ logger.warn(
729
+ `Can not find device with AIN: ${accessory.context.config.ain}`,
730
+ `${accessory.displayName} (${subtype})`
731
+ );
732
+ }
733
+ } catch (err) {
734
+ logger.warn('An error occured during getting state!', `${accessory.displayName} (${subtype})`);
735
+ logger.error(err, `${accessory.displayName} (${subtype})`);
736
+ }
737
+
738
+ return true;
739
+ }
683
740
  case 'smarthome-energy-temperature': {
684
741
  // Energy-temperature: TemperatureSensor used to expose either current
685
742
  // power (W) or cumulative energy (kWh) as a numeric value in HomeKit.
@@ -687,9 +744,7 @@ class Handler {
687
744
  let service = accessory.getService(this.api.hap.Service.TemperatureSensor);
688
745
 
689
746
  try {
690
- let device = this.smarthomeList.devices.find((device) =>
691
- device.ain.includes(accessory.context.config.ain)
692
- );
747
+ let device = this.smarthomeList.devices.find((device) => device.ain.includes(accessory.context.config.ain));
693
748
  logger.debug(device, `${accessory.displayName} (${subtype})`);
694
749
 
695
750
  if (device) {
@@ -755,9 +810,7 @@ class Handler {
755
810
  let service = accessory.getService(this.api.hap.Service.Outlet);
756
811
 
757
812
  try {
758
- let device = this.smarthomeList.devices.find((device) =>
759
- device.ain.includes(accessory.context.config.ain)
760
- );
813
+ let device = this.smarthomeList.devices.find((device) => device.ain.includes(accessory.context.config.ain));
761
814
  logger.debug(device, `${accessory.displayName} (${subtype})`);
762
815
 
763
816
  if (device) {
@@ -774,13 +827,9 @@ class Handler {
774
827
  const currentPower = Math.abs(device.powermeter.power || 0);
775
828
  const totalEnergy = device.powermeter.energy || 0;
776
829
 
777
- service
778
- .getCharacteristic(this.api.hap.Characteristic.CurrentConsumption)
779
- .updateValue(currentPower);
830
+ service.getCharacteristic(this.api.hap.Characteristic.CurrentConsumption).updateValue(currentPower);
780
831
 
781
- service
782
- .getCharacteristic(this.api.hap.Characteristic.TotalConsumption)
783
- .updateValue(totalEnergy);
832
+ service.getCharacteristic(this.api.hap.Characteristic.TotalConsumption).updateValue(totalEnergy);
784
833
 
785
834
  service.getCharacteristic(this.api.hap.Characteristic.On).updateValue(true);
786
835
  service.getCharacteristic(this.api.hap.Characteristic.OutletInUse).updateValue(true);
@@ -6,6 +6,7 @@ module.exports = {
6
6
  SHContactAccessory: require('./contact/contact.accessory'),
7
7
  SHEnergyMeterAccessory: require('./energy-meter/energy-meter.accessory'),
8
8
  SHEnergyTemperatureAccessory: require('./energy-temperature/energy-temperature.accessory'),
9
+ SHEnergyLightAccessory: require('./energy-light/energy-light.accessory'),
9
10
  SHHumidityAccessory: require('./humidity/humidity.accessory'),
10
11
  SHLightbulbAccessory: require('./lightbulb/lightbulb.accessory'),
11
12
  SHOutletAccessory: require('./outlet/outlet.accessory'),
package/src/platform.js CHANGED
@@ -27,6 +27,7 @@ const {
27
27
  SHContactAccessory,
28
28
  SHEnergyMeterAccessory,
29
29
  SHEnergyTemperatureAccessory,
30
+ SHEnergyLightAccessory,
30
31
  SHHumidityAccessory,
31
32
  SHLightbulbAccessory,
32
33
  SHOutletAccessory,
@@ -75,7 +76,9 @@ function FritzPlatform(log, config, api) {
75
76
  HistoryService.prototype.addEntry = function () {};
76
77
  HistoryService.prototype.cleanPersist = function () {};
77
78
  HistoryService.prototype.setExtraPersistedData = function () {};
78
- HistoryService.prototype.getExtraPersistedData = function () { return null; };
79
+ HistoryService.prototype.getExtraPersistedData = function () {
80
+ return null;
81
+ };
79
82
 
80
83
  this.api = api;
81
84
  this.log = log;
@@ -208,9 +211,9 @@ FritzPlatform.prototype = {
208
211
  else if (device.subtype === 'smarthome-window-switch')
209
212
  new SHWindowSwitchAccessory(this.api, accessory, this.accessories, this.meshMaster);
210
213
  else if (device.subtype === 'smarthome-lightbulb')
211
- new SHLightbulbAccessory(this.api, accessory, this.accessories, this.meshMaster);
214
+ new SHLightbulbAccessory(this.api, accessory, this.accessories, this.meshMaster);
212
215
  else if (device.subtype === 'smarthome-smoke')
213
- new SHSmokeAccessory(this.api, accessory, this.accessories, this.meshMaster, HistoryService);
216
+ new SHSmokeAccessory(this.api, accessory, this.accessories, this.meshMaster, HistoryService);
214
217
  else if (device.subtype === 'smarthome-switch-lightbulb' && device.energy)
215
218
  new SHOutletLightbulbAccessory(this.api, accessory, this.accessories, this.meshMaster, HistoryService);
216
219
  else if (device.subtype === 'smarthome-switch-lightbulb' && !device.energy)
@@ -219,6 +222,8 @@ FritzPlatform.prototype = {
219
222
  new SHEnergyMeterAccessory(this.api, accessory, this.accessories, this.meshMaster, HistoryService);
220
223
  else if (device.subtype === 'smarthome-energy-temperature')
221
224
  new SHEnergyTemperatureAccessory(this.api, accessory, this.accessories, this.meshMaster, HistoryService);
225
+ else if (device.subtype === 'smarthome-energy-light')
226
+ new SHEnergyLightAccessory(this.api, accessory, this.accessories, this.meshMaster, HistoryService);
222
227
  break;
223
228
  default:
224
229
  logger.warn(`Can not setup accessory, type (${device.type}) unknown!`, device.name);
@@ -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,