The last message in the map, and it reboots the charger
0108 was the one thing in the MQTT inventory nobody had wired: the device power mode, whose single documented value restarts the charger. It is the only way to reboot a charger that is on neither a CSMS nor the local network — which is most of them — so the cloud transport sends it now, and "reset" reaches it too, since that is what the OCPP path has always called the same act. Nothing waits for a confirmation: the device that would send it is the device rebooting, so the command answers at once and says the charger drops off the cloud for about a minute. The gate is unchanged and now covers both spellings — an explicit confirm plus a password step-up, audited either way. Modbus still refuses, because no register does this, but its refusal now names both transports that can rather than only the CSMS. Both clients already had the reset button and its password prompt; they were hidden in every mode that reads the device, which is why the cloud never showed one. Modbus is now the only mode without it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b2d333a63f
commit
4ff6242c8f
@@ -604,7 +604,7 @@ func (s *Server) handleAnkerControlAction(w http.ResponseWriter, r *http.Request
|
||||
operative = *body.Operative
|
||||
}
|
||||
status, err = sess.ChangeAvailability(ctx, body.ConnectorID, operative)
|
||||
case "reset":
|
||||
case "reset", "restart":
|
||||
status, err = sess.Reset(ctx, body.Hard)
|
||||
case "unlock":
|
||||
status, err = sess.UnlockConnector(ctx, body.ConnectorID)
|
||||
@@ -649,7 +649,10 @@ func (s *Server) handleAnkerControlAction(w http.ResponseWriter, r *http.Request
|
||||
// releases the cable lock — actions that require an explicit confirm:true and a
|
||||
// password re-authentication.
|
||||
func isDestructiveAction(action string) bool {
|
||||
return action == "reset" || action == "unlock"
|
||||
// "restart" is the cloud's name for the same act as OCPP's "reset": both
|
||||
// reboot the charger, so both pass through the confirmation and the password
|
||||
// step-up rather than one slipping past because it is spelled differently.
|
||||
return action == "reset" || action == "restart" || action == "unlock"
|
||||
}
|
||||
|
||||
// reauthenticate verifies the caller's password against PocketBase (a sudo-style
|
||||
|
||||
@@ -43,7 +43,13 @@ func (s *Server) ankerModbusAction(w http.ResponseWriter, r *http.Request, who *
|
||||
// Actions the register map has no equivalent for. Saying which transport is
|
||||
// missing them beats a bare "unknown action" the caller cannot act on.
|
||||
switch action {
|
||||
case "reset", "unlock", "availability", "trigger", "config":
|
||||
case "reset", "restart":
|
||||
// No register reboots the charger. Both of the other transports can, so
|
||||
// the refusal names them rather than only the CSMS.
|
||||
writeError(w, http.StatusBadRequest,
|
||||
"no register reboots the charger; the local Modbus connection cannot restart it. Switch the control mode to Anker cloud (MQTT) or a CSMS mode to use it.")
|
||||
return
|
||||
case "unlock", "availability", "trigger", "config":
|
||||
writeError(w, http.StatusBadRequest,
|
||||
"\""+action+"\" is an OCPP command; the local Modbus connection cannot send it. Switch the control mode to a CSMS mode to use it.")
|
||||
return
|
||||
|
||||
@@ -18,11 +18,11 @@ package api
|
||||
// or certificated on the customer's side; what it costs instead is a dependency
|
||||
// on Anker's cloud being up, and on an unofficial protocol.
|
||||
//
|
||||
// The command set is the charger's, not OCPP's: start, stop, boost, skip-delay
|
||||
// and a current limit, plus the one thing neither other transport can do at all
|
||||
// — writing the charger's own configuration, which is what "settings" is for.
|
||||
// Everything the register map or the CSMS can do that this cannot is refused by
|
||||
// name rather than as an unknown action.
|
||||
// The command set is the charger's, not OCPP's: start, stop, boost, skip-delay,
|
||||
// a current limit and a restart, plus the one thing neither other transport can
|
||||
// do at all — writing the charger's own configuration, which is what "settings"
|
||||
// is for. Everything the register map or the CSMS can do that this cannot is
|
||||
// refused by name rather than as an unknown action.
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -59,7 +59,7 @@ func (s *Server) ankerMqttAction(w http.ResponseWriter, r *http.Request, who *ca
|
||||
// Actions this transport has no equivalent for. Naming the transport that
|
||||
// does have them beats a bare "unknown action" the caller cannot act on.
|
||||
switch action {
|
||||
case "reset", "unlock", "availability", "trigger", "config":
|
||||
case "unlock", "availability", "trigger", "config":
|
||||
writeError(w, http.StatusBadRequest,
|
||||
"\""+action+"\" is an OCPP command; the Anker cloud connection cannot send it. Switch the control mode to a CSMS mode to use it.")
|
||||
return
|
||||
@@ -96,6 +96,11 @@ func (s *Server) ankerMqttAction(w http.ResponseWriter, r *http.Request, who *ca
|
||||
case "limit":
|
||||
params["amps"] = body.Amps
|
||||
payload["command"], payload["amps"] = "limit", body.Amps
|
||||
case "reset", "restart":
|
||||
// The charger's own restart, the cloud's answer to the OCPP reset. It has
|
||||
// already been through the confirmation and the password step-up upstairs,
|
||||
// like any other reboot.
|
||||
payload["command"] = "restart"
|
||||
case "settings":
|
||||
// The values themselves are audited, not just the fact of a write: a
|
||||
// setting that changes what the charger will draw, or whether it answers on
|
||||
|
||||
@@ -24,7 +24,6 @@ func refuse(t *testing.T, action string, body ankerControlBody) *httptest.Respon
|
||||
// unknown.
|
||||
func TestAnkerMqttActionNamesTheTransportThatCan(t *testing.T) {
|
||||
for _, tc := range []struct{ action, want string }{
|
||||
{"reset", "CSMS"},
|
||||
{"unlock", "CSMS"},
|
||||
{"availability", "CSMS"},
|
||||
{"config", "CSMS"},
|
||||
@@ -43,6 +42,21 @@ func TestAnkerMqttActionNamesTheTransportThatCan(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A reboot is a reboot under either name and over any transport: both go through
|
||||
// the confirmation and the password step-up, and nothing else does.
|
||||
func TestBothNamesForARebootAreGated(t *testing.T) {
|
||||
for _, action := range []string{"reset", "restart", "unlock"} {
|
||||
if !isDestructiveAction(action) {
|
||||
t.Errorf("%s should need a confirmation and a password", action)
|
||||
}
|
||||
}
|
||||
for _, action := range []string{"start", "stop", "limit", "boost", "settings", "status"} {
|
||||
if isDestructiveAction(action) {
|
||||
t.Errorf("%s should not demand a password", action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnkerMqttActionRejectsUnknownActions(t *testing.T) {
|
||||
rec := refuse(t, "explode", ankerControlBody{})
|
||||
if rec.Code != http.StatusBadRequest || !strings.Contains(rec.Body.String(), "unknown control action") {
|
||||
|
||||
Reference in New Issue
Block a user