@pellebot/homebridge-fritz-platform-hb2 1.2.1 → 1.3.1

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,66 @@
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.3.1 — 2026-11-15 (Wizard: weniger Knöpfe)
6
+
7
+ **Schritt 5 des Setup-Wizards ist aufgeräumt.** Rückmeldung aus dem Betrieb: zu viele Knöpfe für eine Aufgabe.
8
+
9
+ ## Änderungen
10
+
11
+ - Es gibt nur noch einen Speichern-Knopf: Der „Save“-Knopf von Config UI X wird im Wizard ausgegraut (`homebridge.disableSaveButton()`, bei älteren Versionen ohne diese Funktion bleibt er unverändert).
12
+ - Der Knopf „Homebridge jetzt neu starten“ entfällt. Er konnte aus dem Wizard heraus nicht neu starten (die Plugin-UI-Schnittstelle bietet das nicht an) und zeigte danach einen zweiten Hinweiskasten mit `sudo hb-service restart`. Nach dem Speichern erscheint jetzt ein einziger Hinweis: Neustart über das Symbol oben rechts in Config UI X.
13
+ - Einleitungstext von Schritt 5 gekürzt.
14
+
15
+ ## Breaking Changes
16
+
17
+ Keine. Config-Format unverändert.
18
+
19
+ ---
20
+
21
+ # v1.3.0 — 2026-11-15 (Presence als Opt-in)
22
+
23
+ **WLAN-Anwesenheit („Presence“) ist als optionale Funktion zurück.** Auf Anfrage aus der Community (Haushalt nur mit Apple-Geräten) zeigt das Plugin wieder an, ob ein Gerät — typisch ein iPhone — im Netz der FRITZ!Box ist. Standardmäßig ist die Funktion aus; sie wirkt nur für `presence`-Einträge mit `active: true`.
24
+
25
+ ## Neuerungen
26
+
27
+ - **Presence-Sensor je Gerät**: Zuordnung über IPv4- oder MAC-Adresse (Groß-/Kleinschreibung egal), Accessory-Typ `occupancy` (Belegungssensor, Standard) oder `motion` (Bewegungssensor). Grundlage ist der vorhandene Code des Original-Plugins in `src/accessories/presence/`, nach dem Minimal-Patch-Prinzip wieder eingebunden.
28
+ - **Abmeldeverzögerung `offDelay`** (Standard **300 s**): Der Sensor meldet „weg“ erst, wenn das Gerät so lange nicht mehr verbunden war. Verhindert das Springen, wenn ein schlafendes iPhone kurz aus dem WLAN geht. Unter 60 s warnt das Log. `onDelay` (Standard 0 s) für die Anmeldung.
29
+ - **Optional „Anyone“** (jemand ist zu Hause) und **„Guest“** (Gerät im Gast-WLAN) über `options.presence`.
30
+ - **`config.schema.json`**: neuer Abschnitt „Presence (WLAN, optional)“ und `options.presence`.
31
+ - **Setup-Wizard**: Schritt 4 hat einen aufklappbaren Abschnitt „Anwesenheit per WLAN“ (Geräte anlegen, Anyone/Guest). Bestehende `presence`-Einträge werden beim erneuten Speichern übernommen und nicht mehr verworfen.
32
+ - **CI**: Mock-Test `ci/presence-mock-test.cjs` (Opt-in, Verzögerung, MAC-Abgleich, Anyone/Guest) auf Node 22 und 24.
33
+
34
+ ## Korrekturen am übernommenen Code
35
+
36
+ - MAC-Adressen in Kleinbuchstaben wurden bisher verworfen bzw. nicht gefunden; jetzt werden sie normalisiert und ohne Beachtung der Schreibweise abgeglichen.
37
+ - Fehlt `offDelay`, gilt jetzt 300 s statt 0 s (vorher sprang der Sensor bei jeder kurzen WLAN-Abmeldung). Ein ausdrücklich gesetzter Wert, auch 0, bleibt erhalten.
38
+ - Aufruf von `historyService.getInitialTime()` im Motion-Pfad wird defensiv geprüft (siehe Issue #2).
39
+ - `src/accessories/presence/` ist nicht mehr in `.npmignore` ausgeschlossen (sonst fehlte der Code im npm-Paket).
40
+
41
+ ## Nicht übernommen
42
+
43
+ Ping-Prüfung (`ping`, `threshold`) und Telegram-Meldungen (`blockTelegram`) des Original-Plugins.
44
+
45
+ ## Breaking Changes
46
+
47
+ Keine. Ohne `presence`-Konfiguration verhält sich das Plugin wie v1.2.x. Wer aus dem Original-Plugin noch `presence`-Einträge mit `active: true` in der Config hat, bekommt diese Sensoren jetzt wieder angezeigt.
48
+
49
+ ## Getestet
50
+
51
+ Mock-Test in der CI (Node 22/24) und Hardwaretest am 15.11.2026: Homebridge 2.4.0, FRITZ!Box mit drei FRITZ!Repeatern im Mesh, iPhone mit fester privater WLAN-Adresse. Anmeldung erkannt, 25 min gesperrtes iPhone ohne Fehlwechsel, Abmeldung nach 309 s (300 s Verzögerung + Polling), erneute Anmeldung erkannt. Nicht getestet: Dauerlauf über Nacht, Motion-Typ, Gast-WLAN.
52
+
53
+ ## Bekannte Limitationen
54
+
55
+ - iPhones mit „private WLAN-Adresse: Rotierend“ wechseln die MAC; für das Heim-WLAN auf „Fest“ stellen (README).
56
+ - Die am iPhone angezeigte WLAN-Adresse kann von der abweichen, die die FRITZ!Box führt (beobachtet mit FRITZ!Repeatern im Mesh). MAC-Adresse aus der FRITZ!Box-Oberfläche nehmen oder besser die IPv4-Adresse dort dauerhaft zuweisen und die IP eintragen.
57
+ - Je Sensor nur eine Adresse.
58
+ - Die FRITZ!Box meldet nur „verbunden“, nicht „Person anwesend“. Erkennung frühestens nach einem Polling-Zyklus (`options.polling.timer`) plus Verzögerung.
59
+ - Jeder Zyklus fragt die Host-Liste der FRITZ!Box ab (zusätzliche Last).
60
+ - Der Bewegungssensor zeigt in der Eve-App keinen Verlauf (fakegato gestubbt).
61
+ - Die Charakteristik „Last Activation“ des Bewegungssensors zählt ab Start der Bridge (fakegato-Stub mit `getInitialTime()` aus v1.2.1).
62
+
63
+ ---
64
+
5
65
  # v1.2.1 — 2026-11-14 (Bugfix: Absturz bei offenem Fenster)
6
66
 
7
67
  **Behebt #2.** Sobald ein Thermostat ein offenes Fenster meldete (`windowOpen: 1`) oder ein Kontakt-/Fenstersensor öffnete, stürzte die Child-Bridge mit `TypeError: historyService.getInitialTime is not a function` ab. Nach fünf Abstürzen stoppte Homebridge die Child-Bridge, und das ganze Plugin fiel aus, solange das Fenster offen war.
@@ -19,6 +79,8 @@ Der NoOp-Ersatz für fakegato-history in `src/platform.js` bildete nur `addEntry
19
79
 
20
80
  Keine.
21
81
 
82
+ ---
83
+
22
84
  # v1.2.0 — 2026-05-14 (Wizard Edit-Mode / Phase G)
23
85
 
24
86
  **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.
package/README.md CHANGED
@@ -27,6 +27,7 @@ Production: läuft produktiv beim Maintainer mit 12 DECT-Thermostaten, 1 DECT-La
27
27
  | FRITZ!DECT 200 / 210 (schaltbare Steckdose) | `switch` | An/Aus + optional Energiemessung |
28
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
+ | Handy/Tablet im WLAN (optional, seit v1.3.0) | `presence[]` mit `occupancy` oder `motion` | Anwesenheit: Belegungs- oder Bewegungssensor je Gerät, optional „Anyone“ und „Guest“ |
30
31
 
31
32
  ## Architektur
32
33
 
@@ -42,13 +43,14 @@ Das **Router-Accessory** (Master FritzBox) wird per default mit `hide: true` aus
42
43
  Aus dem ursprünglichen Code-Baum entfernt oder nicht migriert — wer eine dieser Funktionen braucht, ist hier nicht richtig:
43
44
 
44
45
  - **Callmonitor** (Anrufüberwachung)
45
- - **Presence** (WLAN-Anwesenheit)
46
46
  - **Network-Watcher**
47
47
  - **Wake-on-LAN**
48
48
  - **Child Lock** (Internet-Sperren)
49
49
  - **Extras-Subsysteme** (DNS-Switch, Fallback-Internet, Phonebook, Telegram-Notifications) — die WiFi/WPS-Switches der Extras-Datei sind ausgenommen und voll funktional
50
50
  - **`fakegato-history`** (Eve-App History-Graphen — als No-Op gestubbt weil HAP-v2-inkompatibel)
51
51
 
52
+ **Presence** (WLAN-Anwesenheit) war bis v1.2.0 ausgeschlossen und ist seit **v1.3.0 als optionale Funktion** zurück — standardmäßig aus, siehe [Anwesenheit per WLAN](#anwesenheit-per-wlan-optional-seit-v130). Telegram-Meldungen und die Ping-Prüfung des Original-Plugins gehören nicht dazu.
53
+
52
54
  ## Voraussetzungen
53
55
 
54
56
  - **Homebridge** ≥ 1.8.5 oder ≥ 2.0.0
@@ -191,6 +193,50 @@ Beide werden seit v1.1.1 unterstützt. Standard ist `energy-temperature`. Empfeh
191
193
 
192
194
  **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`.
193
195
 
196
+ ### Anwesenheit per WLAN (optional, seit v1.3.0)
197
+
198
+ Zeigt in HomeKit, ob ein Gerät (typisch: iPhone) im Netz der FRITZ!Box ist — als **Belegungssensor** (`occupancy`, Standard) oder **Bewegungssensor** (`motion`). Damit lassen sich Automationen wie „Letzte Person geht → Heizung runter“ bauen, ohne Standortfreigabe in der Home-App.
199
+
200
+ Die Funktion ist **Opt-in**: Ohne `presence`-Einträge mit `active: true` passiert nichts. Einrichtung entweder im Setup-Wizard (Schritt 4, Abschnitt „Anwesenheit per WLAN“) oder in der `config.json`:
201
+
202
+ ```json
203
+ {
204
+ "presence": [
205
+ {
206
+ "active": true,
207
+ "name": "iPhone Anna",
208
+ "address": "AA:BB:CC:DD:EE:FF",
209
+ "accType": "occupancy",
210
+ "offDelay": 300
211
+ }
212
+ ],
213
+ "options": {
214
+ "presence": { "anyone": true, "guest": false, "accType": "occupancy" }
215
+ }
216
+ }
217
+ ```
218
+
219
+ | Feld | Bedeutung |
220
+ |---|---|
221
+ | `address` | IPv4- oder MAC-Adresse wie in der FRITZ!Box unter *Heimnetz → Netzwerk* angezeigt (Groß-/Kleinschreibung egal) |
222
+ | `accType` | `occupancy` (Belegung) oder `motion` (Bewegung) |
223
+ | `onDelay` | Sekunden, die das Gerät verbunden sein muss, bevor der Sensor „erkannt“ meldet (Standard 0) |
224
+ | `offDelay` | Sekunden, die das Gerät weg sein muss, bevor der Sensor „nicht erkannt“ meldet (**Standard 300**) |
225
+ | `options.presence.anyone` | zusätzlicher Sensor „Anyone“: erkannt, solange mindestens ein Gerät erkannt ist |
226
+ | `options.presence.guest` | zusätzlicher Sensor „Guest“: erkannt, solange ein Gerät im Gast-WLAN ist |
227
+
228
+ **Wichtig für iPhone/iPad — private WLAN-Adresse:** iOS nutzt je WLAN eine eigene „private WLAN-Adresse“, die sich je nach Einstellung (*Rotierend*) regelmäßig ändert. Damit die Zuordnung hält:
229
+
230
+ 1. Auf dem iPhone *Einstellungen → WLAN → (i) neben dem Heim-WLAN → Private WLAN-Adresse* auf **Fest** stellen (oder ausschalten). Die neue Adresse gilt erst nach dem nächsten Verbinden: WLAN einmal aus- und wieder einschalten.
231
+ 2. In der FRITZ!Box das Gerät öffnen (*Heimnetz → Netzwerk → Gerät → Heimnetz*) und die **MAC-Adresse von dort** übernehmen, **nicht** die am iPhone angezeigte. Beide können abweichen, im Test mit FRITZ!Repeatern im Mesh war das der Fall.
232
+ 3. **Besser:** dort **„IPv4-Adresse dauerhaft zuweisen“** aktivieren und diese IP als `address` eintragen. Das ist die robusteste Zuordnung.
233
+
234
+ Bleibt ein Sensor dauerhaft auf „nicht erkannt“, obwohl das Gerät im WLAN ist: in der FRITZ!Box nachsehen, unter welcher MAC/IP es gerade geführt wird, und die Config anpassen.
235
+
236
+ **Warum die Abmeldeverzögerung:** Schlafende iPhones trennen sich zum Stromsparen immer wieder kurz vom WLAN. Ohne Verzögerung springt der Sensor ständig zwischen „zu Hause“ und „weg“. `offDelay` unter 60 s erzeugt deshalb eine Warnung im Log; 300 s (5 min) ist ein guter Startwert, bei Fehlalarmen auf 600–900 s erhöhen. Die Erkennung ist so schnell wie das Polling (`options.polling.timer`, Standard 15 s) plus Verzögerung.
237
+
238
+ **Grenzen:** Die FRITZ!Box meldet nur, ob das Gerät verbunden ist — nicht, ob es wirklich jemand bei sich trägt. Jeder Abfragezyklus fragt die Host-Liste der FRITZ!Box ab (zusätzliche Last bei vielen Geräten oder kurzem Polling-Intervall). Meldet das Log beim Abruf der Host-Liste einen Berechtigungsfehler, dem FRITZ!Box-Benutzer zusätzlich *FRITZ!Box-Einstellungen* geben (wie bei den Router-Switches). Beim Bewegungssensor zeigt die Eve-App keine Verlaufsgrafik (fakegato ist gestubbt).
239
+
194
240
  ## Migration vom Original-Plugin
195
241
 
196
242
  Wenn du bisher das unmaintained Original `homebridge-fritz-platform@6.0.19` eingesetzt hast:
@@ -199,7 +245,7 @@ Wenn du bisher das unmaintained Original `homebridge-fritz-platform@6.0.19` eing
199
245
  2. **Update Homebridge auf 2.x**: `sudo npm install -g homebridge@latest`
200
246
  3. **Original-Plugin entfernen**: `sudo npm uninstall -g homebridge-fritz-platform`
201
247
  4. **Dieses Plugin installieren**: `sudo npm install -g @pellebot/homebridge-fritz-platform-hb2`
202
- 5. **Config anpassen**: nicht-mehr-unterstützte Subsysteme aus der Config entfernen (`presence`, `callmonitor`, `wol`, `childLock`, `network`, `extras`, `telegram` — werden eh ignoriert, aber sauberer wenn weg).
248
+ 5. **Config anpassen**: nicht-mehr-unterstützte Subsysteme aus der Config entfernen (`callmonitor`, `wol`, `childLock`, `network`, `extras`, `telegram` — werden eh ignoriert, aber sauberer wenn weg). Bestehende `presence`-Einträge werden seit v1.3.0 wieder ausgewertet; `offDelay` prüfen (Standard jetzt 300 s), `ping`/`threshold`/`blockTelegram` werden nicht unterstützt.
203
249
  6. **Homebridge neu starten**
204
250
 
205
251
  Accessory-UUIDs basieren auf AIN + Name. Wer Device-Namen unverändert lässt, behält das HomeKit-Pairing bei und muss nichts in der Home-App neu zuordnen.
@@ -708,6 +708,69 @@
708
708
  }
