package ocpp import ( "context" "encoding/json" "errors" ) // This file is the outbound control surface: typed helpers that build an OCPP // CALL, send it to the charger via Session.Call, and parse the reply. They work // identically in own and proxy modes (both inject over the same socket). // StatusResponse is the common {status: "..."} reply most control calls return // (e.g. "Accepted", "Rejected", "Scheduled", "NotSupported"). type StatusResponse struct { Status string `json:"status"` } // statusCall issues a control CALL that replies with a {status} object. func (s *Session) statusCall(ctx context.Context, action string, payload any) (string, error) { raw, err := s.Call(ctx, action, payload) if err != nil { return "", err } var r StatusResponse if err := json.Unmarshal(raw, &r); err != nil { return "", err } return r.Status, nil } // RemoteStartTransaction asks the charger to begin a charging session. A blank // idTag is defaulted; connectorID <= 0 lets the charger choose. func (s *Session) RemoteStartTransaction(ctx context.Context, idTag string, connectorID int) (string, error) { if idTag == "" { idTag = "DriverVault" } payload := map[string]any{"idTag": idTag} if connectorID > 0 { payload["connectorId"] = connectorID } return s.statusCall(ctx, "RemoteStartTransaction", payload) } // RemoteStopTransaction stops a session. When transactionID <= 0 the id from the // live status snapshot (own mode) is used. func (s *Session) RemoteStopTransaction(ctx context.Context, transactionID int) (string, error) { if transactionID <= 0 { transactionID = s.Snapshot().TransactionID } if transactionID <= 0 { return "", errors.New("ocpp: no active transaction to stop") } return s.statusCall(ctx, "RemoteStopTransaction", map[string]any{"transactionId": transactionID}) } // SetCurrentLimit caps the charge current (Amperes) via a TxDefaultProfile. A // connectorID <= 0 defaults to connector 1. func (s *Session) SetCurrentLimit(ctx context.Context, connectorID int, amps float64) (string, error) { if connectorID <= 0 { connectorID = 1 } profile := map[string]any{ "chargingProfileId": 1, "stackLevel": 0, "chargingProfilePurpose": "TxDefaultProfile", "chargingProfileKind": "Relative", "chargingSchedule": map[string]any{ "chargingRateUnit": "A", "chargingSchedulePeriod": []any{ map[string]any{"startPeriod": 0, "limit": amps}, }, }, } return s.statusCall(ctx, "SetChargingProfile", map[string]any{ "connectorId": connectorID, "csChargingProfiles": profile, }) } // ClearChargingProfile removes charging profiles (lifting a current limit). A // connectorID <= 0 clears all connectors. func (s *Session) ClearChargingProfile(ctx context.Context, connectorID int) (string, error) { payload := map[string]any{} if connectorID > 0 { payload["connectorId"] = connectorID } return s.statusCall(ctx, "ClearChargingProfile", payload) } // ChangeAvailability sets a connector Operative/Inoperative. connectorID 0 // targets the whole charge point. func (s *Session) ChangeAvailability(ctx context.Context, connectorID int, operative bool) (string, error) { typ := "Inoperative" if operative { typ = "Operative" } return s.statusCall(ctx, "ChangeAvailability", map[string]any{ "connectorId": connectorID, "type": typ, }) } // Reset reboots the charger (Soft or Hard). func (s *Session) Reset(ctx context.Context, hard bool) (string, error) { typ := "Soft" if hard { typ = "Hard" } return s.statusCall(ctx, "Reset", map[string]any{"type": typ}) } // UnlockConnector releases the cable lock on a connector. func (s *Session) UnlockConnector(ctx context.Context, connectorID int) (string, error) { if connectorID <= 0 { connectorID = 1 } return s.statusCall(ctx, "UnlockConnector", map[string]any{"connectorId": connectorID}) } // TriggerMessage asks the charger to proactively send a message (e.g. // "StatusNotification", "MeterValues", "BootNotification", "Heartbeat"). func (s *Session) TriggerMessage(ctx context.Context, requestedMessage string, connectorID int) (string, error) { payload := map[string]any{"requestedMessage": requestedMessage} if connectorID > 0 { payload["connectorId"] = connectorID } return s.statusCall(ctx, "TriggerMessage", payload) } // ChangeConfiguration sets a charger configuration key. func (s *Session) ChangeConfiguration(ctx context.Context, key, value string) (string, error) { return s.statusCall(ctx, "ChangeConfiguration", map[string]any{"key": key, "value": value}) } // ConfigKey is one entry in a GetConfiguration reply. type ConfigKey struct { Key string `json:"key"` Readonly bool `json:"readonly"` Value string `json:"value,omitempty"` } // GetConfigurationResult is the GetConfiguration reply. type GetConfigurationResult struct { ConfigurationKey []ConfigKey `json:"configurationKey"` UnknownKey []string `json:"unknownKey,omitempty"` } // GetConfiguration reads charger configuration keys; nil/empty keys returns all. func (s *Session) GetConfiguration(ctx context.Context, keys []string) (GetConfigurationResult, error) { payload := map[string]any{} if len(keys) > 0 { payload["key"] = keys } raw, err := s.Call(ctx, "GetConfiguration", payload) if err != nil { return GetConfigurationResult{}, err } var r GetConfigurationResult if err := json.Unmarshal(raw, &r); err != nil { return GetConfigurationResult{}, err } return r, nil }