Skip to content

Fix evcc disappearing from the device list - #21

Open
CiNcH83 wants to merge 1 commit into
andig:masterfrom
CiNcH83:fix_evcc
Open

Fix evcc disappearing from the device list#21
CiNcH83 wants to merge 1 commit into
andig:masterfrom
CiNcH83:fix_evcc

Conversation

@CiNcH83

@CiNcH83 CiNcH83 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Short Description

evcc keeps disappearing from the dropdown list of found devices even though the connection is still alive and exchanging heartbeats.

Analysis

The data structure feeding the dropdown is driven purely by mDNS discovery visibility. So for some reason, evcc seems to disappear from the mDNS record.

Workaround

Keep connected devices in the list like proposed in this PR. The dropdown list should never drop a device that's actively connected, regardless of what the latest mDNS record says.

Potential Docker Fix

Claude believes that the root cause is the incomplete pure-Go Zeroconf implementation used by eebus-go as avahi is not made available to the evcc Docker container. So I also applied the following fix to my docker-compose.yml to potentially stabilize mDNS:

services:
  evcc:
    network_mode: host
    volumes:
      - /var/run/dbus:/var/run/dbus
      - /var/run/avahi-daemon/socket:/var/run/avahi-daemon/socket

Maybe we should also add the avahi/dbus volumes to the default evcc EEBus Docker config? Requires avahi to be installed on the host.

Ping @andig

@andig

andig commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Aren't volumes docker compose rather than docker? I don't know for sure. Not convinced by this change it seems to be targeting a sympton. If mDNS wasn't working we shouldn't see any announcement rather than "sometimes"?

@CiNcH83

CiNcH83 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Volumes are Docker in general, either passed via CLI (-v) or docker compose. I use docker compose. I indeed hope that with the evcc Docker container using avahi, it solves the problem. After passing the two volumes, eebus-go indeed uses avahi according to the log. avahi is the recommended mDNS provider for ship-go.

@CiNcH83

CiNcH83 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Looks like there are quite some open issues related to mDNS in ship-go, see here.

@CiNcH83

CiNcH83 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Still experiencing the problem that evcc gets lost in the controlbox dropdown. It now seems to happen when restarting evcc (not sure whether this happens always or is a race-condition). Here is the controlbox log from the evcc restart:

