Skip to content

Two root causes found: port 30000 binds but never serves, and ES.* methods only answer broadcast #136

Description

@Kreeftenboer

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions