iobroker.goodwe-sems
v0.1.19
Published
ioBroker adapter to read GoodWe inverter data from the SEMS Portal cloud API (for installations without local/LAN access to the inverter).
Readme

ioBroker.goodwe-sems
Reads inverter, battery and power-flow data from the GoodWe SEMS Portal (cloud) - for installations that (e.g. because there is no LAN access to the inverter) cannot be polled with the local ioBroker.goodwe adapter (Modbus/UDP, port 8899).
Login uses your normal SEMS Portal account (the same one you use at semsportal.com / in the SEMS app). A GoodWe "organization"/OpenAPI account is not required.
Table of contents
- Why this adapter?
- API origin and limitations (please read)
- Installation
- Configuration
- Object/state structure
- Error handling, backoff and rate limits
- Pushover notifications
- Security & privacy
- Development
- Changelog
- License
Why this adapter?
GoodWe ET/EH/BH/BT inverters can normally be read out locally via Modbus/UDP (see ioBroker.goodwe). If there is no LAN access to the inverter (e.g. because only a WLAN/LTE stick is connected to the SEMS Portal and the target network is otherwise unreachable), the only remaining option is the cloud detour via the SEMS Portal (GoodWe) that the installation is already being monitored through anyway.
API origin and limitations (please read)
GoodWe officially offers three APIs (see the GoodWe API technical document):
- OpenAPI - only for SEMS organization accounts, requires activation by GoodWe.
- Real-time Data Monitoring API - for third parties, requires a license agreement plus a device whitelist.
- Batch Remote Control Interface - Kafka-based, remote control only.
None of these are accessible with a normal SEMS Portal account (the kind most private users have). This adapter instead speaks the same undocumented HTTPS API that the official SEMS app/website itself uses (login via CrossLogin/SEMS+ cross-login, data retrieval via GetMonitorDetailByPowerstationId). These endpoints have not been released or documented by GoodWe for third-party use; the implementation is based on independent traffic analysis as well as the following open-source reference projects:
Consequences:
- GoodWe can change the API at any time without notice - the adapter may (temporarily) break as a result.
- There is no documented real-time/push mechanism (websocket/SignalR) for third parties. An
msgSocketAdrfield appears in some older login responses but is not actually used by any of the reference projects above - using it would be pure reverse engineering without reliable documentation and a significantly higher risk (account lockout, unstable connection). This adapter therefore deliberately polls over HTTPS at a configurable interval (default 5 minutes) instead of faking an untested websocket connection. - A rate-limit code (
GY0429) has been observed (documented, among others, in the Home Assistant integration). The adapter recognizes this code and automatically pauses (default 5-minute cool-down) instead of endangering the account with repeated requests. - Use at your own risk, see LICENSE (MIT, no warranty).
Installation
Once this adapter is listed in the official ioBroker adapter repository, install it the normal way: Admin -> Adapters -> search for "goodwe-sems" -> install.
Until then, an ioBroker administrator can add it manually on the ioBroker host:
iobroker url iobroker.goodwe-semsConfiguration
| Field | Description |
|---|---|
| SEMS account / password | Same credentials as at semsportal.com. The password is stored encrypted by ioBroker. |
| Plant ID (optional) | Leave empty for automatic detection (GetPowerStationIdByOwner). For accounts with several plants: copy the ID manually from the portal URL (.../powerstation/powerstatussnmin/<ID>). |
| Poll interval | Default 300 s. The adapter enforces a minimum of 60 s regardless of configuration. |
| Pushover | See Pushover notifications. |
Object/state structure
goodwe-sems.0.info.connection SEMS Portal reachable (bool)
goodwe-sems.0.info.lastSuccess Timestamp of the last successful poll
goodwe-sems.0.info.lastError Last error message
goodwe-sems.0.info.consecutiveErrors Number of consecutive failed attempts
goodwe-sems.0.info.rateLimited SEMS Portal is currently rate-limiting (bool)
goodwe-sems.0.info.activePollInterval Currently effective interval incl. backoff (s)
goodwe-sems.0.info.rawResponse Raw JSON response (only when the debug option is enabled)
goodwe-sems.0.Station.Name / .Capacity / .Address / .Latitude / .Longitude / .PortalTimestamp / .Status / .StationId
goodwe-sems.0.KPI.CurrentPower / .TodayGeneration / .MonthGeneration / .TotalGeneration / .TodayIncome / .TotalIncome / .Currency
goodwe-sems.0.PowerFlow.PV / .Load / .Grid / .Battery / .LoadStatus / .GridStatus / .PvStatus / .BatteryStatus
goodwe-sems.0.Battery.SOC / .Status
goodwe-sems.0.EVCharger.* (only if reported by the portal)
goodwe-sems.0.Inverters.<serial>.Name / .Model / .Status / .WarningCode
goodwe-sems.0.Inverters.<serial>.CurrentPower / .TodayGeneration / .TotalGeneration / .Temperature
goodwe-sems.0.Inverters.<serial>.PV1..4.Voltage / .Current
goodwe-sems.0.Inverters.<serial>.AC_L1..3.Voltage / .Current / .Frequency
goodwe-sems.0.Inverters.<serial>.Battery.SOC / .Voltage / .CurrentWith two inverters (as in the original requirement this adapter was built for), two Inverters.<serial>.* branches are created automatically - the number is not hardcoded, it is driven entirely by what the portal returns for the configured account.
Fields that the portal delivers but this adapter does not (yet) know about are not lost: with the debug option enabled, the full raw response ends up in info.rawResponse (JSON), so it can be inspected and added via PR if needed.
Error handling, backoff and rate limits
- Every poll cycle is fully wrapped in try/catch; a single failure can never permanently stop the polling loop.
- Dedicated error classes (
SemsAuthError,SemsRateLimitError,SemsNetworkError,SemsProtocolError) drive targeted behaviour:- Rate limit (
GY0429) -> immediate pause (default 300 s),info.rateLimited = true. - Login failure -> exponential backoff (capped at 1 h) so that wrong credentials do not put additional strain on the account.
- Network/protocol errors -> moderate backoff.
- Rate limit (
- After a configurable number of consecutive failures (default 3), the plant is considered "offline" and, if enabled, a Pushover notification is triggered.
- Everything is additionally written to the ioBroker log in a structured way (
error/warn/debugdepending on severity).
Pushover notifications
Configurable in three modes:
- Via an existing
ioBroker.pushoverinstance (sendTo) - recommended, no duplicate credential management. - Directly via the Pushover API (your own user key + API/app token, stored encrypted) - also works without a separate Pushover instance.
- Both at the same time.
Triggered on: SEMS login failure, SEMS rate limit, a prolonged outage, unexpected adapter error - each individually toggleable. An internal cool-down (default 1 h per category) prevents spam during ongoing issues.
Security & privacy
- The SEMS password and the Pushover API token are marked as
encryptedNative/protectedNativeat the root ofio-package.jsonand are stored encrypted by ioBroker, never logged in plain text (the account name is masked in log messages, e.g.st***@gmail.com). - The adapter performs read-only access only (
GetMonitorDetailByPowerstationId,GetPowerStationIdByOwner). There is deliberately no remote-control/write function (SaveRemoteControlInverter) - that would be a considerably larger security and liability risk and was not part of the requirement. - No third-party dependency for HTTP access: the built-in
fetchof Node.js >=22 is used instead of an additional HTTP library - a smaller attack surface, less supply-chain risk. - The API base URL returned by the login response is validated (HTTPS on GoodWe-owned domains only) before any further request uses it, so a manipulated login response cannot redirect the session token to a foreign host.
- All network errors are caught in a typed way; no unchecked data from the API response is ever executed (
eval,Function, and similar are not used anywhere).
Development
npm install
npm run lint
npm test # unit tests (lib/mapping.js, lib/semsApi.js, lib/notify.js) + package consistency checkRecommended additionally before every release:
npx @iobroker/repochecker@latest .Pull requests are welcome, especially to add further fields delivered by the portal (see info.rawResponse with the debug option enabled) or to improve translations.
Changelog
WORK IN PROGRESS
0.1.19 (2026-07-20)
- (Stefan Bühler) removed the classic, version-prefixed
GetMonitorDetailByPowerstationIdendpoint (tried as/v3,/v2,/v1since 0.1.14/0.1.15) entirely - GoodWe has retired it, every account observed during development 404s on all three versions unconditionally.getMonitorDetail()now calls the SEMS+ gateway API (introduced in 0.1.16) directly, making every poll cycle faster and avoiding pointless failing requests - (Stefan Bühler) fix: the gateway session was never automatically refreshed once it expired server-side - the adapter creates a single long-lived API client at startup and reuses its session indefinitely, and unlike the (now removed) classic path, the gateway request helper never re-logged in on a stale session. This caused the adapter to fail permanently after a few hours (confirmed by a real account: worked in the evening, failed every single poll cycle the entire next day) until manually restarted. Every gateway call now automatically re-logs in once and retries on any error before giving up
- (Stefan Bühler) 5 updated/new regression tests (45 unit tests in total) covering the simplified direct-gateway call and the automatic re-login-and-retry behavior (including giving up correctly after exactly one retry)
0.1.18 (2026-07-19)
- (Stefan Bühler) fix: SEMS+ login still got rejected with
code=C0602 "account_login_abnormal"even after the host fix in 0.1.17, because the adapter identified itself as the iOS app (User-Agent: PVMaster/..., tokenclient: "ios") - but the called endpoint (eu-semsplus.goodwe.com) is, per the real browser capture, only ever used by the SEMS+ web client, sendingclient: "semsPlusWeb", a browser User-Agent, andOrigin/Refererheaders. The login call now builds its own matching header identity for just that one request; every other (classic/legacy) endpoint keeps using the established iOS identity, unchanged - (Stefan Bühler) 1 tightened regression test verifying the login call's client identity and headers
0.1.17 (2026-07-19)
- (Stefan Bühler) fix: SEMS+ login failed for some accounts (
code=C0602 "account_login_abnormal") because the adapter called the global endpoint (semsplus.goodwe.com) instead of the EU-regional one (eu-semsplus.goodwe.com). Confirmed via a real browser HAR capture: the identical request body and password hash succeeded against the regional host. Deliberately implemented without a host-fallback loop - repeatedly retrying the same credentials against multiple hosts looks like credential stuffing to the backend and risks a real account lockout - (Stefan Bühler) the login request now also sends the
x-signatureheader (matching real browser traffic exactly), and a genuine SEMS+ session token is now correctly accepted by the gateway API introduced in 0.1.16 - previously, the gateway fallback only ever received a Legacy-CrossLogin-derived token, which the gateway rejected with the same C0602 error since it isn't a real SEMS+ session - (Stefan Bühler) 1 updated regression test verifying the exact login URL and the presence of the login-time signature header
0.1.16 (2026-07-19)
- (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 whyGetMonitorDetailByPowerstationIdcould never succeed under any of thev1/v2/v3paths 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.) - (Stefan Bühler) the gateway API additionally requires every request to carry a computed
x-signatureheader 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 - (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 sameinfo/kpi/inverter[]shape the rest of the adapter already expects - no changes needed in the mapping/state-creation layer - (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
- (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
0.1.15 (2026-07-19)
- (Stefan Bühler) fix: 0.1.14's v3→v2 fallback for
GetMonitorDetailByPowerstationIdwas insufficient - a real-world account's legacy-login backend returned404 Route Not Foundfor both thev2andv3paths. Community references disagree on which version is correct (pygoodwe hardcodesv2, a separate 2023 write-up usesv1, our own traffic inspection observedv3), sogetMonitorDetail()now tries all three versions in sequence (v3→v2→v1) and uses whichever one doesn't 404 - (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
- (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
0.1.14 (2026-07-19)
- (Stefan Bühler) fix:
GetMonitorDetailByPowerstationIdreturned404 Route Not Foundfor accounts whose SEMS+ login is rejected (observed:code=C0602) and that fall back to the legacy CrossLogin API - that backend serves the endpoint under thev2API path, notv3. Root cause found via a real account's debug log plus the community reference implementation pygoodwe, whose legacy-only client hardcodes thev2path.getMonitorDetail()now triesv3first and automatically retries once withv2on a detected 404, so both backend variants work without any user-facing configuration change - (Stefan Bühler) fix: error messages now also surface the API's
error_msgfield (previously silently dropped, resulting in an uninformative "unbekannter Fehler" even when the response body contained a clear error description) - (Stefan Bühler) 2 new regression tests (44 unit tests in total) covering the v3→v2 fallback and the case where both paths fail
0.1.13 (2026-07-19)
- (Stefan Bühler) diagnostics: log the raw JSON envelope of every SEMS API call at debug level, not just the monitor-detail call. Real-account testing surfaced a
SEMS-API-Fehler: ... GetPowerStationIdByOwner ... unbekannter Fehler (code=undefined)report - the success/error code convention this adapter assumes (code: 0/"0"/"00000") was only ever validated against test fixtures, not this specific endpoint on a live account. This logging is the fastest way to see the actual response shape and fix the real bug without needing access to anyone's SEMS credentials
0.1.12 (2026-07-19)
Further fixes from a repochecker recheck on the ioBroker.repositories listing PR:
- (Stefan Bühler) [E2004] removed the
0.1.10entry fromcommon.newsinio-package.json- that version's CI failed before the deploy step, so it was never actually published to npm - (Stefan Bühler) [S3014] declared
needs: check-and-linton theadapter-testsjob so it only runs after linting succeeds - (Stefan Bühler) [W0066] pinned
@types/nodeto^22(was the open-ended>=22, which could resolve to a newer major with mismatched typings) - (Stefan Bühler) [W4040]/[W4042] fixed the JSON schema associations in
.vscode/settings.json:fileMatchentries must not have a leading slash, and the jsonConfig schema must also matchadmin/jsonCustom.jsonandadmin/jsonTab.json - (Stefan Bühler) [S8913] added
.github/workflows/automerge-dependabot.yml(usingiobroker-bot-orga/action-automerge-dependabot@v1) and.github/auto-merge.ymlso patch updates (and minor updates for dev dependencies) from Dependabot are merged automatically
0.1.11 (2026-07-19)
- (Stefan Bühler) fixed a real CI break introduced in 0.1.10: removed Node.js 20.x from the
adapter-testsmatrix in.github/workflows/test-and-release.yml. It is incompatible withengines.node >=22(also introduced in 0.1.10) once the officialioBroker/testing-action-adapter@v1action runsnpm ciwithengine-strictenabled, which crashed that matrix job and cancelled every other job via fail-fast
0.1.10 (2026-07-19)
Second round of fixes, addressing further findings from a stricter automated @iobroker/repochecker recheck on the ioBroker.repositories listing PR:
- (Stefan Bühler) [W0028] raised
engines.nodeto>=22 - (Stefan Bühler) [W0063] removed
chai,chai-as-promised,mocha,sinonfrom devDependencies (already provided by@iobroker/testing) - (Stefan Bühler) [S0065]/[S0085]/[S0087] added
@types/node,@tsconfig/node22and/tsconfig.jsonfor editor type-checking support - (Stefan Bühler) [S5026] added the
@alcalzone/release-script-plugin-manual-reviewrelease plugin - (Stefan Bühler) [W3013]/[W3015]/[W3017] rewrote
.github/workflows/test-and-release.ymlto use the official sharedioBroker/testing-action-check@v1,ioBroker/testing-action-adapter@v1andioBroker/testing-action-deploy@v1GitHub Actions instead of hand-written steps - (Stefan Bühler) added
test/integration.js(adapter startup smoke test via@iobroker/testing's integration harness) sonpm run test:integrationsucceeds - (Stefan Bühler) [E1032] trimmed
common.newsinio-package.jsonto the 7 entries kept by the repository builder - (Stefan Bühler) [E5512] added the required
sizeproperty to the Pushover section header inadmin/jsonConfig.json - (Stefan Bühler) [S5601] migrated
admin/i18nfrom the long{lang}/translations.jsonformat to the short{lang}.jsonformat - (Stefan Bühler) [S4036] added
.vscode/settings.jsonwith JSON schema associations forio-package.jsonandadmin/jsonConfig.json - (Stefan Bühler) [S8901] added
.github/dependabot.yml(npm + github-actions, weekly, with a cooldown and an@types/nodemajor/minor ignore rule)
0.1.9 (2026-07-19)
Addressed the stricter automated @iobroker/repochecker findings surfaced on the ioBroker.repositories listing PR:
- (Stefan Bühler) [E1057] moved
encryptedNative/protectedNativefromcommonto theio-package.jsonroot, matching the current schema - (Stefan Bühler) [E3009]/[E3010]/[E3011]/[E3012] raised
engines.nodeto>=20,@iobroker/adapter-coreto^3.4.1,js-controllerdependency to>=6.0.11,adminglobalDependency to>=7.6.20 - (Stefan Bühler) [E3040] updated devDependencies (
@iobroker/adapter-dev,@iobroker/testing, mocha, esbuild and others) to current major versions - (Stefan Bühler) [E3000-series] rewrote
.github/workflows/test-and-release.ymlto the current official template: renamed jobs (check-and-lint,adapter-tests,adapter-check,deploy), full OS/Node test matrix (ubuntu/windows/macos x 20/22/24),concurrencygroup, deploy job pinned to Node 24 - (Stefan Bühler) [E5005] replaced global
setTimeout/clearTimeoutwith adapter-managed timers (adapter.setTimeout/adapter.clearTimeout) inlib/notify.jsandlib/semsApi.js - (Stefan Bühler) [E5043] switched to
require("node:crypto") - (Stefan Bühler) [E5507]/[E5510]/[E5512]/[E5612] fixed
admin/jsonConfig.json: added missinglg/xlresponsive sizes on every item, replaced a literal label string with a proper i18n key (loginTab, added to all 11 translation files) - (Stefan Bühler) [E6004]/[E6015]/[W0037]/[W0076] translated
README.mdto English (required language), moved the previous German content toREADME.de.md, addedCHANGELOG_OLD.mdfor older entries - (Stefan Bühler) [W9501] removed the redundant
.npmignore(superseded by package.jsonfiles) - (Stefan Bühler) [E9006] added
.commitinfoto.gitignore - (Stefan Bühler) [S4036]/[S5026] added
prettier.config.mjs, re-formatted the codebase, disabledjsdoc/reject-any-typefor the opaque Node timer-handle type with a justifying comment
0.1.8 (2026-07-19)
Addressed ioBroker adapter-check findings:
- (Stefan Bühler) [E254] removed changelog entries for 0.1.1/0.1.2 - those tags were pushed but their npm-publish CI job failed at the time (missing
NPM_TOKEN/ npm CLI too old for OIDC), so the versions never existed on npm - (Stefan Bühler) [W132] this automatically brought the entry count under the repository builder's 7-entry truncation limit for
common.news - (Stefan Bühler) [W184] removed deprecated
common.title(superseded bycommon.titleLang) and deprecated/ignoredcommon.main(the entry point comes frompackage.json) - (Stefan Bühler) [W034] raised
@iobroker/adapter-corefrom ^3.1.6 to ^3.2.2 - (Stefan Bühler) [W173]/[W174]/[E999]/[W401]:
passwordwas already correctly listed inencryptedNative/protectedNative(verified against the published tarball) - these findings, together with the global axios 404 when fetchingsources-dist-latest.json, are side effects of the adapter not yet being listed in the official ioBroker repository
0.1.7 (2026-07-19)
- (Stefan Bühler) branding: replaced the placeholder icon with the official GoodWe logo (used with permission from GoodWe)
0.1.6 (2026-07-18)
- (Stefan Bühler) updated the dev toolchain: mocha 11, sinon 22, @alcalzone/release-script 5, @iobroker/eslint-config 2; remaining transitive CVEs (adm-zip, diff, esbuild, serialize-javascript) resolved via npm
overrides-npm audit: 0 vulnerabilities (including dev dependencies)
Security/quality audit (security tester, maintainer review, fuzzing of the mapping layer):
- (Stefan Bühler) Security: inverter serial numbers from the (untrusted) portal response are sanitized before becoming part of ioBroker object IDs (prevents broken/unexpectedly nested object trees caused by special characters such as
.*]) - (Stefan Bühler) Security: the API base URL returned by the login server is validated - HTTPS on GoodWe-owned domains only (
*.semsportal.com,*.goodwe.com), otherwise falls back to the known regional URL. A manipulated login response can no longer redirect the session token to a foreign host - (Stefan Bühler) Fix:
null/broken entries in the portal'sinverter[]array crashed the entire poll cycle - now skipped, healthy inverters from the same response are still processed - (Stefan Bühler) Fix: numbers in scientific notation (
"1e5") were parsed incorrectly (yielded 15 instead of 100000) - (Stefan Bühler) Fix: obviously invalid portal timestamps (
99/99/9999 ...) produced absurd epoch values via JavaScript'sDaterollover behaviour - now rejected - (Stefan Bühler) Fix: automatic plant discovery now filters out entries without a usable ID (previously caused permanently failing poll cycles)
- (Stefan Bühler) Robustness: no more state writes after adapter unload; the
adapterErrornotification dedupe window is also reset after recovery - (Stefan Bühler) 14 new regression tests (42 unit tests in total);
npm audit: 0 vulnerabilities in production dependencies (remaining findings were dev-toolchain only)
0.1.5 (2026-07-18)
- (Stefan Bühler) fix: corrected the PayPal donation link in the README (button link instead of the old donate link)
Older changelog entries can be found in CHANGELOG_OLD.md.
License
MIT License
Copyright (c) 2026 Stefan Bühler
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