RemoteSKIDisconnected: b28ad70527bc3826147eb3f47e6d1b3e2831c996
VisibleRemoteServicesUpdated, count: 1
Remote SKI: 4bb4ddc3cc35facb2262ac8afde5b3cfea4b0689
RemoteSKIConnected: b28ad70527bc3826147eb3f47e6d1b3e2831c996
2026-08-13 08:09:06 ERROR operation is not supported on function electricalConnectionDescriptionListData
2026-08-13 08:09:06 ERROR operation is not supported on function electricalConnectionParameterDescriptionListData
--> LPC Event: eg-lpc-UseCaseSupportUpdate from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPP Event: eg-lpp-UseCaseSupportUpdate from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPC Event: eg-lpc-DataUpdateLimit from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPP Event: eg-lpp-DataUpdateLimit from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPP Event: eg-lpp-DataUpdateFailsafeProductionActivePowerLimit from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPC Event: eg-lpc-DataUpdateFailsafeConsumptionActivePowerLimit from b28ad70527bc3826147eb3f47e6d1b3e2831c996
Event lpc.DataUpdateLimit b28ad70527bc3826147eb3f47e6d1b3e2831c996 0
New consumption limit received: inactive, 0 W, 0s
Event lpp.DataUpdateLimit b28ad70527bc3826147eb3f47e6d1b3e2831c996 0
New production limit received: inactive, 0 W, 0s
Event lpp.DataUpdateFailsafeProductionActivePowerLimit b28ad70527bc3826147eb3f47e6d1b3e2831c996 0
--> LPP Event: eg-lpp-DataUpdateFailsafeDurationMinimum from b28ad70527bc3826147eb3f47e6d1b3e2831c996
Event lpp.DataUpdateFailsafeDurationMinimum b28ad70527bc3826147eb3f47e6d1b3e2831c996 2h0m0s
Event lpc.DataUpdateFailsafeConsumptionActivePowerLimit b28ad70527bc3826147eb3f47e6d1b3e2831c996 0
--> LPC Event: eg-lpc-DataUpdateFailsafeDurationMinimum from b28ad70527bc3826147eb3f47e6d1b3e2831c996
Event lpc.DataUpdateFailsafeDurationMinimum b28ad70527bc3826147eb3f47e6d1b3e2831c996 2h0m0s
--> LPP Event: eg-lpp-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPC Event: eg-lpc-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPP Event: eg-lpp-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPC Event: eg-lpc-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPP Event: eg-lpp-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPC Event: eg-lpc-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPP Event: eg-lpp-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPC Event: eg-lpc-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPP Event: eg-lpp-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPC Event: eg-lpc-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPP Event: eg-lpp-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPC Event: eg-lpc-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPP Event: eg-lpp-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPC Event: eg-lpc-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPP Event: eg-lpp-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPC Event: eg-lpc-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPP Event: eg-lpp-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPC Event: eg-lpc-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPC Event: eg-lpc-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPP Event: eg-lpp-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPP Event: eg-lpp-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPC Event: eg-lpc-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPC Event: eg-lpc-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPP Event: eg-lpp-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
2026-08-13 08:09:18 ERROR Certificate for SKI 4bb4ddc3cc35facb2262ac8afde5b3cfea4b0689 (CN: 21221300202609620938014543N0) expired 608 days ago
RemoteSKIDisconnected: 4bb4ddc3cc35facb2262ac8afde5b3cfea4b0689
VisibleRemoteServicesUpdated, count: 1
Remote SKI: 4bb4ddc3cc35facb2262ac8afde5b3cfea4b0689
--> LPP Event: eg-lpp-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPC Event: eg-lpc-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPP Event: eg-lpp-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPC Event: eg-lpc-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPP Event: eg-lpp-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPC Event: eg-lpc-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPP Event: eg-lpp-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPC Event: eg-lpc-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPP Event: eg-lpp-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996

controlbox and evcc reconnect but evcc does not seem to appear in the mDNS results anymore. 4bb4ddc3cc35facb2262ac8afde5b3cfea4b0689 is the heat pump.

Restarting controlbox fixes it.

@andig

andig commented Aug 13, 2026

Copy link
Copy Markdown
Owner

The log actually rules out the Docker/avahi theory. The SHIP link is alive the whole time (heartbeats from b28ad7… keep flowing), and the heat pump 4bb4dd… stays in the list — so controlbox's mDNS receive path is working fine. What's lost is only the mDNS visibility of evcc.

Sequence is: evcc restarts → goodbye → VisibleRemoteServicesUpdated, count: 1 → evcc dials back out and reconnects (no mDNS needed for that). So the remove event was processed and the matching re-add never was. It stays lost until the browser is recreated, which is why restarting controlbox fixes it.

Where that happens in ship-go:

  • The list is fed verbatim from the mDNS cache — hub/hub_mdns.go builds remoteServices straight from entries, no filtering, no union with connected services.
  • The only deletion path is mdns/mdns.go removeMdnsEntry, driven by a provider remove callback.
  • There is no re-discovery. RequestMdnsEntries only re-reports the cached map, it sends no query, and checkAutoReannounce can't recover an entry either.

