Bug #1393
進行中Bug #1382: [Branch] OCPP 充電樁離線後 connector 狀態仍停留 AVAILABLE
[Branch Bug] Branch 重啟後 watchdog 以舊 Heartbeat 誤將已重連 Connector 設為 UNAVAILABLE
概述
問題摘要¶
CT3(勝華寓)在 ems_branch 重啟後,CP001 已成功重新建立 OCPP WebSocket、送出 BootNotification,且 CP 本機 CP001-001 為 AVAILABLE、Modbus 通訊正常;但 Branch heartbeat watchdog 在第一個重連後 Heartbeat 到達前,仍以重啟前留下的舊 last_heartbeat 判斷離線,將 e_connectors.CP001-001 誤寫為 UNAVAILABLE。
後續 Heartbeat 只恢復 e_charge_points.ocpp_connection_status=ONLINE,不會恢復 Connector;CP 本機狀態沒有變化,也不會再主動送 StatusNotification,造成 Branch 與 CP 狀態永久分裂,使用者無法啟動充電。
本問題為 #1382 離線同步機制在 Branch restart / reconnect 情境下的回歸。
環境¶
- 案場:CT3 勝華寓
- Charge Point:CP001
- Connector:CP001-001(CloudLink / public / slaveId=1)
- 發生時間:2026-07-28 15:06~15:12(Asia/Taipei)
- Branch watchdog:
- Heartbeat interval:300 秒
- offline timeout:330 秒
- scan fixed delay:60 秒
現場資料(2026-07-28 15:28)¶
Branch¶
- e_charge_points.CP001
- ocpp_connection_status = ONLINE
- status = AVAILABLE
- last_heartbeat = 2026-07-28 15:27:06
- last_boot_notification = 2026-07-28 15:07:20
- e_connectors.CP001-001
- status = UNAVAILABLE
- error_code = NOERROR
- info = OCPP heartbeat stale
- last_status_change = 2026-07-28 15:09:24
charge_point 本機¶
- cp_connectors.CP001-001
- status = AVAILABLE
- slave_id = 1
- last_meter_read 持續更新(15:28 仍正常)
Engineer Modbus audit¶
- 15:16:59 CONNECTION_TEST:COMPLETED / Accepted / result=true
- 15:17:28 READ_SUPPLY_STATE:COMPLETED / Accepted / value=0
上述證據表示 OCPP WebSocket、DataTransfer、Modbus gateway 與 slave 1 都可達;不是硬體或 Modbus 故障。
完整時序¶
- 15:06:50:ems_branch 重啟,舊 WebSocket 關閉。
- 15:07:20:CP001 重新建立 WebSocket 並送 BootNotification;Branch 將 connection 設為 ONLINE。
- 此時 last_heartbeat 仍是重啟前的 15:02:06。WebSocket establish 與 BootNotification 依 #1382 設計不更新 last_heartbeat。
- 15:08:06:ChargePointOfflineWatchdogTask 以 cutoff=15:02:36 找到 CP001,將 connection 設為 OFFLINE,並把無 ACTIVE transaction 的 CP001-001 投影為 UNAVAILABLE / HEARTBEAT_STALE。
- CP 於 15:07:32 產生的初始 StatusNotification(1, AVAILABLE),Branch 在 15:08:24 處理,短暫把 CP001-001 恢復 AVAILABLE。
- 15:09:24:第一個 post-reconnect Heartbeat 尚未到達;watchdog 再次以舊 last_heartbeat 判定 stale,將 CP001-001 覆寫為 UNAVAILABLE。
- 15:12:06:第一個 post-reconnect Heartbeat 到達,connection 恢復 ONLINE,但 Connector 不會自動恢復。
- CP 本機 CP001-001 始終為 AVAILABLE,沒有本機狀態變化,因此後續不再送 StatusNotification;Branch 保持 UNAVAILABLE。
次要觀察:第一次 offline propagation 發布 MQ connector event 時發生 RabbitMQ connect timeout,使該輪 watchdog 約耗時 18 秒,也延後 OCPP StatusNotification 處理;但即使沒有 MQ timeout,下一輪 watchdog 仍會因舊 last_heartbeat 再次覆寫 Connector,故 MQ 不是主要根因。
程式碼根因¶
- ChargePointRepository.findEnabledStaleHeartbeat() 只判斷 lastHeartbeat,不考慮剛收到的 lastBootNotification 或其他 OCPP application-level liveness。
- ChargePointOfflineService.markOfflineIfHeartbeatStale() 的 row-lock recheck 同樣只重讀 lastHeartbeat。
- ChargePointService.markOcppConnected() 與 handleBootNotification() 會將 connection 設為 ONLINE,但刻意不刷新 lastHeartbeat。
- charge_point HeartbeatScheduler 保持原 300 秒 fixed-delay;Branch 重啟後快速重連時,不會立即補送 Heartbeat,而是等到下一個既有週期。
- 重連後 StatusNotification 只發送一次。Branch 若稍後單方面再投影 UNAVAILABLE,CP 本機狀態未改變,不會再次通知。
- RemoteStart 只允許 Branch Connector status=AVAILABLE/PREPARING,因此 e_connectors 的誤狀態會直接造成「無法使用」。
- Engineer Modbus dispatch 只要求 WebSocket 可送達,不檢查 e_connectors.status,因此 Modbus 指令仍能成功。
暫時恢復方式¶
在 Heartbeat 已正常更新後,對 CP001 connector 1 發送:
{
"requestedMessage": "StatusNotification",
"connectorId": 1
}
CP 會依本機狀態重新回報 AVAILABLE。此方式僅恢復現場資料,不解決下次 Branch restart 的競態。
建議修正方向¶
- Watchdog 使用最近一次可信的 OCPP application activity,而不是只看 lastHeartbeat:
- 最小修正:candidate query 與 locked recheck 都以 max(lastHeartbeat, lastBootNotification) 判斷;
- 較完整作法:新增 lastOcppActivity,於 BootNotification、Heartbeat(必要時含有效 StatusNotification)更新。
- charge_point 在 OCPP reconnect 成功後立即送一次 Heartbeat,避免等待原本 300 秒週期;此項可作第二層保護。
- 不建議單純看到 SessionRegistry 有 session 就永久略過 watchdog,否則會破壞 TCP half-open 偵測。
驗收條件¶
- Branch restart 前 lastHeartbeat 已超過 cutoff,但 CP 在 restart 後重新建立 WebSocket並送出 BootNotification時,第一個 post-reconnect Heartbeat 到達前不得將 CP 或 Connector 判為離線。
- fresh BootNotification 的保護只維持設定的 grace/timeout;超過 timeout 仍未收到 Heartbeat 時,watchdog 必須正常判離線。
- CP 回報 Connector AVAILABLE 後,不得在有效 reconnect grace 內被舊 lastHeartbeat 覆寫成 UNAVAILABLE。
- 首個 Heartbeat 到達後,e_charge_points.ocpp_connection_status 應維持 ONLINE,e_connectors 應與 CP StatusNotification 一致。
- TCP half-open、無 BootNotification、無 Heartbeat 的既有離線偵測仍正常。
- ACTIVE transaction/currentTransactionId/cp_transactions ACTIVE 的既有保護不得退化。
- 補充至少以下回歸測試:
- old heartbeat + fresh BootNotification + watchdog before first Heartbeat;
- fresh BootNotification 過期且仍無 Heartbeat;
- reconnect StatusNotification(AVAILABLE) 後 watchdog 不應以舊 Heartbeat再次覆寫;
- 正常 stale heartbeat 與 TCP half-open path。
沒有任何資料可供顯示