Easee: add cmd for (de)activating local ocpp (#29868)

This commit is contained in:
Michael Heß 2026-05-14 12:30:51 +02:00 • committed by GitHub
parent 8ce8d6d9f9
commit 603979167c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 254 additions and 0 deletions

View file

@ -3,6 +3,9 @@ package easee
// API is the Easee API endpoint
const API = "https://api.easee.com/api"
// LocalOcppAPI is the Easee local OCPP configuration API endpoint
const LocalOcppAPI = "https://api.easee.com/local-ocpp"
const (
ChargeStart = "start_charging"
ChargeStop = "stop_charging"

201
cmd/easee_ocpp.go Normal file
View file

@ -0,0 +1,201 @@
package cmd
import (
"fmt"
"net/http"
"strings"
"github.com/evcc-io/evcc/charger/easee"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
"github.com/spf13/cobra"
"golang.org/x/oauth2"
)
var easeeOcppCmd = &cobra.Command{
Use: "easee-ocpp",
Short: "Manage Easee local OCPP configuration",
}
var easeeOcppEnableCmd = &cobra.Command{
Use: "enable",
Short: "Enable local OCPP on Easee charger",
Run: runEaseeOcppEnable,
}
var easeeOcppDisableCmd = &cobra.Command{
Use: "disable",
Short: "Disable local OCPP on Easee charger",
Run: runEaseeOcppDisable,
}
var easeeOcppStatusCmd = &cobra.Command{
Use: "status",
Short: "Show Easee local OCPP configuration",
Run: runEaseeOcppStatus,
}
func init() {
rootCmd.AddCommand(easeeOcppCmd)
easeeOcppCmd.AddCommand(easeeOcppEnableCmd)
easeeOcppCmd.AddCommand(easeeOcppDisableCmd)
easeeOcppCmd.AddCommand(easeeOcppStatusCmd)
easeeOcppCmd.PersistentFlags().String("user", "", "Easee account email")
easeeOcppCmd.PersistentFlags().String("password", "", "Easee account password")
easeeOcppCmd.PersistentFlags().String("charger", "", "Easee charger serial number")
_ = easeeOcppCmd.MarkPersistentFlagRequired("user")
_ = easeeOcppCmd.MarkPersistentFlagRequired("password")
_ = easeeOcppCmd.MarkPersistentFlagRequired("charger")
easeeOcppEnableCmd.Flags().String("url", "", "OCPP Central System URL (e.g. ws://192.168.1.100:8887/)")
_ = easeeOcppEnableCmd.MarkFlagRequired("url")
}
type localOcppConnectionDetails struct {
ConnectivityMode string `json:"connectivityMode"`
WebsocketConnectionArgs *localOcppWebsocketArgs `json:"websocketConnectionArgs,omitempty"`
}
type localOcppWebsocketArgs struct {
URL string `json:"url"`
}
type localOcppVersionResponse struct {
Version string `json:"version"`
}
type localOcppApplyRequest struct {
Version string `json:"version"`
}
type localOcppStatusResponse struct {
ConnectivityMode string `json:"connectivityMode"`
ChargePointID string `json:"chargePointId"`
WebsocketConnectionArgs *localOcppWebsocketArgs `json:"websocketConnectionArgs"`
}
func easeeOcppClient(user, password string) (*request.Helper, error) {
log := util.NewLogger("easee-ocpp")
log.Redact(user, password)
ts, err := easee.TokenSource(log, user, password)
if err != nil {
return nil, fmt.Errorf("authentication failed: %w", err)
}
client := request.NewHelper(log)
client.Client.Transport = &oauth2.Transport{
Source: ts,
Base: client.Client.Transport,
}
return client, nil
}
func easeeOcppApplyConfig(client *request.Helper, serial string, details localOcppConnectionDetails) error {
uri := fmt.Sprintf("%s/v1/connection-details/%s", easee.LocalOcppAPI, serial)
req, err := request.New(http.MethodPost, uri, request.MarshalJSON(details), request.JSONEncoding)
if err != nil {
return err
}
var version localOcppVersionResponse
if err := client.DoJSON(req, &version); err != nil {
return fmt.Errorf("storing config: %w", err)
}
apply := localOcppApplyRequest{Version: version.Version}
uri = fmt.Sprintf("%s/v1/connections/chargers/%s", easee.LocalOcppAPI, serial)
req, err = request.New(http.MethodPost, uri, request.MarshalJSON(apply), request.JSONEncoding)
if err != nil {
return err
}
if _, err := client.DoBody(req); err != nil {
return fmt.Errorf("applying config: %w", err)
}
return nil
}
func runEaseeOcppEnable(cmd *cobra.Command, args []string) {
user, _ := cmd.Flags().GetString("user")
password, _ := cmd.Flags().GetString("password")
charger, _ := cmd.Flags().GetString("charger")
client, err := easeeOcppClient(user, password)
if err != nil {
log.FATAL.Fatal(err)
}
ocppURL, _ := cmd.Flags().GetString("url")
details := localOcppConnectionDetails{
ConnectivityMode: "DualProtocol",
WebsocketConnectionArgs: &localOcppWebsocketArgs{URL: ocppURL},
}
if err := easeeOcppApplyConfig(client, charger, details); err != nil {
log.FATAL.Fatal(err)
}
fmt.Printf("Local OCPP enabled for charger %s\n", charger)
fmt.Printf("OCPP Central System URL: %s\n", ocppURL)
}
func runEaseeOcppDisable(cmd *cobra.Command, args []string) {
user, _ := cmd.Flags().GetString("user")
password, _ := cmd.Flags().GetString("password")
charger, _ := cmd.Flags().GetString("charger")
client, err := easeeOcppClient(user, password)
if err != nil {
log.FATAL.Fatal(err)
}
// GET current config, strip charger-appended serial from URL, set OcppOff
uri := fmt.Sprintf("%s/v1/connection-details/%s", easee.LocalOcppAPI, charger)
var current localOcppConnectionDetails
if err := client.GetJSON(uri, &current); err != nil {
log.FATAL.Fatal(err)
}
current.ConnectivityMode = "OcppOff"
if current.WebsocketConnectionArgs != nil {
current.WebsocketConnectionArgs.URL = strings.TrimSuffix(current.WebsocketConnectionArgs.URL, charger)
}
if err := easeeOcppApplyConfig(client, charger, current); err != nil {
log.FATAL.Fatal(err)
}
fmt.Printf("Local OCPP disabled for charger %s\n", charger)
}
func runEaseeOcppStatus(cmd *cobra.Command, args []string) {
user, _ := cmd.Flags().GetString("user")
password, _ := cmd.Flags().GetString("password")
charger, _ := cmd.Flags().GetString("charger")
client, err := easeeOcppClient(user, password)
if err != nil {
log.FATAL.Fatal(err)
}
uri := fmt.Sprintf("%s/v1/connection-details/%s", easee.LocalOcppAPI, charger)
var status localOcppStatusResponse
if err := client.GetJSON(uri, &status); err != nil {
log.FATAL.Fatal(err)
}
fmt.Printf("Charger: %s\n", charger)
fmt.Printf("Connectivity mode: %s\n", status.ConnectivityMode)
fmt.Printf("Charge point ID: %s\n", status.ChargePointID)
if status.WebsocketConnectionArgs != nil {
fmt.Printf("OCPP URL: %s\n", status.WebsocketConnectionArgs.URL)
}
}

View file

@ -0,0 +1,50 @@
template: ocpp-easee
products:
- brand: Easee
description:
generic: Home (OCPP)
- brand: Easee
description:
generic: Charge (OCPP)
- brand: Easee
description:
generic: Charge Lite (OCPP)
- brand: Easee
description:
generic: Charge Core (OCPP)
capabilities: ["rfid"]
requirements:
description:
de: |
Voraussetzungen:
* Firmware Version >= 328 mit aktiviertem nativem OCPP
* Für Stromsteuerung über OCPP ist Beta-Firmware erforderlich (getestet mit v9012)
* OCPP muss über die Easee API aktiviert werden:
* `evcc easee-ocpp --user <email> --password <passwort> --charger <seriennummer> enable --url ws://<evcc-host>:8887/`
* Zum Deaktivieren: `evcc easee-ocpp ... disable`
* Status prüfen: `evcc easee-ocpp ... status`
* Die Seriennummer des Chargers wird automatisch als Station-ID verwendet
* Protokoll: OCPP-J v1.6, Websocket, ws://
* Verbindung über das lokale Netzwerk
* Minimales Meter-Intervall: 30s
en: |
Requirements:
* Firmware version >= 328 with native OCPP enabled
* Beta firmware required for current control via OCPP (tested with v9012)
* OCPP must be enabled via the Easee API:
* `evcc easee-ocpp --user <email> --password <password> --charger <serial> enable --url ws://<evcc-host>:8887/`
* To disable: `evcc easee-ocpp ... disable`
* Check status: `evcc easee-ocpp ... status`
* The charger serial number is automatically used as station ID
* Protocol: OCPP-J v1.6, Websocket, ws://
* Local network connection
* Minimum meter interval: 30s
evcc: ["sponsorship", "skiptest"]
params:
- preset: ocpp
- name: metervalues
default: Energy.Active.Import.Register,Power.Active.Import,Current.Import
render: |
{{ include "ocpp" . }}
remotestart: true
meterinterval: 30s