iobroker.goodwe-sems 0.1.14 → 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 +14 -0
- package/README.md +14 -0
- package/io-package.json +291 -291
- package/lib/semsApi.js +333 -24
- package/package.json +1 -1
package/README.de.md
CHANGED
|
@@ -151,6 +151,20 @@ 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
|
+
|
|
162
|
+
### 0.1.15 (2026-07-19)
|
|
163
|
+
|
|
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
|
|
165
|
+
- (Stefan Bühler) Diagnose: Debug-Logs enthalten jetzt die vollständige Request-URL (inkl. aufgelöster API-Basis) statt nur des relativen Pfads, und der Login-Erfolgs-Log gibt jetzt ebenfalls die aufgelöste API-Basis aus - so lässt sich genau erkennen, welche Host+Pfad-Kombination fehlschlägt
|
|
166
|
+
- (Stefan Bühler) 2 aktualisierte/neue Regressionstests (45 Unit-Tests insgesamt) für den dreistufigen Versions-Fallback und den Fall, dass alle drei Pfade fehlschlagen
|
|
167
|
+
|
|
154
168
|
### 0.1.14 (2026-07-19)
|
|
155
169
|
|
|
156
170
|
- (Stefan Bühler) Fix: `GetMonitorDetailByPowerstationId` lieferte `404 Route Not Found` für Konten, deren SEMS+-Login abgelehnt wird (beobachtet: `code=C0602`) und die auf die Legacy-CrossLogin-API zurückfallen - dieses Backend stellt den Endpunkt unter dem `v2`-API-Pfad bereit, nicht `v3`. Root Cause gefunden anhand des Debug-Logs eines echten Kontos sowie der Referenzimplementierung [pygoodwe](https://github.com/yaleman/pygoodwe), deren rein-legacy-Client den `v2`-Pfad fest verdrahtet. `getMonitorDetail()` versucht jetzt zuerst `v3` und wiederholt bei erkanntem 404 automatisch einmal mit `v2` - beide Backend-Varianten funktionieren damit ohne jede Konfigurationsänderung durch den Nutzer
|
package/README.md
CHANGED
|
@@ -151,6 +151,20 @@ 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
|
+
|
|
162
|
+
### 0.1.15 (2026-07-19)
|
|
163
|
+
|
|
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
|
|
165
|
+
- (Stefan Bühler) diagnostics: debug logs now include the full request URL (including the resolved API base) instead of just the relative path, and the login success log now also prints the resolved API base, making it possible to see exactly which host+path combination is failing
|
|
166
|
+
- (Stefan Bühler) 2 updated/new regression tests (45 unit tests in total) covering the three-way version fallback and the case where all three paths fail
|
|
167
|
+
|
|
154
168
|
### 0.1.14 (2026-07-19)
|
|
155
169
|
|
|
156
170
|
- (Stefan Bühler) fix: `GetMonitorDetailByPowerstationId` returned `404 Route Not Found` for accounts whose SEMS+ login is rejected (observed: `code=C0602`) and that fall back to the legacy CrossLogin API - that backend serves the endpoint under the `v2` API path, not `v3`. Root cause found via a real account's debug log plus the community reference implementation [pygoodwe](https://github.com/yaleman/pygoodwe), whose legacy-only client hardcodes the `v2` path. `getMonitorDetail()` now tries `v3` first and automatically retries once with `v2` on a detected 404, so both backend variants work without any user-facing configuration change
|
package/io-package.json
CHANGED
|
@@ -1,298 +1,298 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
2
|
+
"common": {
|
|
3
|
+
"name": "goodwe-sems",
|
|
4
|
+
"version": "0.1.16",
|
|
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
|
+
},
|
|
19
|
+
"0.1.15": {
|
|
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.",
|
|
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.",
|
|
22
|
+
"ru": "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.",
|
|
23
|
+
"pt": "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.",
|
|
24
|
+
"nl": "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.",
|
|
25
|
+
"fr": "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.",
|
|
26
|
+
"it": "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.",
|
|
27
|
+
"es": "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.",
|
|
28
|
+
"pl": "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.",
|
|
29
|
+
"zh-cn": "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.",
|
|
30
|
+
"uk": "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."
|
|
31
|
+
},
|
|
32
|
+
"0.1.14": {
|
|
33
|
+
"en": "fix: GetMonitorDetailByPowerstationId returned 404 (\"Route Not Found\") for accounts whose SEMS+ login is rejected and falls back to the legacy CrossLogin API - that backend uses the \"v2\" API path instead of \"v3\". The adapter now automatically retries with the v2 path on a detected 404, and error messages now surface the actual error_msg field instead of \"unbekannter Fehler\"",
|
|
34
|
+
"de": "Fix: GetMonitorDetailByPowerstationId lieferte 404 (\"Route Not Found\") fuer Konten, deren SEMS+-Login abgelehnt wird und die auf die Legacy-CrossLogin-API zurueckfallen - dieses Backend nutzt den \"v2\"-API-Pfad statt \"v3\". Der Adapter versucht bei erkanntem 404 jetzt automatisch den v2-Pfad, und Fehlermeldungen zeigen jetzt das tatsaechliche error_msg-Feld statt \"unbekannter Fehler\"",
|
|
35
|
+
"ru": "исправление: GetMonitorDetailByPowerstationId возвращал 404 для учётных записей с резервным входом через устаревший API - теперь используется путь v2",
|
|
36
|
+
"pt": "correcao: GetMonitorDetailByPowerstationId retornava 404 para contas com login de reserva na API legada - agora usa o caminho v2",
|
|
37
|
+
"nl": "fix: GetMonitorDetailByPowerstationId gaf 404 voor accounts met fallback-login op de legacy API - gebruikt nu het v2-pad",
|
|
38
|
+
"fr": "correction : GetMonitorDetailByPowerstationId renvoyait une 404 pour les comptes utilisant la connexion de secours a l'API historique - utilise desormais le chemin v2",
|
|
39
|
+
"it": "correzione: GetMonitorDetailByPowerstationId restituiva 404 per gli account con login di fallback sull'API legacy - ora usa il percorso v2",
|
|
40
|
+
"es": "correccion: GetMonitorDetailByPowerstationId devolvia 404 para cuentas con inicio de sesion de respaldo en la API heredada - ahora usa la ruta v2",
|
|
41
|
+
"pl": "poprawka: GetMonitorDetailByPowerstationId zwracal 404 dla kont z zapasowym logowaniem do starszego API - teraz uzywana jest sciezka v2",
|
|
42
|
+
"uk": "виправлення: GetMonitorDetailByPowerstationId повертав 404 для облікових записів із резервним входом через застарілий API - тепер використовується шлях v2",
|
|
43
|
+
"zh-cn": "修复:对于回退到旧版登录 API 的账户,GetMonitorDetailByPowerstationId 返回 404 - 现在改用 v2 路径"
|
|
44
|
+
},
|
|
45
|
+
"0.1.13": {
|
|
46
|
+
"en": "diagnostics: log the raw JSON envelope of every SEMS API call at debug level (not just the monitor-detail call). This helps pinpoint reports like \"unbekannter Fehler (code=undefined)\" on GetPowerStationIdByOwner, where the actual success/error convention used by that endpoint differs from what earlier testing assumed",
|
|
47
|
+
"de": "Diagnose: rohe JSON-Antwort jedes SEMS-API-Aufrufs wird jetzt auf Debug-Level geloggt (nicht mehr nur beim Monitor-Detail-Aufruf). Hilft, Meldungen wie \"unbekannter Fehler (code=undefined)\" bei GetPowerStationIdByOwner einzugrenzen, wenn die tatsächliche Erfolgs-/Fehler-Konvention dieses Endpunkts von den bisherigen Annahmen abweicht",
|
|
48
|
+
"ru": "диагностика: сырой JSON-ответ каждого вызова SEMS API теперь логируется на уровне debug",
|
|
49
|
+
"pt": "diagnóstico: a resposta JSON bruta de cada chamada à API SEMS agora é registada ao nível de debug",
|
|
50
|
+
"nl": "diagnostiek: de ruwe JSON-respons van elke SEMS API-aanroep wordt nu op debug-niveau gelogd",
|
|
51
|
+
"fr": "diagnostic : la reponse JSON brute de chaque appel a l'API SEMS est desormais journalisee au niveau debug",
|
|
52
|
+
"it": "diagnostica: la risposta JSON grezza di ogni chiamata all'API SEMS viene ora registrata a livello debug",
|
|
53
|
+
"es": "diagnostico: la respuesta JSON en bruto de cada llamada a la API de SEMS ahora se registra en el nivel debug",
|
|
54
|
+
"pl": "diagnostyka: surowa odpowiedz JSON kazdego wywolania API SEMS jest teraz logowana na poziomie debug",
|
|
55
|
+
"uk": "діагностика: сирий JSON-вiдповiдь кожного виклику SEMS API тепер логується на рiвнi debug",
|
|
56
|
+
"zh-cn": "诊断:每次 SEMS API 调用的原始 JSON 响应现在都会在 debug 级别记录"
|
|
57
|
+
},
|
|
58
|
+
"0.1.12": {
|
|
59
|
+
"en": "further recheck fixes: remove news entry for 0.1.10 (never published to npm - its CI failed before the deploy step), declare \"needs: check-and-lint\" on the adapter-tests job, pin @types/node to \"^22\" (was open-ended \">=22\"), fix .vscode/settings.json schema fileMatch patterns (drop leading slash, add jsonCustom.json/jsonTab.json), add dependabot auto-merge workflow + config",
|
|
60
|
+
"de": "weitere Recheck-Fixes: News-Eintrag fuer 0.1.10 entfernt (nie auf npm veroeffentlicht - CI schlug vor dem Deploy-Schritt fehl), \"needs: check-and-lint\" beim adapter-tests-Job ergaenzt, @types/node auf \"^22\" fixiert (war offenes \">=22\"), Schema-fileMatch in .vscode/settings.json korrigiert (kein fuehrender Slash, jsonCustom.json/jsonTab.json ergaenzt), Dependabot-Automerge-Workflow + Konfiguration ergaenzt",
|
|
61
|
+
"ru": "дальнейшие исправления: удалена запись news для 0.1.10 (никогда не публиковалась в npm), needs: check-and-lint для adapter-tests, @types/node зафиксирован на ^22, исправлены схемы в .vscode/settings.json, добавлен automerge для dependabot",
|
|
62
|
+
"pt": "mais correcoes: removida a entrada de news para 0.1.10 (nunca publicada no npm), needs: check-and-lint no job adapter-tests, @types/node fixado em ^22, esquemas corrigidos em .vscode/settings.json, automerge do dependabot adicionado",
|
|
63
|
+
"nl": "verdere fixes: news-item voor 0.1.10 verwijderd (nooit op npm gepubliceerd), needs: check-and-lint toegevoegd aan adapter-tests, @types/node vastgezet op ^22, schema's in .vscode/settings.json gecorrigeerd, dependabot automerge toegevoegd",
|
|
64
|
+
"fr": "autres corrections : entree news pour 0.1.10 supprimee (jamais publiee sur npm), needs: check-and-lint ajoute au job adapter-tests, @types/node fixe a ^22, schemas corriges dans .vscode/settings.json, automerge dependabot ajoute",
|
|
65
|
+
"it": "ulteriori correzioni: rimossa la voce news per 0.1.10 (mai pubblicata su npm), aggiunto needs: check-and-lint al job adapter-tests, @types/node fissato a ^22, schemi corretti in .vscode/settings.json, aggiunto automerge per dependabot",
|
|
66
|
+
"es": "mas correcciones: se elimino la entrada de news para 0.1.10 (nunca publicada en npm), se agrego needs: check-and-lint al job adapter-tests, @types/node fijado en ^22, esquemas corregidos en .vscode/settings.json, se agrego automerge de dependabot",
|
|
67
|
+
"pl": "kolejne poprawki: usunieto wpis news dla 0.1.10 (nigdy nie opublikowano na npm), dodano needs: check-and-lint do zadania adapter-tests, @types/node ustalono na ^22, poprawiono schematy w .vscode/settings.json, dodano automerge dependabot",
|
|
68
|
+
"uk": "подальші виправлення: видалено запис news для 0.1.10 (ніколи не публікувався на npm), додано needs: check-and-lint для adapter-tests, @types/node зафіксовано на ^22, виправлено схеми в .vscode/settings.json, додано automerge для dependabot",
|
|
69
|
+
"zh-cn": "进一步修复:移除 0.1.10 的 news 条目(从未发布到 npm),为 adapter-tests 任务添加 needs: check-and-lint,将 @types/node 固定为 ^22,修复 .vscode/settings.json 中的 schema 匹配,添加 dependabot 自动合并"
|
|
70
|
+
},
|
|
71
|
+
"0.1.11": {
|
|
72
|
+
"en": "fix broken CI: remove Node.js 20.x from the adapter-tests matrix - it is incompatible with engines.node >=22 (introduced in 0.1.10) under the official action's engine-strict npm ci, which crashed that matrix job and cancelled all others",
|
|
73
|
+
"de": "defekte CI behoben: Node.js 20.x aus der adapter-tests-Matrix entfernt - inkompatibel mit engines.node >=22 (seit 0.1.10) unter dem engine-strict npm ci der offiziellen Action, wodurch dieser Matrix-Job abstürzte und alle anderen abgebrochen wurden",
|
|
74
|
+
"ru": "исправлен неработающий CI: Node.js 20.x удалён из матрицы adapter-tests - несовместим с engines.node >=22 (введено в 0.1.10)",
|
|
75
|
+
"pt": "corrigido o CI quebrado: Node.js 20.x removido da matriz adapter-tests - incompatível com engines.node >=22 (introduzido em 0.1.10)",
|
|
76
|
+
"nl": "defecte CI opgelost: Node.js 20.x verwijderd uit de adapter-tests-matrix - incompatibel met engines.node >=22 (geïntroduceerd in 0.1.10)",
|
|
77
|
+
"fr": "correction du CI cassé : Node.js 20.x retiré de la matrice adapter-tests - incompatible avec engines.node >=22 (introduit en 0.1.10)",
|
|
78
|
+
"it": "risolto il CI non funzionante: rimosso Node.js 20.x dalla matrice adapter-tests - incompatibile con engines.node >=22 (introdotto in 0.1.10)",
|
|
79
|
+
"es": "se corrigió el CI roto: se eliminó Node.js 20.x de la matriz adapter-tests - incompatible con engines.node >=22 (introducido en 0.1.10)",
|
|
80
|
+
"pl": "naprawiono uszkodzone CI: usunięto Node.js 20.x z macierzy adapter-tests - niekompatybilne z engines.node >=22 (wprowadzone w 0.1.10)",
|
|
81
|
+
"uk": "виправлено зламаний CI: видалено Node.js 20.x з матриці adapter-tests - несумісний з engines.node >=22 (введено в 0.1.10)",
|
|
82
|
+
"zh-cn": "修复损坏的 CI:从 adapter-tests 矩阵中移除 Node.js 20.x - 与 engines.node >=22(0.1.10 中引入)不兼容"
|
|
83
|
+
},
|
|
84
|
+
"0.1.9": {
|
|
85
|
+
"en": "address stricter ioBroker.repositories PR check: move encryptedNative/protectedNative to io-package.json root, update deps (adapter-core, testing, adapter-dev, admin, js-controller), rewrite CI workflow (check-and-lint/adapter-tests/deploy jobs, concurrency, full OS+Node matrix), use adapter-managed timers, node:crypto, fix jsonConfig responsive sizes + i18n key, translate README to English (required), add CHANGELOG_OLD.md",
|
|
86
|
+
"de": "strengere ioBroker.repositories-PR-Prüfung behoben: encryptedNative/protectedNative auf io-package.json-Root verschoben, Abhängigkeiten aktualisiert (adapter-core, testing, adapter-dev, admin, js-controller), CI-Workflow neu geschrieben (check-and-lint/adapter-tests/deploy Jobs, Concurrency, volle OS+Node-Matrix), adapter-verwaltete Timer, node:crypto, jsonConfig Responsive-Größen + i18n-Key behoben, README auf Englisch übersetzt (Pflicht), CHANGELOG_OLD.md ergänzt",
|
|
87
|
+
"ru": "устранены более строгие проверки PR ioBroker.repositories: encryptedNative/protectedNative перемещены в корень io-package.json, обновлены зависимости, переписан CI, README переведён на английский (обязательно)",
|
|
88
|
+
"pt": "corrigidas verificações mais rigorosas do PR do ioBroker.repositories: encryptedNative/protectedNative movidos para a raiz do io-package.json, dependências atualizadas, CI reescrito, README traduzido para inglês (obrigatório)",
|
|
89
|
+
"nl": "strengere ioBroker.repositories-PR-controles opgelost: encryptedNative/protectedNative naar de root van io-package.json verplaatst, dependencies bijgewerkt, CI herschreven, README naar het Engels vertaald (verplicht)",
|
|
90
|
+
"fr": "correction des vérifications plus strictes du PR ioBroker.repositories : encryptedNative/protectedNative déplacés à la racine de io-package.json, dépendances mises à jour, CI réécrit, README traduit en anglais (obligatoire)",
|
|
91
|
+
"it": "risolti controlli più severi del PR ioBroker.repositories: encryptedNative/protectedNative spostati alla radice di io-package.json, dipendenze aggiornate, CI riscritta, README tradotto in inglese (obbligatorio)",
|
|
92
|
+
"es": "se resolvieron comprobaciones más estrictas del PR de ioBroker.repositories: encryptedNative/protectedNative movidos a la raíz de io-package.json, dependencias actualizadas, CI reescrita, README traducido al inglés (obligatorio)",
|
|
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)",
|
|
94
|
+
"uk": "усунено суворіші перевірки PR ioBroker.repositories: encryptedNative/protectedNative перенесено в корінь io-package.json, оновлено залежності, переписано CI, README перекладено англійською (обов'язково)",
|
|
95
|
+
"zh-cn": "解决更严格的 ioBroker.repositories PR 检查:将 encryptedNative/protectedNative 移至 io-package.json 根目录,更新依赖项,重写 CI,将 README 翻译为英文(必需)"
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"titleLang": {
|
|
99
|
+
"en": "GoodWe SEMS Portal",
|
|
100
|
+
"de": "GoodWe SEMS Portal",
|
|
101
|
+
"ru": "GoodWe SEMS Portal",
|
|
102
|
+
"pt": "GoodWe SEMS Portal",
|
|
103
|
+
"nl": "GoodWe SEMS Portal",
|
|
104
|
+
"fr": "GoodWe SEMS Portal",
|
|
105
|
+
"it": "GoodWe SEMS Portal",
|
|
106
|
+
"es": "GoodWe SEMS Portal",
|
|
107
|
+
"pl": "GoodWe SEMS Portal",
|
|
108
|
+
"uk": "GoodWe SEMS Portal",
|
|
109
|
+
"zh-cn": "GoodWe SEMS 门户"
|
|
110
|
+
},
|
|
111
|
+
"desc": {
|
|
112
|
+
"en": "Reads inverter, battery and power-flow data from the GoodWe SEMS Portal cloud for installations that have no local/LAN access to the inverter.",
|
|
113
|
+
"de": "Liest Wechselrichter-, Batterie- und Energiefluss-Daten aus dem GoodWe-SEMS-Portal (Cloud) für Anlagen ohne lokalen/LAN-Zugriff auf den Wechselrichter.",
|
|
114
|
+
"ru": "Считывает данные инвертора, батареи и потоков энергии из облачного портала GoodWe SEMS для установок без локального доступа к инвертору.",
|
|
115
|
+
"pt": "Lê dados do inversor, bateria e fluxo de energia do portal SEMS da GoodWe na nuvem para instalações sem acesso local/LAN ao inversor.",
|
|
116
|
+
"nl": "Leest omvormer-, batterij- en energiestroomgegevens uit het GoodWe SEMS-portaal (cloud) voor installaties zonder lokale/LAN-toegang tot de omvormer.",
|
|
117
|
+
"fr": "Lit les données de l'onduleur, de la batterie et des flux d'énergie depuis le portail cloud GoodWe SEMS pour les installations sans accès local/LAN à l'onduleur.",
|
|
118
|
+
"it": "Legge i dati dell'inverter, della batteria e del flusso di energia dal portale cloud GoodWe SEMS per impianti senza accesso locale/LAN all'inverter.",
|
|
119
|
+
"es": "Lee datos del inversor, la batería y el flujo de energía desde el portal en la nube GoodWe SEMS para instalaciones sin acceso local/LAN al inversor.",
|
|
120
|
+
"pl": "Odczytuje dane falownika, baterii i przepływu energii z chmury portalu GoodWe SEMS dla instalacji bez lokalnego dostępu do falownika.",
|
|
121
|
+
"uk": "Зчитує дані інвертора, батареї та потоків енергії з хмарного порталу GoodWe SEMS для установок без локального доступу до інвертора.",
|
|
122
|
+
"zh-cn": "为无法本地访问逆变器的装置从 GoodWe SEMS 云端门户读取逆变器、电池和功率流数据。"
|
|
123
|
+
},
|
|
124
|
+
"authors": [
|
|
125
|
+
"bueste <bueste@users.noreply.github.com>"
|
|
126
|
+
],
|
|
127
|
+
"keywords": [
|
|
128
|
+
"goodwe",
|
|
129
|
+
"sems",
|
|
130
|
+
"semsportal",
|
|
131
|
+
"inverter",
|
|
132
|
+
"solar",
|
|
133
|
+
"pv",
|
|
134
|
+
"photovoltaik",
|
|
135
|
+
"battery"
|
|
136
|
+
],
|
|
137
|
+
"licenseInformation": {
|
|
138
|
+
"type": "free",
|
|
139
|
+
"license": "MIT"
|
|
140
|
+
},
|
|
141
|
+
"platform": "Javascript/Node.js",
|
|
142
|
+
"icon": "goodwe-sems.png",
|
|
143
|
+
"enabled": true,
|
|
144
|
+
"extIcon": "https://raw.githubusercontent.com/bueste/ioBroker.goodwe-sems/main/admin/goodwe-sems.png",
|
|
145
|
+
"readme": "https://github.com/bueste/ioBroker.goodwe-sems/blob/main/README.md",
|
|
146
|
+
"loglevel": "info",
|
|
147
|
+
"mode": "daemon",
|
|
148
|
+
"type": "energy",
|
|
149
|
+
"compact": true,
|
|
150
|
+
"connectionType": "cloud",
|
|
151
|
+
"dataSource": "poll",
|
|
152
|
+
"tier": 3,
|
|
153
|
+
"adminUI": {
|
|
154
|
+
"config": "json"
|
|
155
|
+
},
|
|
156
|
+
"dependencies": [
|
|
157
|
+
{
|
|
158
|
+
"js-controller": ">=6.0.11"
|
|
159
|
+
}
|
|
160
|
+
],
|
|
161
|
+
"globalDependencies": [
|
|
162
|
+
{
|
|
163
|
+
"admin": ">=7.6.20"
|
|
164
|
+
}
|
|
165
|
+
]
|
|
97
166
|
},
|
|
98
|
-
"
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
167
|
+
"native": {
|
|
168
|
+
"account": "",
|
|
169
|
+
"password": "",
|
|
170
|
+
"powerStationId": "",
|
|
171
|
+
"pollInterval": 300,
|
|
172
|
+
"requestTimeout": 15,
|
|
173
|
+
"maxConsecutiveErrors": 3,
|
|
174
|
+
"pushoverMode": "none",
|
|
175
|
+
"pushoverInstance": "pushover.0",
|
|
176
|
+
"pushoverUserKey": "",
|
|
177
|
+
"pushoverApiToken": "",
|
|
178
|
+
"pushoverPriority": 0,
|
|
179
|
+
"notifyOnLoginFailure": true,
|
|
180
|
+
"notifyOnRateLimit": true,
|
|
181
|
+
"notifyOnStationOffline": true,
|
|
182
|
+
"notifyOnAdapterError": true,
|
|
183
|
+
"stationOfflineMinutes": 30,
|
|
184
|
+
"debugRawResponse": false
|
|
110
185
|
},
|
|
111
|
-
"
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
"ru": "Считывает данные инвертора, батареи и потоков энергии из облачного портала GoodWe SEMS для установок без локального доступа к инвертору.",
|
|
115
|
-
"pt": "Lê dados do inversor, bateria e fluxo de energia do portal SEMS da GoodWe na nuvem para instalações sem acesso local/LAN ao inversor.",
|
|
116
|
-
"nl": "Leest omvormer-, batterij- en energiestroomgegevens uit het GoodWe SEMS-portaal (cloud) voor installaties zonder lokale/LAN-toegang tot de omvormer.",
|
|
117
|
-
"fr": "Lit les données de l'onduleur, de la batterie et des flux d'énergie depuis le portail cloud GoodWe SEMS pour les installations sans accès local/LAN à l'onduleur.",
|
|
118
|
-
"it": "Legge i dati dell'inverter, della batteria e del flusso di energia dal portale cloud GoodWe SEMS per impianti senza accesso locale/LAN all'inverter.",
|
|
119
|
-
"es": "Lee datos del inversor, la batería y el flujo de energía desde el portal en la nube GoodWe SEMS para instalaciones sin acceso local/LAN al inversor.",
|
|
120
|
-
"pl": "Odczytuje dane falownika, baterii i przepływu energii z chmury portalu GoodWe SEMS dla instalacji bez lokalnego dostępu do falownika.",
|
|
121
|
-
"uk": "Зчитує дані інвертора, батареї та потоків енергії з хмарного порталу GoodWe SEMS для установок без локального доступу до інвертора.",
|
|
122
|
-
"zh-cn": "为无法本地访问逆变器的装置从 GoodWe SEMS 云端门户读取逆变器、电池和功率流数据。"
|
|
123
|
-
},
|
|
124
|
-
"authors": [
|
|
125
|
-
"bueste <bueste@users.noreply.github.com>"
|
|
126
|
-
],
|
|
127
|
-
"keywords": [
|
|
128
|
-
"goodwe",
|
|
129
|
-
"sems",
|
|
130
|
-
"semsportal",
|
|
131
|
-
"inverter",
|
|
132
|
-
"solar",
|
|
133
|
-
"pv",
|
|
134
|
-
"photovoltaik",
|
|
135
|
-
"battery"
|
|
186
|
+
"encryptedNative": [
|
|
187
|
+
"password",
|
|
188
|
+
"pushoverApiToken"
|
|
136
189
|
],
|
|
137
|
-
"
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
},
|
|
141
|
-
"platform": "Javascript/Node.js",
|
|
142
|
-
"icon": "goodwe-sems.png",
|
|
143
|
-
"enabled": true,
|
|
144
|
-
"extIcon": "https://raw.githubusercontent.com/bueste/ioBroker.goodwe-sems/main/admin/goodwe-sems.png",
|
|
145
|
-
"readme": "https://github.com/bueste/ioBroker.goodwe-sems/blob/main/README.md",
|
|
146
|
-
"loglevel": "info",
|
|
147
|
-
"mode": "daemon",
|
|
148
|
-
"type": "energy",
|
|
149
|
-
"compact": true,
|
|
150
|
-
"connectionType": "cloud",
|
|
151
|
-
"dataSource": "poll",
|
|
152
|
-
"tier": 3,
|
|
153
|
-
"adminUI": {
|
|
154
|
-
"config": "json"
|
|
155
|
-
},
|
|
156
|
-
"dependencies": [
|
|
157
|
-
{
|
|
158
|
-
"js-controller": ">=6.0.11"
|
|
159
|
-
}
|
|
190
|
+
"protectedNative": [
|
|
191
|
+
"password",
|
|
192
|
+
"pushoverApiToken"
|
|
160
193
|
],
|
|
161
|
-
"
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
194
|
+
"objects": [],
|
|
195
|
+
"instanceObjects": [
|
|
196
|
+
{
|
|
197
|
+
"_id": "info",
|
|
198
|
+
"type": "channel",
|
|
199
|
+
"common": {
|
|
200
|
+
"name": "Information"
|
|
201
|
+
},
|
|
202
|
+
"native": {}
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"_id": "info.connection",
|
|
206
|
+
"type": "state",
|
|
207
|
+
"common": {
|
|
208
|
+
"role": "indicator.connected",
|
|
209
|
+
"name": "SEMS Portal reachable",
|
|
210
|
+
"type": "boolean",
|
|
211
|
+
"read": true,
|
|
212
|
+
"write": false,
|
|
213
|
+
"def": false
|
|
214
|
+
},
|
|
215
|
+
"native": {}
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"_id": "info.lastSuccess",
|
|
219
|
+
"type": "state",
|
|
220
|
+
"common": {
|
|
221
|
+
"role": "value.time",
|
|
222
|
+
"name": "Timestamp of last successful poll",
|
|
223
|
+
"type": "number",
|
|
224
|
+
"read": true,
|
|
225
|
+
"write": false,
|
|
226
|
+
"def": 0
|
|
227
|
+
},
|
|
228
|
+
"native": {}
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"_id": "info.lastError",
|
|
232
|
+
"type": "state",
|
|
233
|
+
"common": {
|
|
234
|
+
"role": "text",
|
|
235
|
+
"name": "Last error message",
|
|
236
|
+
"type": "string",
|
|
237
|
+
"read": true,
|
|
238
|
+
"write": false,
|
|
239
|
+
"def": ""
|
|
240
|
+
},
|
|
241
|
+
"native": {}
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
"_id": "info.consecutiveErrors",
|
|
245
|
+
"type": "state",
|
|
246
|
+
"common": {
|
|
247
|
+
"role": "value",
|
|
248
|
+
"name": "Number of consecutive failed polls",
|
|
249
|
+
"type": "number",
|
|
250
|
+
"read": true,
|
|
251
|
+
"write": false,
|
|
252
|
+
"def": 0
|
|
253
|
+
},
|
|
254
|
+
"native": {}
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
"_id": "info.rateLimited",
|
|
258
|
+
"type": "state",
|
|
259
|
+
"common": {
|
|
260
|
+
"role": "indicator",
|
|
261
|
+
"name": "SEMS Portal is currently rate-limiting this account",
|
|
262
|
+
"type": "boolean",
|
|
263
|
+
"read": true,
|
|
264
|
+
"write": false,
|
|
265
|
+
"def": false
|
|
266
|
+
},
|
|
267
|
+
"native": {}
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
"_id": "info.activePollInterval",
|
|
271
|
+
"type": "state",
|
|
272
|
+
"common": {
|
|
273
|
+
"role": "value.interval",
|
|
274
|
+
"name": "Currently active poll interval (seconds, incl. backoff)",
|
|
275
|
+
"type": "number",
|
|
276
|
+
"unit": "s",
|
|
277
|
+
"read": true,
|
|
278
|
+
"write": false,
|
|
279
|
+
"def": 300
|
|
280
|
+
},
|
|
281
|
+
"native": {}
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
"_id": "info.rawResponse",
|
|
285
|
+
"type": "state",
|
|
286
|
+
"common": {
|
|
287
|
+
"role": "json",
|
|
288
|
+
"name": "Raw last SEMS API response (debug, disabled by default)",
|
|
289
|
+
"type": "string",
|
|
290
|
+
"read": true,
|
|
291
|
+
"write": false,
|
|
292
|
+
"def": "",
|
|
293
|
+
"expert": true
|
|
294
|
+
},
|
|
295
|
+
"native": {}
|
|
296
|
+
}
|
|
165
297
|
]
|
|
166
|
-
},
|
|
167
|
-
"native": {
|
|
168
|
-
"account": "",
|
|
169
|
-
"password": "",
|
|
170
|
-
"powerStationId": "",
|
|
171
|
-
"pollInterval": 300,
|
|
172
|
-
"requestTimeout": 15,
|
|
173
|
-
"maxConsecutiveErrors": 3,
|
|
174
|
-
"pushoverMode": "none",
|
|
175
|
-
"pushoverInstance": "pushover.0",
|
|
176
|
-
"pushoverUserKey": "",
|
|
177
|
-
"pushoverApiToken": "",
|
|
178
|
-
"pushoverPriority": 0,
|
|
179
|
-
"notifyOnLoginFailure": true,
|
|
180
|
-
"notifyOnRateLimit": true,
|
|
181
|
-
"notifyOnStationOffline": true,
|
|
182
|
-
"notifyOnAdapterError": true,
|
|
183
|
-
"stationOfflineMinutes": 30,
|
|
184
|
-
"debugRawResponse": false
|
|
185
|
-
},
|
|
186
|
-
"encryptedNative": [
|
|
187
|
-
"password",
|
|
188
|
-
"pushoverApiToken"
|
|
189
|
-
],
|
|
190
|
-
"protectedNative": [
|
|
191
|
-
"password",
|
|
192
|
-
"pushoverApiToken"
|
|
193
|
-
],
|
|
194
|
-
"objects": [],
|
|
195
|
-
"instanceObjects": [
|
|
196
|
-
{
|
|
197
|
-
"_id": "info",
|
|
198
|
-
"type": "channel",
|
|
199
|
-
"common": {
|
|
200
|
-
"name": "Information"
|
|
201
|
-
},
|
|
202
|
-
"native": {}
|
|
203
|
-
},
|
|
204
|
-
{
|
|
205
|
-
"_id": "info.connection",
|
|
206
|
-
"type": "state",
|
|
207
|
-
"common": {
|
|
208
|
-
"role": "indicator.connected",
|
|
209
|
-
"name": "SEMS Portal reachable",
|
|
210
|
-
"type": "boolean",
|
|
211
|
-
"read": true,
|
|
212
|
-
"write": false,
|
|
213
|
-
"def": false
|
|
214
|
-
},
|
|
215
|
-
"native": {}
|
|
216
|
-
},
|
|
217
|
-
{
|
|
218
|
-
"_id": "info.lastSuccess",
|
|
219
|
-
"type": "state",
|
|
220
|
-
"common": {
|
|
221
|
-
"role": "value.time",
|
|
222
|
-
"name": "Timestamp of last successful poll",
|
|
223
|
-
"type": "number",
|
|
224
|
-
"read": true,
|
|
225
|
-
"write": false,
|
|
226
|
-
"def": 0
|
|
227
|
-
},
|
|
228
|
-
"native": {}
|
|
229
|
-
},
|
|
230
|
-
{
|
|
231
|
-
"_id": "info.lastError",
|
|
232
|
-
"type": "state",
|
|
233
|
-
"common": {
|
|
234
|
-
"role": "text",
|
|
235
|
-
"name": "Last error message",
|
|
236
|
-
"type": "string",
|
|
237
|
-
"read": true,
|
|
238
|
-
"write": false,
|
|
239
|
-
"def": ""
|
|
240
|
-
},
|
|
241
|
-
"native": {}
|
|
242
|
-
},
|
|
243
|
-
{
|
|
244
|
-
"_id": "info.consecutiveErrors",
|
|
245
|
-
"type": "state",
|
|
246
|
-
"common": {
|
|
247
|
-
"role": "value",
|
|
248
|
-
"name": "Number of consecutive failed polls",
|
|
249
|
-
"type": "number",
|
|
250
|
-
"read": true,
|
|
251
|
-
"write": false,
|
|
252
|
-
"def": 0
|
|
253
|
-
},
|
|
254
|
-
"native": {}
|
|
255
|
-
},
|
|
256
|
-
{
|
|
257
|
-
"_id": "info.rateLimited",
|
|
258
|
-
"type": "state",
|
|
259
|
-
"common": {
|
|
260
|
-
"role": "indicator",
|
|
261
|
-
"name": "SEMS Portal is currently rate-limiting this account",
|
|
262
|
-
"type": "boolean",
|
|
263
|
-
"read": true,
|
|
264
|
-
"write": false,
|
|
265
|
-
"def": false
|
|
266
|
-
},
|
|
267
|
-
"native": {}
|
|
268
|
-
},
|
|
269
|
-
{
|
|
270
|
-
"_id": "info.activePollInterval",
|
|
271
|
-
"type": "state",
|
|
272
|
-
"common": {
|
|
273
|
-
"role": "value.interval",
|
|
274
|
-
"name": "Currently active poll interval (seconds, incl. backoff)",
|
|
275
|
-
"type": "number",
|
|
276
|
-
"unit": "s",
|
|
277
|
-
"read": true,
|
|
278
|
-
"write": false,
|
|
279
|
-
"def": 300
|
|
280
|
-
},
|
|
281
|
-
"native": {}
|
|
282
|
-
},
|
|
283
|
-
{
|
|
284
|
-
"_id": "info.rawResponse",
|
|
285
|
-
"type": "state",
|
|
286
|
-
"common": {
|
|
287
|
-
"role": "json",
|
|
288
|
-
"name": "Raw last SEMS API response (debug, disabled by default)",
|
|
289
|
-
"type": "string",
|
|
290
|
-
"read": true,
|
|
291
|
-
"write": false,
|
|
292
|
-
"def": "",
|
|
293
|
-
"expert": true
|
|
294
|
-
},
|
|
295
|
-
"native": {}
|
|
296
|
-
}
|
|
297
|
-
]
|
|
298
298
|
}
|
package/lib/semsApi.js
CHANGED
|
@@ -42,15 +42,36 @@ const LEGACY_LOGIN_FALLBACK_API = "https://eu.semsportal.com/api";
|
|
|
42
42
|
|
|
43
43
|
const STATION_LIST_PATH = "/PowerStation/GetPowerStationIdByOwner";
|
|
44
44
|
// GoodWe backends have been observed to expose GetMonitorDetailByPowerstationId
|
|
45
|
-
// under different API versions depending on
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
const
|
|
53
|
-
|
|
45
|
+
// under different API versions depending on account/region/backend generation.
|
|
46
|
+
// Community references disagree: pygoodwe (github.com/yaleman/pygoodwe) hardcodes
|
|
47
|
+
// "v2", a 2023 write-up (binodmx.medium.com) uses "v1", and our own traffic
|
|
48
|
+
// inspection against a SEMS+ account observed "v3". A real-world legacy-login
|
|
49
|
+
// account (SEMS+ login rejected with code=C0602) 404'd on BOTH v3 and v2, so
|
|
50
|
+
// none of the single hardcoded versions are reliable - we try all three,
|
|
51
|
+
// newest first, and use whichever one doesn't 404.
|
|
52
|
+
const MONITOR_DETAIL_PATHS = [
|
|
53
|
+
"/v3/PowerStation/GetMonitorDetailByPowerstationId",
|
|
54
|
+
"/v2/PowerStation/GetMonitorDetailByPowerstationId",
|
|
55
|
+
"/v1/PowerStation/GetMonitorDetailByPowerstationId",
|
|
56
|
+
];
|
|
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";
|
|
54
75
|
|
|
55
76
|
// Observed rate-limit response code. GoodWe does not document a retry-after
|
|
56
77
|
// value, community projects settled on a 5 minute cool-down.
|
|
@@ -111,6 +132,49 @@ async function httpPostJson(url, init, timerFns) {
|
|
|
111
132
|
}
|
|
112
133
|
}
|
|
113
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
|
+
|
|
114
178
|
class SemsApi {
|
|
115
179
|
/**
|
|
116
180
|
* @param {object} opts
|
|
@@ -175,7 +239,7 @@ class SemsApi {
|
|
|
175
239
|
async login() {
|
|
176
240
|
try {
|
|
177
241
|
const session = await this._loginNew();
|
|
178
|
-
this.log("debug",
|
|
242
|
+
this.log("debug", `SEMS-Login über SEMS+ (neue API) erfolgreich. API-Basis: ${session.api}`);
|
|
179
243
|
this.session = session;
|
|
180
244
|
return session;
|
|
181
245
|
} catch (newLoginError) {
|
|
@@ -186,7 +250,7 @@ class SemsApi {
|
|
|
186
250
|
}
|
|
187
251
|
|
|
188
252
|
const session = await this._loginLegacy();
|
|
189
|
-
this.log("debug",
|
|
253
|
+
this.log("debug", `SEMS-Login über Legacy-API erfolgreich. API-Basis: ${session.api}`);
|
|
190
254
|
this.session = session;
|
|
191
255
|
return session;
|
|
192
256
|
}
|
|
@@ -317,12 +381,13 @@ class SemsApi {
|
|
|
317
381
|
this._timerFns,
|
|
318
382
|
);
|
|
319
383
|
|
|
320
|
-
// Always log the raw envelope
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
// accounts without needing access to
|
|
325
|
-
|
|
384
|
+
// Always log the raw envelope (with the full request URL, not just the relative
|
|
385
|
+
// path) at debug level - not just on error. Some SEMS endpoints use success/error
|
|
386
|
+
// conventions, or even entirely different API bases/paths, that differ from the
|
|
387
|
+
// ones this adapter was originally built and tested against, so this is the
|
|
388
|
+
// fastest way to diagnose reports from real accounts without needing access to
|
|
389
|
+
// the account's credentials.
|
|
390
|
+
this.log("debug", `SEMS-API-Antwort ${url}: ${JSON.stringify(json)}`);
|
|
326
391
|
|
|
327
392
|
const code = json && json.code;
|
|
328
393
|
|
|
@@ -392,18 +457,262 @@ class SemsApi {
|
|
|
392
457
|
if (!powerStationId) {
|
|
393
458
|
throw new SemsProtocolError("getMonitorDetail() ohne powerStationId aufgerufen.");
|
|
394
459
|
}
|
|
460
|
+
let lastError;
|
|
461
|
+
for (let i = 0; i < MONITOR_DETAIL_PATHS.length; i++) {
|
|
462
|
+
const path = MONITOR_DETAIL_PATHS[i];
|
|
463
|
+
try {
|
|
464
|
+
return await this._authenticatedPost(path, { powerStationId });
|
|
465
|
+
} catch (error) {
|
|
466
|
+
lastError = error;
|
|
467
|
+
const is404 = error instanceof SemsProtocolError && /404|route not found/i.test(error.message);
|
|
468
|
+
if (!is404) {
|
|
469
|
+
throw error;
|
|
470
|
+
}
|
|
471
|
+
const nextPath = MONITOR_DETAIL_PATHS[i + 1];
|
|
472
|
+
if (nextPath) {
|
|
473
|
+
this.log(
|
|
474
|
+
"debug",
|
|
475
|
+
`${path} nicht gefunden (404) - versuche ${nextPath} als Fallback (API-Version unterschiedlich je nach Konto/Backend).`,
|
|
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 = [];
|
|
395
609
|
try {
|
|
396
|
-
|
|
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
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
397
629
|
} catch (error) {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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 },
|
|
402
647
|
);
|
|
403
|
-
|
|
648
|
+
telemetryFlat = flattenGatewayFactors(telemetry);
|
|
649
|
+
} catch (error) {
|
|
650
|
+
this.log("debug", `SEMS-Gateway: telemetry für ${sn} fehlgeschlagen (${error.message}).`);
|
|
404
651
|
}
|
|
405
|
-
|
|
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
|
+
});
|
|
406
688
|
}
|
|
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
|
+
};
|
|
407
716
|
}
|
|
408
717
|
}
|
|
409
718
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iobroker.goodwe-sems",
|
|
3
|
-
"version": "0.1.
|
|
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",
|