709
709
  }
710
710
  },
711
+ "presence": {
712
+ "title": "Presence (WLAN, optional)",
713
+ "type": "array",
714
+ "description": "Opt-in since v1.3.0. Exposes an occupancy or motion sensor that shows whether a device (e.g. an iPhone) is connected to the FRITZ!Box network. Off unless an entry is set to active.",
715
+ "items": {
716
+ "type": "object",
717
+ "properties": {
718
+ "active": {
719
+ "title": "Active",
720
+ "type": "boolean",
721
+ "default": false,
722
+ "description": "If enabled, the presence sensor will be exposed to HomeKit."
723
+ },
724
+ "name": {
725
+ "title": "Name",
726
+ "type": "string",
727
+ "placeholder": "iPhone Andreas",
728
+ "description": "Name of the sensor in HomeKit. Must be unique and must not be \"Anyone\" or \"Guest\".",
729
+ "required": true
730
+ },
731
+ "address": {
732
+ "title": "IP or MAC address",
733
+ "type": "string",
734
+ "placeholder": "AA:BB:CC:DD:EE:FF",
735
+ "description": "IPv4 or MAC address of the device as shown in the FRITZ!Box (Heimnetz > Netzwerk). iPhones use a private Wi-Fi address: set it to \"Fixed\" for the home network, reconnect once, then take the MAC address from the FRITZ!Box device details (not the one shown on the phone) or, better, assign a permanent IPv4 address there and use that IP.",
736
+ "required": true
737
+ },
738
+ "accType": {
739
+ "title": "Accessory Type",
740
+ "type": "string",
741
+ "default": "occupancy",
742
+ "oneOf": [
743
+ {
744
+ "title": "Occupancy Sensor",
745
+ "enum": [
746
+ "occupancy"
747
+ ]
748
+ },
749
+ {
750
+ "title": "Motion Sensor",
751
+ "enum": [
752
+ "motion"
753
+ ]
754
+ }
755
+ ]
756
+ },
757
+ "onDelay": {
758
+ "title": "Arrival delay (seconds)",
759
+ "type": "integer",
760
+ "minimum": 0,
761
+ "placeholder": 0,
762
+ "description": "Seconds the device must be connected before the sensor switches to detected. Default 0."
763
+ },
764
+ "offDelay": {
765
+ "title": "Leave delay (seconds)",
766
+ "type": "integer",
767
+ "minimum": 0,
768
+ "placeholder": 300,
769
+ "description": "Seconds the device must be disconnected before the sensor switches to not detected. Default 300. Sleeping iPhones leave the Wi-Fi for short periods; values below 60 make the sensor flap."
770
+ }
771
+ }
772
+ }
773
+ },
711
774
  "options": {
712
775
  "titel": "Options",
713
776
  "type": "object",
@@ -740,6 +803,43 @@
740
803
  }
741
804
  }
742
805
  }
806
+ },
807
+ "presence": {
808
+ "title": "Presence",
809
+ "type": "object",
810
+ "properties": {
811
+ "anyone": {
812
+ "title": "\"Anyone\" sensor",
813
+ "type": "boolean",
814
+ "default": false,
815
+ "description": "Additional sensor that is detected as long as at least one active presence device is detected."
816
+ },
817
+ "guest": {
818
+ "title": "\"Guest\" sensor",
819
+ "type": "boolean",
820
+ "default": false,
821
+ "description": "Additional sensor that is detected while at least one device is connected to the guest WLAN."
822
+ },
823
+ "accType": {
824
+ "title": "Accessory Type for Anyone/Guest",
825
+ "type": "string",
826
+ "default": "occupancy",
827
+ "oneOf": [
828
+ {
829
+ "title": "Occupancy Sensor",
830
+ "enum": [
831
+ "occupancy"
832
+ ]
833
+ },
834
+ {
835
+ "title": "Motion Sensor",
836
+ "enum": [
837
+ "motion"
838
+ ]
839
+ }
840
+ ]
841
+ }
842
+ }
743
843
  }
744
844
  }
745
845
  }
@@ -902,6 +1002,33 @@
902
1002
  }
903
1003
  ]
904
1004
  },
1005
+ {
1006
+ "title": "Presence (WLAN, optional)",
1007
+ "description": "Opt-in: shows in HomeKit whether a phone is in the home network. Leave empty to keep the feature off.",
1008
+ "expandable": true,
1009
+ "expanded": false,
1010
+ "orderable": false,
1011
+ "type": "section",
1012
+ "items": [
1013
+ {
1014
+ "key": "presence",
1015
+ "type": "array",
1016
+ "orderable": false,
1017
+ "buttonText": "Add Presence Device",
1018
+ "items": [
1019
+ "presence[].active",
1020
+ "presence[].name",
1021
+ "presence[].address",
1022
+ "presence[].accType",
1023
+ "presence[].onDelay",
1024
+ "presence[].offDelay"
1025
+ ]
1026
+ },
1027
+ "options.presence.anyone",
1028
+ "options.presence.guest",
1029
+ "options.presence.accType"
1030
+ ]
1031
+ },
905
1032
  {
906
1033
  "title": "Options",
907
1034
  "orderable": false,
@@ -7,7 +7,7 @@
7
7
  C. Device discovery — Step 2 with auto-detected accType per device
8
8
  D. Energy 250 OBIS-Reader auto-setup — Step 3 (4 tiles per reader)
9
9
  E. Router-Switches — Step 4 (master.options pre-hydrated from config)
10
- F. Save + Apply — Step 5 (writes config.json, restarts Homebridge)
10
+ F. Save — Step 5 (writes config.json; restart via the Config UI X toolbar)
11
11
  G. Edit-Mode — re-run preserves user edits (tile names, accType, energy
12
12
  display type, router-switches) by hydrating wizardState from the
13
13
  existing config.json. Step 5 shows a diff of pending changes.
@@ -307,6 +307,40 @@
307
307
  </div>
308
308
  </details>
309
309
 
310
+ <!-- v1.3.0: optional WLAN presence (opt-in) -->
311
+ <details id="presenceDetails" style="margin-top: 1rem;">
312
+ <summary style="cursor: pointer;"><strong>Anwesenheit per WLAN (optional)</strong> — zeigt in HomeKit, ob ein Handy im Heimnetz ist</summary>
313
+ <div style="margin-top: 0.5rem; padding: 0.5rem; border-left: 3px solid #339DE0;">
314
+ <p style="font-size: 0.9rem;">
315
+ Je Gerät ein Sensor (Belegung oder Bewegung). Zuordnung über die IP- oder MAC-Adresse,
316
+ wie sie die FRITZ!Box unter <em>Heimnetz → Netzwerk</em> anzeigt.
317
+ <strong>iPhone/iPad:</strong> In den WLAN-Einstellungen des Geräts für das Heim-WLAN
318
+ <em>Private WLAN-Adresse → Fest</em> (bzw. „Aus“) wählen, WLAN einmal neu verbinden und dann die
319
+ MAC-Adresse <strong>aus der FRITZ!Box-Oberfläche</strong> nehmen (nicht die am iPhone angezeigte) oder besser
320
+ dort die IPv4-Adresse dauerhaft zuweisen und die IP eintragen.
321
+ Die <em>Abmeldeverzögerung</em> verhindert, dass der Sensor springt, wenn ein schlafendes
322
+ iPhone kurz aus dem WLAN geht (Standard 300 s).
323
+ </p>
324
+ <div id="presenceList"></div>
325
+ <button id="presenceAdd" type="button" class="btn btn-sm btn-outline-primary">+ Gerät hinzufügen</button>
326
+ <div style="margin-top: 0.75rem;">
327
+ <div class="form-check">
328
+ <input type="checkbox" id="presenceAnyone" class="form-check-input">
329
+ <label class="form-check-label" for="presenceAnyone">Zusätzlicher Sensor <em>„Anyone“</em> (jemand ist zu Hause)</label>
330
+ </div>
331
+ <div class="form-check">
332
+ <input type="checkbox" id="presenceGuest" class="form-check-input">
333
+ <label class="form-check-label" for="presenceGuest">Zusätzlicher Sensor <em>„Guest“</em> (Gerät im Gast-WLAN)</label>
334
+ </div>
335
+ <label for="presenceAccType" style="font-size: 0.9rem; margin-top: 0.25rem;">Typ für Anyone/Guest:</label>
336
+ <select id="presenceAccType" class="form-select form-select-sm" style="max-width: 220px; display: inline-block;">
337
+ <option value="occupancy">Belegungssensor</option>
338
+ <option value="motion">Bewegungssensor</option>
339
+ </select>
340
+ </div>
341
+ </div>
342
+ </details>
343
+
310
344
  <div class="wizard-nav">
311
345
  <button id="backTo3" class="btn btn-outline-secondary">← Zurück zu Schritt 3</button>
312
346
  <button id="goTo5" class="btn btn-primary">Weiter zu Schritt 5 →</button>
@@ -317,9 +351,8 @@
317
351
  <div class="step-pane" id="step-5">
318
352
  <div class="alert alert-info">
319
353
  <strong>Schritt 5 — Konfiguration speichern</strong><br>
320
- Zusammenfassung deiner Auswahl. Klick auf <em>"Konfiguration speichern"</em>
321
- unten um die <code>config.json</code> zu aktualisieren. Danach musst du
322
- Homebridge neu starten damit die Änderungen wirksam werden.
354
+ Prüfe die Zusammenfassung und speichere. Wirksam wird die Änderung nach einem
355
+ Neustart von Homebridge.
323
356
  </div>
324
357
 
325
358
  <div id="saveSummary" style="margin-top: 1rem;"></div>
@@ -352,9 +385,19 @@
352
385
  energyTiles: {}, // { ain: [{ obisChannel, name }, ...] } after step 3
353
386
  energyAccType: 'energy-temperature', // 'energy-temperature' | 'energy-light' — chosen in step 3
354
387
  routerSwitches: {}, // { wifi_guest: bool, wps: bool, ... } after step 4
355
- existingConfig: null, // Phase G: { master, smarthome[] } from current config.json (null = first run)
388
+ existingConfig: null, // Phase G: { master, smarthome[], presence[], presenceOptions } from current config.json (null = first run)
389
+ presence: null, // v1.3.0: [{ active, name, address, accType, onDelay?, offDelay }] (null = not touched yet)
390
+ presenceOptions: null, // v1.3.0: { anyone, guest, accType }
356
391
  };
