iobroker.goodwe-sems 0.1.15 → 0.1.16

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.de.md CHANGED
@@ -151,6 +151,14 @@ Pull Requests willkommen, insbesondere um zusätzliche, vom Portal gelieferte Fe
151
151
  -->
152
152
  ### **WORK IN PROGRESS**
153
153
 
154
+ ### 0.1.16 (2026-07-19)
155
+
156
+ - (Stefan Bühler) Großer Fund: Manche Konten, deren SEMS+-Login abgelehnt wird und die auf die Legacy-CrossLogin-API zurückfallen, landen gar nicht auf dem klassischen `semsportal.com`-artigen Backend - sie bekommen eine Session für eine komplett andere, moderne Microservice-API ("SEMS+-Gateway", `eu-gateway.semsportal.com`). Das erklärt, warum `GetMonitorDetailByPowerstationId` unter keinem der in 0.1.14/0.1.15 versuchten Pfade (`v1`/`v2`/`v3`) je funktionieren konnte. Bestätigt durch einen echten Browser-HAR-Mitschnitt (`eu-semsplus.goodwe.com`), der die tatsächlich genutzten Endpunkte zeigt (`sems-plant/api/stations/...`, `sems-plant/api/equipments/<sn>/telemetry` usw.)
157
+ - (Stefan Bühler) Die Gateway-API verlangt zusätzlich einen berechneten `x-signature`-Header bei jedem Request, sonst wird er stillschweigend abgelehnt. Das Signatur-Schema (`base64(sha256(`${ts}@${uid}@${token}`) + "@" + ts)`) wurde empirisch aus ~230 echten Request/Response-Paaren rekonstruiert - 100 % Treffer, keine Ausnahmen
158
+ - (Stefan Bühler) `getMonitorDetail()` fällt jetzt automatisch auf diese Gateway-API zurück (Stations-Basisdaten, Geräteliste, Telemetrie/Telecounting pro Gerät), wenn alle drei klassischen Pfade 404 liefern, und wandelt das Ergebnis in dieselbe `info`/`kpi`/`inverter[]`-Struktur um, die der Rest des Adapters bereits erwartet - keine Änderungen in der Mapping-/State-Erzeugungs-Schicht nötig
159
+ - (Stefan Bühler) Bewusst konservative erste Version: Nur Felder mit gesichertem Einheiten-/Format-Nachweis werden befüllt (aktuelle Leistung, Tages-/Gesamtertrag, Wechselrichter-Werte für AC/PV/Temperatur); der stationsweite Leistungsfluss (PV/Verbrauch/Netz/Batterie) wird noch nicht befüllt, da alle bisherigen Mitschnitte nachts erfolgten und dafür ein leeres Objekt lieferten
160
+ - (Stefan Bühler) 2 neue Regressionstests (47 Unit-Tests insgesamt), darunter einer, der die tatsächliche Signaturberechnung gegen die echte, reverse-engineerte Formel verifiziert
161
+
154
162
  ### 0.1.15 (2026-07-19)
155
163
 
156
164
  - (Stefan Bühler) Fix: Der v3→v2-Fallback aus 0.1.14 für `GetMonitorDetailByPowerstationId` reichte nicht aus - bei einem echten Konto lieferte das Legacy-Login-Backend `404 Route Not Found` für **beide** Pfade, `v2` und `v3`. Community-Referenzen widersprechen sich, welche Version korrekt ist (pygoodwe verwendet fest `v2`, ein separater Artikel von 2023 nutzt `v1`, unsere eigene Traffic-Analyse beobachtete `v3`) - `getMonitorDetail()` probiert jetzt alle drei Versionen der Reihe nach durch (`v3` → `v2` → `v1`) und nutzt die erste, die keinen 404 liefert
package/README.md CHANGED
@@ -151,6 +151,14 @@ Pull requests are welcome, especially to add further fields delivered by the por
151
151
  -->
152
152
  ### **WORK IN PROGRESS**
153
153
 
