Integrate go-eCharger into Victron Energies Venus OS.
This is a fork of vikt0rm/dbus-goecharger that additionally enables optional, active charge control (Auto/Manual/Scheduled mode with PV surplus) directly from the Venus OS GUI.
With the scripts in this repo it should be easy possible to install, uninstall, restart a service that connects the go-eCharger to the VenusOS and GX devices from Victron. Idea is inspired on @fabian-lauer and @trixing project linked below, many thanks for sharing the knowledge:
- https://github.com/fabian-lauer/dbus-shelly-3em-smartmeter
- https://github.com/trixing/venus.dbus-twc3
- Lifetime energy fixed:
/Ac/Energy/Forwardincorrectly usedwh(session energy) instead ofeto(lifetime energy) whenHardwareVersion >= 4in the original. Both values were effectively swapped/wrong. - Session energy/time added:
/Session/Energyand/Session/Timewere missing entirely. The Venus OS GUI/VRM reads these paths for the "Session" display -/ChargingTimealone (the only thing the original sets) is marked deprecated in the official Victron docs and is no longer used by the GUI for the session display. /Connectednow reflects actual reachability. The original (and this fork, until now) only ever set/Connected = 1once at startup and never updated it again./Connected(and/Status) now correctly switch to "disconnected" if the go-e becomes unreachable (e.g. a mobile wallbox on a different network) and back once it responds again - see "Findings" below for the full story.
Everything below is disabled by default except /AutoStart. Without
EnableChargeControl, this script behaves exactly like the original -
monitoring only, /Mode stays read-only.
- Real Auto/Manual/Scheduled charge mode, controllable directly from the Venus OS "Charge mode" page (in the original, this selector has no effect at all - see Restrictions below).
- PV surplus push: reads PV/grid/battery power from Venus OS and forwards
it to the go-e's own Eco mode (
pGrid/pPv/pAkku), including automatic phase switching handled by the go-e firmware itself. The go-e's own algorithm computes and regulates the actual charge current live - this script does not compute a current itself. - Scheduled mode: activates the go-e's own "Daily Trip" mode (
lmo=5) - target energy, time, and tariff settings remain defined in the go-e app; this script only switches the top-level mode. - Grid target (
pgt): read (not written) to optionally compensate for its effect on the battery buffer feature below - configure the actual value directly in the go-e app. - Battery priority: the EV only charges once the home battery reaches a configurable minimum SOC (with hysteresis against flapping).
- Battery as a charging buffer, with a force-start tier: above a configurable SOC, additional battery power is reported as virtual surplus (optionally gated on real PV also being present); above a second, higher SOC, this applies unconditionally even with zero PV.
/AutoStartrepurposed as a configurable phase-switching override - see its own section below (works independently ofEnableChargeControl).- Prevent home battery discharge during Manual/Scheduled charging - optional, mirrors an equivalent evcc feature; see "Configuration reference" and "Findings" below.
- Grid current safety limit - optional, protects the house connection fuse (SLS) against overload from the EV charger plus other simultaneous loads, in all charge modes; see "Configuration reference" and "Findings" below.
- Sync on external change: if the mode (or, for
/AutoStart,psm) is changed directly in the go-e app, the corresponding Venus OS path follows automatically.
See config.ini.example and "Configuration reference" below for every
option, and "Findings" for the reasoning and live-testing evidence behind
each of these.
Like /StartStop and /SetCurrent, this works regardless of the
EnableChargeControl setting.
Victron's official evcharger dbus spec documents /AutoStart as "start
automatically when a vehicle is connected" - a concept the go-e has no direct
equivalent for. Since this path is otherwise permanently non-functional
(neither the original script nor this fork historically wrote anything to
it, leaving the GUI button greyed out with nothing behind it), it is
deliberately repurposed here for something genuinely useful instead:
manual override of go-e's phase-switching logic (psm).
What the two toggle positions actually do is configurable via
AutoStartMode in config.ini - read once at startup only (a config edit
needs a service restart to take effect, since this defines the meaning of a
control path rather than a tuning value):
AutoStartMode = 0(default): disabled - the button has no function at all,psmis never touched. This is the default deliberately: a repurposed control path should never be silently active on an installation that didn't ask for it, matching howEnableChargeControlalso defaults to off.AutoStartMode = 1("1P-Auto"):/AutoStart = 0->psm = 1(force single-phase);/AutoStart = 1->psm = 0(Auto - go-e's own live, surplus-based 1-/3-phase switching).AutoStartMode = 2("3P-Auto"):/AutoStart = 0->psm = 2(force three-phase);/AutoStart = 1->psm = 0(Auto).AutoStartMode = 3("1P-3P"):/AutoStart = 0->psm = 1(force single-phase);/AutoStart = 1->psm = 2(force three-phase) -psm = 0(Auto) is never used in this mode.
An invalid or unrecognized value (e.g. true, or a number outside 0-3) logs
a warning and falls back to 0 rather than preventing the service from
starting.
This was added because a household with little PV surplus most of the time may prefer to default to forced single-phase and only occasionally check whether enough surplus exists for 3-phase, rather than constantly running Auto - modes 1 and 3 both suit that use case, depending on whether the "probing" toggle position should hand control back to Auto or force 3-phase outright.
Important caveat, by direct analogy with the frc relay-click findings
below: phase switching is reported by other users to involve a real, timed
contactor changeover (~10s to complete), not a soft parameter - so this
control goes through the same "only write if the value actually changed"
tracking as frc does, to avoid unnecessary physical switching. Confirmed
live that the toggle correctly forces single-phase charging via the go-e
app.
External change detection: if psm is changed directly in the go-e app
instead of via the Venus OS toggle, this is detected the same way external
lmo changes are, and /AutoStart is updated to match - the exact mapping
depends on AutoStartMode (mode 0 leaves /AutoStart untouched entirely,
since the toggle has no function to reflect).
Controlling /SetCurrent, /StartStop and /MaxCurrent works as in the
original. The native Venus OS "Auto" mode for third-party charging stations
(as opposed to the official Victron EVCS hardware) is not supported by the
platform's architecture - Venus OS does not forward any computed values for
this. This fork solves that by independently reading the PV surplus
values from Venus OS and passing them directly to the go-e's own Eco mode -
entirely independent of the (for third-party chargers ineffective) native
Venus OS automatic mode.
Phase switching (1/3-phase) is deliberately left to the go-e's own firmware
logic (psm, spl3) and is not manipulated by this script beyond the
/AutoStart override above.
amp/frc/psm whenever their target value changes, and
pGrid/pPv/pAkku roughly every 5 seconds continuously while in Auto
mode. Older go-e hardware/firmware (API v1) is documented elsewhere to
have genuine flash write-cycle concerns for at least some of these same
keys - this fork has no version check, no rate-limiting beyond
change-detection, and no safeguard of any kind against this. If you're
running older hardware, verify independently whether these concerns
still apply to your specific device/firmware before enabling
EnableChargeControl - this fork provides no protection against flash
wear on such hardware.
Visible startup confirmation even at Logging = WARN: at WARN,
essentially all other logging is filtered out - a restarted service left no
confirmation that it had actually started successfully. One line is logged
at WARNING level specifically so it remains visible regardless of the
configured level: "dbus-goecharger started successfully...". (A periodic
heartbeat at WARNING was tried too, but reverted - that level is meant for
something actually noteworthy, not routine "still running" confirmation.
Switch Logging to INFO/DEBUG temporarily for periodic confirmation.)
config.ini.example intentionally only has short comments - full
explanations for every option are here instead, grouped the same way as in
the file.
AccessType, SignOfLifeLog, Deviceinstance, HardwareVersion,
AcPosition, Host, PauseBetweenRequests are all required - there is no
code-level fallback for any of them, so the service will crash on startup if
one is missing. Every other setting in this document has a code-level
default and can be safely omitted.
AccessType(restart): alwaysOnPremise.SignOfLifeLog(restart): minutes between periodic status log entries.Deviceinstance(restart): this device's Venus OS D-Bus instance number.HardwareVersion(restart): go-e hardware generation. Only affects the temperature reading (/MCU/Temperature) - energy calculation (eto) is handled uniformly via the API v2 unit (Wh) in this fork, since/api/statusalways returns API v2 data. If your device predates API v2 (older API v1 hardware), see the flash-wear warning under "Restrictions" above before enablingEnableChargeControl.AcPosition(restart):0= AC Output (critical loads),1= AC Input.Logging(restart):DEBUG/INFO/WARN/etc. AtWARN, one line is still logged on successful startup regardless of this setting - see "Restrictions" above.
EnableChargeControl(restart): whenfalseor omitted, this script behaves exactly like the original - monitoring only,/Modenot writable, no writes tolmo/fup/scheduler, no PV surplus push. Every option below this line has no effect in that case.
The EV only starts charging once the home battery has reached a configured SOC.
BatteryPriorityMinSoc(live):0or omit = feature disabled (default0).BatteryPriorityHysteresis(live): in percentage points. Charging pauses belowBatteryPriorityMinSocand is only released again once SOC reachesBatteryPriorityMinSoc + hysteresis- prevents flapping right at the threshold. Default2.
Above a second, higher SOC, a configurable amount of home battery power is
allowed to help charge the EV too (virtual surplus added to pGrid).
BatterySupportMinSoc(live):0or omit = feature disabled (default0).BatterySupportPower(live): additional power in W reported to the go-e as virtual surplus. Should not exceed the system's actual discharge limit, otherwise the difference is drawn from the grid instead. Default0(meaning: no effect until you set both this andBatterySupportMinSoc).BatterySupportHysteresis(live): in percentage points - the buffer stays active down toBatterySupportMinSoc - hysteresis. Default2.BatterySupportCompensatePgt(live):1(nottrue- this is read as a number) compensates forpgt's continuous reserve while the buffer is active, by addingpgt's magnitude on top ofBatterySupportPower- without this,pgt(configured in the go-e app, see below) reduces the amount that actually reaches the charge current calculation below what you configured here. The exact relationship betweenpgtand the resulting current reduction has only been empirically observed, not confirmed as a precise formula - this is a reasonable approximation, not a guaranteed exact match.0or omit = disabled (default).BatterySupportMinPv(live): minimum real PV production (pPv, in W) required alongside the SOC threshold before the virtual surplus boost is actually applied. Without this, the SOC threshold alone is sufficient to trigger the boost - a fully-charged battery sitting above the threshold at night, with no PV production at all, would still make the go-e think there is solar surplus and start charging purely because of a high SOC, with no sun involved. This does not affect the SOC-based eligibility flag itself (_batterySupportActive, which still only uses the hysteresis logic above, so it doesn't flap due to momentary PV fluctuations) - it only gates whether the boost is actually sent on a given cycle.0or omit = disabled (default, SOC-only behaviour, matching the original implementation) - this reliably means "no gating at all" even if realpPvhappens to read slightly negative.BatteryForceStartSoc(live): a third, higher SOC threshold (evcc calls the equivalent setting on its Battery page "bufferStartSoc") - above this level,BatterySupportMinPv's gate is bypassed entirely and the boost applies unconditionally, even with zero real PV. Tracked with its own hysteresis state (reusingBatterySupportHysteresis), independent of theBatterySupportMinSoceligibility flag. Together, the three thresholds form a coherent tier system, e.g.BatteryPriorityMinSoc=60,BatterySupportMinSoc=90(needs real PV too),BatteryForceStartSoc=95(starts regardless of PV): below 60% the EV doesn't charge at all; 60-90% is normal PV-only Auto behaviour; 90-95% additionally allows the battery to help, but only alongside genuine PV production; above 95% the battery alone is considered enough to start charging even in complete darkness.0or omit = disabled (default).
PvAverageResetCycles(live): how many cycles of pushingpGrid/pPv/pAkku=0 to send after leaving Auto mode (see "Findings" below for the full background).0or omit = disabled (default) - the go-e's rolling averages then simply stay frozen at whatever they were until real values are pushed again.5was confirmed reliable in real Auto-mode operation when this was tested, but defaults to off, consistent with every other feature in this fork defaulting to disabled until explicitly enabled - and because the exact mechanism behind why the go-e's averages behave the way they do remains a partial black box (see "Findings" below), so a value that worked in one specific test shouldn't be silently active everywhere.
Not configured here - set it directly in the go-e app instead (App: PV
surplus -> Grid target / Power preference). See "Findings" below for the
reasoning. This script still reads the current value (only for
BatterySupportCompensatePgt above), never writes it.
AutoStartMode(restart): see the dedicated section above for the full enumeration and reasoning. Read once at startup only - a config.ini edit here needs a service restart, unlike every other option on this page.
-
PreventBatteryDischarge(live):0or omit = disabled (default). When1, the battery's current SOC is temporarily written to/Settings/CGwacs/BatteryLife/MinimumSocLimitwhile the car is actually charging (go-ecar==2) in Manual or Scheduled mode, restoring the original value once charging stops or the option is disabled - the same mechanism evcc uses for Victron systems. Tied to actually charging, not merely to the mode itself - switching to Manual is also commonly used just to disable Eco mode without charging at all (e.g. to use/AutoStart's phase-switching override), and the lock must not engage in that case (found live - see "Findings" below). Applies in both Manual and Scheduled, since Scheduled (the go-e's own Daily Trip) can draw on the battery outside genuine PV surplus too; Auto is deliberately excluded, as it already has its own PV-aware battery logic (BatterySupportMinSoc/BatteryForceStartSoc). See "Findings" below for more reasoning, why this mechanism was chosen over other candidates, and importantly, a real, repeatedly-reported failure mode from evcc's own equivalent feature that applies here too - read this before enabling. Checked every cycle, so it correctly reacts either via the Venus OS/Modeswitch or an externally-detected go-e app mode change. A manual change toMinimumSocLimitdirectly in the Venus OS GUI while the lock is already active is detected and adopted as the new value to restore to later, rather than being silently overwritten. Note: this only governs the ESS inverter's discharge behaviour - DC-coupled PV can still charge the battery normally while the lock is active, with that energy unavailable to the car for the rest of the session; see "Findings" below. -
CheckEssMinSocAtStartup(restart):0or omit = disabled (default). When1,ExpectedEssMinSoc(below) is compared once at every service start against the actual/Settings/CGwacs/BatteryLife/MinimumSocLimit; a mismatch is treated as evidence the lock above was left stuck active after a crash/unclean shutdown (see "Findings" below) and is corrected automatically. A one-time startup check only, not a continuous watchdog - deliberately kept simple rather than building more elaborate monitoring. Follows this fork's normal enable/disable pattern, unlikeExpectedEssMinSocitself, which can't (0is a genuinely validMinimumSocLimiton some systems). -
ExpectedEssMinSoc(restart): the value to compare against - only consulted whenCheckEssMinSocAtStartup=1. If the switch is on but this isn't set, a warning is logged and the check is skipped rather than failing. -
GridCurrentLimitMode(live):0or omit = off (default, feature fully skipped).1= log only - the full state machine below runs exactly as it would live, including everyWARNING-level log line, but the actualamp/frcwrite calls are skipped entirely. Lets you observe what this feature would do against your real house's load patterns before trusting it to actually intervene - every log line carries an explicit[LOG ONLY, not applied]prefix so it's unambiguous.2= active, writesamp/frcfor real. -
GridCurrentLimit(live): the per-phase current limit itself, in Amps (e.g. a "35A SLS" means35, since each phase of a typical 3-phase house connection is individually rated at that current, not the sum). Only takes effect whenGridCurrentLimitModeis1or2- protects the house connection fuse (SLS) against overload from the EV charger plus other simultaneous loads. Applies in ALL charge modes as a safety overlay - the only feature in this fork that writesamp/frcduring Manual. See "Findings" below for the full design rationale and why this monitors actual per-phase current, not power. -
GridCurrentSafetyMargin(live): default3(A). Subtracted fromGridCurrentLimitto get the trigger threshold - e.g.35and3together trigger at32A, not exactly at the SLS's own trip point. -
GridCurrentReleaseMargin(live): default2(A), on top ofGridCurrentSafetyMargin. Deliberate hysteresis: releasing back to full current requires the phase current to drop further below the trigger threshold than what triggered the reduction in the first place (e.g. trigger at32A, only release below30A) - prevents flapping back and forth if the load happens to hover right around a single shared threshold. Safety-first by design: when in doubt (current sitting between the two thresholds), this fork holds whatever state it's currently in rather than switching either way. -
GridCurrentMinAmp(live): default6(A). The safe minimum this fork reducesampto once triggered, before considering a full pause. -
GridCurrentSustainedCycles(live): default3. Consecutive over-threshold cycles required before escalating one step (normal -> reduced -> paused) - avoids reacting to a single brief spike. -
GridCurrentReleaseCycles(live): default3. Consecutive under-threshold cycles required before fully restoring normal operation - fully automatic, no manual re-enable needed.
Both required (no code-level fallback).
Host(restart): the go-e's IP address.PauseBetweenRequests(restart): poll interval in ms. Must stay at or below 5000, since the go-e expectspGrid/pPv/pAkkuto be updated at least every 5 seconds in Auto mode (see "Findings" below).
Every option above is now individually marked (live) or (restart). In
short: AutoStartMode, CheckEssMinSocAtStartup, and ExpectedEssMinSoc
need a restart because each defines the meaning of a control path or is
only ever consulted once, at genuine startup. AccessType, Deviceinstance,
HardwareVersion, AcPosition, Logging, Host, PauseBetweenRequests,
and EnableChargeControl are also read once at startup - these still need
restart.sh after editing, since nothing re-reads them mid-run. Everything
else is actively re-read every Auto-mode cycle (or, for
PreventBatteryDischarge, every cycle regardless of mode) without
needing a restart at all.
At Logging = DEBUG, every cycle also logs a single summary line:
State: car=<X> SOC=<X>% amp_ceiling=<X> frc=<X> ESS_MinSoc=<X>% flags=[...].
Everything else is logged only once per value or only on change, so
reconstructing the full picture at one point in time previously meant
cross-referencing several separate log lines, or falling back to
pvcheck.sh alongside the log. This line exists purely to make deep
troubleshooting from the log alone possible - it has no effect on
behaviour.
Previously logged only the highest reading across all three phases without
saying which one it came from - found live to matter for diagnosing a real
overload (e.g. an unevenly-distributed house load). Every GridCurrentLimit
log line now includes the phase name (L1/L2/L3), e.g. "L2 phase current 18.0A exceeds...".
Every top-level method's except Exception handler used CRITICAL with a
full multi-frame traceback uniformly - appropriate for an actual
programming error, but also applied to a routine, transient network
hiccup (go-e briefly unreachable over WiFi, a request timing out).
Confirmed live: the service kept running normally on the very next cycle
either way, yet the log made a harmless, self-recovering blip look as
alarming as a real crash.
Centralized into _logMethodError(), used by every one of these handlers
except main()'s own (a failure there means the whole service failed to
start, a materially more serious category that stays CRITICAL
regardless of cause). Distinguishes using the requests library's own
exception hierarchy: anything under requests.exceptions.RequestException
(timeouts, connection errors) gets a single short WARNING line with just
the exception's own message - confirmed live: "Network issue in _pushPvSurplusValues (go-e temporarily unreachable? will retry next cycle): ReadTimeout: Read timed out." Anything else (an actual bug -
KeyError, AttributeError, TypeError, etc.) is still logged at
CRITICAL with the full traceback exactly as before, confirmed live to
still work unchanged - this is a distinction, not a general "make errors
quieter" change.
Everything below was found through live testing against one specific device (go-e V4) and isn't officially documented by go-e - some of it may differ on other firmware/hardware versions. Grouped by subsystem, in roughly the order these were discovered.
Firmware note: all findings below were originally established against
firmware 60.6. This installation has since switched to 59.4 (the
current stable release - 60.6 was itself a go-e beta, deliberately
moved away from in favor of stable; only Venus OS currently still runs a
beta, kept for now specifically because of a Shelly integration limitation
unrelated to this fork). Confirmed on 59.4 through dedicated live
re-testing covering: Auto/Manual/Scheduled mode switching (both via
Venus OS and directly in the go-e app), the amp ceiling, start/stop
surplus timing, the full PreventBatteryDischarge lifecycle, the battery
buffer including its force-start tier, and battery-priority pausing. One
genuine regression was found and fixed in the process (see "switching
modes directly in the go-e app skipped this reset entirely" below) -
otherwise no behavioural differences from 60.6 were found.
The single most important finding in this document. Extensive testing
initially suggested that the go-e's Eco algorithm does not regulate the
charge current from pGrid/pPv/pAkku at all - amp stayed fixed at 6 no
matter how large the reported surplus was, across two firmware versions,
with every documented prerequisite correctly set.
The actual cause: amp is not the live-regulated value at all - it is a
ceiling the Eco algorithm will never exceed. The real, live-regulated
current is reflected in nrg[4] (Amps) / nrg[11] (Watts), never in amp
itself. Since amp had been left at 6 (e.g. from Manual mode), the Eco
algorithm was silently capped there the entire time - it may well have been
working correctly throughout, just within an artificially low ceiling that
looked, from the outside, exactly like "no regulation happening at all".
Confirmed live: with amp explicitly raised to 16, a simulated ~2070W
surplus made the real current (nrg[4]) climb from ~5.6A to ~8.2A and still
rising within 40 seconds - clear, genuine regulation that had been invisible
simply because the ceiling itself was the bottleneck being measured.
Fix implemented: whenever Auto mode is (re-)entered, amp is raised to
the device's configured maximum (ama, exposed on D-Bus as /MaxCurrent).
This is deliberately not the script computing or choosing a charge
current itself - it only removes an artificial constraint so the go-e's own
Eco algorithm has its full intended regulation range available, matching
what community integrations expect (e.g.
marq24/ha-goecharger-api2).
Independently confirmed by another fork,
gonzo7734/dbus-goecharger,
which does the exact same amp = MaxCurrent fix for the exact same reason.
Related, unresolved side note: the API key frm ("Strommengen Handling"
in the app, documented value 2 = PreferPowerToGrid) is at least anecdotally
associated by the community with the go-e choosing a lower current than
surplus would allow. This device had frm=2 throughout most testing; it was
changed to frm=1 ("Standard") before the ceiling cause was found, and
whether frm=2 would work fine once the ceiling is also raised was not
retested - worth checking if PV surplus charging ever seems overly
conservative.
A latent bug found later during a full code review: /SetCurrent
writes the same amp key that the Auto-mode ceiling logic manages, but did
so without updating the ceiling tracker. Changing the charge current in the
Venus OS GUI while in Auto mode therefore silently and permanently
re-introduced the capped-Eco-algorithm problem above. Verified reproducible,
fixed by keeping the tracker in sync on every /SetCurrent write too.
Confirmed live via an audible click on the charger itself. This means frc
writes must be minimized, not just for API politeness but to avoid
unnecessary relay wear. Several real bugs caused by this were found and
fixed, and the underlying pattern was then generalized:
- Entering Auto used to click twice. The code used to unconditionally
release charging (
frc=0) and only then, on the next cycle, evaluate battery priority - if SOC was already below threshold, this caused an immediate release followed by a re-lock within seconds. Fixed by evaluating battery priority before writing anyfrcvalue on entry. - Entering Manual used to interrupt active charging, then over-corrected.
The code used to unconditionally force charging off (
frc=1) as a "safe default" - stopping an already-active PV-surplus session just to hand control toSetCurrent/StartStop. First fixed to usefrc=0(neutral) instead - but Basic mode (lmo=3) has no PV-surplus gating of its own, sofrc=0made it start charging immediately at the fullampceiling the moment a vehicle was connected, regardless of whether it was actually charging right before the switch. Corrected again: the go-e's actualcarstate is now checked right before switching to Manual - only if it shows2(actively charging) doesfrcstay at0; otherwisefrc=1is used, taking over whatever state was genuinely active instead of always releasing. - Generalized fix: every
frcwrite anywhere in the script now goes through a single_setFrc()helper, tracking the last value commanded globally, across all modes, and only actually writing (and clicking) when the desired value differs from that tracked value. Switching Auto -> Manual -> Scheduled -> Auto whilefrcstays logically0throughout now produces zero writes and zero clicks. - Found live: the global tracking in fix 3 was itself incomplete for
genuine mode switches. Auto paused by battery priority (
frc=1already commanded there) -> switched to Manual while not actively charging (frc=1desired again, per fix 2 above) ->_setFrc()saw the value as unchanged from the previous mode and skipped the write entirely - the go-e then started charging anyway within seconds. The go-e appears to evaluatefrcper mode context rather than as a value that reliably persists across anlmochange, so an assertion made under the old mode cannot be assumed to still hold once a genuinely new mode has been entered. Fixed by resetting the tracked value at the start of every mode switch (_applyChargeMode()), so the mode-specificfrcvalue is now always freshly (re-)written on every actual switch, while the within-mode skip-if-unchanged behaviour from fix 3 (e.g. repeated Auto cycles while priority state doesn't change) is preserved.
pPv sums AC-coupled production (/Ac/PvOnGrid/L1/Power + L2 + L3,
L1 required, L2/L3 optional/contributing 0 if absent) and
/Dc/Pv/Power (DC-coupled, already system-wide). pGrid sums
/Ac/Grid/L1/Power + L2 + L3 the same way - relevant whenever the grid
connection point is three-phase even if only one phase is actually
managed by a single-phase inverter (this fork's own setup): load or
PV/battery-driven feed-in on the other two phases would otherwise be
invisible to pGrid entirely, even though it directly affects the go-e's
surplus decision. An earlier version of this fork read only L1 for
pGrid, matching
gonzo7734/dbus-goecharger's
explicit L1+L2+L3 summing for PV but missing the same treatment for grid.
Bug found and fixed: an earlier version used /Ac/PvOnGrid/Total/Power
(an aggregate that looked like it should exist) instead of explicit summing -
this path does not exist on this system (confirmed live: the dbus CLI
itself raised AttributeError, i.e. no such object). This silently degraded
to 0 via this fork's own defensive value-reading (_safeGetValue(), see
below) - a real, unnoticed loss of the entire AC-coupled contribution to
pPv (confirmed live: ~1700W of real production was missing, leaving only
~650W DC-coupled, which happened to look plausible enough on its own not to
be immediately obviously wrong). Reverted to explicit summing, matching the
approach already used for pGrid.
Related crash, found and fixed shortly after: a DBusException ("was
not provided by any .service files") from a .get_value() call - not just
at import/construction time, which was already wrapped - crashed the entire
service if the underlying Venus system service was momentarily unavailable,
or a newly added path wasn't served on a given Venus OS version. Every read
of a Venus system item now goes through a dedicated _safeGetValue()
wrapper, so any single missing/unavailable value degrades gracefully instead
of taking down the whole process.
While a vehicle is connected but not charging, the official Venus OS
evcharger status enum distinguishes several reasons (e.g. "waiting for
sun", "waiting for RFID") that go-e's car alone cannot tell apart. This
fork additionally reads go-e's modelStatus/err, and in two cases its
own internal state, to pick the correct Venus status. Full mapping,
confirmed against the official Venus OS dbus-api wiki and the official
go-e API v2 field reference:
Venus /Status |
Meaning | Set when |
|---|---|---|
0 |
Disconnected | car=null, car=1 (Idle), or car invalid |
1 |
Connected | car=3 (WaitCar) and this fork's own frc≠1 (nothing actively holding it back) |
2 |
Charging | car=2 |
3 |
Charged | car=4 and no known modelStatus below matched |
4 |
Waiting for sun | car=4 and modelStatus=17 |
5 |
Waiting for RFID | car=4 and modelStatus=2 |
6 |
Waiting for start | car=3 and this fork's own frc=1 (actively holding it back) or car=4 and modelStatus=4 |
7 |
Low SOC | Override: BatteryPriorityMinSoc pause is active (the home battery, not the EV's) |
8 |
Ground test error | car=5 and err=8 (GndInvalid) |
9 |
Welded contacts error | car=5 and err=9 (ContactorStuck) |
10 |
CP input test error (shorted) | car=5 and err=7 (PpInvalid) |
11 |
Residual current detected | car=5 and err=1 (FiAc) or err=2 (FiDc) - the RCD/FI safety trip, not a grid overload |
12 |
Undervoltage detected | unreachable - go-e's err enum has no undervoltage code |
13 |
Overvoltage detected | car=5 and err=4 (Overvolt) |
14 |
Overheating detected | car=5 and err=13 (Overtemp), or the fallback for every other err value (Phase, Overamp, Diode, ContactorMiss, FiUnknown, Unknown, NoComm, both lock-stuck codes) - Venus has no dedicated "unspecified error" code |
15-19 |
(reserved) | - |
20 |
Charging limit | car=4 and modelStatus=6 (go-e's own per-session dwo energy limit reached), or override: GridCurrentLimit is actively intervening (GridCurrentLimitMode=2 only) |
Live-confirmed: 0-7, 20. Documented against the official references
but not live-tested (no real fault has occurred during development):
8-14.
The car==4 disambiguation was corrected after live testing - it
applies when car==4, NOT car==3 as the state names alone might
suggest. On this device, go-e reports car==4 ("charging finished,
vehicle still connected") for both a genuinely completed session and
paused/force-off states.
car==3 needed a second look, and a self-correction. Per go-e's own
IFTTT integration docs, this state is named "Wait for Car" - the go-e
itself is waiting for the vehicle to request charging (CP handshake), a
normal transient state right after plugging in, not something being
actively held back. That's Status=1, not the 6 this fork used
initially - distinguished using this fork's own last-commanded frc
value, since car/modelStatus alone can't tell the two cases apart.
car==5 ("Error") was previously unhandled entirely, silently
falling through to /Status=0 ("Disconnected") and hiding a real error
behind a misleading display. car can also be null on an internal
error per the docs - handled defensively (/Status=0) instead of
crashing on int(None).
Status=7 ("Low SOC") needed a self-correction too. Initially assumed
unreachable (thought it meant the EV's own SOC, which go-e never reports
over this API) - corrected after finding real Victron community threads
about Victron's own EVCS product, where this exact status is driven by
the GX device communicating system battery SOC to the charger (e.g.
"once house batteries are charged to above start-soc, car charging will
start"). That's precisely what BatteryPriorityMinSoc represents here, so
/Status is now explicitly overridden to 7 whenever that pause is
active, instead of falling through to whatever generic reason the go-e's
own car/modelStatus happens to report for the resulting frc=1.
/Connected now reflects actual reachability, not just a startup value.
Previously set to 1 once at startup and never updated - if the go-e became
unreachable, every dbus path just kept showing its last known value forever,
with no indication anything was wrong. Now set to 0 (and /Status to 0)
as soon as a poll cycle fails, back to 1 once it responds again - only
written when it actually changes. Relevant for a mobile wallbox that isn't
always on the GX device's network. Relatedly: /FirmwareVersion and
/Serial used to only be registered if the go-e answered within the first
few hundred milliseconds of startup - since D-Bus paths can only be added
before the service registers, they'd otherwise be permanently missing for
the whole process lifetime if the charger was briefly unreachable at that
exact moment. Now always registered with placeholder values and a warning
when unavailable.
"Scheduled" activates the go-e's own "Daily Trip" mode (lmo=5). This is
not the same as the go-e's separate weekly on/off timer feature
(sch_week/sch_satur/sch_sund, available under Basic mode) - an earlier
version of this fork mapped "Scheduled" to that timer instead, which behaves
very differently despite both being reachable from a "Scheduled"-sounding
selector. All three Venus OS modes now explicitly disable the weekly timer
- if you want to use it, it must be managed directly in the go-e app.
(Writing the full timer object back initially failed with
ESP_ERR_HTTPD_RESULT_TRUNC - the go-e's ESP32 HTTP server has a limited
request buffer. Compact JSON encoding was tried first and reduced but did
not eliminate this - confirmed live: 100% reproducible on every single
mode switch, 18 failures across 6 switches in one test session, even with
compact encoding. Properly fixed by confirming live via direct curl
testing that the go-e accepts a minimal payload containing only control,
without ranges at all - and that the existing ranges remain completely
unchanged afterwards. No need to read the current object first at all
anymore; this is both simpler and reliably small enough to stay under the
buffer limit.)
Not extensively live-tested - unlike Auto and Manual, which were both tested extensively over many live sessions, Scheduled mode has no real use case for this fork's own installation and has therefore not been verified in real day-to-day use beyond confirming the basic mode switch itself works. If you rely on Scheduled/Daily Trip, please verify carefully and report back if anything doesn't behave as documented here.
Found live: switching Auto (charging on genuine surplus) -> Manual ->
Auto again later, once it had actually gone dark, caused the go-e to
immediately resume charging as if the old surplus still applied. The go-e
appears to keep its last-received pGrid/pPv/pAkku around indefinitely
while lmo != 4 (Eco isn't evaluating them, so nothing invalidates the old
reading). Fixed by reordering _applyChargeMode()'s Auto branch to push
fresh values before setting lmo=4, closing the window where Eco could
start evaluating on stale data.
This alone turned out to be insufficient. Checking pvopt_averagePGrid
etc. (internal rolling averages, undocumented but readable via
/api/status) while in Manual revealed why: these stay frozen indefinitely,
even though the raw instantaneous fields correctly go null - and appear to
be what the Eco algorithm's decisions are actually driven by. A single fresh
sample on re-entering Auto cannot override several minutes of accumulated
average.
A single {"pGrid":0,"pPv":0,"pAkku":0} reset push on leaving Auto was tried
first and confirmed live to not work - the averages stayed frozen even ~40s
later. An initial theory (averages ending in .0/.3/.7 suggesting a
3-sample window) led to pushing zeros over several consecutive cycles
instead of just one, which was confirmed live to reliably bring the
averages to 0 and keep them there. However, a later isolated test - pushing
a known sequence of values directly via curl, bypassing this script
entirely - produced averages a plain mean cannot explain (e.g. a positive
average after only negative/zero pushes, mathematically impossible for a
simple average). The 3-sample theory therefore doesn't fully hold; something
else (possibly a tariff/price signal this fork doesn't control) appears to
factor in too. The exact mechanism remains a partial black box - what's
confirmed from real operation is that pushing zeros for enough cycles
reliably works. The count is configurable (PvAverageResetCycles) rather
than hardcoded for exactly this reason, and defaults to 0 (disabled) -
given the mechanism isn't fully understood, a value confirmed to work in one
specific test shouldn't be silently active by default everywhere.
This reset, when enabled, is also applied once after a service restart, not just on a live mode switch - the go-e keeps its averages regardless of whether this script is running, so a restart while sitting on stale averages from hours earlier would otherwise never get flushed. Cancelled immediately if the first cycle finds the go-e already in Auto, so real values are never overwritten.
The same averaging lag also shows up while continuously staying in Auto,
not just when leaving and re-entering it - confirmed live with a clean,
concrete example: BatterySupportPower was increased while already
charging in Auto. The real pGrid reflected the larger boost immediately
(-3886 to -3963), but pvopt_averagePGrid stayed frozen at its old value
(-2934.7) for a full ~40s (6 cycles) before catching up to -3950.7 - the
charge current only started climbing (5.7A -> 7.1A -> 7.4A and rising) in
the very same cycle the average finally updated, not when the real value
changed. This confirms the averaging behaviour is a general property of how
the Eco algorithm evaluates surplus, not something specific to the Auto/
Manual transition case above.
Found live: switching modes directly in the go-e app skipped this reset
entirely. The Auto-mode state reset (including the rolling-average
flush, plus clearing battery-priority/support/force-start flags and the
amp ceiling tracker) lived only inside the explicit-switch code path
(_applyChargeMode(), triggered by a Venus OS /Mode write) - the
separate external-lmo-change-detection path in _update() only updated
self._chargeMode and the displayed /Mode value, without ever running
this reset. Switching Auto -> Manual directly in the go-e app therefore
silently skipped the flush (and the other resets) - confirmed live.
Fixed by extracting the reset into its own method and calling it from
both trigger paths, so it now runs identically regardless of whether the
mode change came from Venus OS or was detected from the go-e app.
- Could discharge at night with no PV involved at all. The SOC-based
eligibility check considered only charge level - a fully-charged battery
above the threshold at night, with zero PV, would still trigger the boost
and make the go-e think there was solar surplus. Fixed via
BatterySupportMinPv, requiring real PV production alongside the SOC check (see Configuration reference above) - and its own follow-up bug: the gate was implemented aspPv < minPvForSupport, so at the disabled default of0a slightly negative realpPvreading (e.g. inverter idle standby draw at night) would still incorrectly block the boost. Fixed with an explicitminPvForSupport > 0check first. - Flag could get stuck "active" if disabled mid-session. Editing
BatterySupportMinSoc/Powerto0while the buffer was already active never reset the_batterySupportActiveflag, since the code that would normally clear it lived entirely inside the now-skipped guard. Didn't cause a wrongpGridadjustment (that's inside the same skipped block), but misleadingly kept implying the feature was engaged. Fixed by explicitly resetting the flag when found disabled while previously active.
Worth noting, found via evcc's own documentation of a related limitation: even evcc - despite directly deciding start/stop and current, unlike this fork's approach of only feeding data to the go-e's own Eco algorithm - cannot prevent a home battery from covering a real-time shortfall unless the battery/inverter exposes "active battery control" (a direct hold/charge/discharge mode command, e.g. via Modbus). Without that, any system's ESS will physically balance a momentary production/demand gap from the battery regardless of what any external controller "intends" - this fork's settings control what this script itself signals to the go-e, not the underlying physical behaviour of the Multiplus/ESS system. (The same underlying limitation, applied to a different feature, resurfaces below as the DC-coupled PV note under "Preventing battery discharge during Manual charging".)
Requested to mirror an evcc option that keeps the home battery from being drawn on while manually charging the car. Three candidate mechanisms were investigated before picking one:
- VE.Bus "Switch" register (
/Modeoncom.victronenergy.vebus.*, Modbus register 33; 1=Charger Only, 2=Inverter Only, 3=On, 4=Off). Rejected as a general default: multiple independent sources confirm "Charger Only" mode disables AC-Out entirely, regardless of grid presence - not "battery left alone, everything else runs as normal", but a full loss of AC output for anything connected there. Only safe if nothing is actually wired to AC-Out (confirmed to be the case for this fork's own installation) - not implemented, since this fork is used by others whose wiring can't be assumed. - ESS-level
/Settings/CGwacs/MaxDischargePowerset to0. Confirmed via a real community project (t0bias-r/venusos_acload_prioritize) to stop discharge while still allowing PV to be used directly. Not chosen: evcc's own maintainers, in their tracking issue for this exact feature, flag this as unconfirmed whether it also disables Peak Shaving. - DVCC
/Info/MaxDischargeCurrent(the "DCL" a BMS normally publishes), artificially set to0. Confirmed behaviour: at DCL=0, the system drops to grid passthrough (AC-Out stays powered) if the grid is present, or turns the inverter off if it isn't - conceptually the cleanest option, but requires either overriding an existing real BMS's own published value (risky) or registering an entirely new virtual "battery" D-Bus service just to report this one artificial limit. Not implemented, given the added architectural complexity.
What was implemented instead, matching evcc's own actual mechanism for
Victron systems: temporarily raising /Settings/CGwacs/BatteryLife/MinimumSocLimit to
the battery's current SOC while in Manual mode (confirmed via evcc's own
GitHub issue tracker: "The current Victron integration manages the battery
discharge during fast charging by setting minimum SOC = current SOC").
Chosen for being simple, requiring no new D-Bus service, and - unlike
Switch-Mode - not touching AC-Out at all while the grid is present.
Read this before enabling PreventBatteryDischarge - a real,
repeatedly-reported failure mode from evcc's own implementation of this
exact mechanism applies here too: if the restore step fails (a transient
D-Bus error, or this script crashing/being killed while the lock is
active), the raised MinimumSocLimit can be left stuck in place until
manually corrected. This is not a hypothetical risk - it is independently
confirmed multiple times in evcc's own issue tracker, e.g.
evcc-io/evcc#16326 ("the
battery is essentially crippled... it will not discharge any further until
I change the MinSoc manually" - triggered there by a plain Modbus i/o
timeout during the restore step) and a German-language report of the exact
same pattern
(evcc-io/evcc#23557).
A third, related evcc/Victron report describes the inverter getting stuck
in a "Maintenance" state after the lock was lifted, recoverable only by
power-cycling the Multiplus
(evcc-io/evcc#27837).
Deliberately not addressed by a full watchdog/timeout mechanism - this was considered but rejected as disproportionate, given evcc itself doesn't solve this either. Two lighter-weight improvements were added instead, without going as far as continuous monitoring:
- External changes to
MinimumSocLimitwhile the lock is active are detected and respected - concretely: if you (or something else) change it manually mid-session, your new value is visible only until the next cycle (within onePauseBetweenRequestsinterval, so a few seconds) - at that point it gets overwritten again with the current SOC, same as always while charging continues, so the lock keeps working exactly as before. What changes is which value gets restored once charging actually stops: it's now your new value, not the stale one recorded when the lock was first applied. In short - your edit doesn't "stick" while charging is still ongoing, but it is remembered for when it ends. CheckEssMinSocAtStartup+ExpectedEssMinSoc: an optional, one-time check at every service start - if the actualMinimumSocLimitdoesn't match what you've configured as your normal value, this is treated as evidence of exactly the stuck-lock scenario described above (e.g. after a crash) and is corrected automatically. This is not a continuous watchdog - it only runs once, at startup - but directly catches the most common trigger for this failure mode (the service being killed/crashing and then simply being restarted, which happens automatically under daemontools).
If enabling PreventBatteryDischarge, checking
/Settings/CGwacs/BatteryLife/MinimumSocLimit after any unclean restart or crash while a
Manual charging session was active is still worth doing manually too - the
above catches the most common cases, not every conceivable one (e.g. this
script itself functioning normally throughout, but the lock never being
left in the first place due to some other, unrelated logic error).
Found live: tying the lock to the mode alone (Manual) rather than to
actually charging was wrong. Switching to Manual mode is also commonly
used simply to disable Eco mode without charging at all - e.g. to use the
/AutoStart phase-switching override, or just to stop the go-e's own
surplus evaluation temporarily - and the lock must not engage purely
because of that mode switch. Fixed: the lock now additionally requires
car==2 (go-e reports "actively charging"), checked alongside the mode, so
it only engages while a car is genuinely drawing current. Also extended
to Scheduled mode (not just Manual) at the same time, since the go-e's
own Daily Trip charging (Scheduled) is just as able to draw on the battery
outside of genuine PV surplus as Manual charging is - Auto remains
deliberately excluded, since it already has its own PV-aware battery logic
via BatterySupportMinSoc/BatteryForceStartSoc.
Found live: the initially-used path, /Settings/Ess/MinimumSocLimit, was
wrong for this fork's own installation (and for classic VE.Bus systems in
general). That path only exists under com.victronenergy.acsystem -
the newer Multi RS / "acsystem" product line, per the official
victronenergy/venus dbus wiki
(confirmed live: querying com.victronenergy.acsystem on a classic
Multiplus-II GX system raises ServiceUnknown - that service simply
doesn't exist there). The correct path for classic VE.Bus systems (Multi,
Quattro, MultiPlus-II) is /Settings/CGwacs/BatteryLife/MinimumSocLimit
under com.victronenergy.settings, confirmed live by reading back the
exact SOC value that had been set manually beforehand.
Confirmed live: MinimumSocLimit stores fractional values exactly, no
rounding to whole percent. Writing 62.7 directly and reading it back
returned exactly 62.7, not rounded to 62/63. This confirms this
fork's approach - writing whatever /Dc/Battery/Soc reports, which is
itself already limited to one decimal place by Venus OS - is safe as-is,
with no additional rounding needed before writing it.
Known limitation, not addressed by this fork: DC-coupled PV can still end
up "trapped" in the battery instead of reaching the car. MinimumSocLimit
governs the ESS inverter's own charge/discharge behaviour - it does not
instruct DC-coupled solar chargers (MPPTs wired directly to the battery) to
do anything differently. Observed live: while manually charging with the
lock active, real DC-coupled PV production kept charging the battery as
normal (independent MPPT logic, unaware of the lock), while the inverter
itself couldn't then discharge that same energy back out to help power the
car, due to the lock. The PV energy is not lost, but it ends up going into
the battery rather than being available to the car for the remainder of
that charging session - an outcome this fork's own installation (modest
DC-coupled PV) finds acceptable, but installations with substantial
DC-coupled PV capacity may reasonably judge differently, since a
meaningful amount of same-day solar could be diverted away from the car
this way. No fix is provided for this - doing so would require directly
managing the DC-coupled chargers' own behaviour, a materially bigger scope
than anything else in this fork.
go-e's own static load balancing (loe/lot/loty=0, built into the
charger, no extra hardware) only coordinates between multiple go-e
chargers - confirmed via a real go-e community discussion: "static load
balancing... does not consider your other loads like ovens or water
heaters." Genuine dynamic load balancing, which does react to other
house loads, requires the separate "go-e Controller" hardware accessory
with its own current-transformer measurement - not something every go-e
owner has.
This fork achieves an equivalent safety net using current measurement
Venus OS already has from its own grid meter, at no extra cost. Monitors
actual per-phase current (A), not power - deliberately: with typically
single-phase charging, one phase can be near its rated limit while
aggregate power still looks unremarkable. This exact concern was raised in
a real go-e API discussion about avoiding "a situation where aggregate
grid usage is 'medium', but one of the phases is near its limit." A "35A
SLS" (Selektiver Leitungsschutzschalter, the main house fuse) means 35A on
each phase of a 3-phase connection, not a 35A total - GridCurrentLimit
is specified per-phase to match.
Escalates one step at a time, each requiring GridCurrentSustainedCycles
consecutive over-threshold cycles to avoid reacting to a brief spike:
amp forced down to GridCurrentMinAmp first; only if the phase current
is still over threshold even at that reduced amp (meaning other house
loads alone already exceed the safety margin) does it escalate further to
a full pause (frc=1). If amp is already at or below GridCurrentMinAmp
at the moment of the first over-threshold detection, escalates straight to
pausing - reducing further wouldn't have helped.
Recovery is symmetric and fully automatic (per explicit request - no
manual re-enable step): GridCurrentReleaseCycles consecutive
under-threshold cycles restore the exact amp/frc values that were in
effect immediately before this fork's own intervention, read live from the
go-e at the moment of first intervening rather than assumed from mode
logic. This matters specifically for Manual mode, where this fork
otherwise never touches amp at all - restoring the live-captured value
(rather than e.g. the device's absolute max) correctly hands control back
to whatever current the user had actually set themselves, not an assumed
default. In Auto mode, restoring is effectively a no-op in practice, since
that mode's own per-cycle ceiling logic reasserts itself immediately
afterwards regardless.
Deliberately not implemented: using ama (the go-e's own hardware max
current field) instead of amp as the override lever - this was
considered, since it would act as a cleaner, additive ceiling independent
of whatever amp any mode logic wants. Rejected because restoring ama
to "normal" on release would need its own separate known-good value
(the device's true installed maximum), rather than simply replaying back
whatever was live-captured moments before, as amp allows.
GridCurrentLimitMode provides a dry-run step before trusting this with
real hardware. Since this is the one feature in this fork that can pause
charging entirely based on a live measurement it doesn't fully control the
tuning of (every house's load pattern differs), 1 ("log only") runs the
complete state machine - detection, escalation, and recovery - with every
WARNING log line exactly as it would appear live, but skips the actual
amp/frc writes. This lets GridCurrentLimit/GridCurrentSafetyMargin/
the cycle counts be tuned and observed against a real house's load
patterns for a while before switching to 2 ("active") and letting it
actually intervene.
Confirmed live: reacts correctly to overload even without an active
charging session. The check runs completely independent of car state -
if the phase current is already over the limit before any vehicle is even
connected, this fork still escalates to frc=1, which then correctly
blocks a vehicle from starting to charge at all once one is plugged in and
requests it, for as long as the overload persists. This isn't a special
case that needed separate handling - it falls directly out of the design
never having been gated on whether a car happens to be charging right now.
Found and fixed, on explicit request, three places where "safety over convenience" needed to be more than just a stated principle:
- Release used to share the same threshold as the trigger. A load
sitting right at that single threshold could have caused rapid
flapping (reduce -> release -> reduce -> release), which is neither
safe nor good for the contactor (see "frc physically clicks..." above).
GridCurrentReleaseMarginnow creates genuine hysteresis - a deliberate gap between the trigger and release thresholds. Confirmed live: with the current sitting inside that gap for several consecutive cycles, the state holds steady with no flapping at all. - A missing/unavailable current reading used to be silently skipped,
doing nothing - meaning a sensor or communication problem could leave
this feature blind indefinitely, with no indication anything was wrong.
Now escalates to pausing as a fail-safe after
GridCurrentSustainedCyclesconsecutive missing readings - not being able to verify the house connection is within its rated limit is treated the same as knowing it isn't, not as "probably fine". - No distinct
/Statuswas shown while this feature was intervening - a user watching the Venus GUI would see a generic reason (or none) with no indication the charger was actually being safety-limited. Overridden toStatus=20("Charging limit") while active - the same code this fork already uses for go-e's own per-session energy limit, since both genuinely mean the same thing: charging deliberately capped/stopped by an external limit, not a fault.Status=11("Residual current detected") was considered and explicitly rejected for this, despite initially seeming to fit - that specifically means an RCD/GFCI trip (a real ground-fault/leakage condition), a different electrical phenomenon from a phase simply being near its rated current; showing it here would be actively misleading about what's actually happening. This override only applies inGridCurrentLimitMode=2(active) - in log-only mode, nothing was actually done to the charger, so showing it would misrepresent reality.
Disabled by default (GridCurrentLimitMode=0) - this is an opt-in safety
net for installations with a lower-rated house connection, not a universal
default.
Not fixed/handled by this fork, but worth understanding: the go-e has a
minimum phase-toggle wait time (community-documented key mptwt, reportedly
600s/10 minutes by default) that prevents switching between 1-phase and
3-phase more often than this interval, regardless of current pGrid/pPv.
If the go-e switches to 3-phase while genuine surplus exists and conditions
then drop below what 3-phase's minimum current requires before this lockout
expires, it cannot fall back to 1-phase - the home battery may then be drawn
on to sustain the session, and once that stops being sustainable the go-e
may stop, briefly retry (still locked to 3-phase), stop again, repeating
until the lockout window elapses. This is inherent go-e firmware behaviour,
not something this fork's PV surplus push can influence.
pgt is a persistent value (configured directly in the go-e app) and
continuously feeds into the Eco mode's current calculation, not just when
set. Example: with pGrid=-1800 (1800W surplus) and pgt=-200, the go-e
charges at around 6A instead of the ~7-8A one would expect at a full 1800W,
since 200W is subtracted as a buffer before the resulting current is
calculated.
pgt is intentionally never written by this fork - configure it directly
in the go-e app. An earlier version managed pgt via config.ini,
including a fairly involved mechanism to reconcile file edits with direct
app changes (file "winning" on a change, an app-side change otherwise
adopted in-memory) - this added real complexity and, once, a real bug: an
app-side change could be silently reverted on the very next cycle, because
the comparison used to detect "did the file change" couldn't distinguish a
genuine edit from the in-memory value having previously diverged. Given
pgt is just as easy to set once directly where it takes effect, this fork
now simply reads the current value (only for BatterySupportCompensatePgt)
and never writes it.
lmo,fup,frc,amp,pgtcan be set via a direct query parameter (GET /api/set?lmo=4) - works reliably.pGrid,pPv,pAkkuand the scheduler objects must instead be set viaids={"key":value}, properly URL-encoded - a manually built, non-encoded query string results in"value must be null or JsonObject"errors.alwreturns HTTP 500 via a direct query parameter, unlike every other key tested; viaids={...}it works but is unreliable (silently reverts totrueunder a second while the Eco algorithm considers charging justified).frcreliably overrides this and is used throughout instead.- The older
/mqtt?payload=key=valueendpoint worked fine forampbut repeatedly failed foralw(cause never conclusively determined). This fork uses/api/setconsistently. amx(an API v1-only key, documented as not persisted to flash) does not exist on this device's API v2 firmware at all - confirmed absent via both a filtered and a full status dump. Not a bug: an official go-e developer confirmed in API-v2#112 that flash write-cycle limitations were fully resolved on API v2 -ampcan simply be used directly there, exactly as evcc's own source code does. This confirmation is specific to API v2 and has not been verified for older API v1 hardware - see the dedicated warning under "Restrictions" below before using this fork on older devices.
A shared requests.Session() was tried for connection reuse (HTTP
keep-alive), to avoid a full TCP handshake on every call to the go-e's
small ESP32 web server. Confirmed live: the go-e closes its socket after
every response without declaring Connection: close - so urllib3's pool
doesn't know the connection is dead, attempts to reuse it anyway, discovers
the drop, and only then opens a fresh connection (logged as "Resetting
dropped connection") - strictly worse than opening fresh immediately. An
explicit Connection: close header was then tried to stop the pool from
attempting reuse at all - confirmed live that this didn't work either,
since that header only asks the server to close; it doesn't reliably
affect this client's own pooling behaviour. Reverted entirely back to
plain module-level requests.get(...) calls, confirmed to not exhibit the
"attempt reuse, discover dead, reconnect" pattern at all.
| Event | Measured response time |
|---|---|
| Charging starts once surplus is reported (from stable idle) | ~30-35 seconds |
| Brief interruption of surplus (up to ~30s) | no reaction - tolerated |
| Charging stops when surplus is persistently absent | ~2 minutes (120-125s) |
Current ramps up once surplus increases (amp ceiling raised) |
visible increase within ~35-40s |
| Watchdog: no new values arrive -> | stop after ~6s, pgrid/ppv/pakku revert to null |
Side observation: after a watchdog gap (>6s), the next incoming value -
regardless of sign - triggers an immediate session restart (car changes
within 1-2s); the actual surplus evaluation (start/stop timing above) only
kicks in on the cycles that follow. During continuous, gap-free sending this
doesn't occur - only after a restart of the script/service.
Consequence for PauseBetweenRequests: stay noticeably below the
6-second watchdog threshold (recommended: 5000ms, as in the original), so a
single delayed request doesn't already trigger an unwanted pause.
- A single config.ini typo could break every cycle.
_getSetting()calledint()unguarded, so writing e.g.trueinstead of1raisedValueErroron every subsequent cycle, since config.ini is re-read live. Now logs a warning and falls back to the default. AutoStartMode/EnableChargeControlcould prevent the service from starting, for the same reason (getint()/getboolean()raise on anything unparseable). Both now log a warning and fall back to their (off) default.- Log rotation was silently disabled.
RotatingFileHandlerhadmaxBytesbut nobackupCount; at its default of0, Python never rotates at all and the file grows unbounded (verified: a 1000-byte limit produced a 25kB file). Fixed by addingbackupCount=3and raisingmaxBytesfrom 10kB to 2MB. HardwareVersionwas re-read from disk twice per cycle, unguarded - an invalid/removed value would raise continuously during normal operation, not just at startup. It only requires a restart to change anyway, so it's now read once at startup and cached.- Also cleaned up:
import sysappeared three times (inherited from the original), and a bareexcept:was narrowed to the exceptions it actually needs to catch.
Get the code:
wget https://github.com/Gesiima/dbus-goecharger/archive/refs/heads/main.zip
unzip main.zip "dbus-goecharger-main/*" -d /data
mv /data/dbus-goecharger-main /data/dbus-goecharger
chmod a+x /data/dbus-goecharger/install.sh
/data/dbus-goecharger/install.sh
rm main.zip
Check config.ini afterwards - most important is Deviceinstance and
Host. See "Configuration reference" above for every option and what
does/doesn't need a restart.
.py file, or to anything in config.ini that
isn't re-read live (see "What needs a restart, and what doesn't" above): run
restart.sh.
(An earlier version of this fork exposed the Auto-mode tuning values as
writable D-Bus paths under /Settings/* for VRM-based editing; this was
removed after testing showed the Venus OS GUI - Remote Console as well as
the VRM portal - only ever renders the fixed set of paths it already knows
for the evcharger role, making custom paths like these invisible anywhere.
Editing config.ini directly is therefore the only supported way to change
these values.)
- https://github.com/goecharger/go-eCharger-API-v2
- https://github.com/victronenergy/venus/wiki/dbus
- https://github.com/victronenergy/venus/wiki/dbus-api
Based on the work of vikt0rm, inspired by fabian-lauer and trixing.
Several features in this fork (PreventBatteryDischarge, and the general
approach behind GridCurrentLimit) were inspired by concepts found in
evcc (MIT-licensed) - no evcc code was
used anywhere in this project; everything here is an independent Python
implementation against Victron's own dbus API, written from scratch based
on the general idea and this fork's own testing. Where evcc's own
documentation, source code, or issue tracker informed a specific design
decision or surfaced a known failure mode, that's cited inline in
"Findings" above with a direct link.
This project is built collaboratively: the coding is done by Claude (Anthropic), while the features are developed together. All requirements, architectural decisions, real-hardware testing, and every correction and refinement come from Gesiima; Claude turns them into code iteratively. No code was written manually - the implementation happens entirely through step-by-step instructions and joint review.