357
392
 
393
+ // v1.3.1: the wizard saves via its own button. Grey out the Config UI X
394
+ // "Save" button so there is exactly one way to save.
395
+ try {
396
+ homebridge.disableSaveButton();
397
+ } catch (e) {
398
+ // older Config UI X without this API: leave the button as it is
399
+ }
400
+
358
401
  // ============ DOM References ============
359
402
  const stepPanes = {
360
403
  1: document.getElementById('step-1'),
@@ -410,7 +453,10 @@
410
453
  if (!fritz) return null;
411
454
  const master = (fritz.devices || []).find((d) => d && d.master === true) || null;
412
455
  const smarthome = Array.isArray(fritz.smarthome) ? fritz.smarthome : [];
413
- return { master, smarthome };
456
+ // v1.3.0: keep presence entries so a wizard re-run does not drop them
457
+ const presence = Array.isArray(fritz.presence) ? fritz.presence : [];
458
+ const presenceOptions = (fritz.options && fritz.options.presence) || null;
459
+ return { master, smarthome, presence, presenceOptions };
414
460
  } catch (err) {
415
461
  return null;
416
462
  }
@@ -670,17 +716,85 @@
670
716
  goTo4.addEventListener('click', () => {
671
717
  showStep(4);
672
718
  hydrateRouterSwitches();
719
+ hydratePresence();
673
720
  });
