Marstek Device Open API (UDP)
SkillCommunicationHow this integration talks to Marstek devices via the local Open API over UDP (JSON messages) and how to handle errors safely
Instructions available. Your AI can read the instructions. Execution depends on the setup they require.
Account requirements not reviewed. Check the skill instructions before use; ahel provides instructions and does not run this skill.
Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
Then ask your AI: use the Marstek Device Open API (UDP) skill
What this skill tells your AI
The instructions your AI receives, as published by taurgis/has-marstek-local-api in .agents/skills/marstek-open-api-udp/SKILL.md and read by ahel’s review.
This repository communicates with Marstek devices using the Open API over UDP as documented in docs/marstek_device_openapi.MD.
In code, this is encapsulated by the py-marstek library (pymarstek).
When to Use
- You need to change how device data is fetched (polling)
- You’re debugging connectivity or “no data” scenarios
- You’re adding new data points that might require additional Open API methods
Transport & Message Shape
- Transport is UDP to the device (default port 30000).
- Bind the local socket to the same port the device listens on. Several firmware builds reply to that listen port instead of the client’s ephemeral source port. The Open API port is user-configurable, so the integration keeps one socket per unique listen port (devices that share a port share a socket).
SO_REUSEPORTis set so a second bind can share a port, but Linux then load-balances replies across every still-bound socket. Pause does not unbind. Broadcast discovery pauses pooled listeners and reads their sockets (MarstekUDPClient.paused_socket()viapaused_discovery_sockets()); it binds its own socket only for ports no pooled client owns. UnicastMarstek.GetDevice(manual add, Confirm device, repairs) must reuse the pooledMarstekUDPClient.send_request(...)for that port. A second bind after pause producescannot_connect/No valid response from devicewhile the coordinatorRecvs the GetDevice reply. - Messages are JSON objects with a
methodandparams, e.g.:- Discovery:
Marstek.GetDevice - Status:
ES.GetStatus,ES.GetMode,Bat.GetStatus,PV.GetStatus,EM.GetStatus - Control:
ES.SetMode
- Discovery:
Discovery pattern from the spec:
- A UDP broadcast may first receive a
Parse errorresponse (devices reacting to a non-JSON broadcast probe). - Then send a proper JSON request with
method: Marstek.GetDeviceto receive the device’s metadata includingip,ble_mac,wifi_mac,device, andver. wifi_macis the BSSID of the access point the device joined, not the device's own MAC, so every battery on one AP reports the same value (macdefaults to it too). Identify devices byble_mac;mac/wifi_maccount only when no BLE MAC is reported (identity_macs_from_mapping).Ble.Advenable: 1 starts advertising, 0 stops it in the firmware (VNSE3-0/VNSA-0 150), the reverse of the Rev 3.1 PDF table.
Key Objects & Calls (in this repo)
- Library client:
pymarstek.MarstekUDPClient - Used patterns:
await udp_client.discover_devices(...)(config flow + scanner)await udp_client.send_request(...)(setup connectivity check)await udp_client.get_device_status(...)(coordinator polling)
Important library behavior (current implementation):
get_device_status(...)can return default values on failure instead of raising.- The coordinator treats
device_mode == "Unknown"as “no valid data received” and falls back to previouscoordinator.data.
Error Handling Contract
The integration aims to be resilient to device/network flakiness:
- Timeouts / OSError / ValueError
- Meaning: UDP request did not complete, network unreachable, or response parse issues.
- Behavior in polling: log a warning and return previous data (entities keep last-known values instead of flapping).
- Behavior in setup: raise
ConfigEntryNotReadyto let HA retry while the scanner updates IP if needed.
Local API enablement:
- Devices must have OPEN API enabled in the Marstek app or discovery/polling will fail.
Concurrency Guidance
Marstek devices can be sensitive to request bursts.
- Prefer one request per update interval.
- Avoid parallel requests.
- Keep all I/O in the coordinator (entities read from coordinator data).
- All device I/O must stay async; do not introduce blocking sockets or file access on the event loop.
Control actions (ES.SetMode):
- Pause polling for the target host while sending a command + verifying the result (see
custom_components/marstek/device_action.py). - Use retries + backoff; UDP packets may be dropped.
Wi-Fi vs Ethernet
Venus Control images speak Open API on two radios:
- Ethernet: WCH CH395 (
Extract_udp_data_ch395/CH395SendData). Firmware 150 is the vendor fix for Local API send failures on this path. - Wi-Fi: Quectel FC41D
AT+QIOPEN=…,"UDP SERVICE"/AT+QISEND. That path is unchanged in 150. The module UART is shared with MQTT/HTTP. Wi-Fi unicasts after idle can time out even when Ethernet is stable; the public AT manual does not document a deterministic first-packet drop.
For firmware the profile marks openapi_wifi_retransmit_safe (known family, known Control generation, not reset-prone — Venus 150+ / HMG-50 156+), read-only unicasts (Marstek.GetDevice, ES.GetStatus, ES.GetMode, EM.GetStatus, PV.GetStatus, Wifi.GetStatus, Bat.GetStatus) may:
- Send once, wait 500 ms. Retransmit only if that wait is silent (RFC 1122 UDP retransmission is the application's job). Ethernet replies typically land well before this, so LAN and dual-homed Ethernet IPs stay one datagram.
- Wait the remaining configured request timeout for a matching reply without cancelling the pending future (
asyncio.wait, notwait_for). Cap is two datagrams inside one timeout.
Writes (ES.SetMode, DOD.SET, Ble.Adv, Led.Ctrl), unknown models, missing ver, and reset-prone IPs stay at one datagram and one wait. Prefer Ethernet for polling; retries cannot repair AP client isolation or Wi-Fi NAT.
Practical Debugging Steps
- If discovery finds no devices:
- Confirm device is on the same LAN segment and OPEN API is enabled.
- Confirm UDP port (default 30000) is not blocked.
- If polling returns stale/default data:
- Check logs for
device_mode=Unknownwarnings (indicates no valid response). - Validate the device IP in the config entry; scanner should update it automatically.
- Check logs for
When NOT to Use
- Don’t implement raw UDP handling in entities; keep it inside
pymarstek+ coordinator. - Don’t add extra Open API calls per poll cycle unless you can justify the added device load.
Signals
- GitHub stars
- 35
- Forks
- 5
- Last commit
- Sep 2026
Advanced
- Item type
- skill
- Key
marstek-open-api-udp- Source
- github.com/taurgis/has-marstek-local-api
github.com/taurgis/has-marstek-local-api
Related picks
Skill · davila7
The pick for PDFpdf-explore
Skill · xuzhougeng
The pick for PDFpython-performance-optimization
Skill · wshobson
The pick for Pythonpython-pro
Skill · jeffallan
The pick for Pythonslack-gif-creator
Skill · anthropics
More in Communicationerror-handling
Skill · affaan-m
More in Communication