Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/ocpp-ui-honest-toggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"ftw": patch
---

Enabling the OCPP server from the Chargers panel now works on the first
try: the username field carries the real default ("ftw") instead of a
placeholder that validation then rejected, and saving an OCPP change
honestly reports that a restart is required — the central system
listener only starts at boot, so the previous "no restart needed"
answer left the port silently closed after an apparently successful
save.
8 changes: 8 additions & 0 deletions go/internal/config/restart_required.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ func RestartRequiredFor(oldCfg, newCfg *Config) []string {
if oldCfg.FleetPing.Resolved() != newCfg.FleetPing.Resolved() {
reasons = append(reasons, "fleet_ping.endpoint — the sender resolves its endpoint at startup")
}
// The OCPP central system is started once in main.go; the config
// applier neither starts, stops nor re-arms it. Without this entry
// the Chargers panel's enable toggle saved cleanly, reported no
// restart needed, and the listener never opened — the exact silent
// failure the comment at the top of this file warns about.
if !pointerEqual(oldCfg.OCPP, newCfg.OCPP) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ignore inert OCPP sections in the restart diff

When the existing config has no ocpp block, merely visiting the Chargers tab and saving creates a disabled block because the generic capture pass writes the form's defaults (enabled: false, port, path, and username). This raw pointer comparison therefore reports restart_required even when the operator only edited a loadpoint or vehicle and OCPP remains disabled, prompting an unnecessary service restart despite no listener state changing. Treat absent and disabled OCPP configurations as equivalent, while still requiring restart when enabling, disabling, or changing an active listener.

Useful? React with 👍 / 👎.

reasons = append(reasons, "ocpp — the central system listener is started at startup")
}
if !pointerEqual(oldCfg.EVCharger, newCfg.EVCharger) {
reasons = append(reasons, "ev_charger — EV charger client is constructed once at startup")
}
Expand Down
3 changes: 3 additions & 0 deletions go/internal/config/restart_required_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ func TestRestartRequiredFor_BootSections(t *testing.T) {
{"ev_charger added", func(c *Config) {
c.EVCharger = &EVCharger{Provider: "easee", Username: "a@b.c"}
}, "ev_charger"},
{"ocpp enabled", func(c *Config) {
c.OCPP = &OCPP{Enabled: true, Port: 8887, Username: "ftw", Password: "long-random-string"}
}, "ocpp"},
{"caldav credentials changed", func(c *Config) {
c.CalDAV = &CalDAV{Enabled: true, Username: "calendar-user", Password: "rotated"}
}, "caldav"},
Expand Down
2 changes: 1 addition & 1 deletion web/settings/tabs/loadpoints.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@
'</div>' +
'<div>' +
'<label>Username ' + h("The username every charger presents, unless it has a credential of its own in config.yaml.") + '</label>' +
'<input type="text" data-path="ocpp.username" value="' + escHtml(o.username || "") + '" placeholder="ftw">' +
'<input type="text" data-path="ocpp.username" value="' + escHtml(o.username || "ftw") + '">' +
'</div>' +
'</div>' +
'<div class="field-row">' +
Expand Down
4 changes: 4 additions & 0 deletions web/settings/tabs/loadpoints.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,10 @@ describe("OCPP server form", () => {
// Defaults an operator would otherwise have to go and look up.
assert.match(html, /data-path="ocpp\.port" value="8887"/);
assert.match(html, /data-path="ocpp\.path" value="\/"/);
// The username default is a real value, not a placeholder: validation
// requires it when the server is enabled, so a field that only hinted
// "ftw" made every first enable fail with a 400.
assert.match(html, /data-path="ocpp\.username" value="ftw"/);
});

it("never renders the stored password back into the page", () => {
Expand Down