674
721
  backTo3.addEventListener('click', () => showStep(3));
675
722
 
676
723
  const goTo5 = document.getElementById('goTo5');
677
724
  const backTo4 = document.getElementById('backTo4');
678
725
  goTo5.addEventListener('click', () => {
726
+ collectPresence();
679
727
  showStep(5);
680
728
  renderSavePreview();
681
729
  });
682
730
  backTo4.addEventListener('click', () => showStep(4));
683
731
 
732
+ // ============ Step 4b — Presence (optional, v1.3.0) ============
733
+ const presenceList = document.getElementById('presenceList');
734
+
735
+ function presenceRow(entry) {
736
+ const e = entry || { active: true, name: '', address: '', accType: 'occupancy', offDelay: 300 };
737
+ const row = document.createElement('div');
738
+ row.className = 'presence-row device-card';
739
+ row.style.cssText = 'display:flex; flex-wrap:wrap; gap:0.5rem; align-items:center; margin-bottom:0.5rem;';
740
+ if (typeof e.onDelay === 'number') row.dataset.onDelay = String(e.onDelay);
741
+ row.innerHTML = `
742
+ <input type="checkbox" class="form-check-input p-active" title="Aktiv" ${e.active ? 'checked' : ''}>
743
+ <input type="text" class="form-control form-control-sm p-name" placeholder="Name, z.B. iPhone Anna" style="max-width:180px;" value="${escapeHtml(e.name || '')}">
744
+ <input type="text" class="form-control form-control-sm p-address" placeholder="IP oder MAC" style="max-width:170px;" value="${escapeHtml(e.address || '')}">
745
+ <select class="form-select form-select-sm p-acctype" style="max-width:150px;">
746
+ <option value="occupancy" ${e.accType !== 'motion' ? 'selected' : ''}>Belegung</option>
747
+ <option value="motion" ${e.accType === 'motion' ? 'selected' : ''}>Bewegung</option>
748
+ </select>
749
+ <label style="font-size:0.8rem;">Abmeldung nach
750
+ <input type="number" min="0" class="form-control form-control-sm p-offdelay" style="width:80px; display:inline-block;" value="${typeof e.offDelay === 'number' ? e.offDelay : 300}"> s</label>
751
+ <button type="button" class="btn btn-sm btn-outline-danger p-remove" title="Entfernen">✕</button>
752
+ `;
753
+ row.querySelector('.p-remove').addEventListener('click', () => row.remove());
754
+ presenceList.appendChild(row);
755
+ }
756
+
757
+ function hydratePresence() {
758
+ if (presenceList.dataset.hydrated) return; // keep user edits when navigating back
759
+ presenceList.dataset.hydrated = '1';
760
+ const cfg = wizardState.existingConfig || {};
761
+ const entries = cfg.presence || [];
762
+ entries.forEach((e) => presenceRow(e));
763
+ const opts = cfg.presenceOptions || {};
764
+ document.getElementById('presenceAnyone').checked = !!opts.anyone;
765
+ document.getElementById('presenceGuest').checked = !!opts.guest;
766
+ document.getElementById('presenceAccType').value = opts.accType === 'motion' ? 'motion' : 'occupancy';
767
+ if (entries.length || opts.anyone || opts.guest) document.getElementById('presenceDetails').open = true;
768
+ }
769
+
770
+ document.getElementById('presenceAdd').addEventListener('click', () => presenceRow(null));
771
+
772
+ function collectPresence() {
773
+ if (!presenceList.dataset.hydrated) return; // step 4 never shown — keep existing as is
774
+ const entries = [];
775
+ presenceList.querySelectorAll('.presence-row').forEach((row) => {
776
+ const name = row.querySelector('.p-name').value.trim();
777
+ const address = row.querySelector('.p-address').value.trim();
778
+ if (!name && !address) return;
779
+ const off = parseInt(row.querySelector('.p-offdelay').value, 10);
780
+ const entry = {
781
+ active: row.querySelector('.p-active').checked,
782
+ name,
783
+ address,
784
+ accType: row.querySelector('.p-acctype').value,
785
+ offDelay: Number.isFinite(off) && off >= 0 ? off : 300,
786
+ };
787
+ if (row.dataset.onDelay) entry.onDelay = parseInt(row.dataset.onDelay, 10);
788
+ entries.push(entry);
789
+ });
790
+ wizardState.presence = entries;
791
+ wizardState.presenceOptions = {
792
+ anyone: document.getElementById('presenceAnyone').checked,
793
+ guest: document.getElementById('presenceGuest').checked,
794
+ accType: document.getElementById('presenceAccType').value,
795
+ };
796
+ }
797
+
684
798
  // ============ Step 5 — Build + Save ============