154
+ ### 0.1.16 (2026-07-19)
155
+
156
+ - (Stefan Bühler) major finding: some accounts whose SEMS+ login is rejected and fall back to the legacy CrossLogin API do not end up on the classic `semsportal.com`-style backend at all - they get a session for a completely different, modern microservice API ("SEMS+ gateway", `eu-gateway.semsportal.com`), which explains why `GetMonitorDetailByPowerstationId` could never succeed under any of the `v1`/`v2`/`v3` paths tried in 0.1.14/0.1.15. Confirmed via a real account's browser HAR capture (`eu-semsplus.goodwe.com`) showing the actual endpoints in use (`sems-plant/api/stations/...`, `sems-plant/api/equipments/<sn>/telemetry`, etc.)
157
+ - (Stefan Bühler) the gateway API additionally requires every request to carry a computed `x-signature` header or it is silently rejected. The signature scheme (`base64(sha256(`${ts}@${uid}@${token}`) + "@" + ts)`) was reverse-engineered empirically from ~230 real request/response pairs captured from the web app - 100% match, no exceptions
158
+ - (Stefan Bühler) `getMonitorDetail()` now automatically falls back to this gateway API (station basic info, device list, per-device telemetry/telecounting) when all three classic paths 404, and reshapes the result into the same `info`/`kpi`/`inverter[]` shape the rest of the adapter already expects - no changes needed in the mapping/state-creation layer
159
+ - (Stefan Bühler) deliberately conservative first version: only fields with a confirmed unit/shape are populated (current power, today's/total generation, per-inverter AC/PV/temperature values); the station-level power-flow split (PV/load/grid/battery) is not populated yet, since every real-account capture so far happened at night and returned an empty object for it
160
+ - (Stefan Bühler) 2 new regression tests (47 unit tests in total), including one that verifies the actual signature computation against the real, reverse-engineered formula
161
+
154
162
  ### 0.1.15 (2026-07-19)
155
163
 
156
164
  - (Stefan Bühler) fix: 0.1.14's v3→v2 fallback for `GetMonitorDetailByPowerstationId` was insufficient - a real-world account's legacy-login backend returned `404 Route Not Found` for **both** the `v2` and `v3` paths. Community references disagree on which version is correct (pygoodwe hardcodes `v2`, a separate 2023 write-up uses `v1`, our own traffic inspection observed `v3`), so `getMonitorDetail()` now tries all three versions in sequence (`v3` → `v2` → `v1`) and uses whichever one doesn't 404
package/io-package.json CHANGED
@@ -1,8 +1,21 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "goodwe-sems",
4
- "version": "0.1.15",
4
+ "version": "0.1.16",
5
5
  "news": {
6
+ "0.1.16": {
7
+ "de": "Großer Fund: Konten, deren SEMS+-Login abgelehnt wird und die auf die Legacy-CrossLogin-API zurückfallen, landen teils auf einem komplett anderen, modernen SEMS+-Gateway-Backend (eu-gateway.semsportal.com) statt dem klassischen semsportal.com - das erklärt, warum v1/v2/v3 GetMonitorDetailByPowerstationId dort NIE funktionieren konnten. Der Adapter spricht jetzt zusätzlich dieses Gateway-Backend (eigene Endpunkte, eigenes Antwortformat, inkl. der zur Authentifizierung nötigen Request-Signatur) und nutzt es automatisch als letzten Fallback, wenn alle klassischen Pfade fehlschlagen.",
8
+ "en": "Major finding: accounts whose SEMS+ login is rejected and fall back to the legacy CrossLogin API sometimes end up on a completely different, modern SEMS+ gateway backend (eu-gateway.semsportal.com) instead of the classic semsportal.com one - explaining why v1/v2/v3 GetMonitorDetailByPowerstationId could never work there. The adapter now also speaks this gateway backend (its own endpoints, response format, and required request signature) and automatically uses it as a last-resort fallback when all classic paths fail.",
9
+ "ru": "Major finding: accounts whose SEMS+ login is rejected and fall back to the legacy CrossLogin API sometimes end up on a completely different, modern SEMS+ gateway backend (eu-gateway.semsportal.com) instead of the classic semsportal.com one - explaining why v1/v2/v3 GetMonitorDetailByPowerstationId could never work there. The adapter now also speaks this gateway backend (its own endpoints, response format, and required request signature) and automatically uses it as a last-resort fallback when all classic paths fail.",
10
+ "pt": "Major finding: accounts whose SEMS+ login is rejected and fall back to the legacy CrossLogin API sometimes end up on a completely different, modern SEMS+ gateway backend (eu-gateway.semsportal.com) instead of the classic semsportal.com one - explaining why v1/v2/v3 GetMonitorDetailByPowerstationId could never work there. The adapter now also speaks this gateway backend (its own endpoints, response format, and required request signature) and automatically uses it as a last-resort fallback when all classic paths fail.",
11
+ "nl": "Major finding: accounts whose SEMS+ login is rejected and fall back to the legacy CrossLogin API sometimes end up on a completely different, modern SEMS+ gateway backend (eu-gateway.semsportal.com) instead of the classic semsportal.com one - explaining why v1/v2/v3 GetMonitorDetailByPowerstationId could never work there. The adapter now also speaks this gateway backend (its own endpoints, response format, and required request signature) and automatically uses it as a last-resort fallback when all classic paths fail.",
12
+ "fr": "Major finding: accounts whose SEMS+ login is rejected and fall back to the legacy CrossLogin API sometimes end up on a completely different, modern SEMS+ gateway backend (eu-gateway.semsportal.com) instead of the classic semsportal.com one - explaining why v1/v2/v3 GetMonitorDetailByPowerstationId could never work there. The adapter now also speaks this gateway backend (its own endpoints, response format, and required request signature) and automatically uses it as a last-resort fallback when all classic paths fail.",
13
+ "it": "Major finding: accounts whose SEMS+ login is rejected and fall back to the legacy CrossLogin API sometimes end up on a completely different, modern SEMS+ gateway backend (eu-gateway.semsportal.com) instead of the classic semsportal.com one - explaining why v1/v2/v3 GetMonitorDetailByPowerstationId could never work there. The adapter now also speaks this gateway backend (its own endpoints, response format, and required request signature) and automatically uses it as a last-resort fallback when all classic paths fail.",
14
+ "es": "Major finding: accounts whose SEMS+ login is rejected and fall back to the legacy CrossLogin API sometimes end up on a completely different, modern SEMS+ gateway backend (eu-gateway.semsportal.com) instead of the classic semsportal.com one - explaining why v1/v2/v3 GetMonitorDetailByPowerstationId could never work there. The adapter now also speaks this gateway backend (its own endpoints, response format, and required request signature) and automatically uses it as a last-resort fallback when all classic paths fail.",
15
+ "pl": "Major finding: accounts whose SEMS+ login is rejected and fall back to the legacy CrossLogin API sometimes end up on a completely different, modern SEMS+ gateway backend (eu-gateway.semsportal.com) instead of the classic semsportal.com one - explaining why v1/v2/v3 GetMonitorDetailByPowerstationId could never work there. The adapter now also speaks this gateway backend (its own endpoints, response format, and required request signature) and automatically uses it as a last-resort fallback when all classic paths fail.",
16
+ "zh-cn": "Major finding: accounts whose SEMS+ login is rejected and fall back to the legacy CrossLogin API sometimes end up on a completely different, modern SEMS+ gateway backend (eu-gateway.semsportal.com) instead of the classic semsportal.com one - explaining why v1/v2/v3 GetMonitorDetailByPowerstationId could never work there. The adapter now also speaks this gateway backend (its own endpoints, response format, and required request signature) and automatically uses it as a last-resort fallback when all classic paths fail.",
17
+ "uk": "Major finding: accounts whose SEMS+ login is rejected and fall back to the legacy CrossLogin API sometimes end up on a completely different, modern SEMS+ gateway backend (eu-gateway.semsportal.com) instead of the classic semsportal.com one - explaining why v1/v2/v3 GetMonitorDetailByPowerstationId could never work there. The adapter now also speaks this gateway backend (its own endpoints, response format, and required request signature) and automatically uses it as a last-resort fallback when all classic paths fail."
18
+ },
6
19
  "0.1.15": {
7
20
  "de": "Weiterer Fix für die GoodWe-API-Versionierung: getMonitorDetail() probiert jetzt v3, v2 UND v1 der Reihe nach durch (statt nur v3->v2), da GoodWe je nach Konto/Backend unterschiedliche API-Versionen für denselben Endpunkt verwendet. Zusätzlich vollständige Request-URL im Debug-Log für einfachere Diagnose.",
8
21
  "en": "Further fix for GoodWe API versioning: getMonitorDetail() now tries v3, v2 AND v1 in sequence (not just v3->v2), since GoodWe uses different API versions for the same endpoint depending on account/backend. Also logs the full request URL in debug mode for easier diagnosis.",
@@ -80,19 +93,6 @@
80
93
  "pl": "rozwiązano bardziej rygorystyczne kontrole PR ioBroker.repositories: encryptedNative/protectedNative przeniesiono do katalogu głównego io-package.json, zaktualizowano zależności, przepisano CI, README przetłumaczono na angielski (wymagane)",
81
94
  "uk": "усунено суворіші перевірки PR ioBroker.repositories: encryptedNative/protectedNative перенесено в корінь io-package.json, оновлено залежності, переписано CI, README перекладено англійською (обов'язково)",
82
95
  "zh-cn": "解决更严格的 ioBroker.repositories PR 检查:将 encryptedNative/protectedNative 移至 io-package.json 根目录,更新依赖项,重写 CI,将 README 翻译为英文(必需)"
83
- },
84
- "0.1.8": {
85
- "en": "address ioBroker adapter-check findings: remove news entries for versions that were never actually published to npm (0.1.1, 0.1.2), remove deprecated common.title/common.main fields, update @iobroker/adapter-core to ^3.2.2",
86
- "de": "ioBroker-Adapter-Check-Befunde behoben: News-Einträge für nie tatsächlich auf npm veröffentlichte Versionen entfernt (0.1.1, 0.1.2), veraltete Felder common.title/common.main entfernt, @iobroker/adapter-core auf ^3.2.2 aktualisiert",
87
- "ru": "устранены замечания adapter-check: удалены записи news для версий, никогда не публиковавшихся в npm (0.1.1, 0.1.2), удалены устаревшие поля common.title/common.main, обновлён @iobroker/adapter-core до ^3.2.2",
88
- "pt": "resolvidas as constatações do adapter-check: removidas entradas de news de versões nunca publicadas no npm (0.1.1, 0.1.2), removidos campos obsoletos common.title/common.main, @iobroker/adapter-core atualizado para ^3.2.2",
89
- "nl": "adapter-check bevindingen opgelost: news-items voor nooit op npm gepubliceerde versies verwijderd (0.1.1, 0.1.2), verouderde velden common.title/common.main verwijderd, @iobroker/adapter-core bijgewerkt naar ^3.2.2",
90
- "fr": "corrections des remarques adapter-check : suppression des entrées news pour des versions jamais publiées sur npm (0.1.1, 0.1.2), suppression des champs obsolètes common.title/common.main, mise à jour de @iobroker/adapter-core vers ^3.2.2",
91
- "it": "risolti i rilievi di adapter-check: rimosse le voci news per versioni mai pubblicate su npm (0.1.1, 0.1.2), rimossi i campi deprecati common.title/common.main, aggiornato @iobroker/adapter-core a ^3.2.2",
92
- "es": "se resolvieron los hallazgos de adapter-check: se eliminaron entradas de news de versiones nunca publicadas en npm (0.1.1, 0.1.2), se eliminaron los campos obsoletos common.title/common.main, se actualizó @iobroker/adapter-core a ^3.2.2",
93
- "pl": "rozwiązano uwagi adapter-check: usunięto wpisy news dla wersji nigdy niepublikowanych na npm (0.1.1, 0.1.2), usunięto przestarzałe pola common.title/common.main, zaktualizowano @iobroker/adapter-core do ^3.2.2",
94
- "uk": "усунено зауваження adapter-check: видалено записи news для версій, які ніколи не публікувалися в npm (0.1.1, 0.1.2), видалено застарілі поля common.title/common.main, оновлено @iobroker/adapter-core до ^3.2.2",
95
- "zh-cn": "解决 adapter-check 发现的问题:移除从未真正发布到 npm 的版本的 news 条目(0.1.1、0.1.2),移除已弃用的 common.title/common.main 字段,将 @iobroker/adapter-core 更新至 ^3.2.2"
96
96
  }
97
97
  },
98
98
  "titleLang": {
package/lib/semsApi.js CHANGED
@@ -55,6 +55,24 @@ const MONITOR_DETAIL_PATHS = [
55
55
  "/v1/PowerStation/GetMonitorDetailByPowerstationId",
56
56
  ];
57
57
 
58
+ // --- SEMS+ "gateway" API (eu-gateway.semsportal.com) -----------------------
59
+ // Some accounts' session ("api" field from CrossLogin) resolves to a modern
60
+ // microservice gateway host instead of the classic semsportal.com backend -
61
+ // confirmed against a real account where ALL THREE classic paths above
62
+ // 404'd. That gateway speaks a completely different, undocumented API:
63
+ // different paths (e.g. "/sems-plant/api/stations/flow"), a different
64
+ // response envelope (top-level "code":"00000" instead of 0, "description"
65
+ // instead of "msg"), AND every request additionally requires a computed
66
+ // "x-signature" header or the gateway silently rejects it. The signature
67
+ // scheme was reverse-engineered from ~230 real request/response pairs
68
+ // captured from the eu-semsplus.goodwe.com web app (100% match, no
69
+ // exceptions found):
70
+ // x-signature = base64( sha256(`${ts}@${uid}@${token}`).hexdigest() + "@" + ts )
71
+ // where `ts` is the request's own signing time in epoch milliseconds
72
+ // (Date.now() at call time - independent of the token's login timestamp)
73
+ // and uid/token are the same values sent in the "token" header.
74
+ const GATEWAY_CLIENT = "semsPlusWeb";
75
+
58
76
  // Observed rate-limit response code. GoodWe does not document a retry-after
59
77
  // value, community projects settled on a 5 minute cool-down.
60
78
  const RATE_LIMIT_CODE = "GY0429";
@@ -114,6 +132,49 @@ async function httpPostJson(url, init, timerFns) {
114
132
  }
115
133
  }
116
134
 
135
+ /**
136
+ * Converts the gateway API's metadata-driven "factors" arrays (used by both
137
+ * the telemetry and telecounting endpoints) into a flat {code: rawValue}
138
+ * lookup. Each response is an array of named groups (e.g. "ac_parameters",
139
+ * "telecounting_today"), each holding a "factors" array of
140
+ * {code, data, dataType, unit, alias, ...} entries - we only need the inner
141
+ * code/data pairs. A field simply being absent from "data" (observed e.g.
142
+ * at night, when a value is omitted entirely instead of being sent as 0) is
143
+ * preserved as `undefined` rather than guessed at.
144
+ *
145
+ * @param {Array<{code?:string, factors?: Array<{code:string, data?: *}>}>} groups
146
+ * @returns {Record<string, *>}
147
+ */
148
+ function flattenGatewayFactors(groups) {
149
+ const flat = {};
150
+ if (!Array.isArray(groups)) {
151
+ return flat;
152
+ }
153
+ for (const group of groups) {
154
+ const factors = Array.isArray(group && group.factors) ? group.factors : [group];
155
+ for (const factor of factors) {
156
+ if (factor && factor.code !== undefined) {
157
+ flat[factor.code] = factor.data;
158
+ }
159
+ }
160
+ }
161
+ return flat;
162
+ }
163
+
164
+ /**
165
+ * Minimal numeric coercion for gateway values we need to do arithmetic on
166
+ * (kW->W scaling, summing across inverters).
167
+ *
168
+ * @param {*} value
169
+ */
170
+ function gatewayNum(value) {
171
+ if (value === undefined || value === null || value === "") {
172
+ return undefined;
173
+ }
174
+ const n = Number(value);
175
+ return Number.isFinite(n) ? n : undefined;
176
+ }
177
+
117
178
  class SemsApi {
118
179
  /**
119
180
  * @param {object} opts
@@ -413,10 +474,245 @@ class SemsApi {
413
474
  "debug",
414
475
  `${path} nicht gefunden (404) - versuche ${nextPath} als Fallback (API-Version unterschiedlich je nach Konto/Backend).`,
415
476
  );
477
+ } else {
478
+ this.log(
479
+ "debug",
480
+ `${path} nicht gefunden (404) - alle klassischen PowerStation-Pfade fehlgeschlagen, versuche die SEMS+-Gateway-API als letzten Fallback.`,
481
+ );
482
+ }
483
+ }
484
+ }
485
+ try {
486
+ return await this._getMonitorDetailGateway(powerStationId);
487
+ } catch (gatewayError) {
488
+ this.log(
489
+ "debug",
490
+ `SEMS+-Gateway-Fallback ebenfalls fehlgeschlagen (${gatewayError.message}) - melde den ursprünglichen Fehler.`,
491
+ );
492
+ throw lastError;
493
+ }
494
+ }
495
+
496
+ /**
497
+ * SHA-256-based request signature required by the SEMS+ gateway API - see the GATEWAY_CLIENT comment above.
498
+ *
499
+ * @param {string} uid
500
+ * @param {string} token
501
+ */
502
+ _gatewaySignature(uid, token) {
503
+ const ts = Date.now();
504
+ const hash = crypto.createHash("sha256").update(`${ts}@${uid}@${token}`, "utf8").digest("hex");
505
+ return Buffer.from(`${hash}@${ts}`, "utf8").toString("base64");
506
+ }
507
+
508
+ /** Derives the gateway host from the current session's (already validated) API base. */
509
+ _gatewayBase() {
510
+ if (!this.session) {
511
+ throw new SemsAuthError("Keine aktive SEMS-Session - login() wurde nicht (erfolgreich) aufgerufen.");
512
+ }
513
+ let host;
514
+ try {
515
+ host = new URL(this.session.api).hostname;
516
+ } catch {
517
+ host = "eu-gateway.semsportal.com";
518
+ }
519
+ return `https://${host}/web/sems`;
520
+ }
521
+
522
+ _gatewayHeaders(gatewayBase) {
523
+ const tokenPayload = {
524
+ uid: this.session.uid,
525
+ timestamp: String(this.session.timestamp),
526
+ token: this.session.token,
527
+ client: GATEWAY_CLIENT,
528
+ version: "",
529
+ language: "en",
530
+ api: gatewayBase,
531
+ region: "eu",
532
+ };
533
+ return {
534
+ "Content-Type": "application/json",
535
+ Accept: "application/json, text/plain, */*",
536
+ "User-Agent": DEFAULT_USER_AGENT,
537
+ token: JSON.stringify(tokenPayload),
538
+ "x-signature": this._gatewaySignature(this.session.uid, this.session.token),
539
+ };
540
+ }
541
+
542
+ /**
543
+ * Authenticated request against the SEMS+ gateway API (a different
544
+ * backend from the classic PowerStation API - see the GATEWAY_CLIENT
545
+ * comment above). No automatic re-login-and-retry here (unlike
546
+ * _authenticatedPost): if the gateway session is stale, getMonitorDetail()
547
+ * will simply fall through to its own caller's retry/re-login handling
548
+ * on the next poll cycle.
549
+ *
550
+ * @param {"GET"|"POST"} method
551
+ * @param {string} path e.g. "/sems-plant/api/stations/flow"
552
+ * @param {object} [query] query-string params
553
+ */
554
+ async _gatewayRequest(method, path, query) {
555
+ if (!this.session) {
556
+ await this.login();
557
+ }
558
+ const base = this._gatewayBase();
559
+ const qs = query && Object.keys(query).length ? new URLSearchParams(query).toString() : "";
560
+ const url = base + path + (qs ? `?${qs}` : "");
561
+
562
+ const { json } = await httpPostJson(
563
+ url,
564
+ {
565
+ method,
566
+ headers: this._gatewayHeaders(base),
567
+ timeoutMs: this.requestTimeoutMs,
568
+ },
569
+ this._timerFns,
570
+ );
571
+
572
+ this.log("debug", `SEMS-Gateway-Antwort ${url}: ${JSON.stringify(json)}`);
573
+
574
+ const code = json && json.code;
575
+ if (!SUCCESS_CODES.has(code)) {
576
+ const msg = (json && (json.description || json.msg || json.message)) || "unbekannter Fehler";
577
+ throw new SemsProtocolError(`SEMS-Gateway-Aufruf ${path} fehlgeschlagen: ${msg} (code=${code})`);
578
+ }
579
+ return json.data;
580
+ }
581
+
582
+ /**
583
+ * Gateway equivalent of the classic GetMonitorDetailByPowerstationId call:
584
+ * fetches station basic info, the device list and per-device
585
+ * telemetry/telecounting from the SEMS+ gateway API, and reshapes the
586
+ * result into the SAME "detail" object shape the classic API returns
587
+ * (info/kpi/inverter[]/...), so lib/mapping.js's mapMonitorDetail() does
588
+ * not need to know (or care) which backend the data actually came from.
589
+ *
590
+ * Deliberately conservative for its first version: only fields with a
591
+ * confirmed unit/shape (from the gateway's own response metadata or
592
+ * direct observation) are populated. In particular, the station-level
593
+ * "stations/flow" endpoint (PV/load/grid/battery power split) is NOT
594
+ * used yet - every real-account capture so far happened at night, where
595
+ * it returns an empty object, so neither its field names nor its unit
596
+ * could be confirmed. kpi.pac/power/total_power are instead computed by
597
+ * summing the per-inverter telemetry/telecounting values, which DO carry
598
+ * confirmed units ("kW"/"kWh") in the API's own response metadata.
599
+ *
600
+ * @param {string} powerStationId
601
+ */
602
+ async _getMonitorDetailGateway(powerStationId) {
603
+ const basicInfo =
604
+ (await this._gatewayRequest("POST", "/sems-plant/api/portal/stations/basic/info", {
605
+ stationId: powerStationId,
606
+ })) || {};
607
+
608
+ let deviceList = [];
609
+ try {
610
+ const allStatus = await this._gatewayRequest("GET", "/sems-plant/api/stations/device/all-status", {
611
+ stationId: powerStationId,
612
+ });
613
+ const detailLists = (allStatus && allStatus.deviceDetailList) || [];
614
+ for (const typeGroup of detailLists) {
615
+ const statusDetails = Array.isArray(typeGroup && typeGroup.statusDetailList)
616
+ ? typeGroup.statusDetailList
617
+ : [];
618
+ for (const statusDetail of statusDetails) {
619
+ const detailMap = (statusDetail && statusDetail.detailMap) || {};
620
+ for (const sn of Object.keys(detailMap)) {
621
+ deviceList.push({
622
+ sn,
623
+ deviceType: (typeGroup && typeGroup.deviceType) || "INVERTER",
624
+ ...detailMap[sn],
625
+ });
626
+ }
416
627
  }
417
628
  }
629
+ } catch (error) {
630
+ this.log("debug", `SEMS-Gateway: Geräteliste nicht verfügbar (${error.message}).`);
631
+ }
632
+
633
+ const inverters = [];
634
+ for (const device of deviceList) {
635
+ const sn = device && device.sn;
636
+ if (!sn) {
637
+ continue;
638
+ }
639
+ const deviceType = device.deviceType || "INVERTER";
640
+ let telemetryFlat = {};
641
+ let telecountingFlat = {};
642
+ try {
643
+ const telemetry = await this._gatewayRequest(
644
+ "GET",
645
+ `/sems-plant/api/equipments/${encodeURIComponent(sn)}/telemetry`,
646
+ { deviceType, pwId: powerStationId },
647
+ );
648
+ telemetryFlat = flattenGatewayFactors(telemetry);
649
+ } catch (error) {
650
+ this.log("debug", `SEMS-Gateway: telemetry für ${sn} fehlgeschlagen (${error.message}).`);
651
+ }
652
+ try {
653
+ const telecounting = await this._gatewayRequest(
654
+ "GET",
655
+ `/sems-plant/api/equipments/${encodeURIComponent(sn)}/telecounting`,
656
+ { deviceType, pwId: powerStationId },
657
+ );
658
+ telecountingFlat = flattenGatewayFactors(telecounting);
659
+ } catch (error) {
660
+ this.log("debug", `SEMS-Gateway: telecounting für ${sn} fehlgeschlagen (${error.message}).`);
661
+ }
662
+
663
+ const pacKw = gatewayNum(telemetryFlat.pAc);
664
+ inverters.push({
665
+ sn,
666
+ name: device.name,
667
+ status: device.status,
668
+ pac: pacKw === undefined ? undefined : pacKw * 1000,
669
+ eday: gatewayNum(telecountingFlat.proPvStatsToday),
670
+ etotal: gatewayNum(telecountingFlat.proPvStatsTotal),
671
+ temperature: telemetryFlat.Temperature,
672
+ invert_full: {
673
+ vpv1: telemetryFlat["MPPT-1:Vpv"],
674
+ ipv1: telemetryFlat["MPPT-1:Ipv"],
675
+ vpv2: telemetryFlat["MPPT-2:Vpv"],
676
+ ipv2: telemetryFlat["MPPT-2:Ipv"],
677
+ vac1: telemetryFlat["PHASE-A:Vac"],
678
+ iac1: telemetryFlat["PHASE-A:Iac"],
679
+ fac1: telemetryFlat.Fac,
680
+ vac2: telemetryFlat["PHASE-B:Vac"],
681
+ iac2: telemetryFlat["PHASE-B:Iac"],
682
+ fac2: telemetryFlat.Fac,
683
+ vac3: telemetryFlat["PHASE-C:Vac"],
684
+ iac3: telemetryFlat["PHASE-C:Iac"],
685
+ fac3: telemetryFlat.Fac,
686
+ },
687
+ });
418
688
  }
419
- throw lastError;
689
+
690
+ const totalPac = inverters.length
691
+ ? inverters.reduce((sum, inv) => sum + (Number.isFinite(inv.pac) ? inv.pac : 0), 0)
692
+ : undefined;
693
+ const totalToday = inverters.length
694
+ ? inverters.reduce((sum, inv) => sum + (Number.isFinite(inv.eday) ? inv.eday : 0), 0)
695
+ : undefined;
696
+ const totalLifetime = inverters.length
697
+ ? inverters.reduce((sum, inv) => sum + (Number.isFinite(inv.etotal) ? inv.etotal : 0), 0)
698
+ : undefined;
699
+
700
+ return {
701
+ info: {
702
+ stationname: basicInfo.name,
703
+ capacity: basicInfo.pvCapacity !== undefined ? basicInfo.pvCapacity : basicInfo.installedPower,
704
+ address: basicInfo.googleAddress || basicInfo.address,
705
+ latitude: basicInfo.latitude,
706
+ longitude: basicInfo.longitude,
707
+ status: basicInfo.status,
708
+ },
709
+ kpi: {
710
+ pac: totalPac,
711
+ power: totalToday,
712
+ total_power: totalLifetime,
713
+ },
714
+ inverter: inverters,
715
+ };
420
716
  }
421
717
  }
422
718
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.goodwe-sems",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "description": "ioBroker adapter to read GoodWe inverter data from the SEMS Portal cloud API (for installations without local/LAN access to the inverter).",
5
5
  "author": {
6
6
  "name": "bueste",