Repository navigation
Expand file tree
/
Copy pathversion.h
More file actions
2132 lines (2132 loc) · 136 KB
/
Copy pathversion.h
File metadata and controls
2132 lines (2132 loc) · 136 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#pragma once
// Changelog:
// 3.23.1 - KARENZFENSTER UEBERLAUFSICHER. Nach jedem Verbinden verwirft die
// Firmware 5 s lang alle Set-Kommandos (SUBSCRIBE_GRACE, 3.6.1). Die
// Frist stand als millis() + 5000 in der Firmware und wurde mit
// (long)(millis() - frist) < 0 geprueft. Fehlerbehebung, keine neue
// Funktion.
//
// PROBLEM. Das Idiom ist nur fuer Fristen ueberlaufsicher, die nach
// Ablauf nicht weiter verglichen werden - diese wurde es. 2^31 ms =
// 24,86 Tage nach dem letzten Connect kippt die Differenz wieder ins
// Negative, die Bedingung meldet "offen", und JEDES Set-Kommando wird
// verworfen, bis zum naechsten Reconnect. Still: je Kommando eine
// Telnet-Zeile, auch die Bilanzmeldung hatte denselben Vergleich und
// schwieg. Irrefuehrend: Nach 12 min meldet die Webseite "Hausteuerung
// erreichbar, sendet aber seit ... keine Vorgaben" - die Steuerung
// sendete, die Firmware warf weg. Der Notbetrieb war nicht betroffen
// (laeuft vor der Karenzpruefung). Beide frueheren Durchsichten hatten
// die Stelle als "ueberlaufsicher geprueft" abgehakt, weil das Idiom
// bekannt aussieht; gefunden in der dritten (Befund S1, 2026-10-15,
// Massnahmenplan-Codedurchsicht-2026-10-15.md). Die Stelle lag im
// Firmware-Klebecode und nicht in einem arduino-freien Header - kein
// Hosttest hat sie je gesehen.
//
// LIVE-STAND 2026-10-15: nicht betroffen, nur durch den Neustart des
// Adapters mqtt.0 am 2026-10-06 08:28. Ohne ihn waeren beide Stufen
// seit etwa 2026-10-14 betroffen gewesen. Naht zuletzt bestimmt auf
// 2026-10-31 04:01 CET (letzter Connect + 2^31 ms + 5 s).
//
// FIX. Neuer arduino-freier Header karenz.h: Startzeit plus Merker
// statt Frist. karenz_offen() rechnet die Differenz in uint32_t gegen
// die Dauer und schliesst das Fenster beim ersten Aufruf nach Ablauf;
// danach wird nichts mehr verglichen. Vorbedingung: Aufruf haeufiger
// als alle 49,7 Tage - loop() ruft die Regel deshalb in JEDEM
// Durchlauf auf (vor der Bedingung der Bilanzmeldung, nicht in ihr:
// der Kurzschluss haette den Aufruf ohne verworfene Kommandos
// uebersprungen). SUBSCRIBE_GRACE zog samt Begruendung nach karenz.h.
// Unveraendert: 5 s ab dem Abonnieren, zweite Armierung am Ende von
// setup() (3.20.0, M1), Notbetriebszweig vor der Pruefung, Herzschlag
// der Verbindungswacht erst danach. H1 und H2 gleich.
//
// ZUSATZ. test/zeitvergleich_test.py meldet in src/ jeden
// vorzeichenbehafteten oder direkten millis()-Vergleich, der nicht mit
// der Marke "kurzlebige Frist" in derselben Zeile ausgewiesen ist.
// Einzige Marke: die Hydraulik-Lesefrist in notbetrieb.cpp (lebt
// hoechstens HYDRAULIK_LESEFRIST_MS, wird nach der Schleife nie
// wieder geprueft). Code unveraendert, nur Kommentar und Marke.
//
// NACHWEIS: test/karenz_test.cpp (neu, bindet karenz.h direkt ein):
// Raender, Ueberlauf im Fenster, die Naht nach 2^31 ms bis ueber den
// vollen Zaehlerumlauf, Reconnect, zweite Armierung, Nullzeiger, dazu
// die alte Regel als Gegenbeispiel. GEGENPROBE von Hand (alte Regel in
// karenz_offen() eingebaut): Fall 4 bricht, 4 Zusicherungen rot (2^31
// + 5 s, + 1 Tag, 2^32 - 1, voller Umlauf + 1000 ms); zurueckgebaut.
// Dabei fiel eine falsche Erwartung des Tests selbst auf: Die Naht der
// alten Regel liegt bei 2^31 ms PLUS Dauer, nicht bei 2^31 ms - die
// Frist steht 5 s nach dem Beginn. Der Test haette sie sonst um 5 s
// verfehlt. test/zeitvergleich_test.py mit eingebautem Selbsttest der
// Muster; GEGENPROBE: Marke aus notbetrieb.cpp entfernt -> ROT mit
// Datei und Zeile, zurueckgebaut. Gegen den Stand 3.23.0 gelassen
// meldet er genau die zwei Zeilen 814 und 1484 der HeishaMon.cpp. Alle
// Hosttests der CI gruen, alle sechs Envs gebaut. Zusatzabnahme an der
// Anlage (Bilanzzeile nach dem Connect, neue Uhr, Kommandos kommen
// durch): Ergebnis steht in Arbeitsplan-Karenzfenster-3.23.1.md,
// Schritt 18 - nicht hier, weil der Tag dann schon steht.
//
// GROESSE gegen 3.23.0 (heishamon_esp32_h1_ota, frisch im Worktree
// gegen den Rettungsanker gebaut): RAM 61952 -> 61960 Byte (+8),
// Flash 1223417 -> 1223521 Byte (+104). heishamon_esp32_h2_ota: RAM
// 61952 -> 61960 Byte (+8), Flash 1222781 -> 1222877 Byte (+96).
//
// 3.23.0 - ALTERNATIVER AUSSENFUEHLER PER MQTT (SET40/TOP112), TOP66
// ENTFAELLT. Neues Set-Kommando SET40 AltExternalSensor schaltet die
// Fuehlerwahl im Installateurmenue um (Byte 20, Bits 3+4): Off nutzt
// den eingebauten Gehaeusefuehler, On den externen Fuehler auf dem
// Flachdach. TOP112 Alt_External_Sensor liest zurueck.
//
// WARUM. Bisher liess sich nur am Bedienteil umschalten, obwohl die
// Fuehlerwahl zweimal im Jahr wechseln soll: On (extern) im
// Sommer/Kuehlbetrieb, weil der Gehaeusefuehler dort Sonne abbekommt,
// Off (Gehaeuse) im Winter, weil die Abtausteuerung auf ihn angewiesen
// ist. Ohne Ruecklesung wuesste eine Steuerung nie, welcher Fuehler
// gerade gilt - deshalb TOP112 als Pflichtteil, nicht als Kuer (SET-
// TOP-Zuordnung.md).
//
// BEIDE STUFEN GLEICH (Owner-Entscheid E1, 2026-09-19). Beide
// Waermepumpen haben einen eigenen externen Fuehler und standen beim
// Auftrag auf On - keine Abweichung H1/H2.
//
// NUR NOCH UEBER DEN IOBROKER (Owner-Entscheid E2). Der ioBroker
// spielt beim Verbinden den gespeicherten Wert jedes Set-Topics wieder
// ein (SUBSCRIBE_GRACE, 3.6.1) - wer am Bedienteil umschaltet, muss
// den Datenpunkt von Hand nachziehen, sonst dreht die naechste
// Wiedereinspielung den Zustand still zurueck. Folgeauftrag in
// nodered-flows: SET40/TOP112 in den WP_Befehls_Waechter, NICHT in den
// WP_Installer_Waechter - der prueft gegen einen festen Sollstand,
// TOP112 wechselt aber gewollt.
//
// DER NOTBETRIEB FASST DAS NICHT AN (Owner-Entscheid E3) - beide
// Fuehler liefern brauchbare Werte, ein zusaetzlicher Schritt haette
// beide Ablaeufe nur verlaengert, ohne einen Ausfall abzuwenden.
//
// TOP66 LOW_PRESSURE ENTFAELLT IM SELBEN ZUG (Owner-Entscheid
// 2026-09-18). Byte 164 stand an beiden Stufen seit Beginn der Messung
// (2026-07-12) dauerhaft auf 0x01 - rund 98000 Werte, alle 0, auch bei
// laufendem Verdichter (Byte-Zuordnung.md). Zeile gestrichen, NICHT
// umnummeriert (Vorbild Zone 2, 3.4.0); NUMBEROFTOPICS bleibt 99, weil
// TOP112 im selben Zug dazukommt.
//
// NACHWEIS OHNE GERAET: test/byte20_test.cpp, neu in der CI. Legt die
// Merge-Zeile aus commands.cpp und die ECHTEN Dekodierer aus
// decode.cpp nebeneinander, mit dem an der Anlage gemessenen Rohwert
// (siehe unten) als On-Testvektor sowie allen 64 Kombinationen von
// Frostschutz, Optionsplatine und dem Wasser/Glykol-Bitpaar - TOP112
// bleibt in jeder Kombination unberuehrt. Gegenprobe von Hand
// (Parameter 8 statt 16 in der Merge-Zeile): Fall 2 bricht
// vollstaendig, 4 Zusicherungen rot. test/decode_vergleich.py gegen
// den Rettungsanker: 98 uebrige Topics ueber alle Telegramme
// identisch. Alle Hosttests der CI gruen, alle Envs gebaut.
//
// NEBENBEFUND, BEHOBEN: test/decode_vergleich.py stubte
// write_wert_log() nicht mit. Seit dessen Einfuehrung in 3.20.0 war
// das Werkzeug gegen jede neuere Basisversion nicht mehr lauffaehig -
// nur nie aufgefallen, weil zuletzt gegen v3.8.x verglichen wurde.
//
// GEMESSEN VORAB (2026-09-19, beide Stufen, vor dem Umbau): Byte 20 =
// 0x2A an H1 UND H2, waehrend 30 s Mitschnitt je Stufe unveraendert -
// Bits 3+4 = 10 (On, bestaetigt E1), Bit 1 = 0 (Wasser, bestaetigt
// E7). Der Anlagennachweis der Umschaltung selbst (TOP14-Sprung, beide
// Stufen) folgt separat und steht in test/README.md, nicht hier: Nach
// E5 liegt dieser Tag schon vor dem Funktionstest.
//
// GROESSE gegen 3.22.0 (heishamon_esp32_h1_ota, frisch im Worktree
// gegen den Rettungsanker gebaut): RAM 61952 -> 61952 Byte (+0),
// Flash 1223369 -> 1223461 Byte (+92).
//
// 3.22.0 - DER VORDERHAUSSCHRITT LAEUFT AN BEIDEN STUFEN. Bis 3.21.0 stand
// er nur in der Heizen-Folge (Stufe 1). Die Begruendung "nur Stufe 1
// versorgt den Heizkreis" war eine Annahme des Entwurfs
// (Analyse-KNX-Vorderhaus.md, Abschnitt 8) und ist faelschlich als
// Owner-Entscheid in den Arbeitsplan gewandert; der Owner hat es bei
// der Durchsicht am 2026-09-14 gefunden. Welcher Knopf im Ernstfall
// zuerst gedrueckt wird, weiss niemand - also steht der Schritt jetzt
// auch am Ende der Warmwasser-Folge, direkt hinter Heatpump = 1. Ein
// zweiter Lauf ist unschaedlich: Steht der Mischer schon auf 128,
// bestaetigt der Status nach rund 2,5 s; faehrt er noch, wartet der
// Rueckfall die Ankunft ab.
//
// FAELLIG NUR BEI HEIZBETRIEB (Owner-Entscheid 2026-09-14). An Stufe 2
// fehlt die Sperre, die den Schritt an Stufe 1 schuetzt: Die
// Warmwasser-Folge laeuft absichtlich auch im Kuehlbetrieb (M3). Dort
// naehme der Schritt die Zwangsstellung des Sommers zurueck und
// schickte 50 % Kuehlwasser ins Vorderhaus. Neue Regel
// notbetrieb_vorderhaus_faellig() in notbetrieb.h: nur eine sauber
// gelesene 0 an TOP101, dieselbe strenge Lesart wie die Freigabe der
// Rolle Heizen. Sonst ENTFAELLT der Schritt - kein Telegramm, kein
// ROT. Logzeile "Vorderhaus entfaellt", GRUEN-Zeile mit Zusatz, und
// unter GRUEN ein blassgelber Hinweis "Vorderhaus unveraendert"
// (neues elftes Feld der Statusroute, hinten angehaengt).
//
// KNX-SCHNITTSTELLE AN BEIDEN STUFEN PFLICHT. Die Startwarnung gilt
// fuer beide Rollen, ebenso die Pruefung, ob TOP101 in stateTopics[]
// steht. Beim Rollout wird knx_schnittstelle auch an H2 und h2b
// gesetzt (Ablauf-Backup-Boards.md).
//
// Warmwasser-Lauf 56 s statt 48 s (sieben Schritte), Gesamtdeckel
// 360 s statt 120 s (Summe der Schritt-Timeouts, E2). Heizen
// unveraendert.
//
// NACHWEIS: notbetrieb_test um Block 9c erweitert - Faelligkeit fuer
// alle Lesarten von TOP101, Regelfall 56 s, Rueckfall an Stufe 2 bei
// gemeldetem Kuehlen (laeuft weiter, die Warmwasser-Folge bricht nie
// ab), ROT genau nach 240 s mit dem Vorderhaus-Grund; Schrittfolge
// und Deckel beider Rollen angepasst. Alle Hosttests der CI gruen,
// alle sechs Envs gebaut, kein Testzugang in den produktiven
// Abbildern. Der Vorderhausschritt selbst ist seit 3.21.0 an der
// Anlage abgenommen und unveraendert; neu sind nur seine Stelle in
// der Warmwasser-Folge und die Faelligkeitspruefung.
//
// GROESSE gegen 3.21.0 (heishamon_esp32_h1_ota): RAM 61952 -> 61952
// Byte (+0), Flash 1222681 -> 1223413 Byte (+732).
//
// 3.21.0 - DER NOTBETRIEB STELLT DAS VORDERHAUS MIT UM. Mischer und
// Mischerpumpe des Vorderhauses haengen am KNX-Bus und blieben bei
// einem Ausfall der Steuerung auf ihrem letzten Wert - im Sommer oft
// mit Zwangsstellung ZU, der Mischer auf 0. Ein neuer, letzter Schritt
// der Heizen-Folge schickt per KNXnet/IP-Tunneling Telegramme an die
// Schnittstelle: beide Zwangsstellungen zurueck, Mischer auf 128
// (50 %), Pumpe ein - und liest am Aktor zurueck. Recherche,
// Vorabtest an der Anlage und die Owner-Entscheide stehen in
// Analyse-KNX-Vorderhaus.md, der Weg in die Firmware samt der
// Entscheide E1-E4 in Arbeitsplan-KNX-Vorderhaus.md.
//
// EIGENER MINIMALER TUNNEL-CLIENT, KEINE BIBLIOTHEK. Fuer den ESP32
// gibt es keinen gepflegten Tunnel-Client. Rahmen, Sequenzregel,
// Antwortfilter, das Urteil der Rueckleseregel A, die Einstellung und
// die Fristen stehen arduino-frei in src/knxtunnel.h; der Netzteil
// (WiFiUDP, Warten, Quittieren) in src/vorderhaus.cpp. Referenz ist
// test/knx_tunnel.py 1.6.0, an der Anlage erprobt.
//
// WEG A: GANZ HINTEN, HINTER Heatpump = 1. Eine Stoerung am KNX darf
// den Notbetrieb der Waermepumpen nicht verhindern. Scheitert der
// Schritt, endet der Lauf mit dem neuen Grund
// NOTBETRIEB_GRUND_VORDERHAUS (4). Die Seite zeigt dann das normale
// GRUEN und darunter ein oranges Feld mit dem Wortlaut des Owners;
// intern ROT, damit der Knopf zurueckkommt, zu dem der Text einlaedt.
//
// RUECKLESEREGEL A. Die Busbestaetigung (L_Data.con) entscheidet
// nichts - mit der Quelle 1.1.250 kommt keine. Es zaehlen nur
// Antworten der Aktoren 1.1.39 und 1.1.60; openknx beantwortet das
// Lesen aus seinem Zwischenspeicher schneller als der Aktor. Der
// Mischer gilt als umgestellt, wenn der Eingang 128 zuruecklesen UND
// Bewegung 1 kommt (oder der Status schon 128 meldet) - aber nur, wenn
// die Bewegung VOR den Befehlen auf 0 stand. Sonst entscheidet die
// Endstellung, bis 220 s. Die Pumpe wird immer eingeschaltet, auch
// wenn der Mischer ROT ergab.
//
// ZEIT JE SCHRITTTYP (E2). Der Rueckfall sprengt die 20 s je Schritt.
// Der Vorderhausschritt bekommt 240 s als einzige Frist; der
// Gesamtdeckel ist jetzt die Summe der Schritt-Timeouts: Heizen 440 s
// (vorher 200 s), Wasser unveraendert 120 s. Im Regelfall kostet der
// Schritt die Mindestwarte von 8 s, ein Heizen-Lauf dauert 88 s.
//
// NUR KURZE VERBINDUNGEN. Kein Tunnel bleibt ueber mehrere
// loop()-Durchlaeufe offen - die MQTT-Wiederverbindung blockiert bis
// 2 s, ein offener Tunnel muss binnen 1 s quittieren. Groesste
// Blockade von loop(): 20,5 s (harter Deckel der Befehlsverbindung
// plus Trennen), nur wenn die Schnittstelle quittiert und kein Aktor
// antwortet; Regelfall rund 0,3 s. Begruendung im Kopf von
// vorderhaus.cpp.
//
// EINE NEUE EINSTELLUNG: knx_schnittstelle, nur eine IP, wahlweise mit
// :Port (E1). Gruppen- und Aktoradressen stehen fest in knxtunnel.h -
// AENDERT SICH EINE GRUPPENADRESSE IN DER ETS, BRAUCHT ES EINE NEUE
// FIRMWARE. Leer oder ungueltig: Warnung beim Start, ROT beim Druck.
// Die Einstellungsseite lehnt eine ungueltige Eingabe ab.
//
// ZWEI BEFUNDE AM CORE (pioarduino 55.3.311, NetworkUdp.cpp):
// parsePacket() liefert 0, solange vom vorigen Datagramm Bytes im
// Puffer liegen - nach jedem Datagramm deshalb clear() (flush() ist
// dort als veraltet markiert). Und begin() mit Port 0 gibt den
// vergebenen Port nicht heraus; das HPAI braucht ihn, also fester
// lokaler Port 3672.
//
// NEBENBEFUND, BEHOBEN: Die Warnung "keine Adresse fuer den
// Hydraulik-Switch" kam seit 3.15.0 bei JEDEM Start - sie stand in
// notbetrieb_init(), und das laeuft vor setupWifi(), also vor dem
// Laden der config.json. Beide Adresspruefungen stehen jetzt in
// notbetrieb_einstellungen_pruefen(), direkt hinter setupWifi().
//
// NACHWEIS: test/knx_test.cpp neu in der CI, 146 Pruefungen - jeder
// Rahmen byteweise gegen xknx und die Mitschnitte vom 2026-09-12,
// jeder Zweig von Regel A samt Reihenfolge des Lesens, die Einstellung
// und der millis()-Ueberlauf. notbetrieb_test um den Vorderhausschritt
// erweitert: Rueckfall nach 230 s GRUEN statt ROT nach 20 s, Timeout
// genau nach 240 s, der Rand Deckel = Schritt-Timeout, Ueberlauf im
// Rueckfall. Beide Tests reissen nachweislich bei eingebauten Fehlern
// (ctrl1, Quellenfilter, Sequenzregel, Rueckfall, festes Timeout,
// Deckel). Das Seiten-JavaScript mit node geprueft und gegen
// nachgebildete Statusantworten durchgespielt.
//
// AM GERAET (2026-09-13, Pruefling h1b mit Testzugang): alle 13
// Simulatorlaeufe der Soll-Tabelle (Arbeitsplan, Schritt 9) wie
// erwartet. An der Anlage, openknx lief mit und wurde verworfen:
// schneller Weg ab 255 GRUEN nach 0,4 s (Status 128 nach 59,6 s),
// zweiter Druck GRUEN nach 3,4 s, Rueckfall im Nachlauf der Endlage
// GRUEN nach 71,3 s - der Aktor nimmt den Befehl auch im Nachlauf
// an. Protokoll in Ablauf-Notbetrieb.md, Abschnitt 1c.
//
// NICHT IN DIESER VERSION: der Re-Assert fuer die KNX-Befehle in
// nodered-flows - ohne ihn holt die zurueckkehrende Steuerung Mischer
// und Pumpe nicht von selbst zurueck. Und die Anleitung zum Mischer
// in den Notbetriebsunterlagen, auf die die Seite verweist; sie
// schreibt der Owner nach dem Rollout (Entscheid 2026-09-13).
//
// GROESSE gegen den Stand davor (heishamon_esp32_h1_ota, Tag
// rettungsanker-vor-knx-vorderhaus-2026-09-13, dieselbe Plattform):
// RAM 61608 -> 61952 Byte (+344, 18,8 % -> 18,9 %).
// Flash 1213025 -> 1222681 Byte (+9656, 61,7 % -> 62,2 %).
// text +7292, data +2752, bss +344.
//
// 3.20.0 - ROBUSTHEIT UND LANGZEITSTABILITAET. Umsetzung von M1-M4 der
// Codedurchsicht 2026-09-02 in EINEM Versionsschnitt statt in drei
// (Owner-Entscheid): ein Pruefstandslauf, ein Rollout, ein Release.
// Die Massnahmen beruehren einander nicht, jede hat ihren eigenen
// Commit. Mitgefahren sind K2 und die veralteten Zahlen in den
// Kommentaren; K1 (Watchdog fuer loop()) und K3 (config.json atomar)
// bleiben liegen.
//
// M1 - DAS KARENZFENSTER KONNTE ABGELAUFEN SEIN, BEVOR DER SCHWALL
// GELESEN WURDE. setupMqtt() setzt setCommandsIgnoredUntil, sobald die
// Set-Topics abonniert sind. Der Wiedereinspiel-Schwall des
// ioBroker-Adapters liegt danach im TCP-Puffer und wird erst vom ersten
// mqtt_client.loop() gelesen, also erst NACH setup(). Dazwischen steht
// setupTime() und wartet bis zu 30 s auf NTP. Braucht die
// Zeitsynchronisation laenger als die 5 s SUBSCRIBE_GRACE, ist das
// Fenster beim ersten loop() schon zu, und jedes wiedereingespielte
// Set-Kommando laeuft als frisches Kommando in die Waermepumpe - der
// Fall aus 3.6.1 (Vorlauf-Sollwert 55 C nach jedem Neustart), nur mit
// anderer Ursache.
// Im Normalfall (NTP in 1-3 s) hielt das Fenster, deshalb ist es nie
// aufgefallen. Konstruiert ist der Fall trotzdem nicht: Nach einem
// Stromausfall bootet die Bridge in Sekunden, der Router braucht
// Minuten fuer den Internetzugang. Ist der ioBroker schon da und das
// Internet noch nicht, scheitert NTP, der Schwall wartet 30 s im
// Puffer und wird dann ausgefuehrt. Der Keepalive rettet nicht: Die
// Nachrichten sind vor einem etwaigen Abbruch schon angekommen.
// Das Fenster wird jetzt am Ende von setup() ein zweites Mal gesetzt
// und misst ab dem Zeitpunkt, an dem der Schwall wirklich gelesen
// wird. Die Zeile in setupMqtt() bleibt stehen - sie deckt den
// Reconnect-Pfad ab, der ueber mqtt_reconnect() laeuft.
//
// M2 - DIE NOTBETRIEBSWERTE UEBERLEBEN DEN SOFTWARE-RESET. Sie lagen
// seit 3.12.0 nur im RAM. Die Begruendung des Owner-Entscheids vom
// 2026-08-20 war, dass ein Neustart bei weggefallenem Broker einen
// Stromausfall bedeutet - und der laeuft ohne Notbetrieb. DIESE
// ANNAHME TRAEGT NICHT MEHR, weil die Firmware selbst neu startet: der
// WLAN-Watchdog nach 5 min ohne WLAN (3.5.0, also aelter als der
// Notbetrieb), /reboot und jedes OTA. Der unguenstige Fall ist der
// Server im Keller tot - der Notbetriebsfall - waehrend der Router neu
// startet oder ein Update bekommt, das laenger als 5 min dauert. Die
// Bridge startet neu, die Werte sind weg, und der Knopf meldet "Nicht
// bereit - es fehlen Werte" genau dann, wenn jemand ihn braucht.
// Neu: ein Spiegel in RTC_NOINIT_ATTR (src/rtcspiegel.h, arduino-frei,
// test/rtcspiegel_test.cpp mit 42 Zusicherungen in der CI). DIE
// TRENNLINIE DES OWNER-ENTSCHEIDS BLEIBT EXAKT ERHALTEN: Der
// RTC-Speicher ueberlebt ESP.restart(), den Watchdog-Reset und das
// OTA, NICHT aber das Stromlosmachen. Kein Flash-Schreibzugriff, keine
// Datei, kein Verschleiss, nach echtem Stromausfall leer.
// Vier Bedingungen entscheiden ueber Gueltigkeit: Magic MIT
// LAYOUTNUMMER (nach einem OTA liegt dort der Spiegel der Vorversion,
// und die Pruefsumme faellt darauf herein, weil sie nur ueber die
// Felder rechnet, die diese Firmware kennt), dieselbe Rolle (die
// Backup-Boards tragen abwechselnd beide), keine Maskenbits jenseits
// der Wertezahl (sonst macht notbetrieb_vollstaendig() aus einem
// unvollstaendigen Satz ein "ja") und eine FELDWEISE gerechnete
// Pruefsumme (ueber den rohen Speicher gerechnet haenge sie am
// Padding und waere auf dem Geraet zufaellig, im Hosttest aber gruen).
// Jeder uebernommene Wert laeuft erneut durch set_command_range() und
// notbetrieb_wert_annehmen() - denselben Weg wie eine frische
// MQTT-Nachricht. Am Geraet zu sehen ist der Erfolg an einem
// AUSBLEIBEN: "Notbetrieb einsatzbereit" kommt nach dem Neustart nicht
// erneut, weil der Satz beim ersten eintreffenden Wert schon
// vollstaendig ist.
//
// M3 - LANGZEIT-TELEMETRIE UNTER <prefix>/info/. Zwei Fehlerbilder
// waren ueber Monate unsichtbar. Erstens unbemerkte Neustarts: Ein
// Watchdog-Neustart, ein Brownout oder eine Panic hinterliessen keine
// Spur, und "WLAN war X s weg" wird nur gemeldet, wenn das Geraet die
// Zeit OHNE Neustart ueberstanden hat. Ein Geraet, das alle paar Tage
// neu startet, sah von aussen gesund aus - das LWT kommt binnen
// Sekunden zurueck. Zweitens schleichender Heap-Verlust: abrufbar nur
// ueber Telnet M, und dort nur als Prozentwert relativ zum Boot.
// Acht Topics, retained: boot_count, boot_reason und version einmal je
// Verbindung, uptime, heap, heap_min, heap_maxblock und stack im
// 5-min-Takt. Der Bootzaehler liegt im selben RTC-Block wie die Werte
// aus M2 und zaehlt damit genau die Neustarts, die der Spiegel
// ueberlebt; steht er auf 1, war das Geraet stromlos. Zusammen mit
// boot_reason unterscheidet das "Strom war weg" von "die Firmware hat
// neu gestartet".
// ZWEI ABWEICHUNGEN VOM MASSNAHMENPLAN, beide mit Grund. (1) Acht
// Topics mit je EINEM Wert statt vier mit mehreren: Ein Zaehler, den
// der ioBroker als ZAHL fuehrt, laesst sich in InfluxDB auftragen - ein
// Sprung in der Kurve ist ein Neustart, ein fallendes heap_min ueber
// Tage ein Leck. Genau das soll der Waechter koennen, und aus einem
// JSON-String bekommt man es nicht heraus. (2) Die fortlaufenden Werte
// haengen NICHT am 5-min-Vollupdate in publish_heatpump_data(): Jenes
// laeuft nur nach einem gueltigen Antworttelegramm. Reisst die
// serielle Strecke ab, verstummten Uptime, Heap und Stack ausgerechnet
// in dem Zustand, in dem man sie ansehen will. Eigener,
// ueberlaufsicherer Takt in loop().
//
// M4 - DIE DIAGNOSE GING GENAU IM STOERFALL VERLOREN.
// write_mqtt_log() publizierte ins MQTT-Log und sonst nirgends. Alle
// Zeilen des Notbetriebs laufen darueber - "Schritt 3/10", "ROT: ...
// kam nicht zurueck", die Hydraulik-Antwort mit HTTP-Code. Im
// eigentlichen Notbetriebsfall IST der Broker weg, das Publish
// scheitert still, und nach dem 15-min-Anzeigeverfall existierte keine
// Spur mehr, warum ein Lauf ROT war. K4 der ersten Codedurchsicht
// hatte den Logpfad als "nur Diagnose" eingestuft; mit dem Notbetrieb
// ist er zur einzigen Nachweisquelle eines sicherheitsrelevanten
// Vorgangs geworden.
// Jetzt drei Wege: immer in einen Ring aus 32 Zeilen a 128 Byte
// (statisch, kein Heap - der Logpfad ist der letzte Ort, an dem eine
// Allokation scheitern darf), dann ins MQTT-Log, sonst nach Telnet.
// Die Route /log gibt den Ring als text/plain heraus, zeilenweise
// gesendet statt als 4-KB-String, hinter dem NOTBETRIEBS-Zugang: Sie
// gibt echte Meldungstexte heraus, nicht nur "Schritt 3 von 7" wie
// /notbetrieb/status, und das Passwort steht auf dem ausgedruckten
// Notfallblatt. Link ganz unten auf der Notbetriebsseite - wer den
// Knopf drueckt, braucht ihn nicht, er ist fuer die Nachschau danach.
// DIE KORREKTUR AM MASSNAHMENPLAN: Der Ring darf nicht einfach in
// write_mqtt_log() haengen. Von 39 Aufrufstellen sind 38
// Ereignismeldungen, die 39. ist die <PUB>-Zeile aus
// publish_heatpump_data() - sie kommt fuer JEDEN geaenderten Messwert
// in JEDEM 5-s-Zyklus. Der Ring haette nach Sekunden nur noch
// Temperaturen enthalten. Diese eine Stelle geht deshalb ueber die
// neue Funktion write_wert_log().
//
// K2 - DIE LOGZEITSTEMPEL KOMMEN AUS DER SYSTEMUHR. setupTime()
// stellte bis 3.19.0 die TimeLib-Uhr beim Boot EINMAL, danach lief
// now() frei auf millis(): Der Stempel driftete um Minuten, und die
// Sommerzeit-Umstellung erreichte ihn erst mit dem naechsten Neustart.
// Jetzt time() und localtime_r(); paulstoffregen/Time ist entfallen.
// Der Rueckfall auf die Laufzeit ("+01:23:45") gehoert zu M4: Ohne
// gueltige Uhr stuende in jeder Zeile "1970-01-01" - der Normalfall
// nach einem Stromausfall ohne Internet, also genau in der Lage, fuer
// die der Ring gebaut ist.
// MESSBEFUND: Der erste Entwurf benutzte strftime und kostete 12 KB
// Flash, weil newlib dabei den Locale-Apparat mitzieht. snprintf mit
// den tm-Feldern liegt ohnehin im Abbild.
//
// AUFGERAEUMT: Tabellenlaenge 92 -> 99 und hoechste TOP-Nummer
// 104 -> 111 in decode.h, decode.cpp und notbetrieb.cpp, Gesamtdeckel
// 180 -> 200 s in notbetrieb.h. Nachgetragen in test/README.md:
// verbindung_test.cpp fehlte dort in der Werkzeugtabelle UND im
// Befehlsblock, obwohl es seit 3.13.0 in der CI steht - wer vor dem
// Flashen die Liste aus dem README abarbeitet, fuhr weniger als die CI.
//
// GROESSE gegen 3.19.0 (heishamon_esp32_h1_ota):
// RAM 57528 -> 61608 Byte (+4080, 17,6 % -> 18,8 %) - davon 4096 der
// Logring, der Rest hebt sich mit dem Wegfall von TimeLib auf.
// Flash 1211105 -> 1213049 Byte (+1944, 61,6 % -> 61,7 %).
// text +1452, data +464, bss +4072.
//
// 3.19.0 - SIEBEN INSTALLER-EINSTELLUNGEN WERDEN TOPICS. TOP105-107 aus Byte
// 25 (Bodenwannenheizung, Leistung des internen Heizstabs, Speicher-
// Heizstab intern/extern) und TOP108-111 aus Byte 23 (Kompressor-
// kontakt, externes Fehlersignal, Heat/Cool-Schalter, externer
// Steuerkontakt). NUMBEROFTOPICS 92 -> 99, neuer Hosttest
// test/byte23_25_test.cpp.
//
// WARUM JETZT - EIN AUSFALL, DEN NIEMAND SEHEN KONNTE. Am 2026-08-31
// stand an WP2 der Speicher-Heizstab auf EXTERN, obwohl es nur den
// internen gibt. Vermutlich seit der Inbetriebnahme; der Werksstand
// waere INTERN gewesen. Folgenlos, solange der Heizstab gesperrt war -
// mit der Freigabe aus 3.17.0 forderte die Anlage beim ersten
// Warmwasserlauf den externen Stab an, fand dessen Ueberlastschutz
// nicht und ging mit H91 aus. Umwaelzpumpe still, Stufe 2 tot, in
// JEDER Betriebsart, bis der Menuepunkt korrigiert war.
//
// DAS SCHWERE DARAN IST NICHT DER AUSFALL, SONDERN SEIN ZEITPUNKT.
// Ausgeloest hat ihn der Modus, der fuer den Ausfall des Verdichters
// gebaut ist. Bei 22 Grad Aussentemperatur kostet das eine kalte
// Dusche; im Winter mit defektem Verdichter haette dieselbe
// Fehlkonfiguration die Stufe stillgelegt, die den Notbetrieb tragen
// soll.
//
// WARUM DIE ALTE BEGRUENDUNG NICHT MEHR TRAEGT. "Installer-Einstellung,
// wird einmal eingestellt, braucht kein Topic" misst die Aenderungs-
// haeufigkeit. Die entscheidende Groesse ist der Preis einer
// unbemerkten Abweichung. Hier ist die Haeufigkeit null und der Preis
// Anlagenstillstand - die Kombination, die am schlechtesten von selbst
// auffaellt. Die Regel, die daraus folgt, steht in MQTT-Topics.md und
// nennt drei Bedingungen; beide Bytes erfuellen alle drei, die
// Heizkurven und Zonentypen erfuellen sie nicht und bleiben draussen.
//
// GEMESSEN, NICHT ABGESCHRIEBEN. Byte 25: "Tank heater" Internal <->
// External liess das Byte 0x95 <-> 0x96 wandern (test/h2.log). Byte 23:
// "External compressor SW" Yes <-> No liess es 0x99 <-> 0x59 wandern
// (test/h2_ext.log). Beide Male bewegte sich im ganzen Telegramm nur
// dieses eine Konfigurationsbyte. Der zweite Lauf belegt zusaetzlich
// die Feldreihenfolge: Bewegt hat sich das OBERSTE Bitpaar, die
// Referenztabelle ist an dieser Stelle also nicht gespiegelt.
//
// WAS BEWUSST DRAUSSEN BLEIBT. Das oberste Bitpaar von Byte 25 (an WP2
// konstant b10) hat in der Referenz keine Bedeutung. Ein Topic dafuer
// wuerde eine undeutbare Zahl veroeffentlichen. Es steht im Kommentar
// ueber der Tabelle, nicht in MQTT.
//
// WAS DER ROLLOUT SOFORT ZEIGTE (2026-08-31, alle vier Boards auf
// 3.19.0). Byte 23 liest an BEIDEN Stufen dasselbe und passt zur
// Verdrahtung - eine zweite, unabhaengige Bestaetigung dieses Bytes.
// In Byte 25 stehen dagegen zwei Abweichungen, die vorher niemand
// sehen konnte:
// - Der Speicher-Heizstab steht auch an WP1 auf EXTERN. Der Fehlstand
// an WP2 war also kein Ausrutscher bei der Inbetriebnahme, sondern
// der gemeinsame Ausgangszustand beider Anlagen. An WP1 ist er
// derzeit folgenlos: dort haengt kein Speicher (TOP10 = -128, TOP58
// Blocked, TOP91 = 0 Stunden). Er wird scharf, sobald dort ein Tank
// konfiguriert wird - und BLEIBT BEWUSST STEHEN (Owner,
// 2026-08-31): Den Menuepunkt gibt es erst nach Freigabe der
// Tank-Verbindung, und die freizugeben hiesse, Hardware zu
// konfigurieren, die es nicht gibt. Genau daran ist WP2 an diesem
// Morgen ausgefallen. Das Aufraeumen des Wertes wuerde also den
// Fehler riskieren, den der Wert dokumentiert.
// - Die Heizstab-Leistung las an WP1 9 kW, an WP2 3 kW - und dieser
// Befund ist das schaerfste Argument fuer die ganze Aenderung.
// Diesen Geraetetyp gibt es nur mit 3 kW; WP1 war also auf Hardware
// konfiguriert, die es dort nicht gibt. DER WERT WAR UEBER DAS
// BEDIENTEIL GAR NICHT HERSTELLBAR: Beim Aufrufen des Menuepunkts
// am 2026-08-31 stand genau eine Auswahl zur Verfuegung, 3 kW. Das
// Bestaetigen genuegte, Byte 25 ging 0x9E -> 0x96 und TOP106 zog
// auf 3 kW nach. Ein nie aufgerufener Menuepunkt trug also einen
// Wert, den das Panel nicht einmal anbietet - an der Anlage war er
// nirgends zu sehen. Woher er stammt, ist unbekannt.
// Die Korrektur ist zugleich der dritte Umschaltnachweis, diesmal
// an WP1, und sie deckt das Bitpaar ab, das in beiden Mitschnitten
// stillstand. Von Byte 25 ist damit nur noch das Pad-Heater-Feld
// ungeprueft. Elektrisch gemessen ist die Heizstableistung von WP1
// weiter nicht - nur die von WP2 (3000 W an TOP16 am 2026-08-28).
//
// 3.18.0 - DER NOTBETRIEB NIMMT DEN HEIZSTAB ZURUECK. Beide Schrittfolgen
// bekommen an Position 2 den Schritt SET39 ForceHeater = 0, direkt
// hinter der Hydraulik und vor allem anderen an der Waermepumpe.
// Auftrag-Heizstab-Notbetrieb.md, Ablauf-Notbetrieb.md Abschnitt 1b.
//
// WARUM JETZT. Seit dem 2026-08-30 benutzt die Kaskadensteuerung SET39
// im REGELBETRIEB: Drei Heizmodi (App-Menue 9/10/11) ersetzen den
// Verdichter durch den Backup-Heizstab, 3 kW an einer Stufe, 6 kW mit
// beiden. Bis 3.17.0 war ForceHeater ein Kommando, das ausser bei
// Messlaeufen niemand gesetzt hat - seither ist es ein Zustand, den die
// Anlage im Alltag traegt. Der Notbetrieb kannte ihn nicht.
//
// WAS OHNE DEN SCHRITT PASSIERT. Die Heizstab-Modi setzen Heatpump = 0,
// weil die Waermepumpe SET39 nur bei ausgeschalteter Einheit annimmt.
// Wird in dieser Lage der Notbetriebsknopf gedrueckt, schaltet der
// letzte Schritt die Anlage EIN, waehrend ForceHeater weiter auf 1
// steht. Niemand nimmt das zurueck: Die Firmware hat keinen
// Rueckschaltpfad, und im eigentlichen Notbetriebsfall ist die
// Steuerung tot. Dazu kommt die Umwaelzpumpe - sie haengt am KOMMANDO,
// nicht am Stab: Sie laeuft weiter, nachdem der Heizstab thermisch
// abgeschaltet hat, und stoppt erst mit ForceHeater = 0 (gemessen
// 2026-08-28).
//
// WARUM POSITION 2. Bricht der Lauf dort ab, tut die Anlage nichts
// mehr - Stab aus, Pumpe aus, Waermepumpe aus. Das ist die sichere
// Seite, und es ist dieselbe Ueberlegung wie beim Hydraulikschritt an
// Position 1. Anders als bei den Kurvenpunkten muss dieser Wert NICHT
// hinter die Moduswechsel: Ein Werks-Reset wirkte hier in dieselbe
// Richtung und koennte nichts zerstoeren.
//
// DAS SCHRITT-TIMEOUT BLEIBT BEI 20 s. Beim EINSCHALTEN braucht SET39
// bis zu einer halben Minute (MQTT-Topics.md, 2026-08-28) - die
// Waermepumpe prueft erst ihre eigenen Bedingungen. Die RUECKNAHME, um
// die es hier allein geht, lag bei 7 s an beiden Stufen (Erstlauf der
// Steuerungsseite, 2026-08-30). Kommt sie wider Erwarten nicht
// zurueck, ist ROT die richtige Antwort: An der Waermepumpe ist nichts
// verstellt, und der Mensch drueckt erneut.
//
// WAS DER SCHRITT NICHT KANN. Jede Bridge spricht nur mit IHRER
// Waermepumpe. Lief die Anlage im 6-kW-Modus, steht SET39 auch an der
// anderen Stufe - dort muss der Knopf ebenfalls gedrueckt werden. Ohne
// das laeuft deren Umwaelzpumpe weiter. Bewusst nur dokumentiert und
// nicht ins Panel geschrieben (Owner-Entscheid 2026-08-30): Der
// Wortlaut bei GRUEN kommt vom Familienrat und soll knapp bleiben.
//
// LAUFZEITEN. Heizen 72 -> 80 s (zehn Schritte), Warmwasser 40 -> 48 s
// (sechs). Steht TOP68 schon auf 0 - immer, wenn kein Heizstab-Modus
// lief -, kostet der Schritt nur die Mindestwarte von 8 s. Der
// Gesamtdeckel leitet sich weiter aus der Schrittzahl ab und waechst
// auf 200 bzw. 120 s. Der Panel-Text nennt jetzt "anderthalb Minuten"
// statt "eine Minute" - er stimmte schon fuer die 72 s aus 3.15.0
// nicht mehr.
//
// AN DER ANLAGE BELEGT (2026-08-30, beide Rollen). Ausgangslage war
// App-Modus 11 mit tatsaechlich LAUFENDEM Heizstab an beiden Stufen
// (TOP68 und TOP60 auf 1, beide Verdichter aus, Pumpen auf Fix):
// Stufe 2, Rolle Warmwasser: TOP68 1 -> 0 in Schritt 2,
// GRUEN nach rund 48 s, Stab 34 s vor dem Einschalten aus.
// Stufe 1, Rolle Heizen: TOP68 1 -> 0 in Schritt 2,
// GRUEN nach rund 80 s, Stab 64 s vor dem Einschalten aus.
// Endzustand: TOP4 Heat, TOP76 Comp. Curve, Kurve 34/26/15/-10
// aus dem RAM, TOP104 Auto, Switch OFF.
// Die Ruecknahme kam in 6 bis 8 s zurueck (Abtastung 4 s) - innerhalb
// der Mindestwarte, die der Schritt ohnehin absitzt. Und: Waehrend des
// Laufs an H2 blieb H1 auf TOP68 = 1 und heizte weiter - die
// Einschraenkung "jede Bridge raeumt nur ihre eigene Waermepumpe" ist
// damit ebenfalls gemessen, nicht nur abgeleitet.
//
// NACHTRAG VOM SELBEN ABEND: DIE WP SCHALTET MIT STEHENDEM HEIZSTAB
// GAR NICHT EIN. Zwei Versuche an Stufe 2 unter sonst gleichen
// Bedingungen, Kommandos einzeln ueber MQTT:
// TOP68 = 1 -> "Heatpump = 1" blieb ueber 48 s wirkungslos, TOP0
// ruehrte sich nicht, und TOP68 fiel nach 10 s VON SELBST auf 0.
// TOP68 = 0 -> dasselbe Kommando schaltete binnen 10 s ein.
// Die Sperre "nur bei ausgeschalteter Einheit" gilt also in BEIDE
// Richtungen; auf dem Protokollweg meldet sie sich nur nicht, das
// Kommando bewirkt schlicht nichts.
//
// DAS AENDERT DIE BEGRUENDUNG DIESER VERSION. Ohne den Schritt waere
// der Notbetrieb aus einem Heizstab-Modus heraus nicht "gelaufen, aber
// mit stehendem Stab" - der LETZTE Schritt haette nie bestaetigt
// werden koennen, und der Lauf haette nach 20 s in ROT geendet, ohne
// die Anlage einzuschalten. Genau in der Lage, in der jemand auf den
// Knopf angewiesen ist. Die Position weit vorn ist damit nicht nur die
// sichere Seite, sondern Voraussetzung dafuer, dass der Knopf ueberhaupt
// funktioniert. Ungemessen bleibt der Fall, dass beide Kommandos im
// SELBEN Telegramm stehen - hier lagen 15 s dazwischen.
//
// KEINE ABKEHR VON 3.17.0. Dort steht "keine Anbindung an den
// Notbetrieb" - das galt und gilt der NUTZUNG des Stabs als
// Notheizung. Hier wird nichts eingeschaltet, sondern ein Zustand
// geraeumt, den sonst niemand raeumt.
//
// 3.17.0 - HEIZSTAB PER MQTT. Drei neue Set-Kommandos: SET37 RoomHeaterState
// und SET38 DHWHeaterState geben den internen Heizstab frei (Byte 9),
// SET39 ForceHeater schaltet die Ersatzwaermequelle (Byte 5).
// Vorhaben-HeaterSet.md.
//
// WARUM. Ein Komfortgewinn im Heizbetrieb, keine Notwendigkeit. Der
// Heizstab ist an beiden Anlagen deaktiviert, und das aus gutem Grund:
// Selbst bei -15 Grad Aussentemperatur braucht die grosse Masse der
// Fussbodenheizung ihn nicht. Interessant ist deshalb nicht die
// Dauerfreigabe, sondern das GEZIELTE Zuschalten - und dafuer fehlte
// bisher jeder Weg ausser dem Bedienterminal.
//
// FREIGABE, KEIN EINSCHALTER. SET37 ist Bedingung (a) von sechs
// (Servicehandbuch 12.6.1); die uebrigen fuenf entscheidet die
// Waermepumpe selbst: 30 min seit Kompressor thermo-on, 9 min seit
// Pumpenstart, Aussentemperatur unter SET20 HeaterOnOutdoorTemp,
// Vorlauf 4 K unter Soll, 20 min seit dem letzten Heizstab-Aus. Eine
// Steuerung muss deshalb vorausschauend freigeben; schnelles Ein/Aus
// bringt nichts, und bei unpassender Schwelle in SET20 bleibt die
// Freigabe wirkungslos. TOP59 meldet die Freigabe, nicht den
// laufenden Stab - das sagen TOP60 und TOP90.
//
// WARUM FORCEHEATER DOCH DABEI IST. Das Vorhaben hatte SET39 zunaechst
// zurueckgestellt: Das Servicehandbuch (12.9) beschreibt Force Heater
// als Ersatzwaermequelle bei einer STOERUNG der Waermepumpe, und in
// einer Frostphase laeuft die Anlage - dann greift das Kommando nicht.
// Am 2026-08-28 hat der Owner am Bedienpanel gegengeprueft: bei
// AUSGESCHALTETER Waermepumpe laesst sich der Heizstab auch ohne
// anliegende Stoerung einschalten. Damit traegt die Begruendung fuer
// das Zurueckstellen nicht mehr, und das Kommando ist gebaut. Es
// bleibt ein ZUSTAND, kein Impuls wie SET12 ForceDefrost - wer ihn
// setzt, muss ihn zuruecknehmen.
//
// DIE MASKEN SIND HIER PFLICHT, NICHT KOSMETIK. Byte 9 traegt BEIDE
// Freigaben in zwei Bitfeldern (0x03 Raumheizung, 0x0C Warmwasser) -
// ohne Maske legte ein Raumheiz-Kommando die Warmwasser-Freigabe mit
// um. Byte 5 teilt sich SET39 (0x0C) mit SET2 HolidayMode (0x30) und
// dem Zeitprogramm TOP13 (0xC0). Derselbe Fall wie Byte 28 in 3.11.0.
//
// NACHWEIS OHNE GERAET: test/byte9_test.cpp, neu in der CI. Er legt
// die Merge-Zeile aus commands.cpp und die ECHTEN Dekodierer aus
// decode.cpp nebeneinander: jeder gesendete Wert kommt zurueckgelesen
// wieder heraus, jedes Kommando laesst seine Nachbarfelder stehen, und
// die Gegenprobe ohne Maske zeigt, was sonst umfiele.
//
// AM GERAET GEMESSEN (2026-08-28, Stufe 1, Anlage aus, Modus Heizen).
// Die einzige echte Unbekannte ist beantwortet: DIE WH-MDC05H3E5 NIMMT
// BYTE 9 AN. Byte 9 wanderte im laufenden Mitschnitt 0x56 -> 0x55 ->
// 0x56, Byte 5 fuer ForceHeater 0x55 -> 0x59 -> 0x55; in beiden Faellen
// wechselte NUR die eigene Bitgruppe, alle drei Nachbargruppen des
// Bytes blieben stehen. Rueckgelesen ueber TOP59 (Free -> Blocked ->
// Free) und TOP68 (Inactive -> Active -> Inactive). Alle Ausgangswerte
// sind wiederhergestellt. Einzelheiten: Vorhaben-HeaterSet.md
// Abschnitt 8.
//
// DER AUSGANGSZUSTAND WAR NICHT DER DES VORHABENS, UND DAS HAT EINEN
// GRUND: Beim Messen stand TOP59 an beiden Anlagen auf Free (an H2 auch
// TOP58), TOP90 zaehlt an H1 267 Betriebsstunden. Das Vorhaben schrieb
// "deaktiviert, Byte 9 blockiert" - richtig fuer den Stand davor. Der
// Owner hat den Heizstab fuer seine eigenen Panel-Tests im
// Installateurmenue aktiviert und ueber Byte 9 freigegeben. Byte 9 zeigte
// also genau das, was am Panel eingestellt war - eine Bestaetigung der
// Zuordnung, kein Widerspruch. Fuer den Messplan hiess es nur, dass sich
// M1 und M2 vertauschten.
// SET20 HeaterOnOutdoorTemp steht auf 2 Grad, ist also gesetzt; die
// Feineinstellung (Bytes 104-106) steht wie erwartet auf 0x00.
//
// WAS SET39 WIRKLICH TUT, IST AM ABEND DES 2026-08-28 GEMESSEN (Stufe 1
// AUSGESCHALTET, Sollwert kurz auf 30): Der Heizstab lief nach 2:20 min
// mit 3000 W an, und die Anlage regelte dabei mit - er schaltete
// von selbst ab, als der Vorlauf ueber die Stoppschwelle stieg. Force
// Heater ist also kein ungeregeltes Durchheizen, sondern eine
// Ersatzwaermequelle INNERHALB der Vorlaufregelung. Damit taugt SET39 als
// Notheizung bei Kompressordefekt: 3 kW je Stufe ueber MQTT, ohne dass
// eine externe Steuerung die Temperaturfuehrung uebernehmen muss.
//
// UND ER STARTET DIE UMWAELZPUMPE. Sie laeuft an, sobald TOP68 aktiv
// wird, laeuft weiter, nachdem der Heizstab abgeschaltet hat, und stoppt
// erst mit ForceHeater 0. Ein vergessenes Kommando laesst die Pumpe also
// dauerhaft laufen - das lose Ende aus dem Absatz oben in konkreter Form.
// Nebenbei: TOP90 Room_Heater_Operations_Hours blieb ueber den ganzen
// Lauf stehen; kurze Laeufe erfasst der Zaehler nicht, dafuer sind TOP60
// und TOP16 zustaendig.
//
// DIE VERZOEGERUNG BEI SET39 IST BAUART, KEIN BEFUND (Owner-Einordnung):
// Die Waermepumpe prueft erst ihre Randbedingungen und uebernimmt den
// Wert dann - solche Wartezeiten sind bei Panasonic normal. Dass TOP60
// waehrend M4 auf Inactive blieb, lag am fehlenden Heizbedarf bei der
// Aussentemperatur des Tages; am Panel wurde der Heizstab mit kurz
// angehobener Zieltemperatur sehr wohl aktiv.
//
// NICHT DABEI: die Anbindung an den Notbetrieb - NOTBETRIEB_WERTE_
// HEIZEN[] in notbetrieb.h bleibt unveraendert, Komforteinbussen im
// Notbetrieb sind akzeptiert. Ebenso SetReset (Byte 8, Bit 0): Wer
// einen verriegelten Fehler quittiert, gehoert ans Bedienpanel.
//
// 3.16.0 - NUR NOCH DER ESP32-PFAD. Der ESP8266-Zweig (D1 mini) ist aus Code,
// Build-Konfiguration, CI und Doku entfernt. Aus zehn Envs werden
// sechs. Vorhaben-Nur-ESP32-Pfad.md.
//
// WARUM. Es geht um Wartbarkeit, nicht um Funktion. Die Konvention
// verlangt, vor jedem Merge ALLE Envs lokal zu bauen - das traf
// haeufiger als die CI. Dazu kamen zehn #if-Weichen im Code, acht
// davon aus dem 3.0.0-Port, zwei neu aus 3.15.0 (HTTPClient-Kopfdatei
// und das getrennte Verbindungs-/Lesetimeout des Hydraulikschritts).
// Jede davon kostet beim Lesen die Frage, welcher Zweig gerade gilt.
//
// DIE FIRMWARE AENDERT SICH DABEI UM KEIN BYTE. Der ESP8266-Code
// stand in #else-Zweigen, die der Praeprozessor beim ESP32-Build
// ohnehin nie uebersetzt hat. Genau deshalb ist der Groessenvergleich
// hier kein Formalismus, sondern der EINZIGE Weg, einen falsch
// aufgeloesten Zweig zu bemerken - ein getroffener aktiver Zweig
// faellt sonst erst zur Laufzeit auf.
//
// NACHWEIS (alle sechs Envs, vor und nach dem Aufloesen der Weichen):
// text/data/bss Byte fuer Byte identisch
// h1: 1034911 / 225430 / 2192951
// h2: 1034535 / 225142 / 2192951
// xtensa-esp32s3-elf-nm --size-sort: alle 9346 Symbole unveraendert
// Ein md5-Vergleich der .bin taugt dafuer NICHT: Der ESP32-App-
// Descriptor traegt Compile-Datum und -Uhrzeit im Abbild, zwei Builds
// sind hier nie byte-gleich (am 2026-08-25 nachgeprueft).
//
// DIE RUECKFALLEBENE. Bis 3.15.0 war es der D1 mini. Ab jetzt sind es
// zwei baugleiche ESP32-Backup-Boards (Ablauf-Backup-Boards.md). Die
// sind noch nicht da - fuer die Zwischenzeit ist die Rueckfallebene
// nicht weg, sondern EINGEFROREN: Die vier OTA-Abbilder der 3.15.0
// liegen im privaten Release und bleiben flashbar. Neu bauen laesst
// sich der ESP8266-Stand nur noch aus dem Tag v3.15.0. Owner-
// Entscheidung vom 2026-08-27, bewusst so getroffen.
//
// ARDUINOJSON 6.x -> 7 (eigene Etappe, eigener Nachweis). Die
// Festlegung auf 6.x hatte als einzige Begruendung den
// ESP8266-Footprint. DynamicJsonDocument gibt es in 7 nicht mehr; die
// drei Stellen in webfunctions.cpp benutzen jetzt das elastisch
// wachsende JsonDocument. Kosten: text +2916, data +200, bss +/-0.
//
// DABEI EINE GEFAHR MIT NEUEM NAMEN. Bis 3.15.0 hiess sie "feste
// Dokumentgroesse zu klein geschaetzt" - deshalb der Sprung 512 ->
// 1024 in 3.15.0, als hydraulik_switch als siebtes Feld dazukam. Jetzt
// heisst sie "Allokation fehlgeschlagen". Der Ausgang waere derselbe:
// serializeJson schriebe die config.json still unvollstaendig, das
// Geraet startet direkt danach neu, und ein fehlendes MQTT-Passwort
// faellt erst auf, wenn die Hausteuerung nichts mehr bekommt. Beide
// Schreibpfade pruefen deshalb jetzt jsonDoc.overflowed() und
// schreiben im Zweifel GAR NICHT - der alte Stand ist besser als ein
// halber neuer. Bis 3.15.0 wurde ueberhaupt nicht geprueft.
//
// DER PRUEFSTAND IST JETZT EIN ABLAUF, KEIN BOARD. d1_mini_test war
// ein Geraet zum Anstecken; ein drittes ESP32-Board gibt es bewusst
// nicht. Stattdessen wird ein Backup-Board geliehen. Der Test-Prefix
// panasonic_heat_pump32 BLEIBT dafuer erhalten - Ablauf-Backup-
// Boards.md behauptete, er entfalle mit dem toten MQTT-Port 1884; das
// ist richtiggestellt. Ein toter Port sperrt gegen den Broker und
// ersetzt gerade deshalb nicht das Testen MIT erreichbarem Broker auf
// eigenen Topics. Beides zusammen sperrt ein geliehenes Board doppelt.
// Bei der RUECKGABE ist die Reihenfolge sicherheitskritisch: erst
// mqtt_port auf 1884, DANN die Stufen-Firmware. Umgekehrt saesse das
// Backup-Board mit produktivem Prefix am echten Broker und schriebe
// parallel zum laufenden Board mit, ohne an einer WP zu haengen.
// Vollstaendig in test/README.md.
//
// ELF KOMMENTARE UMGESCHRIEBEN, NICHT GELOESCHT. Sie nennen den
// ESP8266 in einer Design-Begruendung (const-Tabellen im RAM, feste
// Puffer statt String, kurzer Statusstring, Statusroute ohne
// Anmeldung). Die Entscheidungen bleiben richtig, nur ihr Grund ist
// weniger zwingend. Wer den Satz statt des Plattformnamens streicht,
// nimmt einem spaeteren Leser den Grund und laedt dazu ein, die
// Entscheidung versehentlich rueckgaengig zu machen. Zwei Sonderfaelle
// stehen ausdruecklich da: In decode.h/commands.cpp lagen const-
// Tabellen auf dem ESP8266 im RAM, auf dem ESP32 liegen sie im Flash -
// die 400 Byte Ersparnis der Feldreihenfolge bleiben, sie kosten nur
// eine andere Ressource. Und der 20-ms-Unterschied je sendContent()
// in webfunctions.cpp ist ein Messergebnis, kein Plattformzwang.
//
// SECHS TOTE PIOTOOLS ENTFERNT (espupload, gzip-firmware,
// http-uploader, sftp-uploader, strip-floats, obj-dump). Keines war in
// extra_scripts eingebunden. obj-dump.py rief seit dem 3.0.0-Port
// xtensa-lx106-elf-objdump auf - eine Toolchain, die dieses Projekt
// gar nicht mehr installiert; es war also schon vorher kaputt, nur
// unbemerkt. Umstellen haette ein weiterhin nicht eingebundenes
// Skript ergeben. Groessennachweise laufen hier ueber
// xtensa-esp32s3-elf-size und -nm auf der .elf.
//
// WAS AUSDRUECKLICH KEIN GEWINN IST: kein Byte kleiner, keine
// Schleife schneller, kein RAM gespart, keine Hosttest-Aenderung. Der
// Gewinn ist die Freiheit, alte Designzwaenge zu ueberdenken - nicht
// die Pflicht dazu.
//
// ABNAHME AM GERAET (2026-08-27). Beide Stufen per OTA, Tabellen-
// vergleich ueber /tablerefresh: H2 90 von 92, H1 89 von 92 Zeilen
// identisch - die Abweichungen sind ausschliesslich laufende
// Messwerte. Der config.json-Pfad an H2 gegengeprueft (Feld geaendert,
// Neustart, zurueckgelesen, uebrige Felder erhalten, Verbindungslage
// 0). Der Notbetrieb an H1 hat real einen Heizlauf ausgeloest
// (Pump_Speed 2200, Pump_Duty 91, Pump_Flow 11,95 l/min), den der
// 5-min-Re-Assert der Steuerung danach erwartungsgemaess zurueckholte.
//
// ANTWORTQUOTE NACHGEMESSEN, weil waehrend dieses Notbetriebslaufs
// zweimal "Telegramm verworfen (unvollstaendig): Typ 0x00, Laenge 0"
// im Log stand. Laenge 0 heisst: gar keine Antwort - das 0x00 ist der
// Platzhalter der Logzeile, kein Byte von der Leitung. Beide Male lag
// ein zweites Telegramm dicht hinter dem ersten (Re-Assert der
// Steuerung fiel in den letzten Notbetriebsschritt). Verloren ist
// dabei eine Leserunde, kein Kommando: Der Notbetrieb liest jeden
// Schritt zurueck, die Steuerung wiederholt alle 5 min.
// Passiver 7-Minuten-Mitschnitt an H1 unter 3.16.0 (05:33:27-05:40:22)
// zum Vergleich mit dem Referenzlauf vom 2026-08-13 (3.6.0):
// 68 Abfragen + 2 Kommandos = 70 gesendet, 70x "Valid data"
// 0 verworfen, 0 Serial-Timeout, 0 Restdaten, 0 Mqtt-Reconnect,
// 0 "Lesefenster laeuft noch"
// Zyklus 6 s (63x), 7 s (2x); die zwei Ausreisser mit 11 und 12 s
// liegen genau dort, wo ein Kommandotelegramm dazwischen lag -
// halbduplex, ein Kommando kostet eine Abfragerunde.
// Damals: 68 Abfragen + 4 Kommandos, ebenfalls kein verworfenes. Die
// Quote ist also unveraendert 100 %; die beiden Aussetzer haengen an
// der dichten Kommandofolge eines Notbetriebslaufs, nicht an 3.16.0.
// Der serielle Pfad ist von diesem Umbau ohnehin nicht beruehrt -
// readSerial, send_pana_command und timeout_serial sind unter den
// 9346 unveraenderten Symbolen.
// Zugabe: Der Anzeigeverfall des Notbetriebs griff auf die Sekunde
// genau (GRUEN 05:21:35 -> "Anzeige zurueckgesetzt" 05:36:35).
//
// FREIGESCHALTET: Analyse-Relais-statt-KNX.md Abschnitt 6.3 ("Die
// Rueckfallebene D1 mini kann es nicht") ist damit erledigt. Die
// Backup-Boards sind dieselbe Hardware und haben die Relais.
//
// 3.15.0 - DER NOTBETRIEB STELLT DIE HYDRAULIK SELBST AUF 1-STUFIG. Neuer
// Schritt 1 in BEIDEN Schrittfolgen: Die Firmware legt den
// Tasmota-Switch der Hydraulik auf AUS und bricht ab, wenn das nicht
// gelingt. Vorhaben-Hydraulik-Notbetrieb.md.
//
// WARUM. Der Notbetrieb setzt hydraulisch 1-stufigen Betrieb voraus,
// und bisher stellte das niemand sicher. Steht die Hydraulik auf
// 2-stufig, waehrend eine Stufe im Warmwasser-Notbetrieb laeuft,
// schiebt der Warmwasserbetrieb bis zu 57 C in den Heizkreis - die
// Fussbodenheizung vertraegt das nicht (Owner, 2026-08-26). Das ist
// kein Randfall, sondern der Regelfall des Notbetriebs an Stufe 2:
// Der Warmwasserknopf ist der, der im Sommer traegt, und der, den
// jemand aus der Familie im Ernstfall druecken soll. Im Normalbetrieb
// schaltet die Kaskadensteuerung den Switch - und genau die ist im
// Notbetriebsfall weg.
//
// DIE UMWAELZPUMPE KOMMT MIT (WaterPump = 0, SET14, TOP104). Am
// 2026-08-27 an beiden Stufen beobachtet: Nach dem Umschalten auf
// 2-stufiges Umpumpen stand TOP104 auf 1 (Fix), die Pumpe lief
// dauerhaft durch (Pump_Flow rund 16 l/min). Gesetzt hat das die
// Kaskadensteuerung - also genau die, die im Notbetriebsfall weg ist.
// Ohne diesen Schritt liefe die Pumpe im Notbetrieb ohne Not weiter.
// Der Schritt steht VOR dem Einschalten und HINTER allen
// Moduswechseln: Ob ein Wechsel der Betriebsart den Pumpenmodus
// zurueckstellt, ist nicht gemessen - an dieser Stelle kann er ihn
// jedenfalls nicht mehr ueberschreiben (dieselbe Vorsicht wie bei den
// Kurvenpunkten). An der Anlage belegt: TOP104 1 -> 0, Pump_Flow
// 16,24 -> 0,13 l/min; der Re-Assert stellt beides hinterher wieder
// her, der Kreislauf ist also derselbe wie bei allen anderen Werten.
//
// GANZ VORN, vor OperationMode. Zwei Gruende: Bricht der Schritt ab,
// steht die Waermepumpe genau so da wie vorher (kein Kommando
// abgesetzt, kein Sammelfenster offen, kein halber Notbetrieb zum
// Aufraeumen). Und die 90 s der beiden Stellantriebe laufen ab dem
// fruehestmoeglichen Moment, parallel zur restlichen Schrittfolge.
//
// DIE 90 s ERZWINGEN KEINE WARTEZEIT. Die Waermepumpe braucht nach
// dem Einschalten rund drei Minuten bis zum Kompressor; zunaechst
// laeuft nur die Umwaelzpumpe (Owner, 2026-08-26). Der Beleg ist der
// Normalbetrieb selbst: Dort gehen Switch- und WP-Kommandos
// GLEICHZEITIG raus, seit jeher und ohne Schaden. Der Notbetrieb hat
// zwischen beiden sogar 16 s (Wasser) bzw. 48 s (Heizen) Vorsprung.
//
// ABBRUCH STATT WARNUNG. Antwortet der Switch nicht, meldet er
// weiterhin ON oder etwas Undeutbares, endet der Lauf in ROT - mit
// einer eigenen Meldung, die den Schalter im Waschraum nennt und
// nicht auf das Bedienfeld der Waermepumpe verweist (dort ist nichts
// verstellt worden). Der Knopf steht nach ROT sofort wieder da: Wer
// den Schalter von Hand legt und wiederkommt, drueckt erneut, der
// Lesevorgang meldet dann OFF, und die Folge laeuft durch.
//
// ERST LESEN, DANN NUR BEI BEDARF SCHALTEN. Ein einzelnes "Power Off"
// wuerde beide Faelle abdecken (Tasmota antwortet auch dann mit OFF,
// wenn der Schalter schon aus war). Der Unterschied steht trotzdem im
// Log - nur so ist nachlesbar, ob tatsaechlich umgeschaltet wurde.
//
// DER BLOCKIERENDE REQUEST ist Absicht. HTTPClient::GET() haelt
// loop() an, bis die Antwort da ist oder das Timeout greift (1,5 s
// statt der voreingestellten 5 s). Genau deshalb steht der Schritt
// vorn: Es ist kein Kommando an die Waermepumpe unterwegs und kein
// Sammelfenster offen, die Blockade trifft nur den Abfragezyklus, der
// ohnehin nur liest. Hoechstens ein Timeout je Lauf, kein
// Wiederholungsversuch - der Mensch vor der Seite ist der bessere.
// Eine asynchrone Loesung ist verworfen: Nebenlaeufigkeit in einem
// Automaten, der vollstaendig aus loop() getrieben wird, gegen 1,5 s
// in einem Fall, der ohnehin im Abbruch endet.
//
// ZWEI ZEITEN AM GERAET KORRIGIERT (2026-08-27, beide im Prueflauf
// gefunden - der Entwurf hatte sie geschaetzt):
//
// 1. TIMEOUT 5000 STATT 1500 ms. Die Entwurfsbegruendung lautete "der
// Switch haengt im selben Subnetz - antwortet er in 1,5 s nicht,
// antwortet er nicht". Fuer den ERSTEN Kontakt nach einem Neustart
// stimmt das nicht: Dreimal hintereinander endete der erste Druck
// nach dem Boot mit "HTTP -1" (Verbindung nicht zustande gekommen),
// waehrend jeder weitere Lauf durchlief. Das ist kein Laborfall,
// sondern der Regelfall - nach einem Stromausfall startet die
// Bridge neu und hat mit dem Switch noch nie gesprochen.
//
// 2. LESEFRIST = TIMEOUT statt eigener 300 ms. Hier stand zuerst eine
// eigene, kleine Frist mit der Begruendung "der Rumpf ist rund 15
// Byte lang und liegt praktisch immer schon im Puffer". Das ist
// fuers LESEN richtig und fuers SCHALTEN falsch: Auf "Power Off"
// legt Tasmota erst das Relais um und veroeffentlicht den neuen
// Zustand per MQTT, bevor es antwortet. Der Lauf endete dann mit
// "liess sich nicht schalten (HTTP 200)" - die Verbindung stand,
// der Switch hatte sauber gearbeitet, und die Firmware hatte die
// Antwort verpasst. Eine eigene kleinere Frist hat hier keinen
// Nutzen: Im Normalfall bricht die Schleife ohnehin ab, sobald
// "OFF" oder "ON" dasteht.
//
// DIE DIAGNOSE TRAEGT JETZT DEN GRUND MIT. Beide Fehler kosteten
// Suchzeit, weil im Log nur "antwortet nicht" stand. Jetzt stehen dort
// der HTTP-Code (negativ = HTTPClient-Fehler, -1 = Verbindung
// gescheitert) UND die ersten Bytes der Antwort. "HTTP -1, Antwort """
// und "HTTP 200, Antwort "..."" sind zwei verschiedene Ursachen mit
// zwei verschiedenen Abhilfen; ohne diese Auskunft sind sie nicht zu
// unterscheiden.
//
// GEMESSEN AM ECHTEN SWITCH (2026-08-27, Tasmota 12.0.2): Die Antwort
// auf /cm?cmnd=Power kommt CHUNKED, ohne Content-Length. Das hat drei
// naheliegende Zeilen ausgeschlossen. getSize() liefert -1, also gibt
// es kein "lies so viele Byte wie angekuendigt". readBytes(puffer, 63)
// wartet, bis 63 Byte da sind oder das Timeout ablaeuft - bei einer
// 15 Byte langen Antwort saesse jeder Lauf die vollen 1,5 s ab, auch
// wenn alles klappt. Und getString() loest die Chunks zwar sauber auf,
// allokiert aber in der Groesse der Antwort auf dem Heap; zeigt die
// eingetragene Adresse versehentlich auf einen richtigen Webserver,
// waere das eine ganze HTML-Seite auf einem ESP8266 mit rund 30 kB
// freiem Heap. Gelesen wird deshalb byteweise in einen festen Puffer,
// mit harter Frist (300 ms) und Schluss, sobald "OFF" oder "ON"
// dasteht. Die Chunk-Laengen im rohen Strom stoeren dabei nicht.
//
// DER ERSTE SCHRITT GEHT JETZT AUS loop() RAUS, nicht mehr aus dem
// POST-Handler. Bis 3.14.2 setzte notbetrieb_starten() das erste
// Kommando selbst ab - fuer ein Set-Kommando eine Sache von
// Mikrosekunden, fuer einen HTTP-Request von bis zu 1,5 s eine
// haengende Seite. NotbetriebLauf bekommt dafuer das Feld
// schritt_gesendet; ohne es koennte ein TOP, das den Sollwert
// zufaellig schon traegt, einen nie abgesetzten Schritt bestaetigen.
//
// NEU AUF DER SEITE: Der Abbruchgrund. Feld 9 des Statusstrings,
// hinten angehaengt - die Felder 5 bis 8 muessen stehen bleiben, weil
// die Startseite dieselbe Route liest. Aus ihm macht die Seite bei
// Hydraulik-Abbruechen den Satz aus Abschnitt 8 des Vorhabens statt
// des generischen "Hat nicht geklappt".
//
// NEUE EINSTELLUNG: hydraulik_switch (IP oder Hostname des Tasmota).
// Sie gehoert NICHT fest in den Code - sie steht in derselben
// Groessenordnung wie der MQTT-Broker und wird sich irgendwann
// aendern. Leeres Feld heisst "nicht eingerichtet": Der Notbetrieb
// bricht dann im ersten Schritt ab, statt die Hydraulik ungeprueft zu
// lassen. Das JSON-Dokument der Settings-Seite waechst dafuer von 512
// auf 1024 Byte - mit dem siebten Feld haetten 512 nicht mehr sicher
// gereicht, und ein Ueberlauf schriebe die config.json still
// unvollstaendig.
//
// ZURUECK SCHALTET WEITERHIN NIEMAND IN DER FIRMWARE (Entscheidung 7).
// Das macht der Re-Assert der Kaskadensteuerung, seit dem 2026-08-26
// mit einer Frische-Bedingung: Ein Schaltbefehl an den Switch geht nur
// raus, wenn der Betriebsmodus von Stufe 2 nicht aelter als zwoelf
// Minuten ist. Der Grund ist, dass Wärmepumpen- und Switch-Kommandos
// den ioBroker ueber VERSCHIEDENE Adapter verlassen (mqtt 1883 gegen
// sonoff 1886) - faellt der eine aus, laeuft der andere weiter und
// legte die Hydraulik mitten im Notbetrieb zurueck auf 2-stufig.
//
// GROESSE (.elf, text/data/bss gegen 3.14.2):
// esp32_h1_ota 1018319/221386/2192879 -> 1034911/225430/2192951
// esp32_h2_ota 1017983/221018/2192879 -> 1034535/225142/2192951
// d1_mini_h1 469923/2440/31216 -> 476491/2440/31304
// Der Zuwachs von rund 20 kB (ESP32) bzw. 6,5 kB (ESP8266) Flash ist
// fast vollstaendig der HTTPClient; im RAM sind es 72 bzw. 88 Byte.
//
// SCHRITTZAHLEN UND ZEITEN: Heizen 9 Schritte / 72 s (Deckel 180 s),
// Warmwasser 5 Schritte / 40 s (Deckel 100 s). An H2 gemessen: GRUEN
// nach 43 s.
//
// AN DER ANLAGE ABGENOMMEN (2026-08-27, H2): Switch von EIN auf AUS
// geschaltet und GRUEN; Switch stand schon auf AUS und wurde nicht
// angefasst ("stand bereits"); Switch unerreichbar -> ROT nach rund
// 5 s mit der Waschraum-Meldung und KEIN Kommando an der Waermepumpe;
// der Abfragezyklus zeigte dabei einen einmaligen Versatz von 4-5 s
// und lief danach ohne Nacharbeit weiter; der Re-Assert stellte
// 2-stufig und den Fix-Modus der Pumpe jedes Mal wieder her. Der
// POST-Handler antwortete in 36 ms - der Request laeuft also
// tatsaechlich aus loop() und nicht im Handler.
//
// MITGEZOGEN: test/notbetrieb_test.cpp (neuer Abschnitt 9a, alle
// Schrittindizes verschoben), Ablauf-Notbetrieb.md und README.md.
//
// 3.14.2 - Der Kuehlkurven-Aussenpunkt Z1CoolCurveOutsideLowTemp (SET33) war
// auf 20..30 begrenzt. Richtig ist 15..30. Die Firmware wies 15 mit
// "Value 15 out of range [20..30]" ab, bevor das Kommando ueberhaupt
// zur WP ging - ein Wert, den die Anlage annimmt, kam so nie an.
//
// DER NACHWEIS kommt vom Geraet selbst: Am Bedienterminal laesst sich
// der untere Punkt der Kuehlkurve auf 15 C stellen, die WP uebernimmt
// und zeigt ihn (pictures/Kuehlen_Kurve.png, 2026-08-25). Die X-Achse