685
799
 
686
800
  /**
@@ -786,7 +900,7 @@
786
900
  }
787
901
  }
788
902
 
789
- return {
903
+ const config = {
790
904
  platform: 'FritzPlatform',
791
905
  name: 'FritzBox',
792
906
  devices: [master],
@@ -795,6 +909,19 @@
795
909
  polling: { timer: 30 },
796
910
  },
797
911
  };
912
+
913
+ // v1.3.0: presence (opt-in). Use the wizard input if step 4 was shown,
914
+ // otherwise carry over what is already in config.json.
915
+ const existing = wizardState.existingConfig || {};
916
+ const presence = wizardState.presence !== null ? wizardState.presence : existing.presence || [];
917
+ const presenceOptions =
918
+ wizardState.presenceOptions !== null ? wizardState.presenceOptions : existing.presenceOptions || null;
919
+ if (presence.length) config.presence = presence;
920
+ if (presenceOptions && (presenceOptions.anyone || presenceOptions.guest)) {
921
+ config.options.presence = presenceOptions;
922
+ }
923
+
924
+ return config;
798
925
  }
799
926
 
800
927
  /**
@@ -868,6 +995,13 @@
868
995
  lines.push(`Entfernt: <code>${escapeHtml(o.ain)}</code>${chan} — war <em>${escapeHtml(o.accType)}</em> "${escapeHtml(o.name || '')}"`);
869
996
  }
870
997
 
998
+ // -- Presence (v1.3.0) --
999
+ const oldPresence = (existing.presence || []).filter((p) => p && p.active).map((p) => `${p.name}|${p.address}|${p.accType || 'occupancy'}|${p.offDelay}`);
1000
+ const newPresence = (newConfig.presence || []).filter((p) => p && p.active).map((p) => `${p.name}|${p.address}|${p.accType || 'occupancy'}|${p.offDelay}`);
1001
+ if (oldPresence.sort().join(';') !== newPresence.sort().join(';')) {
1002
+ lines.push(`Anwesenheit: ${oldPresence.length} → ${newPresence.length} aktive(r) Sensor(en)`);
1003
+ }
1004
+
871
1005
  if (lines.length === 0) {
872
1006
  return `
873
1007
  <div class="alert alert-secondary">
@@ -911,6 +1045,7 @@
911
1045
  <li><strong>${nDevices}</strong> Smart-Home-Gerät(e) (Thermostate, Lampen, Sensoren, Schalter)</li>
912
1046
  <li><strong>${nEnergy}</strong> Energy-Tile(s) für FRITZ!Smart Energy 250 OBIS-Reader</li>
913
1047
  <li><strong>${nRouter}</strong> Router-Switch(es) als HomeKit-Tiles</li>
1048
+ <li><strong>${(config.presence || []).filter((p) => p.active).length}</strong> Anwesenheitssensor(en) (WLAN, optional)</li>
914
1049
  </ul>
915
1050
  </div>
916
1051
  `;
@@ -948,31 +1083,10 @@
948
1083
 
949
1084
  saveResult.innerHTML = `
950
1085
  <div class="alert alert-success">
951
- <strong>✓ Konfiguration gespeichert.</strong><br>
952
- Damit die Änderungen wirksam werden, muss Homebridge neu gestartet werden.
953
- </div>
954
- <div class="text-center" style="margin-top: 1rem;">
955
- <button id="restartBtn" class="btn btn-warning btn-lg">🔄 Homebridge jetzt neu starten</button>
956
- <div style="margin-top: 0.5rem;">
957
- <small class="text-muted">Alternativ später via Config UI X "Restart"-Button.</small>
958
- </div>
1086
+ <strong>✓ Gespeichert.</strong>
1087
+ Zum Aktivieren Homebridge neu starten: in Config UI X oben rechts über das Neustart-Symbol.
959
1088
  </div>
960
1089
  `;
961
-
962
- document.getElementById('restartBtn').addEventListener('click', async () => {
963
- try {
964
- await homebridge.restartHomebridge();
965
- } catch (err) {
966
- // If restartHomebridge isn't available (e.g. standalone mode without
967
- // hb-service), show a manual instruction.
968
- saveResult.innerHTML +=
969
- '<div class="alert alert-info" style="margin-top: 1rem;">' +
970
- 'Automatischer Restart nicht verfügbar. Bitte Homebridge manuell ' +
971
- 'neu starten: <code>sudo hb-service restart</code> oder über die ' +
972
- 'Config UI X Toolbar.' +
973
- '</div>';
974
- }
975
- });
976
1090
  } catch (err) {
977
1091
  saveResult.innerHTML = `
978
1092
  <div class="alert alert-danger">
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@pellebot/homebridge-fritz-platform-hb2",
3
3
  "displayName": "Homebridge FRITZ!Box Platform (HB2)",
4
- "version": "1.2.1",
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.",
4
+ "version": "1.3.1",
5
+ "description": "Homebridge 2.0 plugin for AVM FRITZ!DECT devices (thermostats, lamps, sensors, OBIS smart-meter readers) FRITZ!Box router switches (Guest WLAN, WPS) and optional WLAN presence sensors. Node 22 / 24 ready.",
6
6
  "icon": "https://raw.githubusercontent.com/PelleBot/homebridge-fritz-platform-ng/main/branding/Icon.png",
7
7
  "type": "module",
8
8
  "main": "index.mjs",
@@ -42,7 +42,8 @@
42
42
  "avm",
43
43
  "thermostat",
44
44
  "fritz-dect",
45
- "homebridge-2"
45
+ "homebridge-2",
46
+ "presence"
46
47
  ],
47
48
  "engines": {
48
49
  "node": "^22.12.0 || ^24.0.0",