Two root causes found: port 30000 binds but never serves, and ES.* methods only answer broadcast
Venus E, firmware 153 (build 2024-09-09), BMS 213, WiFi, single device.
Integration v1.0.0, Home Assistant 2026.8.2.
I spent a day diagnosing this with packet captures and port scans, and found two
separate problems. I think together they explain #135, and possibly #127.
Finding 1 — On FW 153, UDP/30000 is bound but never answers
The Local API was confirmed enabled on port 30000 by the device itself,
read over BLE with marstek-venus-monitor (command 0x28):
Local API (0x65/0x66): Enabled — port 30000
Raw Data: 0x01 0x30 0x75 # enabled=1, port=0x7530=30000
Yet every request timed out. Crucially, the port is bound — the device
answers ICMP port-unreachable for closed ports but stays silent on 30000:
$ nmap -sU -p 53,123,502,1010,2220,5683,8888,9000,10000,12345,22222,30000,30001,49152 <ip>
53/udp closed
123/udp closed
502/udp closed
1010/udp closed
2220/udp closed
5683/udp closed
8888/udp closed
9000/udp closed
10000/udp closed
12345/udp closed
30001/udp closed
49152/udp closed
22222/udp open|filtered <- device's own outbound socket to the CT emulator
30000/udp open|filtered <- Local API: bound, but never replies
tcpdump confirms our packets arrive and nothing comes back — no reply, no ICMP:
15:13:42 IP <ha>.30000 > <bat>.30000 UDP, length 62
15:13:47 IP <ha>.30000 > <bat>.22222 UDP, length 62
15:13:53 IP <ha>.34690 > <bat>.30000 UDP, length 62
(no response of any kind; meanwhile <bat>.22222 <-> <ha>.1010 traffic flows fine
both ways, so there is no firewall or L2 issue)
Around 40 payload variants were tried — spec-exact {"id":0,...}, id>=1,
compact JSON, \n/\0 terminators, unicast, subnet broadcast, global broadcast,
from two different machines, with and without a src field. All silent.
Disable/enable of the Local API via BLE, an app-level reset and a full power
cycle all made no difference.
The fix: change the port. Using Enable Local API (Custom Port) in
marstek-venus-monitor and setting 49200 made the device answer immediately:
{"id":0,"src":"VenusE-<mac>","result":{"device":"VenusE","ver":153,
"ble_mac":"...","wifi_mac":"...","wifi_name":"...","ip":"192.168.1.3"}}
After the change, 30000/udp reads closed and 49200/udp reads open|filtered.
This matches a line in the official Marstek Open API document (Rev 2.0, §2.2.1)
that reads like a workaround for exactly this:
"The default port number is 30000, and the recommended port number is between
49152 and 65535."
Suggestion: mention this in the README/troubleshooting, and consider warning
during config flow when port 30000 is chosen and setup fails.
Finding 2 — ES.* methods only answer BROADCAST; the integration sends unicast
This is the one I think explains #135 and the ES.GetMode/ES.GetStatus
timeouts in #127.
With the API working on 49200, I probed each method twice — once unicast to the
device IP, once to the subnet broadcast address, from a socket bound to the API
port:
| Method |
unicast to device IP |
broadcast |
Marstek.GetDevice |
silent |
replies |
Bat.GetStatus |
replies |
replies |
ES.GetStatus |
silent |
replies |
ES.GetMode |
silent |
replies |
PV.GetStatus |
silent |
replies |
api.py::_send_to_host() sends unicast whenever self.host is set, which is
always the case after a manual/DHCP config entry. So on this firmware only
Bat.GetStatus gets through.
That produces exactly the symptoms reported elsewhere:
- SoC, voltage, current, temperature populate (they come from
Bat.GetStatus)
sensor.*_power, *_grid_power, *_operating_mode, select.*_operating_mode
stay unknown, and the mode select silently does nothing
- recurring
ES.GetMode / ES.GetStatus / EM.GetStatus timeouts in the log
ES.SetMode behaves the same way. Sent by the integration it has no effect;
sent as broadcast it works — I verified a switch to Passive and back to Auto,
confirmed by reading ES.GetMode afterwards:
{"id":0,"src":"VenusE-<mac>","result":{"id":0,"mode":"Auto",
"ongrid_power":0,"offgrid_power":0,"bat_soc":11}}
One detail worth knowing: the device always sends its reply to the API port,
regardless of the requester's source port. So a helper script can send from an
ephemeral port, but the reply then lands on the socket the integration holds.
Suggested fix: send ES.* and Marstek.GetDevice to the broadcast address
(filtering replies by src/IP as the code already does), or add a per-entry
option such as use_broadcast: true. Falling back to broadcast after N
consecutive unicast timeouts for a given method would fix this transparently.
Possibly related
Happy to run further captures or test a patch — I have tcpdump and nmap available
on the host and can reproduce this on demand.
Two root causes found: port 30000 binds but never serves, and ES.* methods only answer broadcast
Venus E, firmware 153 (build 2024-09-09), BMS 213, WiFi, single device.
Integration v1.0.0, Home Assistant 2026.8.2.
I spent a day diagnosing this with packet captures and port scans, and found two
separate problems. I think together they explain #135, and possibly #127.
Finding 1 — On FW 153, UDP/30000 is bound but never answers
The Local API was confirmed enabled on port 30000 by the device itself,
read over BLE with
marstek-venus-monitor(command 0x28):Yet every request timed out. Crucially, the port is bound — the device
answers ICMP port-unreachable for closed ports but stays silent on 30000:
tcpdump confirms our packets arrive and nothing comes back — no reply, no ICMP:
Around 40 payload variants were tried — spec-exact
{"id":0,...},id>=1,compact JSON,
\n/\0terminators, unicast, subnet broadcast, global broadcast,from two different machines, with and without a
srcfield. All silent.Disable/enable of the Local API via BLE, an app-level reset and a full power
cycle all made no difference.
The fix: change the port. Using
Enable Local API (Custom Port)inmarstek-venus-monitor and setting 49200 made the device answer immediately:
{"id":0,"src":"VenusE-<mac>","result":{"device":"VenusE","ver":153, "ble_mac":"...","wifi_mac":"...","wifi_name":"...","ip":"192.168.1.3"}}After the change,
30000/udpreadsclosedand49200/udpreadsopen|filtered.This matches a line in the official Marstek Open API document (Rev 2.0, §2.2.1)
that reads like a workaround for exactly this:
Suggestion: mention this in the README/troubleshooting, and consider warning
during config flow when port 30000 is chosen and setup fails.
Finding 2 —
ES.*methods only answer BROADCAST; the integration sends unicastThis is the one I think explains #135 and the
ES.GetMode/ES.GetStatustimeouts in #127.
With the API working on 49200, I probed each method twice — once unicast to the
device IP, once to the subnet broadcast address, from a socket bound to the API
port:
Marstek.GetDeviceBat.GetStatusES.GetStatusES.GetModePV.GetStatusapi.py::_send_to_host()sends unicast wheneverself.hostis set, which isalways the case after a manual/DHCP config entry. So on this firmware only
Bat.GetStatusgets through.That produces exactly the symptoms reported elsewhere:
Bat.GetStatus)sensor.*_power,*_grid_power,*_operating_mode,select.*_operating_modestay
unknown, and the mode select silently does nothingES.GetMode/ES.GetStatus/EM.GetStatustimeouts in the logES.SetModebehaves the same way. Sent by the integration it has no effect;sent as broadcast it works — I verified a switch to
Passiveand back toAuto,confirmed by reading
ES.GetModeafterwards:{"id":0,"src":"VenusE-<mac>","result":{"id":0,"mode":"Auto", "ongrid_power":0,"offgrid_power":0,"bat_soc":11}}One detail worth knowing: the device always sends its reply to the API port,
regardless of the requester's source port. So a helper script can send from an
ephemeral port, but the reply then lands on the socket the integration holds.
Suggested fix: send
ES.*andMarstek.GetDeviceto the broadcast address(filtering replies by
src/IP as the code already does), or add a per-entryoption such as
use_broadcast: true. Falling back to broadcast after Nconsecutive unicast timeouts for a given method would fix this transparently.
Possibly related
That interval matches
UPDATE_INTERVAL_MEDIUM(300 s) for mode data: the valueis only ever set when a reply happens to arrive, and clears again on the next
failed poll.
EM.GetStatus/ES.GetMode/Bat.GetStatustimeouts.The
ES.*part is explained by the above; theBat.GetStatustimeouts may bea separate issue.
Happy to run further captures or test a patch — I have tcpdump and nmap available
on the host and can reproduce this on demand.