Add OCPP control for the Anker Solix EV charger (Own/Proxy CSMS)
The Anker Solix connector was read-only (cloud monitoring only). Add an
OCPP 1.6J control path with a per-user, cascading control mode:
- off monitoring only (default, unchanged behavior)
- own DriverVault is the charger's Central System (full control)
- proxy DriverVault relays to Anker's cloud and injects commands
New internal/ocpp subsystem (stdlib-only, hand-rolled RFC 6455): a CSMS
with session management, inbound dispatch, and typed control commands
(RemoteStart/Stop, SetChargingProfile current limit, ChangeAvailability,
Reset, UnlockConnector, TriggerMessage, Get/ChangeConfiguration). Own- and
proxy-mode paths are verified end-to-end against a simulated charge point.
The charger connects to /ocpp/{serial}, authenticated with OCPP Basic auth
(serial + a per-charger control token) resolved to the owning user via an
in-memory token index. Control REST endpoints mirror the monitoring ones and
reuse the same cascade gate plus a live-session check. controlMode is a new
cascade field (global -> org -> user) advertised as a select on the plugin.
Frontend: control-mode select + provisioning card in Settings, and a real
Start/Stop/limit/reset control panel in Charging, gated on the active mode.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
367113f538
commit
a1519f6e89
@@ -143,6 +143,17 @@ func (p *Plugin) Descriptor() plugins.Descriptor {
|
||||
Help: "Your Anker account password. Stored locally, sent only (encrypted) to Anker's login endpoint."},
|
||||
{Key: "country", Label: "Country", Type: "text", Default: "DE",
|
||||
Help: "ISO country code of your Anker account (e.g. DE, GB, US). Determines which Anker server the account lives on; a wrong value logs in but shows no devices."},
|
||||
// controlMode selects the OCPP control path. It does not affect this
|
||||
// (read-only) cloud plugin — the CSMS that acts on it lives in the api
|
||||
// package (internal/ocpp) — but it is advertised here so a superadmin can
|
||||
// set/lock it at the global layer, and it cascades like the other fields.
|
||||
{Key: "controlMode", Label: "Control mode", Type: "select", Default: "off",
|
||||
Help: "How DriverVault controls the charger over OCPP. Off = monitoring only (default). Own CSMS = the charger connects directly to DriverVault. Proxy CSMS = DriverVault relays to Anker's cloud and can inject commands.",
|
||||
Options: []plugins.SelectOption{
|
||||
{Value: "off", Label: "Off (monitoring only)"},
|
||||
{Value: "own", Label: "Own CSMS (full control)"},
|
||||
{Value: "proxy", Label: "Proxy CSMS (relay + control)"},
|
||||
}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,29 @@ func TestDescriptor(t *testing.T) {
|
||||
if !fields["password"].Secret {
|
||||
t.Error("password field must be marked secret")
|
||||
}
|
||||
|
||||
// controlMode is a select advertising the three OCPP control paths.
|
||||
cm, ok := fields["controlMode"]
|
||||
if !ok {
|
||||
t.Fatal("controlMode config field should be present")
|
||||
}
|
||||
if cm.Type != "select" {
|
||||
t.Errorf("controlMode type = %q, want select", cm.Type)
|
||||
}
|
||||
if cm.Default != "off" {
|
||||
t.Errorf("controlMode default = %q, want off", cm.Default)
|
||||
}
|
||||
want := map[string]bool{"off": false, "own": false, "proxy": false}
|
||||
for _, o := range cm.Options {
|
||||
if _, known := want[o.Value]; known {
|
||||
want[o.Value] = true
|
||||
}
|
||||
}
|
||||
for v, seen := range want {
|
||||
if !seen {
|
||||
t.Errorf("controlMode is missing option %q", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistered(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user