Which provider controlbox uses decides the root cause:

  • avahi (default selection is avahi-first, and controlbox doesn't override it) fits the symptom exactly. ItemNew triggers a synchronous D-Bus ResolveService in mdns/avahi.go; on failure it just returns an error logged at Debug, with no retry. avahi does not re-emit ItemNew for a service already in its cache, so the entry is gone permanently. Same permanent loss if the resolved address is unusable, or one layer up in processShipMdnsEntry when a mandatory TXT element (txtvers/id/path/ski/register) is missing — silent return, no retry. An evcc restart is precisely the window where a resolve races an incomplete or empty TXT.
  • zeroconf would self-heal: it drops entries without an A/AAAA record, but periodicQuery keeps re-querying with backoff capped at 60s. That does not match "stays lost until controlbox restart".

Could you check, while it's broken, on the controlbox host:

avahi-browse -r _ship._tcp

If evcc is listed there, avahi is fine and ship-go dropped it — that pins it on the avahi resolve path and it's an enbility/ship-go issue. If evcc is absent, it's the announcing side after all. Also useful: which provider controlbox picked at startup (avahi, or the zeroconf fallback).

On the PR itself: I'd keep the change regardless. A device with a live connection should never disappear from the list — that's a UI invariant, not a workaround. But the real fix belongs upstream: ship-go should retry a failed resolve, and/or the hub should union the mDNS entries with the connected SKIs so no consumer can see this symptom (evcc's own device list has the same exposure).

Unrelated, but visible in your log: the heat pump's certificate expired 608 days ago, and its disconnect/reconnect churn is a separate problem.

🤖 Generated with Claude Code

@CiNcH83

CiNcH83 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Does not seem to happen always, like in:

RemoteSKIDisconnected: b28ad70527bc3826147eb3f47e6d1b3e2831c996
VisibleRemoteServicesUpdated, count: 2
Remote SKI: b28ad70527bc3826147eb3f47e6d1b3e2831c996
Remote SKI: 4bb4ddc3cc35facb2262ac8afde5b3cfea4b0689
VisibleRemoteServicesUpdated, count: 1
Remote SKI: 4bb4ddc3cc35facb2262ac8afde5b3cfea4b0689
VisibleRemoteServicesUpdated, count: 2
Remote SKI: b28ad70527bc3826147eb3f47e6d1b3e2831c996
Remote SKI: 4bb4ddc3cc35facb2262ac8afde5b3cfea4b0689
VisibleRemoteServicesUpdated, count: 2
Remote SKI: 4bb4ddc3cc35facb2262ac8afde5b3cfea4b0689
Remote SKI: b28ad70527bc3826147eb3f47e6d1b3e2831c996
RemoteSKIConnected: b28ad70527bc3826147eb3f47e6d1b3e2831c996
2026-08-13 09:13:29 ERROR operation is not supported on function electricalConnectionDescriptionListData
2026-08-13 09:13:29 ERROR operation is not supported on function electricalConnectionParameterDescriptionListData
--> LPC Event: eg-lpc-UseCaseSupportUpdate from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPP Event: eg-lpp-UseCaseSupportUpdate from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPP Event: eg-lpp-DataUpdateFailsafeProductionActivePowerLimit from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPC Event: eg-lpc-DataUpdateFailsafeConsumptionActivePowerLimit from b28ad70527bc3826147eb3f47e6d1b3e2831c996
Event lpp.DataUpdateFailsafeProductionActivePowerLimit b28ad70527bc3826147eb3f47e6d1b3e2831c996 0
--> LPP Event: eg-lpp-DataUpdateFailsafeDurationMinimum from b28ad70527bc3826147eb3f47e6d1b3e2831c996
Event lpc.DataUpdateFailsafeConsumptionActivePowerLimit b28ad70527bc3826147eb3f47e6d1b3e2831c996 0
--> LPC Event: eg-lpc-DataUpdateFailsafeDurationMinimum from b28ad70527bc3826147eb3f47e6d1b3e2831c996
Event lpc.DataUpdateFailsafeDurationMinimum b28ad70527bc3826147eb3f47e6d1b3e2831c996 2h0m0s
Event lpp.DataUpdateFailsafeDurationMinimum b28ad70527bc3826147eb3f47e6d1b3e2831c996 2h0m0s
--> LPC Event: eg-lpc-DataUpdateLimit from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPP Event: eg-lpp-DataUpdateFailsafeProductionActivePowerLimit from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPP Event: eg-lpp-DataUpdateLimit from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPC Event: eg-lpc-DataUpdateFailsafeConsumptionActivePowerLimit from b28ad70527bc3826147eb3f47e6d1b3e2831c996
Event lpc.DataUpdateLimit b28ad70527bc3826147eb3f47e6d1b3e2831c996 0
New consumption limit received: inactive, 0 W, 0s
Event lpp.DataUpdateFailsafeProductionActivePowerLimit b28ad70527bc3826147eb3f47e6d1b3e2831c996 0
--> LPP Event: eg-lpp-DataUpdateFailsafeDurationMinimum from b28ad70527bc3826147eb3f47e6d1b3e2831c996
Event lpp.DataUpdateFailsafeDurationMinimum b28ad70527bc3826147eb3f47e6d1b3e2831c996 2h0m0s
Event lpp.DataUpdateLimit b28ad70527bc3826147eb3f47e6d1b3e2831c996 0
New production limit received: inactive, 0 W, 0s
Event lpc.DataUpdateFailsafeConsumptionActivePowerLimit b28ad70527bc3826147eb3f47e6d1b3e2831c996 0
--> LPC Event: eg-lpc-DataUpdateFailsafeDurationMinimum from b28ad70527bc3826147eb3f47e6d1b3e2831c996
Event lpc.DataUpdateFailsafeDurationMinimum b28ad70527bc3826147eb3f47e6d1b3e2831c996 2h0m0s
--> LPP Event: eg-lpp-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPC Event: eg-lpc-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPP Event: eg-lpp-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPC Event: eg-lpc-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPC Event: eg-lpc-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPP Event: eg-lpp-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPC Event: eg-lpc-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPP Event: eg-lpp-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPP Event: eg-lpp-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPC Event: eg-lpc-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPP Event: eg-lpp-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPC Event: eg-lpc-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPP Event: eg-lpp-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPC Event: eg-lpc-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPP Event: eg-lpp-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPC Event: eg-lpc-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
2026-08-13 09:13:38 ERROR Certificate for SKI 4bb4ddc3cc35facb2262ac8afde5b3cfea4b0689 (CN: 21221300202609620938014543N0) expired 608 days ago
RemoteSKIDisconnected: 4bb4ddc3cc35facb2262ac8afde5b3cfea4b0689
VisibleRemoteServicesUpdated, count: 2
Remote SKI: b28ad70527bc3826147eb3f47e6d1b3e2831c996
Remote SKI: 4bb4ddc3cc35facb2262ac8afde5b3cfea4b0689
--> LPP Event: eg-lpp-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPC Event: eg-lpc-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPC Event: eg-lpc-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPP Event: eg-lpp-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPP Event: eg-lpp-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPC Event: eg-lpc-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPP Event: eg-lpp-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996
--> LPC Event: eg-lpc-DataUpdateHeartbeat from b28ad70527bc3826147eb3f47e6d1b3e2831c996

Can't reproduce right now.

@CiNcH83

CiNcH83 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

I probably fixed the bad situation when I previously restarted controlbox. So the stack probably needs to go in the bad state first...

Does it make sense to update eebus-go in controlbox?

@CiNcH83

CiNcH83 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Small addition... I am quite sure that controlbox uses avahi. At least it is installed on the Raspberry on which controlbox runs.

I can execute avahi-browse -r _ship._tcp when it is broken again.

@CiNcH83

CiNcH83 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Again in that state after restarting evcc:
image

VisibleRemoteServicesUpdated, count: 1
Remote SKI: 4bb4ddc3cc35facb2262ac8afde5b3cfea4b0689

Only Vaillant heat pump is reported by controlbox.

Output of avahi-browse -r _ship._tcp:

Pi@raspberrypi:~/repos/controlbox $ avahi-browse -r _ship._tcp
+ docker0 IPv4 Demo-ControlBox-123456789                     _ship._tcp           local
+ br-39d70eec1672 IPv4 Demo-ControlBox-123456789                     _ship._tcp           local
+ br-0cd7d3b1f2a9 IPv4 Demo-ControlBox-123456789                     _ship._tcp           local
+   eth0 IPv6 EVCC_HEMS_01                                  _ship._tcp           local
+   eth0 IPv6 Demo-ControlBox-123456789                     _ship._tcp           local
+   eth0 IPv4 EVCC_HEMS_01                                  _ship._tcp           local
+   eth0 IPv4 vr921_21221300202609620938014543N0            _ship._tcp           local
+   eth0 IPv4 Demo-ControlBox-123456789                     _ship._tcp           local
+     lo IPv4 Demo-ControlBox-123456789                     _ship._tcp           local
=   eth0 IPv4 EVCC_HEMS_01                                  _ship._tcp           local
   hostname = [raspberrypi5-2.local]
   address = [192.168.1.8]
   port = [4712]
   txt = ["cat=2" "serial=EVCC-3530303362373638" "register=false" "type=EnergyManagementSystem" "model=HEMS" "brand=EVCC" "ski=b28ad70527bc3826147eb3f47e6d1b3e2831c996" "id=EVCC-3530303362373638" "path=/ship/" "txtvers=1"]
=   eth0 IPv4 vr921_21221300202609620938014543N0            _ship._tcp           local
   hostname = [vr921-21221300202609620938014543N0.local]
   address = [192.168.1.6]
   port = [12480]
   txt = ["txtvers=1" "id=21221300202609620938014543N0" "path=/ship/" "register=false" "ski=4bb4ddc3cc35facb2262ac8afde5b3cfea4b0689" "brand=Vaillant" "type=Gateway" "model=VR921" "cat=4"]
=   eth0 IPv4 Demo-ControlBox-123456789                     _ship._tcp           local
   hostname = [raspberrypi.local]
   address = [192.168.1.7]
   port = [4713]
   txt = ["cat=1" "serial=123456789" "register=false" "type=ElectricitySupplySystem" "model=ControlBox" "brand=Demo" "ski=6988789f96be5797a3fc62614ddf9154367036bb" "id=ControlBox Simulator SN-123456789" "path=/ship/" "txtvers=1"]
=   eth0 IPv6 Demo-ControlBox-123456789                     _ship._tcp           local
   hostname = [raspberrypi.local]
   address = [fd2d:240f:cafa:7b4f:e811:c64f:f4f6:68b0]
   port = [4713]
   txt = ["cat=1" "serial=123456789" "register=false" "type=ElectricitySupplySystem" "model=ControlBox" "brand=Demo" "ski=6988789f96be5797a3fc62614ddf9154367036bb" "id=ControlBox Simulator SN-123456789" "path=/ship/" "txtvers=1"]
= docker0 IPv4 Demo-ControlBox-123456789                     _ship._tcp           local
   hostname = [raspberrypi.local]
   address = [172.17.0.1]
   port = [4713]
   txt = ["cat=1" "serial=123456789" "register=false" "type=ElectricitySupplySystem" "model=ControlBox" "brand=Demo" "ski=6988789f96be5797a3fc62614ddf9154367036bb" "id=ControlBox Simulator SN-123456789" "path=/ship/" "txtvers=1"]
= br-39d70eec1672 IPv4 Demo-ControlBox-123456789                     _ship._tcp           local
   hostname = [raspberrypi.local]
   address = [172.18.0.1]
   port = [4713]
   txt = ["cat=1" "serial=123456789" "register=false" "type=ElectricitySupplySystem" "model=ControlBox" "brand=Demo" "ski=6988789f96be5797a3fc62614ddf9154367036bb" "id=ControlBox Simulator SN-123456789" "path=/ship/" "txtvers=1"]
= br-0cd7d3b1f2a9 IPv4 Demo-ControlBox-123456789                     _ship._tcp           local
   hostname = [raspberrypi.local]
   address = [172.19.0.1]
   port = [4713]
   txt = ["cat=1" "serial=123456789" "register=false" "type=ElectricitySupplySystem" "model=ControlBox" "brand=Demo" "ski=6988789f96be5797a3fc62614ddf9154367036bb" "id=ControlBox Simulator SN-123456789" "path=/ship/" "txtvers=1"]
=     lo IPv4 Demo-ControlBox-123456789                     _ship._tcp           local
   hostname = [raspberrypi.local]
   address = [127.0.0.1]
   port = [4713]
   txt = ["cat=1" "serial=123456789" "register=false" "type=ElectricitySupplySystem" "model=ControlBox" "brand=Demo" "ski=6988789f96be5797a3fc62614ddf9154367036bb" "id=ControlBox Simulator SN-123456789" "path=/ship/" "txtvers=1"]
=   eth0 IPv6 EVCC_HEMS_01                                  _ship._tcp           local
   hostname = [raspberrypi5-2.local]
   address = [fd2d:240f:cafa:7b4f:c458:1516:6227:28ca]
   port = [4712]
   txt = ["cat=2" "serial=EVCC-3530303362373638" "register=false" "type=EnergyManagementSystem" "model=HEMS" "brand=EVCC" "ski=b28ad70527bc3826147eb3f47e6d1b3e2831c996" "id=EVCC-3530303362373638" "path=/ship/" "txtvers=1"]

Only restarting controlbox fixes it.

@andig

andig commented Aug 14, 2026

Copy link
Copy Markdown
Owner

Thanks, that avahi-browse output settles it — and it rules out my own leading theory from the last comment.

What the output proves

evcc is in avahi, fully resolved: EVCC_HEMS_01 on eth0, 192.168.1.8:4712, with all five mandatory TXT elements present (txtvers, id, path, ski, register), and the SKI matches the one you're exchanging heartbeats with. So avahi is fine and the announcing side is fine.

Two things die here: the Docker/avahi-socket theory, and the "missing mandatory TXT element" branch I raised last time. ship-go dropped the entry, not the network.

What I verified in ship-go v0.6.0 (the version this repo pins)

The list is a verbatim projection of the mDNS map. hub/hub_mdns.go ReportMdnsEntries builds remoteServices by ranging over entries with no filtering — isSkiConnected is consulted only for connection initiation, never for list membership. So the premise of this PR is exactly right: nothing today stops a live connection from vanishing off the list.

Every failure branch in the avahi provider is terminal. mdns/avahi.go chanListener logs the error at Debug and moves on — no retry, no re-query, no re-resolve. Once an ItemNew is lost it stays lost until the browser is recreated, which is precisely why only a controlbox restart recovers it.

Two tempting theories I had to discard after reading the code:

  • select reordered add before remove — no. go-avahi's handleSignals is a single goroutine and DispatchSignal does a blocking send on an unbuffered channel, so add and remove can never be in flight at the same time. Ordering is preserved.
  • ResolveService deadlocks against the signal pump — no. It's an ordinary D-Bus method call; godbus delivers the reply on the reply path, not the signal channel.

A version bump won't help. ship-go HEAD has byte-identical resolve-error handling. attemptReconnect covers D-Bus connection loss, not per-service resolve failure.

The blocker is in controlbox

Three candidates remain, all terminal, all logged at Debug:

branch message
resolve failed error resolving service: EVCC_HEMS_01 …
resolved address unusable service provides unusable address: …
ItemNew never fired (nothing)

controlbox_log.go throws all three away — Trace, Tracef, Debug and Debugf are stubs with their bodies commented out. The one line that would identify the root cause is being discarded by controlbox itself.

Could you uncomment those four bodies, reproduce, and look for mdns: avahi - around the evcc restart? A message pins it on ship-go's resolve path and makes it an enbility/ship-go issue; no message pins it on avahi not re-emitting ItemNew for the re-announcement.

On the PR

I'd merge it. A device with a live connection disappearing from a discovery list is an invariant violation, not a cosmetic workaround — and per ReportMdnsEntries above, evcc's own device list has the identical exposure. The upstream fix (retry failed resolves, and/or union the connected SKIs into the hub's list so no consumer can see this at all) is a separate and slower track.

One note on the diff, not a blocker: it reads h.isConnected and h.currentRemoteServices without holding h.mutex, while isConnected is written from RemoteSKIConnected/RemoteSKIDisconnected. That's pre-existing in this file rather than introduced here — the existing reads elsewhere are unguarded too — so it's fine to merge as is, but the map is worth putting under the mutex at some point.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants