diff --git a/charger/ocpp/const.go b/charger/ocpp/const.go index 4a5433d4c..5ccf8672d 100644 --- a/charger/ocpp/const.go +++ b/charger/ocpp/const.go @@ -4,6 +4,11 @@ import "time" var Timeout = time.Minute // default request / response timeout on protocol level +// triggerBootDelay defines how long to wait after WebSocket connect before +// proactively triggering a BootNotification. This allows the connection to +// stabilize and gives the charger a chance to send a spontaneous BootNotification. +const triggerBootDelay = 5 * time.Second + const ( // Core profile keys KeyMeterValueSampleInterval = "MeterValueSampleInterval" diff --git a/charger/ocpp/cp.go b/charger/ocpp/cp.go index 52419fa04..0f8c8e51c 100644 --- a/charger/ocpp/cp.go +++ b/charger/ocpp/cp.go @@ -8,6 +8,7 @@ import ( "github.com/evcc-io/evcc/util" "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/remotetrigger" "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" ) @@ -142,12 +143,47 @@ func (cp *CP) connect(connect bool) { // onTransportConnect is called when the WebSocket connection is established. // Instead of marking the CP as connected immediately, it waits for the // BootNotification handshake to complete (or a timeout to expire). +// +// Some chargers (e.g. Wallbox Pulsar Pro FW 6.x) do not send BootNotification +// spontaneously on connect. For these chargers, we proactively trigger it +// after a short delay, which typically yields a response within 1 second +// instead of waiting for the full timeout. func (cp *CP) onTransportConnect() { cp.mu.Lock() defer cp.mu.Unlock() cp.stopBootTimer() cp.bootTimer = time.AfterFunc(Timeout, cp.onBootTimeout) + + // Proactively trigger BootNotification after a short delay. + // This helps chargers that don't send it spontaneously (e.g. Wallbox FW 6.x). + // The TriggerMessage is sent directly via the OCPP instance, bypassing the + // Connected() check which would fail at this point. + time.AfterFunc(triggerBootDelay, func() { + cp.mu.RLock() + // If BootNotification already arrived or timer was cancelled (disconnect), + // there is nothing to do. + if cp.bootTimer == nil || cp.BootNotificationResult != nil { + cp.mu.RUnlock() + return + } + cp.mu.RUnlock() + + cp.log.DEBUG.Printf("proactively triggering BootNotification") + + if err := Instance().TriggerMessage( + cp.id, + func(conf *remotetrigger.TriggerMessageConfirmation, err error) { + if err != nil { + cp.log.ERROR.Printf("trigger BootNotification response error: %v", err) + } + }, + core.BootNotificationFeatureName, + func(request *remotetrigger.TriggerMessageRequest) {}, + ); err != nil { + cp.log.ERROR.Printf("failed to trigger BootNotification: %v", err) + } + }) } // onBootTimeout is called when the BootNotification wait timer expires.