iobroker.hassemu 1.35.2 → 1.35.3

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/README.md CHANGED
@@ -160,6 +160,9 @@ Got scripts that still write to `visUrl`? Update them — write to `manualUrl` i
160
160
  Placeholder for the next version (at the beginning of the line):
161
161
  ### **WORK IN PROGRESS**
162
162
  -->
163
+ ### 1.35.3 (2026-06-15)
164
+ - Fixed Home Assistant discovery pointing the display at the wrong address on multi-interface hosts; it now uses the address the adapter actually listens on.
165
+
163
166
  ### 1.35.2 (2026-06-12)
164
167
 
165
168
  - Displays whose registration became stale after an adapter restart now re-register automatically — the server previously answered in a way the companion app did not recognize as "please register again"
@@ -177,10 +180,6 @@ Got scripts that still write to `visUrl`? Update them — write to `manualUrl` i
177
180
 
178
181
  - Home Assistant Companion App and Shelly Wall Display (firmware 2.6.0+): sign-out and device registration now complete reliably.
179
182
 
180
- ### 1.33.2 (2026-05-23)
181
-
182
- - Changelog rewritten in user-centric style across all versions.
183
-
184
183
  [Older changelogs can be found there](CHANGELOG_OLD.md)
185
184
 
186
185
  ## Support Development
package/build/lib/mdns.js CHANGED
@@ -60,8 +60,8 @@ class MDNSService {
60
60
  /** Start mDNS broadcasting via bonjour-service */
61
61
  start() {
62
62
  var _a, _b, _c;
63
- const localIP = (0, import_network.getLocalIp)();
64
- const baseUrl = `http://${localIP}:${this.config.port}`;
63
+ const host = (0, import_network.resolveAdvertisedHost)(this.config.bindAddress);
64
+ const baseUrl = `http://${host}:${this.config.port}`;
65
65
  const serviceName = this.config.serviceName || "ioBroker";
66
66
  try {
67
67
  this.bonjour = new import_bonjour_service.default();
@@ -96,7 +96,7 @@ class MDNSService {
96
96
  });
97
97
  this.active = true;
98
98
  this.adapter.log.debug(
99
- `mDNS: Broadcasting ${serviceName}._home-assistant._tcp.local on ${localIP}:${this.config.port}`
99
+ `mDNS: Broadcasting ${serviceName}._home-assistant._tcp.local on ${host}:${this.config.port}`
100
100
  );
101
101
  this.adapter.log.debug(`mDNS: UUID: ${this.uuid}`);
102
102
  } catch (error) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/mdns.ts"],
4
- "sourcesContent": ["import Bonjour from \"bonjour-service\";\nimport { HA_VERSION } from \"./constants\";\nimport { getLocalIp } from \"./network\";\nimport type { AdapterConfig, AdapterInterface } from \"./types\";\n\ntype PublishedService = ReturnType<InstanceType<typeof Bonjour>[\"publish\"]>;\n\n/** mDNS service for Home Assistant discovery via bonjour-service */\nexport class MDNSService {\n private readonly adapter: AdapterInterface;\n private readonly config: AdapterConfig;\n public readonly uuid: string;\n private active = false;\n private bonjour: Bonjour | null = null;\n private published: PublishedService | null = null;\n\n /** Read-only flag \u2014 true between successful `start()` and `stop()`. */\n public isActive(): boolean {\n return this.active;\n }\n\n /**\n * Creates a new MDNSService instance\n *\n * @param adapter - Adapter interface for logging\n * @param config - Adapter configuration\n * @param uuid - Shared UUID for consistent identity across WebServer and mDNS\n */\n constructor(adapter: AdapterInterface, config: AdapterConfig, uuid: string) {\n this.adapter = adapter;\n this.config = config;\n this.uuid = uuid;\n }\n\n /** Start mDNS broadcasting via bonjour-service */\n start(): void {\n const localIP = getLocalIp();\n const baseUrl = `http://${localIP}:${this.config.port}`;\n const serviceName = this.config.serviceName || \"ioBroker\";\n\n try {\n this.bonjour = new Bonjour();\n\n // Empty TXT records are dropped \u2014 bonjour-service publishes them as\n // empty strings otherwise, which clutters the discovery payload.\n const txt: Record<string, string> = {\n base_url: baseUrl,\n internal_url: baseUrl,\n version: HA_VERSION,\n uuid: this.uuid,\n location_name: serviceName,\n // mDNS-TXT ist string-only \u2014 boolean explizit zu \u201ETrue\"/\u201EFalse\" mappen.\n // Vorher hardcoded 'True' unabh\u00E4ngig von authRequired \u2192 Spec-Drift (HA-Clients\n // mit strict-mode triggerten Auth-Flow auch bei authRequired=false).\n requires_api_password: this.config.authRequired ? \"True\" : \"False\",\n };\n\n this.published = this.bonjour.publish({\n name: serviceName,\n type: \"home-assistant\",\n protocol: \"tcp\",\n port: this.config.port,\n txt,\n });\n\n // v1.15.0 (D12): Bonjour wirft Bind-Fehler (z.B. Port 5353 belegt)\n // ASYNCHRON in dgram-Sockets \u2014 der sync try/catch oben f\u00E4ngt das\n // nicht. Listener auf 'error' anh\u00E4ngen, dann active=false zur\u00FCcksetzen.\n this.published.on?.(\"error\", (err: Error) => {\n this.adapter.log.warn(`mDNS async publish error: ${err.message}`);\n this.active = false;\n try {\n this.bonjour?.destroy();\n } catch {\n /* best effort */\n }\n this.bonjour = null;\n this.published = null;\n });\n\n this.active = true;\n\n this.adapter.log.debug(\n `mDNS: Broadcasting ${serviceName}._home-assistant._tcp.local on ${localIP}:${this.config.port}`,\n );\n this.adapter.log.debug(`mDNS: UUID: ${this.uuid}`);\n } catch (error) {\n const err = error as Error;\n this.adapter.log.warn(`mDNS failed to start: ${err.message}`);\n // Wichtig: bonjour-instance freigeben sonst leakt der UDP-Socket\n // \u00FCber die Adapter-Lifetime. `stop()` short-circuit'd auf\n // `!this.active` und w\u00FCrde nichts cleanen.\n try {\n this.bonjour?.destroy();\n } catch {\n /* destroy darf re-throwen \u2014 wir wollen nur die Resource lossen */\n }\n this.bonjour = null;\n this.published = null;\n }\n }\n\n /** Stop mDNS broadcasting */\n stop(): void {\n if (!this.active) {\n return;\n }\n\n try {\n if (this.published) {\n this.published.stop?.();\n this.published = null;\n }\n if (this.bonjour) {\n this.bonjour.destroy();\n this.bonjour = null;\n }\n this.adapter.log.debug(\"mDNS: Service stopped\");\n } catch (error) {\n const err = error as Error;\n this.adapter.log.warn(`mDNS could not stop cleanly: ${err.message}`);\n }\n\n this.active = false;\n }\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,6BAAoB;AACpB,uBAA2B;AAC3B,qBAA2B;AAMpB,MAAM,YAAY;AAAA,EACN;AAAA,EACA;AAAA,EACD;AAAA,EACR,SAAS;AAAA,EACT,UAA0B;AAAA,EAC1B,YAAqC;AAAA;AAAA,EAGtC,WAAoB;AACzB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,YAAY,SAA2B,QAAuB,MAAc;AAC1E,SAAK,UAAU;AACf,SAAK,SAAS;AACd,SAAK,OAAO;AAAA,EACd;AAAA;AAAA,EAGA,QAAc;AAnChB;AAoCI,UAAM,cAAU,2BAAW;AAC3B,UAAM,UAAU,UAAU,OAAO,IAAI,KAAK,OAAO,IAAI;AACrD,UAAM,cAAc,KAAK,OAAO,eAAe;AAE/C,QAAI;AACF,WAAK,UAAU,IAAI,uBAAAA,QAAQ;AAI3B,YAAM,MAA8B;AAAA,QAClC,UAAU;AAAA,QACV,cAAc;AAAA,QACd,SAAS;AAAA,QACT,MAAM,KAAK;AAAA,QACX,eAAe;AAAA;AAAA;AAAA;AAAA,QAIf,uBAAuB,KAAK,OAAO,eAAe,SAAS;AAAA,MAC7D;AAEA,WAAK,YAAY,KAAK,QAAQ,QAAQ;AAAA,QACpC,MAAM;AAAA,QACN,MAAM;AAAA,QACN,UAAU;AAAA,QACV,MAAM,KAAK,OAAO;AAAA,QAClB;AAAA,MACF,CAAC;AAKD,uBAAK,WAAU,OAAf,4BAAoB,SAAS,CAAC,QAAe;AApEnD,YAAAC;AAqEQ,aAAK,QAAQ,IAAI,KAAK,6BAA6B,IAAI,OAAO,EAAE;AAChE,aAAK,SAAS;AACd,YAAI;AACF,WAAAA,MAAA,KAAK,YAAL,gBAAAA,IAAc;AAAA,QAChB,QAAQ;AAAA,QAER;AACA,aAAK,UAAU;AACf,aAAK,YAAY;AAAA,MACnB;AAEA,WAAK,SAAS;AAEd,WAAK,QAAQ,IAAI;AAAA,QACf,sBAAsB,WAAW,kCAAkC,OAAO,IAAI,KAAK,OAAO,IAAI;AAAA,MAChG;AACA,WAAK,QAAQ,IAAI,MAAM,eAAe,KAAK,IAAI,EAAE;AAAA,IACnD,SAAS,OAAO;AACd,YAAM,MAAM;AACZ,WAAK,QAAQ,IAAI,KAAK,yBAAyB,IAAI,OAAO,EAAE;AAI5D,UAAI;AACF,mBAAK,YAAL,mBAAc;AAAA,MAChB,QAAQ;AAAA,MAER;AACA,WAAK,UAAU;AACf,WAAK,YAAY;AAAA,IACnB;AAAA,EACF;AAAA;AAAA,EAGA,OAAa;AAvGf;AAwGI,QAAI,CAAC,KAAK,QAAQ;AAChB;AAAA,IACF;AAEA,QAAI;AACF,UAAI,KAAK,WAAW;AAClB,yBAAK,WAAU,SAAf;AACA,aAAK,YAAY;AAAA,MACnB;AACA,UAAI,KAAK,SAAS;AAChB,aAAK,QAAQ,QAAQ;AACrB,aAAK,UAAU;AAAA,MACjB;AACA,WAAK,QAAQ,IAAI,MAAM,uBAAuB;AAAA,IAChD,SAAS,OAAO;AACd,YAAM,MAAM;AACZ,WAAK,QAAQ,IAAI,KAAK,gCAAgC,IAAI,OAAO,EAAE;AAAA,IACrE;AAEA,SAAK,SAAS;AAAA,EAChB;AACF;",
4
+ "sourcesContent": ["import Bonjour from \"bonjour-service\";\nimport { HA_VERSION } from \"./constants\";\nimport { resolveAdvertisedHost } from \"./network\";\nimport type { AdapterConfig, AdapterInterface } from \"./types\";\n\ntype PublishedService = ReturnType<InstanceType<typeof Bonjour>[\"publish\"]>;\n\n/** mDNS service for Home Assistant discovery via bonjour-service */\nexport class MDNSService {\n private readonly adapter: AdapterInterface;\n private readonly config: AdapterConfig;\n public readonly uuid: string;\n private active = false;\n private bonjour: Bonjour | null = null;\n private published: PublishedService | null = null;\n\n /** Read-only flag \u2014 true between successful `start()` and `stop()`. */\n public isActive(): boolean {\n return this.active;\n }\n\n /**\n * Creates a new MDNSService instance\n *\n * @param adapter - Adapter interface for logging\n * @param config - Adapter configuration\n * @param uuid - Shared UUID for consistent identity across WebServer and mDNS\n */\n constructor(adapter: AdapterInterface, config: AdapterConfig, uuid: string) {\n this.adapter = adapter;\n this.config = config;\n this.uuid = uuid;\n }\n\n /** Start mDNS broadcasting via bonjour-service */\n start(): void {\n const host = resolveAdvertisedHost(this.config.bindAddress);\n const baseUrl = `http://${host}:${this.config.port}`;\n const serviceName = this.config.serviceName || \"ioBroker\";\n\n try {\n this.bonjour = new Bonjour();\n\n // Empty TXT records are dropped \u2014 bonjour-service publishes them as\n // empty strings otherwise, which clutters the discovery payload.\n const txt: Record<string, string> = {\n base_url: baseUrl,\n internal_url: baseUrl,\n version: HA_VERSION,\n uuid: this.uuid,\n location_name: serviceName,\n // mDNS-TXT ist string-only \u2014 boolean explizit zu \u201ETrue\"/\u201EFalse\" mappen.\n // Vorher hardcoded 'True' unabh\u00E4ngig von authRequired \u2192 Spec-Drift (HA-Clients\n // mit strict-mode triggerten Auth-Flow auch bei authRequired=false).\n requires_api_password: this.config.authRequired ? \"True\" : \"False\",\n };\n\n this.published = this.bonjour.publish({\n name: serviceName,\n type: \"home-assistant\",\n protocol: \"tcp\",\n port: this.config.port,\n txt,\n });\n\n // v1.15.0 (D12): Bonjour wirft Bind-Fehler (z.B. Port 5353 belegt)\n // ASYNCHRON in dgram-Sockets \u2014 der sync try/catch oben f\u00E4ngt das\n // nicht. Listener auf 'error' anh\u00E4ngen, dann active=false zur\u00FCcksetzen.\n this.published.on?.(\"error\", (err: Error) => {\n this.adapter.log.warn(`mDNS async publish error: ${err.message}`);\n this.active = false;\n try {\n this.bonjour?.destroy();\n } catch {\n /* best effort */\n }\n this.bonjour = null;\n this.published = null;\n });\n\n this.active = true;\n\n this.adapter.log.debug(\n `mDNS: Broadcasting ${serviceName}._home-assistant._tcp.local on ${host}:${this.config.port}`,\n );\n this.adapter.log.debug(`mDNS: UUID: ${this.uuid}`);\n } catch (error) {\n const err = error as Error;\n this.adapter.log.warn(`mDNS failed to start: ${err.message}`);\n // Wichtig: bonjour-instance freigeben sonst leakt der UDP-Socket\n // \u00FCber die Adapter-Lifetime. `stop()` short-circuit'd auf\n // `!this.active` und w\u00FCrde nichts cleanen.\n try {\n this.bonjour?.destroy();\n } catch {\n /* destroy darf re-throwen \u2014 wir wollen nur die Resource lossen */\n }\n this.bonjour = null;\n this.published = null;\n }\n }\n\n /** Stop mDNS broadcasting */\n stop(): void {\n if (!this.active) {\n return;\n }\n\n try {\n if (this.published) {\n this.published.stop?.();\n this.published = null;\n }\n if (this.bonjour) {\n this.bonjour.destroy();\n this.bonjour = null;\n }\n this.adapter.log.debug(\"mDNS: Service stopped\");\n } catch (error) {\n const err = error as Error;\n this.adapter.log.warn(`mDNS could not stop cleanly: ${err.message}`);\n }\n\n this.active = false;\n }\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,6BAAoB;AACpB,uBAA2B;AAC3B,qBAAsC;AAM/B,MAAM,YAAY;AAAA,EACN;AAAA,EACA;AAAA,EACD;AAAA,EACR,SAAS;AAAA,EACT,UAA0B;AAAA,EAC1B,YAAqC;AAAA;AAAA,EAGtC,WAAoB;AACzB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,YAAY,SAA2B,QAAuB,MAAc;AAC1E,SAAK,UAAU;AACf,SAAK,SAAS;AACd,SAAK,OAAO;AAAA,EACd;AAAA;AAAA,EAGA,QAAc;AAnChB;AAoCI,UAAM,WAAO,sCAAsB,KAAK,OAAO,WAAW;AAC1D,UAAM,UAAU,UAAU,IAAI,IAAI,KAAK,OAAO,IAAI;AAClD,UAAM,cAAc,KAAK,OAAO,eAAe;AAE/C,QAAI;AACF,WAAK,UAAU,IAAI,uBAAAA,QAAQ;AAI3B,YAAM,MAA8B;AAAA,QAClC,UAAU;AAAA,QACV,cAAc;AAAA,QACd,SAAS;AAAA,QACT,MAAM,KAAK;AAAA,QACX,eAAe;AAAA;AAAA;AAAA;AAAA,QAIf,uBAAuB,KAAK,OAAO,eAAe,SAAS;AAAA,MAC7D;AAEA,WAAK,YAAY,KAAK,QAAQ,QAAQ;AAAA,QACpC,MAAM;AAAA,QACN,MAAM;AAAA,QACN,UAAU;AAAA,QACV,MAAM,KAAK,OAAO;AAAA,QAClB;AAAA,MACF,CAAC;AAKD,uBAAK,WAAU,OAAf,4BAAoB,SAAS,CAAC,QAAe;AApEnD,YAAAC;AAqEQ,aAAK,QAAQ,IAAI,KAAK,6BAA6B,IAAI,OAAO,EAAE;AAChE,aAAK,SAAS;AACd,YAAI;AACF,WAAAA,MAAA,KAAK,YAAL,gBAAAA,IAAc;AAAA,QAChB,QAAQ;AAAA,QAER;AACA,aAAK,UAAU;AACf,aAAK,YAAY;AAAA,MACnB;AAEA,WAAK,SAAS;AAEd,WAAK,QAAQ,IAAI;AAAA,QACf,sBAAsB,WAAW,kCAAkC,IAAI,IAAI,KAAK,OAAO,IAAI;AAAA,MAC7F;AACA,WAAK,QAAQ,IAAI,MAAM,eAAe,KAAK,IAAI,EAAE;AAAA,IACnD,SAAS,OAAO;AACd,YAAM,MAAM;AACZ,WAAK,QAAQ,IAAI,KAAK,yBAAyB,IAAI,OAAO,EAAE;AAI5D,UAAI;AACF,mBAAK,YAAL,mBAAc;AAAA,MAChB,QAAQ;AAAA,MAER;AACA,WAAK,UAAU;AACf,WAAK,YAAY;AAAA,IACnB;AAAA,EACF;AAAA;AAAA,EAGA,OAAa;AAvGf;AAwGI,QAAI,CAAC,KAAK,QAAQ;AAChB;AAAA,IACF;AAEA,QAAI;AACF,UAAI,KAAK,WAAW;AAClB,yBAAK,WAAU,SAAf;AACA,aAAK,YAAY;AAAA,MACnB;AACA,UAAI,KAAK,SAAS;AAChB,aAAK,QAAQ,QAAQ;AACrB,aAAK,UAAU;AAAA,MACjB;AACA,WAAK,QAAQ,IAAI,MAAM,uBAAuB;AAAA,IAChD,SAAS,OAAO;AACd,YAAM,MAAM;AACZ,WAAK,QAAQ,IAAI,KAAK,gCAAgC,IAAI,OAAO,EAAE;AAAA,IACrE;AAEA,SAAK,SAAS;AAAA,EAChB;AACF;",
6
6
  "names": ["Bonjour", "_a"]
7
7
  }
@@ -30,7 +30,8 @@ var network_exports = {};
30
30
  __export(network_exports, {
31
31
  generateClientId: () => generateClientId,
32
32
  getLocalIp: () => getLocalIp,
33
- isWildcardBind: () => isWildcardBind
33
+ isWildcardBind: () => isWildcardBind,
34
+ resolveAdvertisedHost: () => resolveAdvertisedHost
34
35
  });
35
36
  module.exports = __toCommonJS(network_exports);
36
37
  var import_node_crypto = __toESM(require("node:crypto"));
@@ -70,6 +71,12 @@ function isWildcardBind(bindAddress) {
70
71
  }
71
72
  return bindAddress === "0.0.0.0" || bindAddress === "::";
72
73
  }
74
+ function resolveAdvertisedHost(bindAddress) {
75
+ if (bindAddress && !isWildcardBind(bindAddress)) {
76
+ return bindAddress;
77
+ }
78
+ return getLocalIp();
79
+ }
73
80
  function generateClientId() {
74
81
  return import_node_crypto.default.randomBytes(3).toString("hex");
75
82
  }
@@ -77,6 +84,7 @@ function generateClientId() {
77
84
  0 && (module.exports = {
78
85
  generateClientId,
79
86
  getLocalIp,
80
- isWildcardBind
87
+ isWildcardBind,
88
+ resolveAdvertisedHost
81
89
  });
82
90
  //# sourceMappingURL=network.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/network.ts"],
4
- "sourcesContent": ["import crypto from \"node:crypto\";\nimport os from \"node:os\";\n\n/**\n * Returns the first non-internal IPv4 address, falls back to non-internal\n * IPv6, finally `127.0.0.1`.\n *\n * v1.15.0 (D10): Pure-IPv6-LAN-Hosts hatten nur `127.0.0.1` als advertise-IP\n * \u2014 mDNS broadcastete dann unbrauchbares Loopback. Jetzt: IPv6-Fallback\n * vor Loopback. IPv4 hat weiterhin Vorrang weil HA-Clients (Wall Display\n * etc.) traditionell IPv4 erwarten.\n *\n * v1.21.0 (E6): Docker-Bridge-IPs (172.17.x.x default + \u00E4hnliche Container-\n * Bridges) werden gegen\u00FCber \u201Eechten\" LAN-IPs deprioritisiert \u2014 `bind: 0.0.0.0`\n * + Docker f\u00FChrte sonst dazu, dass mDNS die Container-Bridge advertised, die\n * vom LAN aus nicht erreichbar ist. Echte LAN-IPs (192.168.x.x, 10.x.x.x,\n * 172.16-31.x.x au\u00DFer 172.17) haben Vorrang.\n */\nexport function getLocalIp(): string {\n const interfaces = os.networkInterfaces();\n let dockerBridgeFallback: string | null = null;\n let ipv6Fallback: string | null = null;\n for (const ifaces of Object.values(interfaces)) {\n if (!ifaces) {\n continue;\n }\n for (const iface of ifaces) {\n if (iface.internal) {\n continue;\n }\n if (iface.family === \"IPv4\") {\n if (iface.address.startsWith(\"172.17.\") || iface.address.startsWith(\"172.18.\")) {\n // Default Docker-Bridge \u2014 only use as last resort.\n if (!dockerBridgeFallback) {\n dockerBridgeFallback = iface.address;\n }\n continue;\n }\n return iface.address;\n }\n if (iface.family === \"IPv6\" && !ipv6Fallback) {\n ipv6Fallback = iface.address;\n }\n }\n }\n return dockerBridgeFallback ?? ipv6Fallback ?? \"127.0.0.1\";\n}\n\n/**\n * Returns true if the bind address means \"any interface\" (0.0.0.0, empty or undefined).\n *\n * @param bindAddress The configured bind address.\n */\nexport function isWildcardBind(bindAddress: string | undefined | null): boolean {\n if (!bindAddress) {\n return true;\n }\n return bindAddress === \"0.0.0.0\" || bindAddress === \"::\";\n}\n\n/**\n * Generates a short (6-char), URL-safe, lowercase hex client ID.\n * 16^6 = 16.7 million combinations \u2014 sufficient for home networks, readable as\n * a datapoint segment. Uses `crypto.randomBytes` for consistency with the rest\n * of the codebase (cookies, session ids, tokens are all crypto-secure).\n */\nexport function generateClientId(): string {\n return crypto.randomBytes(3).toString(\"hex\");\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAAmB;AACnB,qBAAe;AAiBR,SAAS,aAAqB;AAlBrC;AAmBE,QAAM,aAAa,eAAAA,QAAG,kBAAkB;AACxC,MAAI,uBAAsC;AAC1C,MAAI,eAA8B;AAClC,aAAW,UAAU,OAAO,OAAO,UAAU,GAAG;AAC9C,QAAI,CAAC,QAAQ;AACX;AAAA,IACF;AACA,eAAW,SAAS,QAAQ;AAC1B,UAAI,MAAM,UAAU;AAClB;AAAA,MACF;AACA,UAAI,MAAM,WAAW,QAAQ;AAC3B,YAAI,MAAM,QAAQ,WAAW,SAAS,KAAK,MAAM,QAAQ,WAAW,SAAS,GAAG;AAE9E,cAAI,CAAC,sBAAsB;AACzB,mCAAuB,MAAM;AAAA,UAC/B;AACA;AAAA,QACF;AACA,eAAO,MAAM;AAAA,MACf;AACA,UAAI,MAAM,WAAW,UAAU,CAAC,cAAc;AAC5C,uBAAe,MAAM;AAAA,MACvB;AAAA,IACF;AAAA,EACF;AACA,UAAO,2DAAwB,iBAAxB,YAAwC;AACjD;AAOO,SAAS,eAAe,aAAiD;AAC9E,MAAI,CAAC,aAAa;AAChB,WAAO;AAAA,EACT;AACA,SAAO,gBAAgB,aAAa,gBAAgB;AACtD;AAQO,SAAS,mBAA2B;AACzC,SAAO,mBAAAC,QAAO,YAAY,CAAC,EAAE,SAAS,KAAK;AAC7C;",
4
+ "sourcesContent": ["import crypto from \"node:crypto\";\nimport os from \"node:os\";\n\n/**\n * Returns the first non-internal IPv4 address, falls back to non-internal\n * IPv6, finally `127.0.0.1`.\n *\n * v1.15.0 (D10): Pure-IPv6-LAN-Hosts hatten nur `127.0.0.1` als advertise-IP\n * \u2014 mDNS broadcastete dann unbrauchbares Loopback. Jetzt: IPv6-Fallback\n * vor Loopback. IPv4 hat weiterhin Vorrang weil HA-Clients (Wall Display\n * etc.) traditionell IPv4 erwarten.\n *\n * v1.21.0 (E6): Docker-Bridge-IPs (172.17.x.x default + \u00E4hnliche Container-\n * Bridges) werden gegen\u00FCber \u201Eechten\" LAN-IPs deprioritisiert \u2014 `bind: 0.0.0.0`\n * + Docker f\u00FChrte sonst dazu, dass mDNS die Container-Bridge advertised, die\n * vom LAN aus nicht erreichbar ist. Echte LAN-IPs (192.168.x.x, 10.x.x.x,\n * 172.16-31.x.x au\u00DFer 172.17) haben Vorrang.\n */\nexport function getLocalIp(): string {\n const interfaces = os.networkInterfaces();\n let dockerBridgeFallback: string | null = null;\n let ipv6Fallback: string | null = null;\n for (const ifaces of Object.values(interfaces)) {\n if (!ifaces) {\n continue;\n }\n for (const iface of ifaces) {\n if (iface.internal) {\n continue;\n }\n if (iface.family === \"IPv4\") {\n if (iface.address.startsWith(\"172.17.\") || iface.address.startsWith(\"172.18.\")) {\n // Default Docker-Bridge \u2014 only use as last resort.\n if (!dockerBridgeFallback) {\n dockerBridgeFallback = iface.address;\n }\n continue;\n }\n return iface.address;\n }\n if (iface.family === \"IPv6\" && !ipv6Fallback) {\n ipv6Fallback = iface.address;\n }\n }\n }\n return dockerBridgeFallback ?? ipv6Fallback ?? \"127.0.0.1\";\n}\n\n/**\n * Returns true if the bind address means \"any interface\" (0.0.0.0, empty or undefined).\n *\n * @param bindAddress The configured bind address.\n */\nexport function isWildcardBind(bindAddress: string | undefined | null): boolean {\n if (!bindAddress) {\n return true;\n }\n return bindAddress === \"0.0.0.0\" || bindAddress === \"::\";\n}\n\n/**\n * Returns the host this server should advertise as its OWN address \u2014 used by\n * both the mDNS `base_url`/`internal_url` TXT records and `/api/discovery_info`.\n *\n * A concrete (non-wildcard) bind address is exactly where the server listens,\n * so advertise it verbatim. Only for a wildcard bind (`0.0.0.0` / `::` / empty)\n * do we fall back to the first routable non-internal IPv4 via {@link getLocalIp}.\n *\n * Single source of truth so the two discovery channels never diverge: before\n * this, `mdns.ts` advertised `getLocalIp()` unconditionally while\n * `/api/discovery_info` already preferred the bind address \u2014 on a multi-homed\n * host mDNS could point Home Assistant clients at a different interface than the\n * one actually bound, breaking auto-discovery.\n *\n * @param bindAddress The configured bind address.\n */\nexport function resolveAdvertisedHost(bindAddress: string | undefined | null): string {\n // The `bindAddress &&` keeps the type narrowed to a non-empty string for the\n // return (isWildcardBind alone doesn't narrow); it also short-circuits the\n // wildcard/empty cases to getLocalIp below.\n if (bindAddress && !isWildcardBind(bindAddress)) {\n return bindAddress;\n }\n return getLocalIp();\n}\n\n/**\n * Generates a short (6-char), URL-safe, lowercase hex client ID.\n * 16^6 = 16.7 million combinations \u2014 sufficient for home networks, readable as\n * a datapoint segment. Uses `crypto.randomBytes` for consistency with the rest\n * of the codebase (cookies, session ids, tokens are all crypto-secure).\n */\nexport function generateClientId(): string {\n return crypto.randomBytes(3).toString(\"hex\");\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAAmB;AACnB,qBAAe;AAiBR,SAAS,aAAqB;AAlBrC;AAmBE,QAAM,aAAa,eAAAA,QAAG,kBAAkB;AACxC,MAAI,uBAAsC;AAC1C,MAAI,eAA8B;AAClC,aAAW,UAAU,OAAO,OAAO,UAAU,GAAG;AAC9C,QAAI,CAAC,QAAQ;AACX;AAAA,IACF;AACA,eAAW,SAAS,QAAQ;AAC1B,UAAI,MAAM,UAAU;AAClB;AAAA,MACF;AACA,UAAI,MAAM,WAAW,QAAQ;AAC3B,YAAI,MAAM,QAAQ,WAAW,SAAS,KAAK,MAAM,QAAQ,WAAW,SAAS,GAAG;AAE9E,cAAI,CAAC,sBAAsB;AACzB,mCAAuB,MAAM;AAAA,UAC/B;AACA;AAAA,QACF;AACA,eAAO,MAAM;AAAA,MACf;AACA,UAAI,MAAM,WAAW,UAAU,CAAC,cAAc;AAC5C,uBAAe,MAAM;AAAA,MACvB;AAAA,IACF;AAAA,EACF;AACA,UAAO,2DAAwB,iBAAxB,YAAwC;AACjD;AAOO,SAAS,eAAe,aAAiD;AAC9E,MAAI,CAAC,aAAa;AAChB,WAAO;AAAA,EACT;AACA,SAAO,gBAAgB,aAAa,gBAAgB;AACtD;AAkBO,SAAS,sBAAsB,aAAgD;AAIpF,MAAI,eAAe,CAAC,eAAe,WAAW,GAAG;AAC/C,WAAO;AAAA,EACT;AACA,SAAO,WAAW;AACpB;AAQO,SAAS,mBAA2B;AACzC,SAAO,mBAAAC,QAAO,YAAY,CAAC,EAAE,SAAS,KAAK;AAC7C;",
6
6
  "names": ["os", "crypto"]
7
7
  }
@@ -413,8 +413,7 @@ class WebServer {
413
413
  this.app.get("/api/", () => ({ message: "API running." }));
414
414
  this.app.get("/api/config", () => this.buildHaConfig());
415
415
  this.app.get("/api/discovery_info", () => {
416
- const isWildcard = !this.config.bindAddress || (0, import_network.isWildcardBind)(this.config.bindAddress);
417
- const host = isWildcard ? (0, import_network.getLocalIp)() : this.config.bindAddress;
416
+ const host = (0, import_network.resolveAdvertisedHost)(this.config.bindAddress);
418
417
  const baseUrl = `http://${host}:${this.config.port}`;
419
418
  return {
420
419
  base_url: baseUrl,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/webserver.ts"],
4
- "sourcesContent": ["import crypto from \"node:crypto\";\nimport dns from \"node:dns/promises\";\nimport fastifyCookie from \"@fastify/cookie\";\nimport fastifyFormbody from \"@fastify/formbody\";\nimport fastifyWebsocket from \"@fastify/websocket\";\nimport Fastify, { type FastifyInstance, type FastifyReply, type FastifyRequest } from \"fastify\";\nimport type { WebSocket } from \"ws\";\nimport {\n HA_VERSION,\n SESSION_TTL_MS,\n CLEANUP_INTERVAL_MS,\n LOGIN_SCHEMA,\n OAUTH_ACCESS_TOKEN_TTL_S,\n SESSIONS_CAP,\n WEBHOOK_REGISTRATIONS_CAP,\n REQUEST_ERROR_COOLDOWN_MS,\n REQUEST_ERROR_COOLDOWN_CAP,\n COOKIE_MAX_AGE_S,\n WS_AUTH_TIMEOUT_MS,\n} from \"./constants\";\nimport { coerceString, coerceUuid, evictOldest, isValidRedirectUri, safeStringEqual } from \"./coerce\";\nimport { buildRedirectUrl, renderAuthorizeError, renderAuthorizeForm, renderAuthorizeRedirect } from \"./auth-page\";\nimport type { ClientRegistry } from \"./client-registry\";\nimport type { GlobalConfig } from \"./global-config\";\nimport { renderLandingPage } from \"./landing-page\";\nimport { getLocalIp, isWildcardBind } from \"./network\";\nimport { renderRedirectWrapper } from \"./redirect-wrapper\";\nimport type { AdapterConfig, AdapterInterface, ClientRecord, SessionData } from \"./types\";\n\n// v1.22.0 (F5): `safeStringEqual` ist nach `coerce.ts` verschoben \u2014 generischer\n// crypto-Helper, kein webserver-spezifischer Belang.\n\n// v1.32.0: `renderRedirectWrapper` ist nach `lib/redirect-wrapper.ts` ausgelagert\n// f\u00FCr Symmetrie zu `landing-page.ts` / `auth-page.ts`. `evictOldest` ist shared\n// helper aus `coerce.ts`.\n\n/** Adapter surface the WebServer depends on \u2014 adds `namespace` for the setup page. */\nexport type WebServerAdapter = AdapterInterface & Pick<ioBroker.Adapter, \"namespace\">;\n\n/**\n * Light-my-request injection surface \u2014 exposed read-only as a TEST-ONLY seam so\n * unit tests can drive routes without opening a real socket. Not used in production.\n */\nexport type WebserverInject = FastifyInstance[\"inject\"];\n\n/** Browser cookie name. Client identity lives here \u2014 auto-sent on every page navigation. */\nexport const CLIENT_COOKIE = \"hassemu_client\";\n\n/**\n * HA mobile_app registration response shape (home-assistant/android\n * RegisterDeviceResponse.kt): `webhookId` required, the cloud/remote/secret\n * fields null (no Nabu Casa cloud, the webhookId itself is the secret). Used\n * by the registration POST, the PUT update and the webhook `update_registration`.\n *\n * @param webhookId The issued webhook id (URL secret) to echo back to the App.\n */\nfunction mobileRegResponse(webhookId: string): {\n webhook_id: string;\n cloudhook_url: null;\n remote_ui_url: null;\n secret: null;\n} {\n return { webhook_id: webhookId, cloudhook_url: null, remote_ui_url: null, secret: null };\n}\n\n/**\n * Fastify web server emulating the HA REST API.\n *\n * Each incoming request is identified by cookie \u2192 {@link ClientRegistry} entry; new clients\n * get a channel created on first hit. Express was swapped for Fastify in 1.1.0 for first-party\n * cookie support, schema validation and a lighter runtime.\n */\nexport class WebServer {\n private readonly adapter: WebServerAdapter;\n private readonly config: AdapterConfig;\n private readonly registry: ClientRegistry;\n private readonly globalConfig: GlobalConfig;\n private readonly app: FastifyInstance;\n public readonly sessions: Map<string, SessionData> = new Map();\n /**\n * Mobile-App webhook registrations from `POST /api/mobile_app/registrations`\n * (v1.29.1). Key = webhookId (URL secret), Value = owning client cookie id.\n * Subsequent `POST /api/webhook/<id>` requests are validated against this\n * map. FIFO-capped at {@link WEBHOOK_REGISTRATIONS_CAP}; entries whose\n * owning client was removed are pruned in {@link cleanupSessions} (v1.35.2).\n *\n * Reused for Shelly Wall Display FW 2.6.0+ onboarding \u2014 the on-device HA\n * Companion App requires this endpoint to complete device registration\n * after the OAuth2 sign-in. Without it the App refuses to proceed with a\n * \"Mobile-App-Integration nicht verf\u00FCgbar\" error.\n *\n * **Design \u2014 in-memory only, by intent.** The map is NOT persisted across\n * adapter restarts. Restart-recovery relies on the\n * `POST /api/webhook/<unknown-id>` branch returning HTTP 200 with a\n * truly EMPTY body \u2014 the HA Companion App reads that as a stale webhook\n * and re-runs `registerDevice`, which on hassemu issues a fresh\n * webhookId. (Source, verified at tag 2026.4.4: home-assistant/android\n * IntegrationRepositoryImpl.kt:167-171 \u2014 the trigger is\n * `response.code() == 200 && response.body()?.contentLength() == 0L`.)\n *\n * If a future refactor changes the unknown-webhookId response from\n * `200 empty` to `404` or to any non-empty body (even JSON `null`),\n * displays will silently break across adapter restarts. Keep that\n * response shape OR add real persistence here.\n */\n public readonly webhookRegistrations: Map<string, string> = new Map();\n /**\n * v1.32.0 F1: last redirect-target seen per client by `/api/redirect_check`.\n * Used to log only-on-change (instead of every 30s poll). Pruned in\n * {@link cleanupSessions} against `registry.listAll()` \u2014 stale entries from\n * removed clients are dropped within max 5 min.\n */\n private readonly lastRedirectTargetByClient: Map<string, string | null> = new Map();\n private cleanupTimer: ioBroker.Interval | null = null;\n /**\n * Test-only injection surface ({@link WebserverInject}). v1.14.0 (H8): bound\n * once in the constructor instead of via a getter \u2014 a getter allocated a new\n * bound function on every `s.inject({...})` call, and tests call it in loops.\n */\n public readonly inject!: WebserverInject;\n public readonly instanceUuid: string;\n /** ioBroker system language for the setup page \u2014 resolved on startup. */\n public readonly systemLanguage: string;\n /** Set of IPs whose reverse DNS lookup is already in-flight \u2014 prevents duplicate work. */\n private readonly dnsInFlight = new Set<string>();\n /**\n * Per-message cooldown timestamps for 5xx error logging. First occurrence\n * of a unique message logs at warn; repeats within {@link REQUEST_ERROR_COOLDOWN_MS}\n * fall to debug to prevent log-spam under attack/probe traffic.\n */\n private readonly errorLogCooldown: Map<string, number> = new Map();\n\n /**\n * @param adapter Adapter instance used for logging, timers and namespace.\n * @param config Resolved runtime config.\n * @param registry Multi-client registry.\n * @param globalConfig Global redirect override.\n * @param instanceUuid Stable UUID shared with the mDNS advert.\n * @param systemLanguage ioBroker system language (`en`, `de`, \u2026) used for the setup page.\n */\n constructor(\n adapter: WebServerAdapter,\n config: AdapterConfig,\n registry: ClientRegistry,\n globalConfig: GlobalConfig,\n instanceUuid: string,\n systemLanguage: string = \"en\",\n ) {\n this.adapter = adapter;\n this.config = config;\n this.registry = registry;\n this.globalConfig = globalConfig;\n this.instanceUuid = instanceUuid;\n this.systemLanguage = systemLanguage;\n // v1.25.0 (C11): trustProxy ist Opt-In \u00FCber config \u2014 nur aktivieren\n // wenn der Adapter HINTER einem trusted Reverse-Proxy mit TLS-\n // Termination l\u00E4uft. Mit trustProxy=true holt Fastify `req.ip` aus\n // `X-Forwarded-For` (statt aus dem Socket), `req.protocol` aus\n // `X-Forwarded-Proto` etc. \u2014 Voraussetzung: der Proxy bereinigt diese\n // Header (sonst kann jeder Client seine sichtbare IP f\u00E4lschen \u2192 verf\u00E4lscht\n // Logs + die per-IP-Burst-Erkennung defekter Cookies).\n this.app = Fastify({ logger: false, trustProxy: this.config.trustProxy === true });\n // v1.14.0 (H8): inject einmal binden, nicht pro Getter-Access.\n (this as { inject: WebserverInject }).inject = this.app.inject.bind(this.app);\n }\n\n /** Human-readable service name advertised in responses and mDNS. */\n get serviceName(): string {\n return this.config.serviceName || \"ioBroker\";\n }\n\n /** Resolved listener address once `start()` has completed, or null otherwise. */\n get boundAddress(): { address: string; port: number } | null {\n const addr = this.app.server.address();\n if (!addr || typeof addr === \"string\") {\n return null;\n }\n return { address: addr.address, port: addr.port };\n }\n\n // --- lifecycle ---\n\n /** Registers plugins and starts the HTTP listener. */\n async start(): Promise<void> {\n // v1.14.0 (H9): defensive \u2014 wenn start() jemals doppelt gerufen wird\n // (Refactor, Test-Setup-Bug), Timer aus dem Vorlauf clearen statt zu\n // leaken.\n if (this.cleanupTimer) {\n this.adapter.clearInterval(this.cleanupTimer);\n this.cleanupTimer = null;\n }\n await this.app.register(fastifyCookie);\n // OAuth2-Spec verlangt `application/x-www-form-urlencoded` f\u00FCr `/auth/token`.\n // Echte HA-Reference-Clients (frontend/Wall Display SDK) folgen dem.\n // Fastify hat by-default nur einen JSON-Bodyparser \u2014 ohne diesen Plugin\n // beantwortet `/auth/token` mit form-Body 415 und der Login bleibt komplett\n // h\u00E4ngen. Tests via `app.inject({payload:{...}})` serialisieren zu JSON\n // und maskieren das.\n await this.app.register(fastifyFormbody);\n // v1.34.0: minimal read-only WebSocket for the HA Companion App. Its\n // `registerDevice` makes a best-effort `auth/current_user` WS call after the\n // REST registration; without a WS endpoint that fails (and the username is\n // not stored). Registered before the routes so `{ websocket: true }` works.\n await this.app.register(fastifyWebsocket);\n this.setupAuthGuard();\n this.setupErrorHandler();\n this.setupRoutes();\n\n const bindAddress = this.config.bindAddress || \"0.0.0.0\";\n try {\n await this.app.listen({ port: this.config.port, host: bindAddress });\n } catch (err) {\n const e = err as NodeJS.ErrnoException;\n const msg =\n e.code === \"EADDRINUSE\"\n ? `Port ${this.config.port} is already in use \u2014 another service is bound to it`\n : `Server error during startup: ${e.message}`;\n this.adapter.log.error(msg);\n throw err;\n }\n this.adapter.log.debug(`Web server listening on ${bindAddress}:${this.config.port}`);\n\n this.cleanupTimer = this.adapter.setInterval(() => this.cleanupSessions(), CLEANUP_INTERVAL_MS) ?? null;\n }\n\n /** Stops the listener and cancels the session cleanup timer. */\n async stop(): Promise<void> {\n if (this.cleanupTimer) {\n this.adapter.clearInterval(this.cleanupTimer);\n this.cleanupTimer = null;\n }\n try {\n await this.app.close();\n this.adapter.log.debug(\"Web server stopped\");\n } catch (err) {\n // v1.18.0 (G6+G8): debug statt error \u2014 bei intended shutdown\n // (onUnload) ist ein close-error meist ein \"already-closed\"-Race\n // ohne Konsequenz. Caller (main.ts onUnload) loggt nicht doppelt.\n this.adapter.log.debug(`Web server stop error: ${String(err)}`);\n }\n // v1.28.3 (HW1): drop in-flight DNS markers so a slow reverse-lookup\n // started just before stop() doesn't keep an IP entry pinned for the\n // whole process lifetime. The Promise.race(timeout) finally-handler\n // would do that eventually, but only after up to 5s \u2014 racy if the\n // adapter is restarted during that window.\n this.dnsInFlight.clear();\n }\n\n // v1.14.0 (H8): `inject` ist jetzt ein readonly Field (oben deklariert,\n // im Constructor einmalig gebunden). Der fr\u00FChere Getter allokierte bei\n // jedem Access eine neue Funktion.\n\n /** Periodic cleanup of expired in-flight auth sessions and stale redirect-target entries. */\n public cleanupSessions(): void {\n const now = Date.now();\n let cleanedSessions = 0;\n for (const [key, session] of this.sessions) {\n if (now - session.created > SESSION_TTL_MS) {\n this.sessions.delete(key);\n cleanedSessions++;\n }\n }\n if (cleanedSessions > 0) {\n this.adapter.log.debug(`Session cleanup: removed ${cleanedSessions} expired sessions`);\n }\n\n // v1.32.0 F1: prune lastRedirectTargetByClient against currently known\n // clients. A removed client leaves a stale entry that would never get\n // cleared otherwise \u2014 bounded growth over months.\n const activeClients = new Set(this.registry.listAll().map(r => r.id));\n let prunedTargets = 0;\n for (const clientId of this.lastRedirectTargetByClient.keys()) {\n if (!activeClients.has(clientId)) {\n this.lastRedirectTargetByClient.delete(clientId);\n prunedTargets++;\n }\n }\n if (prunedTargets > 0) {\n this.adapter.log.debug(`Cleanup: pruned ${prunedTargets} stale redirect-target entries`);\n }\n\n // v1.35.2: prune webhook registrations whose owning client was removed\n // (remove button / stale-GC). Without this, an orphaned display keeps\n // getting 200s on its webhook instead of falling into re-registration.\n // ownerId === \"\" means \"unowned\" (authRequired=false registration without\n // a Bearer token) \u2014 those have no client to check against and must stay.\n let prunedWebhooks = 0;\n for (const [webhookId, ownerId] of this.webhookRegistrations) {\n if (ownerId !== \"\" && !activeClients.has(ownerId)) {\n this.webhookRegistrations.delete(webhookId);\n prunedWebhooks++;\n }\n }\n if (prunedWebhooks > 0) {\n this.adapter.log.debug(`Cleanup: pruned ${prunedWebhooks} webhook registrations of removed clients`);\n }\n }\n\n /**\n * Cooldown-Decision f\u00FCr 5xx-Error-Logging. Liefert `true` f\u00FCr die erste\n * Beobachtung pro `key` innerhalb {@link REQUEST_ERROR_COOLDOWN_MS} und\n * markiert den Eintrag \u2014 Wiederholungen liefern `false` bis das Fenster\n * abgelaufen ist. Map ist FIFO-gedeckelt auf {@link REQUEST_ERROR_COOLDOWN_CAP}.\n *\n * @param key Eindeutiger Error-Identifier (\u00FCblicherweise `error.message`).\n * @param now Aktuelle Zeit in ms (testbar).\n */\n public shouldEmitRequestErrorWarn(key: string, now: number): boolean {\n const lastSeen = this.errorLogCooldown.get(key) ?? 0;\n if (lastSeen !== 0 && now - lastSeen <= REQUEST_ERROR_COOLDOWN_MS) {\n return false;\n }\n if (!this.errorLogCooldown.has(key)) {\n evictOldest(this.errorLogCooldown, REQUEST_ERROR_COOLDOWN_CAP);\n }\n this.errorLogCooldown.set(key, now);\n return true;\n }\n\n /**\n * Inserts a session, dropping the oldest entry if {@link SESSIONS_CAP} is exceeded.\n *\n * @param key Session key (flow id or auth code).\n * @param data Session payload.\n */\n private storeSession(key: string, data: SessionData): void {\n evictOldest(this.sessions, SESSIONS_CAP);\n this.sessions.set(key, data);\n }\n\n // --- client identification ---\n\n /**\n * v1.15.0 (F6): zentraler Extract `req.ip \u2192 coerced string|null`. Vorher\n * 3\u00D7 inline `coerceString(req.ip)` in identify/login/token-Handlern.\n *\n * @param req Fastify request (uses `req.ip`).\n */\n private static getClientIp(req: FastifyRequest): string | null {\n return coerceString(req.ip);\n }\n\n private async identify(req: FastifyRequest, reply: FastifyReply): Promise<ClientRecord> {\n const cookie = coerceUuid(req.cookies?.[CLIENT_COOKIE]);\n const ip = WebServer.getClientIp(req);\n // v1.17.0 (C8): UA durchreichen damit NAT-Co-Located Displays nicht\n // im selben Pending-Lock landen (siehe identifyOrCreate-Kommentar).\n const userAgent = coerceString(req.headers[\"user-agent\"]);\n const record = await this.registry.identifyOrCreate(cookie, ip, null, userAgent);\n // v1.32.0 A1: cookie-state explizit traced. Drei Branches:\n // hit \u2014 cookie matched a known client, no setCookie needed\n // stale/new \u2014 cookie present but unknown, OR no cookie at all \u2192 new client created\n if (cookie === record.cookie) {\n this.adapter.log.debug(`identify: cookie-hit client=${record.id} ip=${ip ?? \"?\"}`);\n } else {\n const reason = cookie ? \"cookie-stale (unknown)\" : \"no-cookie\";\n this.adapter.log.debug(`identify: ${reason}, new client=${record.id} ip=${ip ?? \"?\"}`);\n // v1.25.0 (C11): Cookie `secure: true` wenn TLS \u2014 Browser sendet\n // den Cookie dann nur \u00FCber HTTPS. Bei trustProxy=true kommt\n // `req.protocol` aus `X-Forwarded-Proto`-Header. Default ohne\n // trustProxy: `req.protocol === 'http'` (Adapter ist HTTP only),\n // also Cookie nicht-secure \u2014 sonst w\u00FCrde der Browser ihn nie senden.\n const useSecure = req.protocol === \"https\";\n // v1.32.0 A2: Cookie-Secure-Decision tracen \u2014 wenn trustProxy-config\n // falsch ist, kriegt Display den Cookie evtl. nie zur\u00FCck.\n this.adapter.log.debug(`identify: setting cookie secure=${useSecure} (req.protocol=${req.protocol})`);\n reply.setCookie(CLIENT_COOKIE, record.cookie, {\n path: \"/\",\n httpOnly: true,\n sameSite: \"lax\",\n secure: useSecure,\n maxAge: COOKIE_MAX_AGE_S,\n });\n }\n if (ip) {\n this.resolveHostnameAsync(record, ip);\n }\n return record;\n }\n\n private resolveHostnameAsync(record: ClientRecord, ip: string): void {\n if (record.hostname || this.dnsInFlight.has(ip)) {\n return;\n }\n this.dnsInFlight.add(ip);\n // v1.8.1 (D5): DNS-Lookup mit hartem 5s-Timeout. Default-Node-DNS hat\n // KEIN Timeout \u2014 bei broken Resolver (Captive-Portal, Misconfig) blieb\n // der Promise unendlich pending \u2192 IP f\u00FCr Adapter-Lifetime in dnsInFlight\n // blockiert, hostname auf record.ip gefroren.\n // v1.34.0: adapter-managed Timer (cancelt automatisch bei onUnload) + clear\n // sobald `dns.reverse` das Race gewinnt \u2014 sonst dangelt der Timer bis 5s\n // \u00FCber einen Restart hinaus (W5005).\n let timeoutHandle: ioBroker.Timeout | undefined;\n const timeout = new Promise<string[]>((_, reject) => {\n timeoutHandle = this.adapter.setTimeout(() => reject(new Error(\"dns reverse-lookup timeout\")), 5_000);\n });\n Promise.race([dns.reverse(ip), timeout])\n .then(names => {\n const name = names[0];\n if (name) {\n // v1.32.0 A4: Success-Trace \u2014 bei Diagnose \u201Ewarum hat Display\n // X den hostname Y?\" ist die IP\u2192hostname-Aufl\u00F6sung der Anker.\n this.adapter.log.debug(`resolveHostname: ip=${ip} \u2192 hostname=${name}`);\n this.registry.identifyOrCreate(record.cookie, ip, name).catch(() => {\n /* registry itself logs */\n });\n }\n })\n .catch(err => {\n // v1.32.0 A3: vorher silent. Reverse DNS scheitert auf LAN oft\n // legitim \u2014 daher debug-only (kein warn-Spam), aber jetzt mit\n // Diagnose-Anker f\u00FCr \u201Ehostname fehlt\"-Reports.\n this.adapter.log.debug(\n `resolveHostname: ip=${ip} failed \u2014 ${err instanceof Error ? err.message : String(err)}`,\n );\n })\n .finally(() => {\n if (timeoutHandle) {\n this.adapter.clearTimeout(timeoutHandle);\n }\n this.dnsInFlight.delete(ip);\n });\n }\n\n // --- auth guard ---\n\n /**\n * Pre-handler hook der `/api/*`-Routen sch\u00FCtzt wenn `authRequired=true`.\n *\n * Vorher: `/api/states`, `/api/services`, `/api/events`, `/api/error_log`,\n * `/api/discovery_info` lieferten unauthenticated alle ihre Daten \u2014\n * pure Information-Disclosure. Echte HA verlangt `Authorization: Bearer\n * <token>` f\u00FCr alle `/api/*` au\u00DFer dem `/api/`-Heartbeat.\n *\n * Whitelist (kein Auth n\u00F6tig):\n * - `/`, `/manifest.json`, `/health`, `/api/` \u2014 public Endpoints (Heartbeat, PWA)\n * - `/api/discovery_info` \u2014 HA-Clients fragen das VOR dem Auth-Flow ab um\n * zu erkennen ob `requires_api_password` true ist (Spec-Verhalten)\n * - `/auth/*` \u2014 der Auth-Flow selbst\n *\n * Bei `authRequired=false`: Hook macht nichts (no-op), bestehender Verhalten.\n */\n private setupAuthGuard(): void {\n this.app.addHook(\"preHandler\", async (req, reply) => {\n if (!this.config.authRequired) {\n return;\n }\n const path = (req.url ?? \"/\").split(\"?\")[0];\n // Public endpoints \u2014 explicitly allowed\n if (\n path === \"/\" ||\n path === \"/api/\" ||\n path === \"/api/discovery_info\" ||\n path === \"/manifest.json\" ||\n path === \"/health\" ||\n path.startsWith(\"/auth/\") ||\n // v1.34.0: the WebSocket does its own auth in the handshake\n // (`auth_required` \u2192 `auth` frame), not via a Bearer header \u2014 so the\n // HTTP upgrade itself must pass the guard.\n path === \"/api/websocket\" ||\n // v1.29.1: Mobile-App webhooks carry the secret in the URL\n // (`webhookId`) \u2014 HA core also serves these unauthenticated.\n // Source: home-assistant/core/.../mobile_app/webhook.py.\n path.startsWith(\"/api/webhook/\")\n ) {\n return;\n }\n // From here on: protected (`/api/*` apart from `/api/`)\n const authHeader = req.headers.authorization;\n if (typeof authHeader !== \"string\" || !authHeader.startsWith(\"Bearer \")) {\n this.adapter.log.debug(`Auth required for ${path} \u2014 missing Bearer token`);\n reply.status(401).send({ error: \"unauthorized\" });\n return;\n }\n const token = authHeader.substring(\"Bearer \".length).trim();\n const client = this.registry.getByToken(token);\n if (!client) {\n this.adapter.log.debug(`Auth required for ${path} \u2014 unknown Bearer token`);\n reply.status(401).send({ error: \"invalid_token\" });\n return;\n }\n // OK \u2014 handler runs\n });\n }\n\n // --- error handling ---\n\n private setupErrorHandler(): void {\n this.app.setErrorHandler((err, _req, reply) => {\n const error = err as Error & { validation?: unknown; statusCode?: number };\n if (error.validation) {\n this.adapter.log.debug(`Validation error: ${error.message}`);\n reply.status(400).send({ error: \"Invalid request\", details: error.message });\n return;\n }\n // Fastify body-parsing / client errors already set statusCode in 4xx range\n const code = typeof error.statusCode === \"number\" ? error.statusCode : 500;\n if (code >= 400 && code < 500) {\n this.adapter.log.debug(`Client error ${code}: ${error.message}`);\n reply.status(code).send({ error: error.message });\n return;\n }\n // 5xx: ein attacker kann mit malformed paths/oversized bodies viele\n // 500er triggern. Per-Message-Dedup-Map mit 60s-Cooldown \u2014 das erste\n // Auftreten pro unique message kommt als warn, alle Wiederholungen\n // im 60s-Fenster auf debug. Memory `feedback_no_log_spam`.\n const key = error.message || \"unknown\";\n if (this.shouldEmitRequestErrorWarn(key, Date.now())) {\n this.adapter.log.warn(`Request error: ${error.message}`);\n } else {\n this.adapter.log.debug(`Request error (repeat): ${error.message}`);\n }\n reply.status(500).send({ error: \"Internal server error\" });\n });\n }\n\n // --- routes ---\n\n private setupRoutes(): void {\n this.setupApiRoutes();\n this.setupAuthRoutes();\n this.setupWebSocket();\n this.setupMiscRoutes();\n this.setupNotFound();\n }\n\n /**\n * HA `/api/config`-shaped object. Single source for REST `/api/config`, the\n * Companion webhook `get_config` and the WebSocket `get_config` command.\n * `mobile_app` in `components` advertises the integration the HA Companion App\n * probes during onboarding (v1.29.1, Shelly FW 2.6.0+).\n */\n private buildHaConfig(): Record<string, unknown> {\n return {\n components: [\"http\", \"api\", \"frontend\", \"homeassistant\", \"mobile_app\"],\n config_dir: \"/config\",\n elevation: 0,\n latitude: 0,\n longitude: 0,\n location_name: this.serviceName,\n time_zone: \"UTC\",\n unit_system: { length: \"km\", mass: \"g\", temperature: \"\u00B0C\", volume: \"L\" },\n version: HA_VERSION,\n whitelist_external_dirs: [],\n };\n }\n\n private setupApiRoutes(): void {\n // CRITICAL: trailing slash \u2014 HA clients check this endpoint for discovery\n this.app.get(\"/api/\", () => ({ message: \"API running.\" }));\n\n this.app.get(\"/api/config\", () => this.buildHaConfig());\n\n this.app.get(\"/api/discovery_info\", () => {\n // v1.17.0 (E11): NICHT mehr `req.hostname` \u2014 der Host-Header ist\n // client-controlled und ein Angreifer k\u00F6nnte mit `Host: attacker.lan`\n // andere HA-Clients zur falschen URL umleiten. Stattdessen die\n // tats\u00E4chlich gebundene Adresse: bindAddress (ggf. wildcard) oder\n // ersten lokalen non-internal IPv4 via getLocalIp.\n const isWildcard = !this.config.bindAddress || isWildcardBind(this.config.bindAddress);\n const host = isWildcard ? getLocalIp() : this.config.bindAddress;\n const baseUrl = `http://${host}:${this.config.port}`;\n return {\n base_url: baseUrl,\n external_url: null,\n internal_url: baseUrl,\n location_name: this.serviceName,\n // Vorher hardcoded `true` unabh\u00E4ngig von authRequired \u2014 strict HA-Clients\n // versuchten Auth auch bei authRequired=false und scheiterten am leeren Login-Flow.\n requires_api_password: this.config.authRequired,\n uuid: this.instanceUuid,\n version: HA_VERSION,\n };\n });\n\n for (const path of [\"/api/states\", \"/api/services\", \"/api/events\"]) {\n this.app.get(path, () => []);\n }\n this.app.get(\"/api/error_log\", () => \"\");\n\n // ---- Mobile-App integration (HA Companion + Shelly FW 2.6.0+) ----\n //\n // Source: home-assistant/android IntegrationRepositoryImpl.kt:120-159\n // calls POST /api/mobile_app/registrations after the OAuth2 sign-in.\n // A 404 here surfaces as \u201EMobile-App-Integration nicht verf\u00FCgbar\" in\n // the App's onboarding screen and blocks the display from finishing\n // setup. Detail in Ressourcen/hassemu/oauth2-browser-flow-shelly-fw26.md.\n //\n // The Bearer-token check is already done by the existing auth\n // pre-handler \u2014 `/api/mobile_app/registrations` is protected by\n // default, so by the time the handler runs we know the caller has\n // a valid access_token from /auth/token.\n this.app.post<{\n Body: {\n app_id?: string;\n app_name?: string;\n device_name?: string;\n device_id?: string;\n manufacturer?: string;\n model?: string;\n os_name?: string;\n os_version?: string;\n };\n }>(\"/api/mobile_app/registrations\", async (req, reply) => {\n const body = req.body ?? {};\n // Identify by Bearer token \u2014 the pre-handler already validated it.\n const authHeader = (req.headers.authorization as string) ?? \"\";\n const token = authHeader.startsWith(\"Bearer \") ? authHeader.substring(7).trim() : \"\";\n const client = this.registry.getByToken(token);\n const ownerId = client?.id ?? \"\";\n\n const webhookId = crypto.randomUUID().replace(/-/g, \"\");\n evictOldest(this.webhookRegistrations, WEBHOOK_REGISTRATIONS_CAP);\n this.webhookRegistrations.set(webhookId, ownerId);\n\n this.adapter.log.debug(\n `Mobile-App registration \u2014 client=${ownerId} app_id=${body.app_id ?? \"?\"} device_name=${body.device_name ?? \"?\"} \u2192 webhook=${webhookId}`,\n );\n\n reply.status(201);\n return mobileRegResponse(webhookId);\n });\n\n // PUT and DELETE on /api/mobile_app/registrations/:webhookId \u2014 the App\n // calls PUT to update its registration on token refresh or sensor\n // re-register. PUT echoes the registration for a KNOWN webhookId (200), but\n // returns 404 for an unknown one so a stale Pre-Restart token re-registers;\n // DELETE drops the registration and returns 204.\n this.app.put<{ Params: { webhookId: string } }>(\"/api/mobile_app/registrations/:webhookId\", async (req, reply) => {\n const id = req.params.webhookId;\n if (!this.webhookRegistrations.has(id)) {\n // v1.32.0 E1: stale-id signaliert dass Companion einen Token\n // aus Pre-Restart-Era hat \u2014 diagnostisch wertvoll f\u00FCr\n // re-registration-loop-Bugs.\n this.adapter.log.debug(`Mobile-App PUT registration: unknown webhookId=${id.substring(0, 8)}\u2026 \u2014 returning 404`);\n reply.status(404);\n return { error: \"unknown_registration\" };\n }\n return mobileRegResponse(id);\n });\n\n this.app.delete<{ Params: { webhookId: string } }>(\n \"/api/mobile_app/registrations/:webhookId\",\n async (req, reply) => {\n const id = req.params.webhookId;\n const wasPresent = this.webhookRegistrations.has(id);\n this.webhookRegistrations.delete(id);\n // v1.32.0 E2: Companion-Maintenance-Trace.\n this.adapter.log.debug(\n `Mobile-App DELETE registration: webhookId=${id.substring(0, 8)}\u2026 removed (was-present=${wasPresent})`,\n );\n reply.status(204);\n return null;\n },\n );\n\n // POST /api/webhook/:webhookId \u2014 Companion-App sensor updates,\n // location pings, registration updates etc. Public by design (URL\n // contains the webhookId secret). HA core dispatches on `type` field\n // in the JSON body and returns shape per type. For hassemu we accept\n // any payload and respond with the minimal-correct success per type;\n // the display use-case doesn't need actual state propagation, but\n // returning 200 prevents the App from re-trying in a loop and\n // surfacing onboarding-failure banners.\n this.app.post<{\n Params: { webhookId: string };\n Body: { type?: string; data?: unknown };\n }>(\"/api/webhook/:webhookId\", async (req, reply) => {\n const id = req.params.webhookId;\n if (!this.webhookRegistrations.has(id)) {\n // Unknown webhookId \u2014 match HA's 200-empty for stale webhooks so the\n // App re-registers. Source (verified at tag 2026.4.4):\n // home-assistant/android IntegrationRepositoryImpl.kt:167-171 \u2014\n // `updateRegistration` re-runs `registerDevice` ONLY when\n // `response.code() == 200 && response.body()?.contentLength() == 0L`.\n // The body MUST therefore be truly empty: `return null` would let\n // Fastify serialize the 4-byte JSON text \"null\" (contentLength 4),\n // the Companion would take the success branch and the display would\n // stay broken silently (v1.35.2 fix).\n // v1.32.0 E3: stale-id ist DAS Symptom f\u00FCr re-registration-loop \u2014\n // Companion macht webhook-call mit Token aus Pre-Restart-Era.\n this.adapter.log.debug(\n `Webhook fallthrough: stale id=${id.substring(0, 8)}\u2026 \u2014 App will trigger re-registration`,\n );\n return reply.status(200).send();\n }\n const body = req.body ?? {};\n const type = typeof body.type === \"string\" ? body.type : \"\";\n this.adapter.log.debug(`Webhook ${id.substring(0, 8)}\u2026 type=${type || \"(no type)\"}`);\n\n switch (type) {\n case \"get_config\":\n return this.buildHaConfig();\n case \"get_zones\":\n return [];\n case \"render_template\":\n return {};\n case \"update_registration\":\n return mobileRegResponse(id);\n case \"register_sensor\":\n return { success: true };\n case \"update_sensor_states\":\n return {};\n default:\n // Generic success for unknown types \u2014 fire_event,\n // call_service, conversation_process, update_location,\n // get_zones-with-data, etc. The display doesn't need\n // their semantics, just an HTTP 200 acknowledgement.\n return {};\n }\n });\n }\n\n /**\n * Issue a fresh authorization code and persist it in the sessions map.\n *\n * Single source for both the JSON login flow (`/auth/login_flow/<flowId>`\n * \u2192 `create_entry`) and the browser OAuth2 flow (`/auth/authorize` \u2192\n * 302). The code is exchanged for tokens at `/auth/token` (`grant_type =\n * authorization_code`); the existing token-view consumes the same map.\n *\n * @param clientId Identity cookie value of the requesting display, or\n * undefined for headless OAuth2-only flows.\n */\n private issueAuthorizationCode(clientId: string | null): string {\n const code = crypto.randomUUID();\n this.storeSession(code, { created: Date.now(), clientId });\n return code;\n }\n\n /**\n * Shared validation for GET and POST `/auth/authorize`. On failure it sets the\n * `400 text/html` reply and returns the rendered error page; on success it\n * returns the validated (string-typed) `client_id` / `redirect_uri`. Never\n * redirects on failure \u2014 the endpoint must not become an open redirector.\n *\n * @param reply Fastify reply (status + content-type set on failure).\n * @param method `\"GET\"` or `\"POST\"` \u2014 only used to label the debug log.\n * @param responseType The OAuth2 `response_type` (must be `\"code\"`).\n * @param clientId The OAuth2 `client_id` (must be a string).\n * @param redirectUri The OAuth2 `redirect_uri` (must be a string + allowlisted).\n */\n private validateAuthorizeRequest(\n reply: FastifyReply,\n method: \"GET\" | \"POST\",\n responseType: unknown,\n clientId: unknown,\n redirectUri: unknown,\n ): { ok: true; clientId: string; redirectUri: string } | { ok: false; html: string } {\n if (responseType !== \"code\") {\n this.adapter.log.debug(`Authorize ${method} rejected: response_type=${String(responseType)} (expected 'code')`);\n reply.status(400).type(\"text/html\");\n return {\n ok: false,\n html: renderAuthorizeError(\n \"unsupported_response_type\",\n \"This authorization server supports `response_type=code` only.\",\n ),\n };\n }\n if (typeof clientId !== \"string\" || typeof redirectUri !== \"string\") {\n this.adapter.log.debug(\n `Authorize ${method} rejected: missing client_id or redirect_uri (cid=${typeof clientId}, ru=${typeof redirectUri})`,\n );\n reply.status(400).type(\"text/html\");\n return {\n ok: false,\n html: renderAuthorizeError(\"invalid_request\", \"Missing or invalid `client_id` or `redirect_uri` parameter.\"),\n };\n }\n if (!isValidRedirectUri(clientId, redirectUri)) {\n this.adapter.log.debug(\n `Authorize ${method} rejected: redirect_uri \"${redirectUri}\" not allowed for client_id \"${clientId}\"`,\n );\n reply.status(400).type(\"text/html\");\n return {\n ok: false,\n html: renderAuthorizeError(\n \"invalid_redirect_uri\",\n \"The `redirect_uri` parameter is not on the allowlist for this client.\",\n ),\n };\n }\n return { ok: true, clientId, redirectUri };\n }\n\n /**\n * Issue an auth code, build the redirect target and render the auto-submit redirect page.\n *\n * @param reply Fastify reply (content-type set to text/html).\n * @param clientId Identity of the requesting display, or null for headless flows.\n * @param redirectUri Already-validated `redirect_uri` to append the code to.\n * @param state Optional OAuth2 `state` round-tripped verbatim.\n */\n private issueAuthorizeRedirect(\n reply: FastifyReply,\n clientId: string | null,\n redirectUri: string,\n state: string | undefined,\n ): string {\n const code = this.issueAuthorizationCode(clientId);\n const target = buildRedirectUrl(redirectUri, code, state);\n reply.type(\"text/html\");\n return renderAuthorizeRedirect(target);\n }\n\n /**\n * Best-effort token revocation, shared by `POST /auth/revoke` (HA \u22652022.9)\n * and the legacy `POST /auth/token` with `action=revoke`. The HA Companion\n * sends the refresh token; we look it up and clear both the refresh and the\n * access token of the owning client. Always succeeds from the caller's view \u2014\n * an unknown/missing token still yields 200 (matches HA, which never leaks\n * whether a token existed). Source: AuthenticationRepositoryImpl.revokeSession.\n *\n * @param token Refresh token to revoke (from the `token` form field).\n */\n private async revokeToken(token: string | undefined): Promise<void> {\n const refresh = typeof token === \"string\" ? token : \"\";\n const owner = refresh ? this.registry.getByRefreshToken(refresh) : null;\n if (owner) {\n await this.registry.setRefreshToken(owner.id, null);\n await this.registry.setToken(owner.id, null);\n this.adapter.log.debug(`Token revoked \u2014 client ${owner.id}`);\n } else {\n this.adapter.log.debug(\"Revoke: unknown/missing token \u2014 returning 200 (HA behavior)\");\n }\n }\n\n private setupAuthRoutes(): void {\n this.app.get(\"/auth/providers\", () => [{ name: \"Home Assistant Local\", type: \"homeassistant\", id: null }]);\n\n // Browser-OAuth2 flow at GET/POST /auth/authorize. Needed by the\n // HA Companion Android App (Shelly Wall Display FW 2.6.0+ embeds\n // the Companion App). Source-verified flow:\n // home-assistant/android UrlUtil.kt:buildAuthenticationUrl\n // home-assistant/core indieauth.py:verify_redirect_uri\n // home-assistant/frontend src/data/auth.ts:redirectWithAuthCode\n // Detail: Ressourcen/hassemu/oauth2-browser-flow-shelly-fw26.md\n this.app.get<{\n Querystring: { response_type?: string; client_id?: string; redirect_uri?: string; state?: string };\n }>(\"/auth/authorize\", async (req, reply) => {\n const { response_type, client_id, redirect_uri, state } = req.query ?? {};\n\n // v1.32.0 D2: rejection-Pfade traced \u2014 Triage \u201Ewarum bricht OAuth ab\"\n const v = this.validateAuthorizeRequest(reply, \"GET\", response_type, client_id, redirect_uri);\n if (!v.ok) {\n return v.html;\n }\n\n const client = await this.identify(req, reply);\n\n // No auth required \u2192 issue the code right away and redirect.\n if (!this.config.authRequired) {\n this.adapter.log.debug(`Authorize auto-grant \u2014 client ${client.id}`);\n return this.issueAuthorizeRedirect(reply, client.id, v.redirectUri, state);\n }\n\n // v1.32.0 D1: Form-render Trace \u2014 wenn Companion die Form nie absendet,\n // sieht User hier dass sie \u00FCberhaupt gerendert wurde.\n let redirectHost = \"?\";\n try {\n redirectHost = new URL(v.redirectUri).host || v.redirectUri;\n } catch {\n redirectHost = v.redirectUri;\n }\n this.adapter.log.debug(`Authorize form rendered \u2014 client_id=${v.clientId} redirect_uri-host=${redirectHost}`);\n reply.type(\"text/html\");\n return renderAuthorizeForm({ clientId: v.clientId, redirectUri: v.redirectUri, state });\n });\n\n this.app.post<{\n Body: {\n response_type?: string;\n client_id?: string;\n redirect_uri?: string;\n state?: string;\n username?: string;\n password?: string;\n };\n }>(\"/auth/authorize\", async (req, reply) => {\n const { response_type, client_id, redirect_uri, state, username, password } = req.body ?? {};\n\n const v = this.validateAuthorizeRequest(reply, \"POST\", response_type, client_id, redirect_uri);\n if (!v.ok) {\n return v.html;\n }\n\n const client = await this.identify(req, reply);\n\n // No auth required \u2192 straight to redirect even on POST.\n if (!this.config.authRequired) {\n return this.issueAuthorizeRedirect(reply, client.id, v.redirectUri, state);\n }\n\n const ip = WebServer.getClientIp(req);\n const userOk = typeof username === \"string\" && safeStringEqual(username, this.config.username);\n const passOk = typeof password === \"string\" && safeStringEqual(password, this.config.password);\n if (!userOk || !passOk) {\n const ipSuffix = ip ? ` (IP ${ip})` : \"\";\n this.adapter.log.warn(`Invalid credentials${ipSuffix}`);\n reply.status(401).type(\"text/html\");\n return renderAuthorizeForm(\n { clientId: v.clientId, redirectUri: v.redirectUri, state },\n \"Invalid username or password.\",\n );\n }\n\n this.adapter.log.debug(`Authorize grant \u2014 client ${client.id}`);\n return this.issueAuthorizeRedirect(reply, client.id, v.redirectUri, state);\n });\n\n this.app.post(\"/auth/login_flow\", async (req, reply) => {\n const client = await this.identify(req, reply);\n const flowId = crypto.randomUUID();\n this.storeSession(flowId, { created: Date.now(), clientId: client.id });\n this.adapter.log.debug(`Auth flow created: ${flowId} for client ${client.id}`);\n\n return {\n type: \"form\",\n flow_id: flowId,\n handler: [\"homeassistant\", null],\n step_id: \"init\",\n data_schema: LOGIN_SCHEMA,\n description_placeholders: null,\n errors: null,\n };\n });\n\n this.app.post<{\n Params: { flowId: string };\n Body: { username?: string; password?: string };\n }>(\n \"/auth/login_flow/:flowId\",\n {\n schema: {\n params: {\n type: \"object\",\n properties: { flowId: { type: \"string\", minLength: 1 } },\n required: [\"flowId\"],\n },\n },\n },\n async (req, reply) => {\n const flowId = req.params.flowId;\n const session = this.sessions.get(flowId);\n if (!session) {\n // v1.8.0: nach Session-TTL (10 min) feuert das bei jedem\n // legit returning user \u2014 nicht actionable. debug, nicht warn.\n this.adapter.log.debug(`Unknown flow_id: ${flowId}`);\n reply.status(400);\n return { type: \"abort\", flow_id: flowId, reason: \"unknown_flow\" };\n }\n\n if (this.config.authRequired) {\n const ip = WebServer.getClientIp(req);\n const { username, password } = req.body ?? {};\n const userOk = typeof username === \"string\" && safeStringEqual(username, this.config.username);\n const passOk = typeof password === \"string\" && safeStringEqual(password, this.config.password);\n if (!userOk || !passOk) {\n const ipSuffix = ip ? ` (IP ${ip})` : \"\";\n this.adapter.log.warn(`Invalid credentials${ipSuffix}`);\n reply.status(400);\n return {\n type: \"form\",\n flow_id: flowId,\n handler: [\"homeassistant\", null],\n step_id: \"init\",\n data_schema: LOGIN_SCHEMA,\n errors: { base: \"invalid_auth\" },\n description_placeholders: null,\n };\n }\n }\n\n this.sessions.delete(flowId);\n const code = crypto.randomUUID();\n this.storeSession(code, { created: Date.now(), clientId: session.clientId });\n this.adapter.log.debug(\"Auth flow completed \u2014 code issued\");\n\n return {\n version: 1,\n type: \"create_entry\",\n flow_id: flowId,\n handler: [\"homeassistant\", null],\n result: code,\n description: null,\n description_placeholders: null,\n };\n },\n );\n\n // HA \u22652022.9 logout: POST /auth/revoke with form field `token` (the refresh\n // token). Always 200 with empty body. Whitelisted by the `/auth/` prefix in\n // the auth guard. Source: AuthenticationRepositoryImpl.revokeSession.\n this.app.post<{ Body: { token?: string } }>(\"/auth/revoke\", async req => {\n await this.revokeToken(req.body?.token);\n return {};\n });\n\n this.app.post<{\n Body: { code?: string; grant_type?: string; refresh_token?: string; action?: string; token?: string };\n }>(\"/auth/token\", async (req, reply) => {\n const { code, grant_type, refresh_token, action } = req.body ?? {};\n\n // Legacy logout (HA <2022.9): POST /auth/token with action=revoke + token.\n // Newer apps use /auth/revoke; we accept both so a 400 never surfaces.\n if (action === \"revoke\") {\n await this.revokeToken(req.body?.token ?? refresh_token);\n return {};\n }\n\n if (grant_type === \"authorization_code\" && code && this.sessions.has(code)) {\n const session = this.sessions.get(code)!;\n this.sessions.delete(code);\n const token = crypto.randomUUID();\n const refreshToken = crypto.randomUUID();\n if (session.clientId) {\n // Persist VOR Response-Build: ein Crash zwischen Issue + Persist\n // w\u00FCrde sonst dem Client einen Token in der Hand lassen, den der\n // Server nicht kennt \u2014 beim ersten Refresh dann invalid_grant.\n await this.registry.setToken(session.clientId, token);\n await this.registry.setRefreshToken(session.clientId, refreshToken);\n this.adapter.log.debug(`Display authenticated \u2014 client ${session.clientId}`);\n }\n return {\n access_token: token,\n token_type: \"Bearer\",\n refresh_token: refreshToken,\n expires_in: OAUTH_ACCESS_TOKEN_TTL_S,\n };\n }\n\n if (grant_type === \"refresh_token\") {\n // Validate the refresh token against issued ones \u2014 was previously\n // accepting any string and minting a new access_token (security fix v1.2.0).\n const incoming = typeof refresh_token === \"string\" ? refresh_token : \"\";\n const ownerRecord = incoming ? this.registry.getByRefreshToken(incoming) : null;\n if (!ownerRecord) {\n this.adapter.log.debug(\"Refresh token rejected \u2014 unknown or missing\");\n reply.status(400);\n return { error: \"invalid_grant\", error_description: \"Invalid refresh token\" };\n }\n // v1.31.0: refresh_token bleibt valid (NICHT mehr rotated). HA Core\n // selbst (homeassistant/components/auth/__init__.py:334-348) liefert\n // beim refresh-grant nie einen neuen refresh_token, nur access_token\n // + token_type + expires_in. HA Android Companion\n // (AuthenticationRepositoryImpl.kt:147) speichert beim Refresh den\n // GESENDETEN refresh_token (Function-Parameter), ignoriert den in der\n // Response zur\u00FCckgegebenen \u2014 Companion beh\u00E4lt daher immer ihren\n // initialen refresh_token. v1.28.3 (HW5) Rotation war RFC 6819\n // \u00A75.2.2.3-konform aber inkompatibel mit dem Companion-Datenmodell:\n // Server-Rotation killte den Companion-Token beim ersten Refresh.\n const newAccess = crypto.randomUUID();\n await this.registry.setToken(ownerRecord.id, newAccess);\n this.adapter.log.debug(`Refresh-token-grant \u2014 client=${ownerRecord.id} new access_token issued`);\n return {\n access_token: newAccess,\n token_type: \"Bearer\",\n refresh_token: incoming,\n expires_in: OAUTH_ACCESS_TOKEN_TTL_S,\n };\n }\n\n // \u201Ewrong grant_type\" ist ein Client-Format-Fehler, kein Server-Concern\n // \u2014 daher nur debug (legitime Client-Bugs sollen das Log nicht fluten).\n this.adapter.log.debug(`Token exchange failed: grant_type=${String(grant_type)}`);\n reply.status(400);\n return { error: \"invalid_request\", error_description: \"Invalid or expired code\" };\n });\n }\n\n /**\n * Minimal read-only HA WebSocket at `/api/websocket`. The HA Companion App's\n * `registerDevice` makes a best-effort `auth/current_user` WS call after the\n * REST registration to store the username (home-assistant/android\n * IntegrationRepositoryImpl.kt at tag 2026.4.4, line 154). Without a WS\n * endpoint that throws and the registration logs \"Unable to save device registration\".\n *\n * Auth happens in-band: server sends `auth_required`, client replies with an\n * `auth` frame, we validate the access token against the registry. FAIL-FAST:\n * a missing/invalid token or a missing `auth` frame within\n * {@link WS_AUTH_TIMEOUT_MS} closes the socket \u2014 so the WS never hangs the\n * App's call (which previously failed fast against a clean 404).\n */\n private setupWebSocket(): void {\n this.app.get(\"/api/websocket\", { websocket: true }, (socket: WebSocket) => {\n let authed = false;\n let authTimer: ioBroker.Timeout | undefined = this.adapter.setTimeout(() => {\n if (!authed) {\n this.adapter.log.debug(\"WS: no auth frame within timeout \u2014 closing\");\n this.wsSend(socket, { type: \"auth_invalid\", message: \"Authentication timed out\" });\n socket.close();\n }\n }, WS_AUTH_TIMEOUT_MS);\n\n this.wsSend(socket, { type: \"auth_required\", ha_version: HA_VERSION });\n\n socket.on(\"message\", raw => {\n // ws delivers text frames as Buffer by default; normalize every RawData\n // variant to a UTF-8 string (avoids Object's default stringification).\n const text = Buffer.isBuffer(raw)\n ? raw.toString(\"utf8\")\n : Array.isArray(raw)\n ? Buffer.concat(raw).toString(\"utf8\")\n : Buffer.from(raw).toString(\"utf8\");\n let msg: Record<string, unknown>;\n try {\n msg = JSON.parse(text) as Record<string, unknown>;\n } catch {\n return; // ignore non-JSON frames\n }\n if (!authed) {\n const token = typeof msg.access_token === \"string\" ? msg.access_token : \"\";\n if (msg.type === \"auth\" && token && this.registry.getByToken(token)) {\n authed = true;\n if (authTimer) {\n this.adapter.clearTimeout(authTimer);\n authTimer = undefined;\n }\n this.wsSend(socket, { type: \"auth_ok\", ha_version: HA_VERSION });\n } else {\n this.adapter.log.debug(\"WS: auth_invalid \u2014 unknown or missing access token\");\n this.wsSend(socket, { type: \"auth_invalid\", message: \"Invalid access token\" });\n socket.close();\n }\n return;\n }\n this.handleWsCommand(socket, msg);\n });\n\n socket.on(\"error\", () => {\n // Client vanished mid-stream \u2014 the socket is gone; the auth timer is\n // cleared by the close handler below.\n });\n\n socket.on(\"close\", () => {\n // Clear the auth timer if the client disconnects before authenticating,\n // so it never fires against an already-closed socket.\n if (authTimer) {\n this.adapter.clearTimeout(authTimer);\n authTimer = undefined;\n }\n });\n });\n }\n\n /**\n * Safely serialize + send a WS frame; swallows errors from an already-closed socket.\n *\n * @param socket The client WebSocket to write to.\n * @param payload Plain object serialized to a JSON text frame.\n */\n private wsSend(socket: WebSocket, payload: Record<string, unknown>): void {\n try {\n socket.send(JSON.stringify(payload));\n } catch {\n /* socket closing/closed \u2014 drop the frame */\n }\n }\n\n /**\n * Handle one authenticated WS command. hassemu emulates an empty-but-valid HA\n * server with only the components it advertises (http/api/frontend/\n * homeassistant/mobile_app). Responses use only shapes that are either\n * source-verified or trivially correct for an empty server:\n * - data queries \u2192 correct empty shape ([] / {}),\n * - subscriptions \u2192 ack that never emits (no entities/events on a shim),\n * - everything hassemu does NOT implement (call_service on a service-less\n * server, conversation, Matter/Thread, assist_pipeline, \u2026) \u2192 `unknown_command`,\n * which is exactly what real HA returns for an unregistered command type.\n *\n * The command SET is verified against home-assistant/android\n * WebSocketRepositoryImpl at tag 2026.4.4; the error code against\n * home-assistant/core websocket_api/const.py at tag 2026.4.0 (ERR_UNKNOWN_COMMAND).\n * No speculative response shapes are emitted.\n *\n * @param socket The authenticated client WebSocket.\n * @param msg The parsed incoming command frame (`{ id, type, ... }`).\n */\n private handleWsCommand(socket: WebSocket, msg: Record<string, unknown>): void {\n const id = msg.id;\n const type = typeof msg.type === \"string\" ? msg.type : \"\";\n const result = (r: unknown): void => this.wsSend(socket, { id, type: \"result\", success: true, result: r });\n switch (type) {\n case \"ping\":\n this.wsSend(socket, { id, type: \"pong\" });\n return;\n case \"auth/current_user\":\n // CurrentUserResponse.kt @2026.4.4: { id, name, isOwner, isAdmin } \u2014\n // the HA wire format is snake_case (is_owner / is_admin).\n result({\n id: this.instanceUuid,\n name: this.config.username || this.serviceName,\n is_owner: true,\n is_admin: true,\n });\n return;\n case \"get_config\":\n result(this.buildHaConfig());\n return;\n case \"get_states\":\n result([]);\n return;\n case \"get_services\":\n result({});\n return;\n // Registries on an entity-less emulated server \u2192 empty lists.\n case \"config/area_registry/list\":\n case \"config/device_registry/list\":\n case \"config/entity_registry/list\":\n result([]);\n return;\n // Valid subscriptions on an empty server \u2014 they ack but never emit.\n // mobile_app/* is an advertised component, so both its WS commands ack\n // consistently (the channel subscribe + the confirm).\n case \"subscribe_events\":\n case \"subscribe_entities\":\n case \"supported_features\":\n case \"mobile_app/push_notification_channel\":\n case \"mobile_app/push_notification_confirm\":\n result(null);\n return;\n default:\n // hassemu doesn't implement this command (call_service has no services;\n // conversation / matter / thread / assist_pipeline are integrations it\n // doesn't advertise). Real HA returns ERR_UNKNOWN_COMMAND for an\n // unregistered command type \u2014 a reply (no hang), honest (no fake success),\n // and grounded (no guessed response shape).\n this.wsSend(socket, {\n id,\n type: \"result\",\n success: false,\n error: { code: \"unknown_command\", message: `Command \"${type}\" is not supported by this server` },\n });\n return;\n }\n }\n\n private setupMiscRoutes(): void {\n // Liveness only \u2014 no config leak. Earlier versions exposed the global\n // redirect URL via /health which is unauthenticated; removed in v1.2.0.\n // v1.5.0: auch der `config: { mdns, auth }`-Block raus \u2014 Auth-Status leakte\n // unauthenticated und lie\u00DF sich von einem Network-Attacker zur Reconnaissance\n // nutzen (auth-disabled Instances quickly mappen).\n this.app.get(\"/health\", () => ({\n status: \"ok\",\n adapter: \"hassemu\",\n version: HA_VERSION,\n }));\n\n this.app.get(\"/manifest.json\", () => ({\n // `name` MUST be \"Home Assistant\" exactly \u2014 the HA Companion App\n // verifies the server identity by parsing this field. Source:\n // home-assistant/android DefaultConnectivityChecker.kt:isHomeAssistant\n // checks `name === \"Home Assistant\"`. Anything else (e.g. `serviceName`\n // = \"ioBroker\") fails the onboarding probe with \"Server ist nicht\n // Home Assistant\". Detail in Ressourcen/hassemu/oauth2-browser-flow-shelly-fw26.md.\n name: \"Home Assistant\",\n short_name: \"Home Assistant\",\n start_url: \"/\",\n display: \"standalone\",\n background_color: \"#ffffff\",\n theme_color: \"#03a9f4\",\n }));\n\n // Root \u2014 HTML-Wrapper (iframe + auto-reload), oder Landing-Page wenn keine URL.\n //\n // v1.7.0 (A3): statt 302 liefern wir ein iframe-HTML + 30s-poll auf\n // /api/redirect_check. Wenn die Mode-/URL-Config sich \u00E4ndert (User edit\n // im Adapter), pollt das Display den Wechsel und macht `location.reload()`\n // \u2014 ohne Soft-Reboot des Displays. Vorher musste der User das Display\n // manuell rebooten.\n //\n // WebViews wie Shelly Wall Display rendern iframes + JavaScript korrekt.\n // Falls ein User direkten 302-Redirect will (Browser-Test, Bookmarklet\n // etc.), kann er die Target-URL direkt eingeben \u2014 der Wrapper l\u00E4uft nur\n // beim Aufruf von `/`.\n this.app.get(\"/\", async (req, reply) => {\n const client = await this.identify(req, reply);\n // v1.32.0 B1: Resolver-Chain als Triage-Anker. Ohne Chain musste der\n // Maintainer den Resolver-Code lesen um zu verstehen warum genau\n // diese URL f\u00FCr diesen Client gew\u00E4hlt wurde.\n const { url, chain } = this.globalConfig.resolveUrlForWithChain(client);\n if (!url) {\n this.adapter.log.debug(`GET / client=${client.id} \u2192 landing (chain=${chain})`);\n return reply\n .status(200)\n .type(\"text/html; charset=utf-8\")\n .send(renderLandingPage(client.id, this.adapter.namespace, this.systemLanguage, client.ip));\n }\n this.adapter.log.debug(`GET / client=${client.id} \u2192 URL (chain=${chain})`);\n return reply\n .status(200)\n .type(\"text/html; charset=utf-8\")\n .send(renderRedirectWrapper(url, client.id, this.systemLanguage, client.ip));\n });\n\n // /api/redirect_check \u2014 Display polled das alle 30s; wenn der target\n // sich ge\u00E4ndert hat (User edit), gibt der Wrapper `location.reload()`\n // ab. Cookie-basiert \u2014 Display schickt seinen `hassemu_client`-Cookie\n // automatisch mit.\n this.app.get(\"/api/redirect_check\", async (req, reply) => {\n const client = await this.identify(req, reply);\n const url = this.globalConfig.resolveUrlFor(client);\n // v1.32.0 F1: only-on-change-Trace. Jeder Poll (alle 30s \u00D7 N Displays)\n // w\u00E4re Flood \u2014 diagnostisch wertvoll ist nur der Target-Wechsel.\n // First-time-poll-pro-restart wird auch geloggt weil Map leer ist.\n const prev = this.lastRedirectTargetByClient.get(client.id);\n const next = url ?? null;\n if (prev !== next) {\n this.adapter.log.debug(\n `redirect_check client=${client.id}: ${prev === undefined ? \"first-poll\" : (prev ?? \"none\")} \u2192 ${next ?? \"none\"}`,\n );\n this.lastRedirectTargetByClient.set(client.id, next);\n }\n return { target: next };\n });\n }\n\n private setupNotFound(): void {\n this.app.setNotFoundHandler((req, reply) => {\n this.adapter.log.debug(`404: ${req.method} ${req.url}`);\n reply.status(404).send({ error: \"Not Found\", path: req.url });\n });\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import crypto from \"node:crypto\";\nimport dns from \"node:dns/promises\";\nimport fastifyCookie from \"@fastify/cookie\";\nimport fastifyFormbody from \"@fastify/formbody\";\nimport fastifyWebsocket from \"@fastify/websocket\";\nimport Fastify, { type FastifyInstance, type FastifyReply, type FastifyRequest } from \"fastify\";\nimport type { WebSocket } from \"ws\";\nimport {\n HA_VERSION,\n SESSION_TTL_MS,\n CLEANUP_INTERVAL_MS,\n LOGIN_SCHEMA,\n OAUTH_ACCESS_TOKEN_TTL_S,\n SESSIONS_CAP,\n WEBHOOK_REGISTRATIONS_CAP,\n REQUEST_ERROR_COOLDOWN_MS,\n REQUEST_ERROR_COOLDOWN_CAP,\n COOKIE_MAX_AGE_S,\n WS_AUTH_TIMEOUT_MS,\n} from \"./constants\";\nimport { coerceString, coerceUuid, evictOldest, isValidRedirectUri, safeStringEqual } from \"./coerce\";\nimport { buildRedirectUrl, renderAuthorizeError, renderAuthorizeForm, renderAuthorizeRedirect } from \"./auth-page\";\nimport type { ClientRegistry } from \"./client-registry\";\nimport type { GlobalConfig } from \"./global-config\";\nimport { renderLandingPage } from \"./landing-page\";\nimport { resolveAdvertisedHost } from \"./network\";\nimport { renderRedirectWrapper } from \"./redirect-wrapper\";\nimport type { AdapterConfig, AdapterInterface, ClientRecord, SessionData } from \"./types\";\n\n// v1.22.0 (F5): `safeStringEqual` ist nach `coerce.ts` verschoben \u2014 generischer\n// crypto-Helper, kein webserver-spezifischer Belang.\n\n// v1.32.0: `renderRedirectWrapper` ist nach `lib/redirect-wrapper.ts` ausgelagert\n// f\u00FCr Symmetrie zu `landing-page.ts` / `auth-page.ts`. `evictOldest` ist shared\n// helper aus `coerce.ts`.\n\n/** Adapter surface the WebServer depends on \u2014 adds `namespace` for the setup page. */\nexport type WebServerAdapter = AdapterInterface & Pick<ioBroker.Adapter, \"namespace\">;\n\n/**\n * Light-my-request injection surface \u2014 exposed read-only as a TEST-ONLY seam so\n * unit tests can drive routes without opening a real socket. Not used in production.\n */\nexport type WebserverInject = FastifyInstance[\"inject\"];\n\n/** Browser cookie name. Client identity lives here \u2014 auto-sent on every page navigation. */\nexport const CLIENT_COOKIE = \"hassemu_client\";\n\n/**\n * HA mobile_app registration response shape (home-assistant/android\n * RegisterDeviceResponse.kt): `webhookId` required, the cloud/remote/secret\n * fields null (no Nabu Casa cloud, the webhookId itself is the secret). Used\n * by the registration POST, the PUT update and the webhook `update_registration`.\n *\n * @param webhookId The issued webhook id (URL secret) to echo back to the App.\n */\nfunction mobileRegResponse(webhookId: string): {\n webhook_id: string;\n cloudhook_url: null;\n remote_ui_url: null;\n secret: null;\n} {\n return { webhook_id: webhookId, cloudhook_url: null, remote_ui_url: null, secret: null };\n}\n\n/**\n * Fastify web server emulating the HA REST API.\n *\n * Each incoming request is identified by cookie \u2192 {@link ClientRegistry} entry; new clients\n * get a channel created on first hit. Express was swapped for Fastify in 1.1.0 for first-party\n * cookie support, schema validation and a lighter runtime.\n */\nexport class WebServer {\n private readonly adapter: WebServerAdapter;\n private readonly config: AdapterConfig;\n private readonly registry: ClientRegistry;\n private readonly globalConfig: GlobalConfig;\n private readonly app: FastifyInstance;\n public readonly sessions: Map<string, SessionData> = new Map();\n /**\n * Mobile-App webhook registrations from `POST /api/mobile_app/registrations`\n * (v1.29.1). Key = webhookId (URL secret), Value = owning client cookie id.\n * Subsequent `POST /api/webhook/<id>` requests are validated against this\n * map. FIFO-capped at {@link WEBHOOK_REGISTRATIONS_CAP}; entries whose\n * owning client was removed are pruned in {@link cleanupSessions} (v1.35.2).\n *\n * Reused for Shelly Wall Display FW 2.6.0+ onboarding \u2014 the on-device HA\n * Companion App requires this endpoint to complete device registration\n * after the OAuth2 sign-in. Without it the App refuses to proceed with a\n * \"Mobile-App-Integration nicht verf\u00FCgbar\" error.\n *\n * **Design \u2014 in-memory only, by intent.** The map is NOT persisted across\n * adapter restarts. Restart-recovery relies on the\n * `POST /api/webhook/<unknown-id>` branch returning HTTP 200 with a\n * truly EMPTY body \u2014 the HA Companion App reads that as a stale webhook\n * and re-runs `registerDevice`, which on hassemu issues a fresh\n * webhookId. (Source, verified at tag 2026.4.4: home-assistant/android\n * IntegrationRepositoryImpl.kt:167-171 \u2014 the trigger is\n * `response.code() == 200 && response.body()?.contentLength() == 0L`.)\n *\n * If a future refactor changes the unknown-webhookId response from\n * `200 empty` to `404` or to any non-empty body (even JSON `null`),\n * displays will silently break across adapter restarts. Keep that\n * response shape OR add real persistence here.\n */\n public readonly webhookRegistrations: Map<string, string> = new Map();\n /**\n * v1.32.0 F1: last redirect-target seen per client by `/api/redirect_check`.\n * Used to log only-on-change (instead of every 30s poll). Pruned in\n * {@link cleanupSessions} against `registry.listAll()` \u2014 stale entries from\n * removed clients are dropped within max 5 min.\n */\n private readonly lastRedirectTargetByClient: Map<string, string | null> = new Map();\n private cleanupTimer: ioBroker.Interval | null = null;\n /**\n * Test-only injection surface ({@link WebserverInject}). v1.14.0 (H8): bound\n * once in the constructor instead of via a getter \u2014 a getter allocated a new\n * bound function on every `s.inject({...})` call, and tests call it in loops.\n */\n public readonly inject!: WebserverInject;\n public readonly instanceUuid: string;\n /** ioBroker system language for the setup page \u2014 resolved on startup. */\n public readonly systemLanguage: string;\n /** Set of IPs whose reverse DNS lookup is already in-flight \u2014 prevents duplicate work. */\n private readonly dnsInFlight = new Set<string>();\n /**\n * Per-message cooldown timestamps for 5xx error logging. First occurrence\n * of a unique message logs at warn; repeats within {@link REQUEST_ERROR_COOLDOWN_MS}\n * fall to debug to prevent log-spam under attack/probe traffic.\n */\n private readonly errorLogCooldown: Map<string, number> = new Map();\n\n /**\n * @param adapter Adapter instance used for logging, timers and namespace.\n * @param config Resolved runtime config.\n * @param registry Multi-client registry.\n * @param globalConfig Global redirect override.\n * @param instanceUuid Stable UUID shared with the mDNS advert.\n * @param systemLanguage ioBroker system language (`en`, `de`, \u2026) used for the setup page.\n */\n constructor(\n adapter: WebServerAdapter,\n config: AdapterConfig,\n registry: ClientRegistry,\n globalConfig: GlobalConfig,\n instanceUuid: string,\n systemLanguage: string = \"en\",\n ) {\n this.adapter = adapter;\n this.config = config;\n this.registry = registry;\n this.globalConfig = globalConfig;\n this.instanceUuid = instanceUuid;\n this.systemLanguage = systemLanguage;\n // v1.25.0 (C11): trustProxy ist Opt-In \u00FCber config \u2014 nur aktivieren\n // wenn der Adapter HINTER einem trusted Reverse-Proxy mit TLS-\n // Termination l\u00E4uft. Mit trustProxy=true holt Fastify `req.ip` aus\n // `X-Forwarded-For` (statt aus dem Socket), `req.protocol` aus\n // `X-Forwarded-Proto` etc. \u2014 Voraussetzung: der Proxy bereinigt diese\n // Header (sonst kann jeder Client seine sichtbare IP f\u00E4lschen \u2192 verf\u00E4lscht\n // Logs + die per-IP-Burst-Erkennung defekter Cookies).\n this.app = Fastify({ logger: false, trustProxy: this.config.trustProxy === true });\n // v1.14.0 (H8): inject einmal binden, nicht pro Getter-Access.\n (this as { inject: WebserverInject }).inject = this.app.inject.bind(this.app);\n }\n\n /** Human-readable service name advertised in responses and mDNS. */\n get serviceName(): string {\n return this.config.serviceName || \"ioBroker\";\n }\n\n /** Resolved listener address once `start()` has completed, or null otherwise. */\n get boundAddress(): { address: string; port: number } | null {\n const addr = this.app.server.address();\n if (!addr || typeof addr === \"string\") {\n return null;\n }\n return { address: addr.address, port: addr.port };\n }\n\n // --- lifecycle ---\n\n /** Registers plugins and starts the HTTP listener. */\n async start(): Promise<void> {\n // v1.14.0 (H9): defensive \u2014 wenn start() jemals doppelt gerufen wird\n // (Refactor, Test-Setup-Bug), Timer aus dem Vorlauf clearen statt zu\n // leaken.\n if (this.cleanupTimer) {\n this.adapter.clearInterval(this.cleanupTimer);\n this.cleanupTimer = null;\n }\n await this.app.register(fastifyCookie);\n // OAuth2-Spec verlangt `application/x-www-form-urlencoded` f\u00FCr `/auth/token`.\n // Echte HA-Reference-Clients (frontend/Wall Display SDK) folgen dem.\n // Fastify hat by-default nur einen JSON-Bodyparser \u2014 ohne diesen Plugin\n // beantwortet `/auth/token` mit form-Body 415 und der Login bleibt komplett\n // h\u00E4ngen. Tests via `app.inject({payload:{...}})` serialisieren zu JSON\n // und maskieren das.\n await this.app.register(fastifyFormbody);\n // v1.34.0: minimal read-only WebSocket for the HA Companion App. Its\n // `registerDevice` makes a best-effort `auth/current_user` WS call after the\n // REST registration; without a WS endpoint that fails (and the username is\n // not stored). Registered before the routes so `{ websocket: true }` works.\n await this.app.register(fastifyWebsocket);\n this.setupAuthGuard();\n this.setupErrorHandler();\n this.setupRoutes();\n\n const bindAddress = this.config.bindAddress || \"0.0.0.0\";\n try {\n await this.app.listen({ port: this.config.port, host: bindAddress });\n } catch (err) {\n const e = err as NodeJS.ErrnoException;\n const msg =\n e.code === \"EADDRINUSE\"\n ? `Port ${this.config.port} is already in use \u2014 another service is bound to it`\n : `Server error during startup: ${e.message}`;\n this.adapter.log.error(msg);\n throw err;\n }\n this.adapter.log.debug(`Web server listening on ${bindAddress}:${this.config.port}`);\n\n this.cleanupTimer = this.adapter.setInterval(() => this.cleanupSessions(), CLEANUP_INTERVAL_MS) ?? null;\n }\n\n /** Stops the listener and cancels the session cleanup timer. */\n async stop(): Promise<void> {\n if (this.cleanupTimer) {\n this.adapter.clearInterval(this.cleanupTimer);\n this.cleanupTimer = null;\n }\n try {\n await this.app.close();\n this.adapter.log.debug(\"Web server stopped\");\n } catch (err) {\n // v1.18.0 (G6+G8): debug statt error \u2014 bei intended shutdown\n // (onUnload) ist ein close-error meist ein \"already-closed\"-Race\n // ohne Konsequenz. Caller (main.ts onUnload) loggt nicht doppelt.\n this.adapter.log.debug(`Web server stop error: ${String(err)}`);\n }\n // v1.28.3 (HW1): drop in-flight DNS markers so a slow reverse-lookup\n // started just before stop() doesn't keep an IP entry pinned for the\n // whole process lifetime. The Promise.race(timeout) finally-handler\n // would do that eventually, but only after up to 5s \u2014 racy if the\n // adapter is restarted during that window.\n this.dnsInFlight.clear();\n }\n\n // v1.14.0 (H8): `inject` ist jetzt ein readonly Field (oben deklariert,\n // im Constructor einmalig gebunden). Der fr\u00FChere Getter allokierte bei\n // jedem Access eine neue Funktion.\n\n /** Periodic cleanup of expired in-flight auth sessions and stale redirect-target entries. */\n public cleanupSessions(): void {\n const now = Date.now();\n let cleanedSessions = 0;\n for (const [key, session] of this.sessions) {\n if (now - session.created > SESSION_TTL_MS) {\n this.sessions.delete(key);\n cleanedSessions++;\n }\n }\n if (cleanedSessions > 0) {\n this.adapter.log.debug(`Session cleanup: removed ${cleanedSessions} expired sessions`);\n }\n\n // v1.32.0 F1: prune lastRedirectTargetByClient against currently known\n // clients. A removed client leaves a stale entry that would never get\n // cleared otherwise \u2014 bounded growth over months.\n const activeClients = new Set(this.registry.listAll().map(r => r.id));\n let prunedTargets = 0;\n for (const clientId of this.lastRedirectTargetByClient.keys()) {\n if (!activeClients.has(clientId)) {\n this.lastRedirectTargetByClient.delete(clientId);\n prunedTargets++;\n }\n }\n if (prunedTargets > 0) {\n this.adapter.log.debug(`Cleanup: pruned ${prunedTargets} stale redirect-target entries`);\n }\n\n // v1.35.2: prune webhook registrations whose owning client was removed\n // (remove button / stale-GC). Without this, an orphaned display keeps\n // getting 200s on its webhook instead of falling into re-registration.\n // ownerId === \"\" means \"unowned\" (authRequired=false registration without\n // a Bearer token) \u2014 those have no client to check against and must stay.\n let prunedWebhooks = 0;\n for (const [webhookId, ownerId] of this.webhookRegistrations) {\n if (ownerId !== \"\" && !activeClients.has(ownerId)) {\n this.webhookRegistrations.delete(webhookId);\n prunedWebhooks++;\n }\n }\n if (prunedWebhooks > 0) {\n this.adapter.log.debug(`Cleanup: pruned ${prunedWebhooks} webhook registrations of removed clients`);\n }\n }\n\n /**\n * Cooldown-Decision f\u00FCr 5xx-Error-Logging. Liefert `true` f\u00FCr die erste\n * Beobachtung pro `key` innerhalb {@link REQUEST_ERROR_COOLDOWN_MS} und\n * markiert den Eintrag \u2014 Wiederholungen liefern `false` bis das Fenster\n * abgelaufen ist. Map ist FIFO-gedeckelt auf {@link REQUEST_ERROR_COOLDOWN_CAP}.\n *\n * @param key Eindeutiger Error-Identifier (\u00FCblicherweise `error.message`).\n * @param now Aktuelle Zeit in ms (testbar).\n */\n public shouldEmitRequestErrorWarn(key: string, now: number): boolean {\n const lastSeen = this.errorLogCooldown.get(key) ?? 0;\n if (lastSeen !== 0 && now - lastSeen <= REQUEST_ERROR_COOLDOWN_MS) {\n return false;\n }\n if (!this.errorLogCooldown.has(key)) {\n evictOldest(this.errorLogCooldown, REQUEST_ERROR_COOLDOWN_CAP);\n }\n this.errorLogCooldown.set(key, now);\n return true;\n }\n\n /**\n * Inserts a session, dropping the oldest entry if {@link SESSIONS_CAP} is exceeded.\n *\n * @param key Session key (flow id or auth code).\n * @param data Session payload.\n */\n private storeSession(key: string, data: SessionData): void {\n evictOldest(this.sessions, SESSIONS_CAP);\n this.sessions.set(key, data);\n }\n\n // --- client identification ---\n\n /**\n * v1.15.0 (F6): zentraler Extract `req.ip \u2192 coerced string|null`. Vorher\n * 3\u00D7 inline `coerceString(req.ip)` in identify/login/token-Handlern.\n *\n * @param req Fastify request (uses `req.ip`).\n */\n private static getClientIp(req: FastifyRequest): string | null {\n return coerceString(req.ip);\n }\n\n private async identify(req: FastifyRequest, reply: FastifyReply): Promise<ClientRecord> {\n const cookie = coerceUuid(req.cookies?.[CLIENT_COOKIE]);\n const ip = WebServer.getClientIp(req);\n // v1.17.0 (C8): UA durchreichen damit NAT-Co-Located Displays nicht\n // im selben Pending-Lock landen (siehe identifyOrCreate-Kommentar).\n const userAgent = coerceString(req.headers[\"user-agent\"]);\n const record = await this.registry.identifyOrCreate(cookie, ip, null, userAgent);\n // v1.32.0 A1: cookie-state explizit traced. Drei Branches:\n // hit \u2014 cookie matched a known client, no setCookie needed\n // stale/new \u2014 cookie present but unknown, OR no cookie at all \u2192 new client created\n if (cookie === record.cookie) {\n this.adapter.log.debug(`identify: cookie-hit client=${record.id} ip=${ip ?? \"?\"}`);\n } else {\n const reason = cookie ? \"cookie-stale (unknown)\" : \"no-cookie\";\n this.adapter.log.debug(`identify: ${reason}, new client=${record.id} ip=${ip ?? \"?\"}`);\n // v1.25.0 (C11): Cookie `secure: true` wenn TLS \u2014 Browser sendet\n // den Cookie dann nur \u00FCber HTTPS. Bei trustProxy=true kommt\n // `req.protocol` aus `X-Forwarded-Proto`-Header. Default ohne\n // trustProxy: `req.protocol === 'http'` (Adapter ist HTTP only),\n // also Cookie nicht-secure \u2014 sonst w\u00FCrde der Browser ihn nie senden.\n const useSecure = req.protocol === \"https\";\n // v1.32.0 A2: Cookie-Secure-Decision tracen \u2014 wenn trustProxy-config\n // falsch ist, kriegt Display den Cookie evtl. nie zur\u00FCck.\n this.adapter.log.debug(`identify: setting cookie secure=${useSecure} (req.protocol=${req.protocol})`);\n reply.setCookie(CLIENT_COOKIE, record.cookie, {\n path: \"/\",\n httpOnly: true,\n sameSite: \"lax\",\n secure: useSecure,\n maxAge: COOKIE_MAX_AGE_S,\n });\n }\n if (ip) {\n this.resolveHostnameAsync(record, ip);\n }\n return record;\n }\n\n private resolveHostnameAsync(record: ClientRecord, ip: string): void {\n if (record.hostname || this.dnsInFlight.has(ip)) {\n return;\n }\n this.dnsInFlight.add(ip);\n // v1.8.1 (D5): DNS-Lookup mit hartem 5s-Timeout. Default-Node-DNS hat\n // KEIN Timeout \u2014 bei broken Resolver (Captive-Portal, Misconfig) blieb\n // der Promise unendlich pending \u2192 IP f\u00FCr Adapter-Lifetime in dnsInFlight\n // blockiert, hostname auf record.ip gefroren.\n // v1.34.0: adapter-managed Timer (cancelt automatisch bei onUnload) + clear\n // sobald `dns.reverse` das Race gewinnt \u2014 sonst dangelt der Timer bis 5s\n // \u00FCber einen Restart hinaus (W5005).\n let timeoutHandle: ioBroker.Timeout | undefined;\n const timeout = new Promise<string[]>((_, reject) => {\n timeoutHandle = this.adapter.setTimeout(() => reject(new Error(\"dns reverse-lookup timeout\")), 5_000);\n });\n Promise.race([dns.reverse(ip), timeout])\n .then(names => {\n const name = names[0];\n if (name) {\n // v1.32.0 A4: Success-Trace \u2014 bei Diagnose \u201Ewarum hat Display\n // X den hostname Y?\" ist die IP\u2192hostname-Aufl\u00F6sung der Anker.\n this.adapter.log.debug(`resolveHostname: ip=${ip} \u2192 hostname=${name}`);\n this.registry.identifyOrCreate(record.cookie, ip, name).catch(() => {\n /* registry itself logs */\n });\n }\n })\n .catch(err => {\n // v1.32.0 A3: vorher silent. Reverse DNS scheitert auf LAN oft\n // legitim \u2014 daher debug-only (kein warn-Spam), aber jetzt mit\n // Diagnose-Anker f\u00FCr \u201Ehostname fehlt\"-Reports.\n this.adapter.log.debug(\n `resolveHostname: ip=${ip} failed \u2014 ${err instanceof Error ? err.message : String(err)}`,\n );\n })\n .finally(() => {\n if (timeoutHandle) {\n this.adapter.clearTimeout(timeoutHandle);\n }\n this.dnsInFlight.delete(ip);\n });\n }\n\n // --- auth guard ---\n\n /**\n * Pre-handler hook der `/api/*`-Routen sch\u00FCtzt wenn `authRequired=true`.\n *\n * Vorher: `/api/states`, `/api/services`, `/api/events`, `/api/error_log`,\n * `/api/discovery_info` lieferten unauthenticated alle ihre Daten \u2014\n * pure Information-Disclosure. Echte HA verlangt `Authorization: Bearer\n * <token>` f\u00FCr alle `/api/*` au\u00DFer dem `/api/`-Heartbeat.\n *\n * Whitelist (kein Auth n\u00F6tig):\n * - `/`, `/manifest.json`, `/health`, `/api/` \u2014 public Endpoints (Heartbeat, PWA)\n * - `/api/discovery_info` \u2014 HA-Clients fragen das VOR dem Auth-Flow ab um\n * zu erkennen ob `requires_api_password` true ist (Spec-Verhalten)\n * - `/auth/*` \u2014 der Auth-Flow selbst\n *\n * Bei `authRequired=false`: Hook macht nichts (no-op), bestehender Verhalten.\n */\n private setupAuthGuard(): void {\n this.app.addHook(\"preHandler\", async (req, reply) => {\n if (!this.config.authRequired) {\n return;\n }\n const path = (req.url ?? \"/\").split(\"?\")[0];\n // Public endpoints \u2014 explicitly allowed\n if (\n path === \"/\" ||\n path === \"/api/\" ||\n path === \"/api/discovery_info\" ||\n path === \"/manifest.json\" ||\n path === \"/health\" ||\n path.startsWith(\"/auth/\") ||\n // v1.34.0: the WebSocket does its own auth in the handshake\n // (`auth_required` \u2192 `auth` frame), not via a Bearer header \u2014 so the\n // HTTP upgrade itself must pass the guard.\n path === \"/api/websocket\" ||\n // v1.29.1: Mobile-App webhooks carry the secret in the URL\n // (`webhookId`) \u2014 HA core also serves these unauthenticated.\n // Source: home-assistant/core/.../mobile_app/webhook.py.\n path.startsWith(\"/api/webhook/\")\n ) {\n return;\n }\n // From here on: protected (`/api/*` apart from `/api/`)\n const authHeader = req.headers.authorization;\n if (typeof authHeader !== \"string\" || !authHeader.startsWith(\"Bearer \")) {\n this.adapter.log.debug(`Auth required for ${path} \u2014 missing Bearer token`);\n reply.status(401).send({ error: \"unauthorized\" });\n return;\n }\n const token = authHeader.substring(\"Bearer \".length).trim();\n const client = this.registry.getByToken(token);\n if (!client) {\n this.adapter.log.debug(`Auth required for ${path} \u2014 unknown Bearer token`);\n reply.status(401).send({ error: \"invalid_token\" });\n return;\n }\n // OK \u2014 handler runs\n });\n }\n\n // --- error handling ---\n\n private setupErrorHandler(): void {\n this.app.setErrorHandler((err, _req, reply) => {\n const error = err as Error & { validation?: unknown; statusCode?: number };\n if (error.validation) {\n this.adapter.log.debug(`Validation error: ${error.message}`);\n reply.status(400).send({ error: \"Invalid request\", details: error.message });\n return;\n }\n // Fastify body-parsing / client errors already set statusCode in 4xx range\n const code = typeof error.statusCode === \"number\" ? error.statusCode : 500;\n if (code >= 400 && code < 500) {\n this.adapter.log.debug(`Client error ${code}: ${error.message}`);\n reply.status(code).send({ error: error.message });\n return;\n }\n // 5xx: ein attacker kann mit malformed paths/oversized bodies viele\n // 500er triggern. Per-Message-Dedup-Map mit 60s-Cooldown \u2014 das erste\n // Auftreten pro unique message kommt als warn, alle Wiederholungen\n // im 60s-Fenster auf debug. Memory `feedback_no_log_spam`.\n const key = error.message || \"unknown\";\n if (this.shouldEmitRequestErrorWarn(key, Date.now())) {\n this.adapter.log.warn(`Request error: ${error.message}`);\n } else {\n this.adapter.log.debug(`Request error (repeat): ${error.message}`);\n }\n reply.status(500).send({ error: \"Internal server error\" });\n });\n }\n\n // --- routes ---\n\n private setupRoutes(): void {\n this.setupApiRoutes();\n this.setupAuthRoutes();\n this.setupWebSocket();\n this.setupMiscRoutes();\n this.setupNotFound();\n }\n\n /**\n * HA `/api/config`-shaped object. Single source for REST `/api/config`, the\n * Companion webhook `get_config` and the WebSocket `get_config` command.\n * `mobile_app` in `components` advertises the integration the HA Companion App\n * probes during onboarding (v1.29.1, Shelly FW 2.6.0+).\n */\n private buildHaConfig(): Record<string, unknown> {\n return {\n components: [\"http\", \"api\", \"frontend\", \"homeassistant\", \"mobile_app\"],\n config_dir: \"/config\",\n elevation: 0,\n latitude: 0,\n longitude: 0,\n location_name: this.serviceName,\n time_zone: \"UTC\",\n unit_system: { length: \"km\", mass: \"g\", temperature: \"\u00B0C\", volume: \"L\" },\n version: HA_VERSION,\n whitelist_external_dirs: [],\n };\n }\n\n private setupApiRoutes(): void {\n // CRITICAL: trailing slash \u2014 HA clients check this endpoint for discovery\n this.app.get(\"/api/\", () => ({ message: \"API running.\" }));\n\n this.app.get(\"/api/config\", () => this.buildHaConfig());\n\n this.app.get(\"/api/discovery_info\", () => {\n // v1.17.0 (E11): NICHT mehr `req.hostname` \u2014 der Host-Header ist\n // client-controlled und ein Angreifer k\u00F6nnte mit `Host: attacker.lan`\n // andere HA-Clients zur falschen URL umleiten. Stattdessen die\n // tats\u00E4chlich gebundene Adresse via resolveAdvertisedHost (konkrete\n // bindAddress, sonst getLocalIp) \u2014 identisch zum mDNS-Advert.\n const host = resolveAdvertisedHost(this.config.bindAddress);\n const baseUrl = `http://${host}:${this.config.port}`;\n return {\n base_url: baseUrl,\n external_url: null,\n internal_url: baseUrl,\n location_name: this.serviceName,\n // Vorher hardcoded `true` unabh\u00E4ngig von authRequired \u2014 strict HA-Clients\n // versuchten Auth auch bei authRequired=false und scheiterten am leeren Login-Flow.\n requires_api_password: this.config.authRequired,\n uuid: this.instanceUuid,\n version: HA_VERSION,\n };\n });\n\n for (const path of [\"/api/states\", \"/api/services\", \"/api/events\"]) {\n this.app.get(path, () => []);\n }\n this.app.get(\"/api/error_log\", () => \"\");\n\n // ---- Mobile-App integration (HA Companion + Shelly FW 2.6.0+) ----\n //\n // Source: home-assistant/android IntegrationRepositoryImpl.kt:120-159\n // calls POST /api/mobile_app/registrations after the OAuth2 sign-in.\n // A 404 here surfaces as \u201EMobile-App-Integration nicht verf\u00FCgbar\" in\n // the App's onboarding screen and blocks the display from finishing\n // setup. Detail in Ressourcen/hassemu/oauth2-browser-flow-shelly-fw26.md.\n //\n // The Bearer-token check is already done by the existing auth\n // pre-handler \u2014 `/api/mobile_app/registrations` is protected by\n // default, so by the time the handler runs we know the caller has\n // a valid access_token from /auth/token.\n this.app.post<{\n Body: {\n app_id?: string;\n app_name?: string;\n device_name?: string;\n device_id?: string;\n manufacturer?: string;\n model?: string;\n os_name?: string;\n os_version?: string;\n };\n }>(\"/api/mobile_app/registrations\", async (req, reply) => {\n const body = req.body ?? {};\n // Identify by Bearer token \u2014 the pre-handler already validated it.\n const authHeader = (req.headers.authorization as string) ?? \"\";\n const token = authHeader.startsWith(\"Bearer \") ? authHeader.substring(7).trim() : \"\";\n const client = this.registry.getByToken(token);\n const ownerId = client?.id ?? \"\";\n\n const webhookId = crypto.randomUUID().replace(/-/g, \"\");\n evictOldest(this.webhookRegistrations, WEBHOOK_REGISTRATIONS_CAP);\n this.webhookRegistrations.set(webhookId, ownerId);\n\n this.adapter.log.debug(\n `Mobile-App registration \u2014 client=${ownerId} app_id=${body.app_id ?? \"?\"} device_name=${body.device_name ?? \"?\"} \u2192 webhook=${webhookId}`,\n );\n\n reply.status(201);\n return mobileRegResponse(webhookId);\n });\n\n // PUT and DELETE on /api/mobile_app/registrations/:webhookId \u2014 the App\n // calls PUT to update its registration on token refresh or sensor\n // re-register. PUT echoes the registration for a KNOWN webhookId (200), but\n // returns 404 for an unknown one so a stale Pre-Restart token re-registers;\n // DELETE drops the registration and returns 204.\n this.app.put<{ Params: { webhookId: string } }>(\"/api/mobile_app/registrations/:webhookId\", async (req, reply) => {\n const id = req.params.webhookId;\n if (!this.webhookRegistrations.has(id)) {\n // v1.32.0 E1: stale-id signaliert dass Companion einen Token\n // aus Pre-Restart-Era hat \u2014 diagnostisch wertvoll f\u00FCr\n // re-registration-loop-Bugs.\n this.adapter.log.debug(`Mobile-App PUT registration: unknown webhookId=${id.substring(0, 8)}\u2026 \u2014 returning 404`);\n reply.status(404);\n return { error: \"unknown_registration\" };\n }\n return mobileRegResponse(id);\n });\n\n this.app.delete<{ Params: { webhookId: string } }>(\n \"/api/mobile_app/registrations/:webhookId\",\n async (req, reply) => {\n const id = req.params.webhookId;\n const wasPresent = this.webhookRegistrations.has(id);\n this.webhookRegistrations.delete(id);\n // v1.32.0 E2: Companion-Maintenance-Trace.\n this.adapter.log.debug(\n `Mobile-App DELETE registration: webhookId=${id.substring(0, 8)}\u2026 removed (was-present=${wasPresent})`,\n );\n reply.status(204);\n return null;\n },\n );\n\n // POST /api/webhook/:webhookId \u2014 Companion-App sensor updates,\n // location pings, registration updates etc. Public by design (URL\n // contains the webhookId secret). HA core dispatches on `type` field\n // in the JSON body and returns shape per type. For hassemu we accept\n // any payload and respond with the minimal-correct success per type;\n // the display use-case doesn't need actual state propagation, but\n // returning 200 prevents the App from re-trying in a loop and\n // surfacing onboarding-failure banners.\n this.app.post<{\n Params: { webhookId: string };\n Body: { type?: string; data?: unknown };\n }>(\"/api/webhook/:webhookId\", async (req, reply) => {\n const id = req.params.webhookId;\n if (!this.webhookRegistrations.has(id)) {\n // Unknown webhookId \u2014 match HA's 200-empty for stale webhooks so the\n // App re-registers. Source (verified at tag 2026.4.4):\n // home-assistant/android IntegrationRepositoryImpl.kt:167-171 \u2014\n // `updateRegistration` re-runs `registerDevice` ONLY when\n // `response.code() == 200 && response.body()?.contentLength() == 0L`.\n // The body MUST therefore be truly empty: `return null` would let\n // Fastify serialize the 4-byte JSON text \"null\" (contentLength 4),\n // the Companion would take the success branch and the display would\n // stay broken silently (v1.35.2 fix).\n // v1.32.0 E3: stale-id ist DAS Symptom f\u00FCr re-registration-loop \u2014\n // Companion macht webhook-call mit Token aus Pre-Restart-Era.\n this.adapter.log.debug(\n `Webhook fallthrough: stale id=${id.substring(0, 8)}\u2026 \u2014 App will trigger re-registration`,\n );\n return reply.status(200).send();\n }\n const body = req.body ?? {};\n const type = typeof body.type === \"string\" ? body.type : \"\";\n this.adapter.log.debug(`Webhook ${id.substring(0, 8)}\u2026 type=${type || \"(no type)\"}`);\n\n switch (type) {\n case \"get_config\":\n return this.buildHaConfig();\n case \"get_zones\":\n return [];\n case \"render_template\":\n return {};\n case \"update_registration\":\n return mobileRegResponse(id);\n case \"register_sensor\":\n return { success: true };\n case \"update_sensor_states\":\n return {};\n default:\n // Generic success for unknown types \u2014 fire_event,\n // call_service, conversation_process, update_location,\n // get_zones-with-data, etc. The display doesn't need\n // their semantics, just an HTTP 200 acknowledgement.\n return {};\n }\n });\n }\n\n /**\n * Issue a fresh authorization code and persist it in the sessions map.\n *\n * Single source for both the JSON login flow (`/auth/login_flow/<flowId>`\n * \u2192 `create_entry`) and the browser OAuth2 flow (`/auth/authorize` \u2192\n * 302). The code is exchanged for tokens at `/auth/token` (`grant_type =\n * authorization_code`); the existing token-view consumes the same map.\n *\n * @param clientId Identity cookie value of the requesting display, or\n * undefined for headless OAuth2-only flows.\n */\n private issueAuthorizationCode(clientId: string | null): string {\n const code = crypto.randomUUID();\n this.storeSession(code, { created: Date.now(), clientId });\n return code;\n }\n\n /**\n * Shared validation for GET and POST `/auth/authorize`. On failure it sets the\n * `400 text/html` reply and returns the rendered error page; on success it\n * returns the validated (string-typed) `client_id` / `redirect_uri`. Never\n * redirects on failure \u2014 the endpoint must not become an open redirector.\n *\n * @param reply Fastify reply (status + content-type set on failure).\n * @param method `\"GET\"` or `\"POST\"` \u2014 only used to label the debug log.\n * @param responseType The OAuth2 `response_type` (must be `\"code\"`).\n * @param clientId The OAuth2 `client_id` (must be a string).\n * @param redirectUri The OAuth2 `redirect_uri` (must be a string + allowlisted).\n */\n private validateAuthorizeRequest(\n reply: FastifyReply,\n method: \"GET\" | \"POST\",\n responseType: unknown,\n clientId: unknown,\n redirectUri: unknown,\n ): { ok: true; clientId: string; redirectUri: string } | { ok: false; html: string } {\n if (responseType !== \"code\") {\n this.adapter.log.debug(`Authorize ${method} rejected: response_type=${String(responseType)} (expected 'code')`);\n reply.status(400).type(\"text/html\");\n return {\n ok: false,\n html: renderAuthorizeError(\n \"unsupported_response_type\",\n \"This authorization server supports `response_type=code` only.\",\n ),\n };\n }\n if (typeof clientId !== \"string\" || typeof redirectUri !== \"string\") {\n this.adapter.log.debug(\n `Authorize ${method} rejected: missing client_id or redirect_uri (cid=${typeof clientId}, ru=${typeof redirectUri})`,\n );\n reply.status(400).type(\"text/html\");\n return {\n ok: false,\n html: renderAuthorizeError(\"invalid_request\", \"Missing or invalid `client_id` or `redirect_uri` parameter.\"),\n };\n }\n if (!isValidRedirectUri(clientId, redirectUri)) {\n this.adapter.log.debug(\n `Authorize ${method} rejected: redirect_uri \"${redirectUri}\" not allowed for client_id \"${clientId}\"`,\n );\n reply.status(400).type(\"text/html\");\n return {\n ok: false,\n html: renderAuthorizeError(\n \"invalid_redirect_uri\",\n \"The `redirect_uri` parameter is not on the allowlist for this client.\",\n ),\n };\n }\n return { ok: true, clientId, redirectUri };\n }\n\n /**\n * Issue an auth code, build the redirect target and render the auto-submit redirect page.\n *\n * @param reply Fastify reply (content-type set to text/html).\n * @param clientId Identity of the requesting display, or null for headless flows.\n * @param redirectUri Already-validated `redirect_uri` to append the code to.\n * @param state Optional OAuth2 `state` round-tripped verbatim.\n */\n private issueAuthorizeRedirect(\n reply: FastifyReply,\n clientId: string | null,\n redirectUri: string,\n state: string | undefined,\n ): string {\n const code = this.issueAuthorizationCode(clientId);\n const target = buildRedirectUrl(redirectUri, code, state);\n reply.type(\"text/html\");\n return renderAuthorizeRedirect(target);\n }\n\n /**\n * Best-effort token revocation, shared by `POST /auth/revoke` (HA \u22652022.9)\n * and the legacy `POST /auth/token` with `action=revoke`. The HA Companion\n * sends the refresh token; we look it up and clear both the refresh and the\n * access token of the owning client. Always succeeds from the caller's view \u2014\n * an unknown/missing token still yields 200 (matches HA, which never leaks\n * whether a token existed). Source: AuthenticationRepositoryImpl.revokeSession.\n *\n * @param token Refresh token to revoke (from the `token` form field).\n */\n private async revokeToken(token: string | undefined): Promise<void> {\n const refresh = typeof token === \"string\" ? token : \"\";\n const owner = refresh ? this.registry.getByRefreshToken(refresh) : null;\n if (owner) {\n await this.registry.setRefreshToken(owner.id, null);\n await this.registry.setToken(owner.id, null);\n this.adapter.log.debug(`Token revoked \u2014 client ${owner.id}`);\n } else {\n this.adapter.log.debug(\"Revoke: unknown/missing token \u2014 returning 200 (HA behavior)\");\n }\n }\n\n private setupAuthRoutes(): void {\n this.app.get(\"/auth/providers\", () => [{ name: \"Home Assistant Local\", type: \"homeassistant\", id: null }]);\n\n // Browser-OAuth2 flow at GET/POST /auth/authorize. Needed by the\n // HA Companion Android App (Shelly Wall Display FW 2.6.0+ embeds\n // the Companion App). Source-verified flow:\n // home-assistant/android UrlUtil.kt:buildAuthenticationUrl\n // home-assistant/core indieauth.py:verify_redirect_uri\n // home-assistant/frontend src/data/auth.ts:redirectWithAuthCode\n // Detail: Ressourcen/hassemu/oauth2-browser-flow-shelly-fw26.md\n this.app.get<{\n Querystring: { response_type?: string; client_id?: string; redirect_uri?: string; state?: string };\n }>(\"/auth/authorize\", async (req, reply) => {\n const { response_type, client_id, redirect_uri, state } = req.query ?? {};\n\n // v1.32.0 D2: rejection-Pfade traced \u2014 Triage \u201Ewarum bricht OAuth ab\"\n const v = this.validateAuthorizeRequest(reply, \"GET\", response_type, client_id, redirect_uri);\n if (!v.ok) {\n return v.html;\n }\n\n const client = await this.identify(req, reply);\n\n // No auth required \u2192 issue the code right away and redirect.\n if (!this.config.authRequired) {\n this.adapter.log.debug(`Authorize auto-grant \u2014 client ${client.id}`);\n return this.issueAuthorizeRedirect(reply, client.id, v.redirectUri, state);\n }\n\n // v1.32.0 D1: Form-render Trace \u2014 wenn Companion die Form nie absendet,\n // sieht User hier dass sie \u00FCberhaupt gerendert wurde.\n let redirectHost = \"?\";\n try {\n redirectHost = new URL(v.redirectUri).host || v.redirectUri;\n } catch {\n redirectHost = v.redirectUri;\n }\n this.adapter.log.debug(`Authorize form rendered \u2014 client_id=${v.clientId} redirect_uri-host=${redirectHost}`);\n reply.type(\"text/html\");\n return renderAuthorizeForm({ clientId: v.clientId, redirectUri: v.redirectUri, state });\n });\n\n this.app.post<{\n Body: {\n response_type?: string;\n client_id?: string;\n redirect_uri?: string;\n state?: string;\n username?: string;\n password?: string;\n };\n }>(\"/auth/authorize\", async (req, reply) => {\n const { response_type, client_id, redirect_uri, state, username, password } = req.body ?? {};\n\n const v = this.validateAuthorizeRequest(reply, \"POST\", response_type, client_id, redirect_uri);\n if (!v.ok) {\n return v.html;\n }\n\n const client = await this.identify(req, reply);\n\n // No auth required \u2192 straight to redirect even on POST.\n if (!this.config.authRequired) {\n return this.issueAuthorizeRedirect(reply, client.id, v.redirectUri, state);\n }\n\n const ip = WebServer.getClientIp(req);\n const userOk = typeof username === \"string\" && safeStringEqual(username, this.config.username);\n const passOk = typeof password === \"string\" && safeStringEqual(password, this.config.password);\n if (!userOk || !passOk) {\n const ipSuffix = ip ? ` (IP ${ip})` : \"\";\n this.adapter.log.warn(`Invalid credentials${ipSuffix}`);\n reply.status(401).type(\"text/html\");\n return renderAuthorizeForm(\n { clientId: v.clientId, redirectUri: v.redirectUri, state },\n \"Invalid username or password.\",\n );\n }\n\n this.adapter.log.debug(`Authorize grant \u2014 client ${client.id}`);\n return this.issueAuthorizeRedirect(reply, client.id, v.redirectUri, state);\n });\n\n this.app.post(\"/auth/login_flow\", async (req, reply) => {\n const client = await this.identify(req, reply);\n const flowId = crypto.randomUUID();\n this.storeSession(flowId, { created: Date.now(), clientId: client.id });\n this.adapter.log.debug(`Auth flow created: ${flowId} for client ${client.id}`);\n\n return {\n type: \"form\",\n flow_id: flowId,\n handler: [\"homeassistant\", null],\n step_id: \"init\",\n data_schema: LOGIN_SCHEMA,\n description_placeholders: null,\n errors: null,\n };\n });\n\n this.app.post<{\n Params: { flowId: string };\n Body: { username?: string; password?: string };\n }>(\n \"/auth/login_flow/:flowId\",\n {\n schema: {\n params: {\n type: \"object\",\n properties: { flowId: { type: \"string\", minLength: 1 } },\n required: [\"flowId\"],\n },\n },\n },\n async (req, reply) => {\n const flowId = req.params.flowId;\n const session = this.sessions.get(flowId);\n if (!session) {\n // v1.8.0: nach Session-TTL (10 min) feuert das bei jedem\n // legit returning user \u2014 nicht actionable. debug, nicht warn.\n this.adapter.log.debug(`Unknown flow_id: ${flowId}`);\n reply.status(400);\n return { type: \"abort\", flow_id: flowId, reason: \"unknown_flow\" };\n }\n\n if (this.config.authRequired) {\n const ip = WebServer.getClientIp(req);\n const { username, password } = req.body ?? {};\n const userOk = typeof username === \"string\" && safeStringEqual(username, this.config.username);\n const passOk = typeof password === \"string\" && safeStringEqual(password, this.config.password);\n if (!userOk || !passOk) {\n const ipSuffix = ip ? ` (IP ${ip})` : \"\";\n this.adapter.log.warn(`Invalid credentials${ipSuffix}`);\n reply.status(400);\n return {\n type: \"form\",\n flow_id: flowId,\n handler: [\"homeassistant\", null],\n step_id: \"init\",\n data_schema: LOGIN_SCHEMA,\n errors: { base: \"invalid_auth\" },\n description_placeholders: null,\n };\n }\n }\n\n this.sessions.delete(flowId);\n const code = crypto.randomUUID();\n this.storeSession(code, { created: Date.now(), clientId: session.clientId });\n this.adapter.log.debug(\"Auth flow completed \u2014 code issued\");\n\n return {\n version: 1,\n type: \"create_entry\",\n flow_id: flowId,\n handler: [\"homeassistant\", null],\n result: code,\n description: null,\n description_placeholders: null,\n };\n },\n );\n\n // HA \u22652022.9 logout: POST /auth/revoke with form field `token` (the refresh\n // token). Always 200 with empty body. Whitelisted by the `/auth/` prefix in\n // the auth guard. Source: AuthenticationRepositoryImpl.revokeSession.\n this.app.post<{ Body: { token?: string } }>(\"/auth/revoke\", async req => {\n await this.revokeToken(req.body?.token);\n return {};\n });\n\n this.app.post<{\n Body: { code?: string; grant_type?: string; refresh_token?: string; action?: string; token?: string };\n }>(\"/auth/token\", async (req, reply) => {\n const { code, grant_type, refresh_token, action } = req.body ?? {};\n\n // Legacy logout (HA <2022.9): POST /auth/token with action=revoke + token.\n // Newer apps use /auth/revoke; we accept both so a 400 never surfaces.\n if (action === \"revoke\") {\n await this.revokeToken(req.body?.token ?? refresh_token);\n return {};\n }\n\n if (grant_type === \"authorization_code\" && code && this.sessions.has(code)) {\n const session = this.sessions.get(code)!;\n this.sessions.delete(code);\n const token = crypto.randomUUID();\n const refreshToken = crypto.randomUUID();\n if (session.clientId) {\n // Persist VOR Response-Build: ein Crash zwischen Issue + Persist\n // w\u00FCrde sonst dem Client einen Token in der Hand lassen, den der\n // Server nicht kennt \u2014 beim ersten Refresh dann invalid_grant.\n await this.registry.setToken(session.clientId, token);\n await this.registry.setRefreshToken(session.clientId, refreshToken);\n this.adapter.log.debug(`Display authenticated \u2014 client ${session.clientId}`);\n }\n return {\n access_token: token,\n token_type: \"Bearer\",\n refresh_token: refreshToken,\n expires_in: OAUTH_ACCESS_TOKEN_TTL_S,\n };\n }\n\n if (grant_type === \"refresh_token\") {\n // Validate the refresh token against issued ones \u2014 was previously\n // accepting any string and minting a new access_token (security fix v1.2.0).\n const incoming = typeof refresh_token === \"string\" ? refresh_token : \"\";\n const ownerRecord = incoming ? this.registry.getByRefreshToken(incoming) : null;\n if (!ownerRecord) {\n this.adapter.log.debug(\"Refresh token rejected \u2014 unknown or missing\");\n reply.status(400);\n return { error: \"invalid_grant\", error_description: \"Invalid refresh token\" };\n }\n // v1.31.0: refresh_token bleibt valid (NICHT mehr rotated). HA Core\n // selbst (homeassistant/components/auth/__init__.py:334-348) liefert\n // beim refresh-grant nie einen neuen refresh_token, nur access_token\n // + token_type + expires_in. HA Android Companion\n // (AuthenticationRepositoryImpl.kt:147) speichert beim Refresh den\n // GESENDETEN refresh_token (Function-Parameter), ignoriert den in der\n // Response zur\u00FCckgegebenen \u2014 Companion beh\u00E4lt daher immer ihren\n // initialen refresh_token. v1.28.3 (HW5) Rotation war RFC 6819\n // \u00A75.2.2.3-konform aber inkompatibel mit dem Companion-Datenmodell:\n // Server-Rotation killte den Companion-Token beim ersten Refresh.\n const newAccess = crypto.randomUUID();\n await this.registry.setToken(ownerRecord.id, newAccess);\n this.adapter.log.debug(`Refresh-token-grant \u2014 client=${ownerRecord.id} new access_token issued`);\n return {\n access_token: newAccess,\n token_type: \"Bearer\",\n refresh_token: incoming,\n expires_in: OAUTH_ACCESS_TOKEN_TTL_S,\n };\n }\n\n // \u201Ewrong grant_type\" ist ein Client-Format-Fehler, kein Server-Concern\n // \u2014 daher nur debug (legitime Client-Bugs sollen das Log nicht fluten).\n this.adapter.log.debug(`Token exchange failed: grant_type=${String(grant_type)}`);\n reply.status(400);\n return { error: \"invalid_request\", error_description: \"Invalid or expired code\" };\n });\n }\n\n /**\n * Minimal read-only HA WebSocket at `/api/websocket`. The HA Companion App's\n * `registerDevice` makes a best-effort `auth/current_user` WS call after the\n * REST registration to store the username (home-assistant/android\n * IntegrationRepositoryImpl.kt at tag 2026.4.4, line 154). Without a WS\n * endpoint that throws and the registration logs \"Unable to save device registration\".\n *\n * Auth happens in-band: server sends `auth_required`, client replies with an\n * `auth` frame, we validate the access token against the registry. FAIL-FAST:\n * a missing/invalid token or a missing `auth` frame within\n * {@link WS_AUTH_TIMEOUT_MS} closes the socket \u2014 so the WS never hangs the\n * App's call (which previously failed fast against a clean 404).\n */\n private setupWebSocket(): void {\n this.app.get(\"/api/websocket\", { websocket: true }, (socket: WebSocket) => {\n let authed = false;\n let authTimer: ioBroker.Timeout | undefined = this.adapter.setTimeout(() => {\n if (!authed) {\n this.adapter.log.debug(\"WS: no auth frame within timeout \u2014 closing\");\n this.wsSend(socket, { type: \"auth_invalid\", message: \"Authentication timed out\" });\n socket.close();\n }\n }, WS_AUTH_TIMEOUT_MS);\n\n this.wsSend(socket, { type: \"auth_required\", ha_version: HA_VERSION });\n\n socket.on(\"message\", raw => {\n // ws delivers text frames as Buffer by default; normalize every RawData\n // variant to a UTF-8 string (avoids Object's default stringification).\n const text = Buffer.isBuffer(raw)\n ? raw.toString(\"utf8\")\n : Array.isArray(raw)\n ? Buffer.concat(raw).toString(\"utf8\")\n : Buffer.from(raw).toString(\"utf8\");\n let msg: Record<string, unknown>;\n try {\n msg = JSON.parse(text) as Record<string, unknown>;\n } catch {\n return; // ignore non-JSON frames\n }\n if (!authed) {\n const token = typeof msg.access_token === \"string\" ? msg.access_token : \"\";\n if (msg.type === \"auth\" && token && this.registry.getByToken(token)) {\n authed = true;\n if (authTimer) {\n this.adapter.clearTimeout(authTimer);\n authTimer = undefined;\n }\n this.wsSend(socket, { type: \"auth_ok\", ha_version: HA_VERSION });\n } else {\n this.adapter.log.debug(\"WS: auth_invalid \u2014 unknown or missing access token\");\n this.wsSend(socket, { type: \"auth_invalid\", message: \"Invalid access token\" });\n socket.close();\n }\n return;\n }\n this.handleWsCommand(socket, msg);\n });\n\n socket.on(\"error\", () => {\n // Client vanished mid-stream \u2014 the socket is gone; the auth timer is\n // cleared by the close handler below.\n });\n\n socket.on(\"close\", () => {\n // Clear the auth timer if the client disconnects before authenticating,\n // so it never fires against an already-closed socket.\n if (authTimer) {\n this.adapter.clearTimeout(authTimer);\n authTimer = undefined;\n }\n });\n });\n }\n\n /**\n * Safely serialize + send a WS frame; swallows errors from an already-closed socket.\n *\n * @param socket The client WebSocket to write to.\n * @param payload Plain object serialized to a JSON text frame.\n */\n private wsSend(socket: WebSocket, payload: Record<string, unknown>): void {\n try {\n socket.send(JSON.stringify(payload));\n } catch {\n /* socket closing/closed \u2014 drop the frame */\n }\n }\n\n /**\n * Handle one authenticated WS command. hassemu emulates an empty-but-valid HA\n * server with only the components it advertises (http/api/frontend/\n * homeassistant/mobile_app). Responses use only shapes that are either\n * source-verified or trivially correct for an empty server:\n * - data queries \u2192 correct empty shape ([] / {}),\n * - subscriptions \u2192 ack that never emits (no entities/events on a shim),\n * - everything hassemu does NOT implement (call_service on a service-less\n * server, conversation, Matter/Thread, assist_pipeline, \u2026) \u2192 `unknown_command`,\n * which is exactly what real HA returns for an unregistered command type.\n *\n * The command SET is verified against home-assistant/android\n * WebSocketRepositoryImpl at tag 2026.4.4; the error code against\n * home-assistant/core websocket_api/const.py at tag 2026.4.0 (ERR_UNKNOWN_COMMAND).\n * No speculative response shapes are emitted.\n *\n * @param socket The authenticated client WebSocket.\n * @param msg The parsed incoming command frame (`{ id, type, ... }`).\n */\n private handleWsCommand(socket: WebSocket, msg: Record<string, unknown>): void {\n const id = msg.id;\n const type = typeof msg.type === \"string\" ? msg.type : \"\";\n const result = (r: unknown): void => this.wsSend(socket, { id, type: \"result\", success: true, result: r });\n switch (type) {\n case \"ping\":\n this.wsSend(socket, { id, type: \"pong\" });\n return;\n case \"auth/current_user\":\n // CurrentUserResponse.kt @2026.4.4: { id, name, isOwner, isAdmin } \u2014\n // the HA wire format is snake_case (is_owner / is_admin).\n result({\n id: this.instanceUuid,\n name: this.config.username || this.serviceName,\n is_owner: true,\n is_admin: true,\n });\n return;\n case \"get_config\":\n result(this.buildHaConfig());\n return;\n case \"get_states\":\n result([]);\n return;\n case \"get_services\":\n result({});\n return;\n // Registries on an entity-less emulated server \u2192 empty lists.\n case \"config/area_registry/list\":\n case \"config/device_registry/list\":\n case \"config/entity_registry/list\":\n result([]);\n return;\n // Valid subscriptions on an empty server \u2014 they ack but never emit.\n // mobile_app/* is an advertised component, so both its WS commands ack\n // consistently (the channel subscribe + the confirm).\n case \"subscribe_events\":\n case \"subscribe_entities\":\n case \"supported_features\":\n case \"mobile_app/push_notification_channel\":\n case \"mobile_app/push_notification_confirm\":\n result(null);\n return;\n default:\n // hassemu doesn't implement this command (call_service has no services;\n // conversation / matter / thread / assist_pipeline are integrations it\n // doesn't advertise). Real HA returns ERR_UNKNOWN_COMMAND for an\n // unregistered command type \u2014 a reply (no hang), honest (no fake success),\n // and grounded (no guessed response shape).\n this.wsSend(socket, {\n id,\n type: \"result\",\n success: false,\n error: { code: \"unknown_command\", message: `Command \"${type}\" is not supported by this server` },\n });\n return;\n }\n }\n\n private setupMiscRoutes(): void {\n // Liveness only \u2014 no config leak. Earlier versions exposed the global\n // redirect URL via /health which is unauthenticated; removed in v1.2.0.\n // v1.5.0: auch der `config: { mdns, auth }`-Block raus \u2014 Auth-Status leakte\n // unauthenticated und lie\u00DF sich von einem Network-Attacker zur Reconnaissance\n // nutzen (auth-disabled Instances quickly mappen).\n this.app.get(\"/health\", () => ({\n status: \"ok\",\n adapter: \"hassemu\",\n version: HA_VERSION,\n }));\n\n this.app.get(\"/manifest.json\", () => ({\n // `name` MUST be \"Home Assistant\" exactly \u2014 the HA Companion App\n // verifies the server identity by parsing this field. Source:\n // home-assistant/android DefaultConnectivityChecker.kt:isHomeAssistant\n // checks `name === \"Home Assistant\"`. Anything else (e.g. `serviceName`\n // = \"ioBroker\") fails the onboarding probe with \"Server ist nicht\n // Home Assistant\". Detail in Ressourcen/hassemu/oauth2-browser-flow-shelly-fw26.md.\n name: \"Home Assistant\",\n short_name: \"Home Assistant\",\n start_url: \"/\",\n display: \"standalone\",\n background_color: \"#ffffff\",\n theme_color: \"#03a9f4\",\n }));\n\n // Root \u2014 HTML-Wrapper (iframe + auto-reload), oder Landing-Page wenn keine URL.\n //\n // v1.7.0 (A3): statt 302 liefern wir ein iframe-HTML + 30s-poll auf\n // /api/redirect_check. Wenn die Mode-/URL-Config sich \u00E4ndert (User edit\n // im Adapter), pollt das Display den Wechsel und macht `location.reload()`\n // \u2014 ohne Soft-Reboot des Displays. Vorher musste der User das Display\n // manuell rebooten.\n //\n // WebViews wie Shelly Wall Display rendern iframes + JavaScript korrekt.\n // Falls ein User direkten 302-Redirect will (Browser-Test, Bookmarklet\n // etc.), kann er die Target-URL direkt eingeben \u2014 der Wrapper l\u00E4uft nur\n // beim Aufruf von `/`.\n this.app.get(\"/\", async (req, reply) => {\n const client = await this.identify(req, reply);\n // v1.32.0 B1: Resolver-Chain als Triage-Anker. Ohne Chain musste der\n // Maintainer den Resolver-Code lesen um zu verstehen warum genau\n // diese URL f\u00FCr diesen Client gew\u00E4hlt wurde.\n const { url, chain } = this.globalConfig.resolveUrlForWithChain(client);\n if (!url) {\n this.adapter.log.debug(`GET / client=${client.id} \u2192 landing (chain=${chain})`);\n return reply\n .status(200)\n .type(\"text/html; charset=utf-8\")\n .send(renderLandingPage(client.id, this.adapter.namespace, this.systemLanguage, client.ip));\n }\n this.adapter.log.debug(`GET / client=${client.id} \u2192 URL (chain=${chain})`);\n return reply\n .status(200)\n .type(\"text/html; charset=utf-8\")\n .send(renderRedirectWrapper(url, client.id, this.systemLanguage, client.ip));\n });\n\n // /api/redirect_check \u2014 Display polled das alle 30s; wenn der target\n // sich ge\u00E4ndert hat (User edit), gibt der Wrapper `location.reload()`\n // ab. Cookie-basiert \u2014 Display schickt seinen `hassemu_client`-Cookie\n // automatisch mit.\n this.app.get(\"/api/redirect_check\", async (req, reply) => {\n const client = await this.identify(req, reply);\n const url = this.globalConfig.resolveUrlFor(client);\n // v1.32.0 F1: only-on-change-Trace. Jeder Poll (alle 30s \u00D7 N Displays)\n // w\u00E4re Flood \u2014 diagnostisch wertvoll ist nur der Target-Wechsel.\n // First-time-poll-pro-restart wird auch geloggt weil Map leer ist.\n const prev = this.lastRedirectTargetByClient.get(client.id);\n const next = url ?? null;\n if (prev !== next) {\n this.adapter.log.debug(\n `redirect_check client=${client.id}: ${prev === undefined ? \"first-poll\" : (prev ?? \"none\")} \u2192 ${next ?? \"none\"}`,\n );\n this.lastRedirectTargetByClient.set(client.id, next);\n }\n return { target: next };\n });\n }\n\n private setupNotFound(): void {\n this.app.setNotFoundHandler((req, reply) => {\n this.adapter.log.debug(`404: ${req.method} ${req.url}`);\n reply.status(404).send({ error: \"Not Found\", path: req.url });\n });\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["Fastify", "fastifyCookie", "fastifyFormbody", "fastifyWebsocket", "dns", "crypto"]
7
7
  }
package/io-package.json CHANGED
@@ -1,8 +1,21 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "hassemu",
4
- "version": "1.35.2",
4
+ "version": "1.35.3",
5
5
  "news": {
6
+ "1.35.3": {
7
+ "en": "Fixed Home Assistant discovery pointing the display at the wrong address on multi-interface hosts; it now uses the address the adapter actually listens on.",
8
+ "de": "Behoben: Home-Assistant-Discovery verwies das Display auf Multi-Interface-Hosts auf die falsche Adresse; jetzt wird die Adresse genutzt, auf der der Adapter wirklich lauscht.",
9
+ "ru": "Исправлено: обнаружение Home Assistant на хостах с несколькими интерфейсами направляло дисплей на неверный адрес; теперь используется адрес, который адаптер действительно слушает.",
10
+ "pt": "Corrigido: a deteção do Home Assistant apontava o ecrã para o endereço errado em hosts com várias interfaces; agora usa o endereço em que o adaptador realmente escuta.",
11
+ "nl": "Opgelost: Home Assistant-detectie wees het display op multi-interface hosts naar het verkeerde adres; nu wordt het adres gebruikt waarop de adapter echt luistert.",
12
+ "fr": "Corrigé : la découverte Home Assistant dirigeait l'écran vers la mauvaise adresse sur les hôtes multi-interfaces ; elle utilise maintenant l'adresse réelle d'écoute de l'adaptateur.",
13
+ "it": "Corretto: il rilevamento di Home Assistant indirizzava il display all'indirizzo sbagliato su host multi-interfaccia; ora usa l'indirizzo su cui l'adattatore è davvero in ascolto.",
14
+ "es": "Corregido: la detección de Home Assistant dirigía la pantalla a la dirección equivocada en hosts con varias interfaces; ahora usa la dirección en la que el adaptador realmente escucha.",
15
+ "pl": "Naprawiono: wykrywanie Home Assistant kierowało wyświetlacz na zły adres na hostach z wieloma interfejsami; teraz używa adresu, na którym adapter faktycznie nasłuchuje.",
16
+ "uk": "Виправлено: виявлення Home Assistant спрямовувало дисплей на неправильну адресу на хостах із кількома інтерфейсами; тепер використовується адреса, яку адаптер дійсно слухає.",
17
+ "zh-cn": "修复了在多网络接口主机上 Home Assistant 发现将显示设备指向错误地址的问题;现在使用适配器实际监听的地址。"
18
+ },
6
19
  "1.35.2": {
7
20
  "en": "Displays with a stale registration now re-register automatically after an adapter restart; removing a display also clears its leftover app registration",
8
21
  "de": "Displays mit veralteter Registrierung registrieren sich nach einem Adapter-Neustart jetzt automatisch neu; beim Entfernen eines Displays wird auch seine App-Registrierung aufgeräumt",
@@ -80,19 +93,6 @@
80
93
  "pl": "Poprawiono nieprawidłowe przypisanie roli selektora trybu globalnego.",
81
94
  "uk": "Виправлено неправильне призначення ролі глобального селектора режиму.",
82
95
  "zh-cn": "修复全局模式选择器上不正确的角色分配。"
83
- },
84
- "1.33.0": {
85
- "en": "User-modified state names are no longer overwritten on adapter restart.",
86
- "de": "Vom Benutzer geänderte Datenpunktnamen werden beim Neustart nicht mehr überschrieben.",
87
- "ru": "Пользовательские имена состояний больше не перезаписываются при перезапуске адаптера.",
88
- "pt": "Nomes de estados modificados pelo utilizador já não são substituídos ao reiniciar o adaptador.",
89
- "nl": "Door gebruiker gewijzigde statusnamen worden niet meer overschreven bij herstart van de adapter.",
90
- "fr": "Les noms d états modifiés par l utilisateur ne sont plus écrasés au redémarrage de l adaptateur.",
91
- "it": "I nomi degli stati modificati dall utente non vengono più sovrascritti al riavvio dell adattatore.",
92
- "es": "Los nombres de estados modificados por el usuario ya no se sobrescriben al reiniciar el adaptador.",
93
- "pl": "Nazwy stanów zmienione przez użytkownika nie są już nadpisywane przy restarcie adaptera.",
94
- "uk": "Змінені користувачем назви станів більше не перезаписуються при перезапуску адаптера.",
95
- "zh-cn": "用户修改的状态名称在适配器重启时不再被覆盖。"
96
96
  }
97
97
  },
98
98
  "plugins": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.hassemu",
3
- "version": "1.35.2",
3
+ "version": "1.35.3",
4
4
  "description": "Emulates a minimal Home Assistant server so devices expecting a Home Assistant dashboard can display any custom web URL.",
5
5
  "author": {
6
6
  "name": "krobi",