Bug #1517
是由 陳國瑋 於 5 天 前更新
## 1. 本單要解決什麼問題
寶台 A17 的充電樁列表中,`CP003-001`、`CP003-004`、`CP003-008` 持續顯示「無法使用」。然而,2026-09-26 凌晨查核時,CP003 已經與 Branch 恢復通訊,設備端記錄這三支 Connector 的狀態都是 `AVAILABLE`(可用),而且仍持續成功讀取電表。
本次已確認的問題是:
**CP 曾經主動把「可用」狀態送給 Branch,Branch 也已成功收到;但 Branch 後來只根據過舊的心跳時間,將三支 Connector 改成「無法使用」。心跳恢復後,Branch 沒有重新取得並更新 Connector 狀態,導致列表長時間保留錯誤的不可用狀態。**
本單需要同時處理兩件事:
1. **避免誤判離線:** CP 還在傳送有效 OCPP 訊息時,Branch 應將這些訊息納入「設備仍有通訊」的判斷,不能只看最後一筆 Heartbeat。
2. **讓狀態可以恢復:** 若 Branch 已經因離線把 Connector 標成不可用,通訊恢復後應重新取得 CP 的實際狀態,避免永久停留在舊的不可用狀態。
目前的證據可以確認「後台狀態與設備端狀態不一致」,但本次沒有操作實機開始充電,不能把電表通訊正常直接當成「實際充電功能已驗證正常」。
## 2. 系統角色與名詞
本單使用的名詞如下,避免把「通訊在線」和「可以充電」混為一談。
| 名稱 | 在本案中的意思 |
| --- | --- |
| CP(Charge Point) | 設備端。本案是 CP003,由它與實體設備通訊,再透過 OCPP 回報給 Branch。 |
| Connector | 個別充電連接器。本案 CP003 底下有 001、004、005、008。 |
| Branch/CS | 接收 OCPP 的後台服務。Branch 在本系統扮演 OCPP 的 Central System(CS),充電樁列表所使用的狀態也由它提供。 |
| Heartbeat | CP 傳給 Branch 的心跳訊息,用來表示仍有通訊。它不包含每一支 Connector 的充電狀態。 |
| StatusNotification | CP 主動回報設備或 Connector 狀態的訊息,例如 Available、Charging、Faulted、Unavailable。 |
| BootNotification | CP 啟動註冊時使用的訊息。現行 CP 程式在部分重連流程也會嘗試發送,但有冷卻時間,短時間內不一定再次發送。 |
| Watchdog | Branch 定時執行的離線檢查程式。若判定通訊已逾時,會把 CP 標成離線,並依交易保護規則處理其 Connectors。 |
| TriggerMessage | Branch 請 CP 重新送出某類 OCPP 訊息的標準指令。本案可用來請 CP 回報目前的 StatusNotification。 |
**CP 為 ONLINE,只能證明通訊狀態;某支 Connector 究竟是可用、故障或正在充電,仍須依該支的實際狀態判斷。** 因此不能在收到 Heartbeat 時,把所有 Connector 一律改成 AVAILABLE。
## 3. 現場症狀與查核結果
以下是 2026-09-26 約 01:19(台灣時間)的查核快照,並非持續即時監控結果。
| 項目 | 查核結果 | 代表的意義 |
| --- | --- | --- |
| CP003 的 Branch 連線狀態 | ONLINE;最近 Heartbeat 為 01:18:01 | CP 與 Branch 當時已恢復通訊。 |
| Branch 的 CP003-001 | UNAVAILABLE;原因為 `OCPP heartbeat stale` | 後台仍保存「心跳逾時造成的不可用」。 |
| Branch 的 CP003-004 | 同上 | 同上。 |
| Branch 的 CP003-008 | 同上 | 同上。 |
| 三支 Connector 的 Branch 狀態變更時間 | 均為 2026-09-25 06:11:57 | 三支是同一輪離線判定一起被改寫。 |
| CP 設備端的三支 Connector | 均為 AVAILABLE,`current_transaction_id` 均為 NULL | 設備端記錄與 Branch 不一致。 |
| 三支 Connector 的電表讀取 | 持續更新;當時可讀到約 230V、0A,polling log 顯示繼電器斷開 | 當時仍能取得設備量測,但尚未做實際充電測試。 |
| 06:11:57 之後的 CP003 StatusNotification | 截至查核時,OCPP audit 中 request 共 0 筆 | Branch 改成不可用後,沒有再收到可用來恢復狀態的通知。 |
使用者看到的直接影響,是這三支在列表中長時間顯示「無法使用」,無法正確判斷設備是否可用。本次未實際送出啟動充電指令,因此不以此查核宣稱已驗證每一個啟動入口的阻擋結果。
## 4. 事件經過:CP 有回報,Branch 後來又改掉狀態
以下時間均為 2026-09-25,時區為 Asia/Taipei。
| 時間 | 發生的事 | 與問題的關係 |
| --- | --- | --- |
| 05:59:47 | Branch 保存的上一筆 Heartbeat 時間。 | 後續離線檢查仍使用這個舊時間。 |
| 06:06:02 | Branch 收到 BootNotification 並接受。 | 這是當時最後一次 BootNotification 時間。 |
| 06:08:01 | CP003 的 WebSocket 因未及時收到 pong 而關閉。 | 觸發本次重連;為何未收到 pong 的底層原因尚未查明。 |
| 06:08:31 | WebSocket 重連成功。CP 因 BootNotification 冷卻時間尚餘約 150 秒,略過再次發送 BootNotification。 | Branch 的 Boot 時間仍停在 06:06:02。 |
| 06:08:42~06:08:50 | CP 主動送出各 Connector 的 AVAILABLE StatusNotification;Branch 收到並回覆成功。 | 這證明 CP 有主動回報,而且 Branch 已經收到最新狀態。 |
| 06:11:56~06:11:57 | Branch watchdog 判定 Heartbeat 與 BootNotification 均已過期,將 CP003 標成離線,並將 001、004、008 改成 UNAVAILABLE。 | 問題發生點:近期收到的 StatusNotification 沒有被納入存活判定,剛恢復的可用狀態又被覆寫。 |
| 06:13:01 | CP 下一筆 Heartbeat 成功,其後每五分鐘持續成功。 | CP 的連線狀態恢復,但 Heartbeat 本身不包含 Connector 狀態。 |
| 至 9/26 約 01:19 | 沒有再收到新的 StatusNotification,三支仍是 UNAVAILABLE。 | 設備端維持 AVAILABLE,沒有新的狀態變化可自然觸發通知;後台也沒有完成重新同步。 |
為什麼 CP003-005 沒有一起變成「無法使用」?
Branch log 明確記錄:005 當時的 `currentTransactionId=865`,觸發既有交易保護,因此略過不可用狀態的覆寫。這可以解釋為什麼同屬 CP003,只有 001、004、008 受到本次覆寫影響;但單靠這個交易指標,不能推論 005 當時一定有實際供電。
## 5. 原因分析:目前有兩個缺口
### 5.1 缺口一:Branch 沒有把其他有效 OCPP 訊息納入存活判斷
現行 watchdog 主要查看:
- 最後 Heartbeat:`lastHeartbeat`。
- 最後 BootNotification:`lastBootNotification`。
- 兩者是否都超過離線門檻;現場門檻為 330 秒。
這個判斷會漏掉一種情況:**CP 沒有新 Heartbeat,但剛剛確實透過同一條連線送來 StatusNotification 或 MeterValues。**
本案在 06:08:42~50 收到狀態,06:11:56 就判離線,中間約三分鐘,仍在 330 秒範圍內。Branch 卻沒有使用這份新的通訊證據,而是依較舊的 Heartbeat/Boot 時間執行離線處理。
### 5.2 缺口二:連線恢復後,Connector 狀態沒有重新同步
Branch 將 Connector 改成 UNAVAILABLE,是後台自行做的離線處理,並不代表 CP 設備端也把自己的狀態改成 UNAVAILABLE。
因此,可能出現:
- CP 自己一直是 AVAILABLE。
- Branch 保存的是 UNAVAILABLE。
- CP 後來繼續送 Heartbeat,但 Heartbeat 沒有 Connector 狀態。
- CP 沒有發生新的設備狀態變化,也就沒有再次自然送出 StatusNotification。
- Branch 的舊狀態因此一直保留。
所以本單所說的「補送設備狀態」,是**重新請 CP 回報目前狀態,讓 Branch 有實際依據更新紀錄**,並不是認定 CP 在 06:08 沒有回報過。
### 5.3 已確認與尚未確認的界線
已確認:狀態回報成功、Branch 後續以 heartbeat stale 覆寫、心跳恢復後狀態仍未同步。
尚未確認:
- 造成 WebSocket pong timeout 的最底層原因,例如通訊延遲或程式處理阻塞。
- A17 CP003 當時實際執行的 JAR 是否包含 #1456 全部修正;本次尚未比對正式 artifact。
- 三支實際啟動充電後的物理行為;本次僅唯讀查核。
這些待查項目不能先寫成已證實的根因。
## 6. OCPP 1.6 規格如何要求
本單以專案保存的 OCPP 1.6 Edition 2 與 Errata 為依據。SHALL 表示規格要求必須遵守;SHOULD 表示原則上應遵守的建議;MAY 表示允許。
### 6.1 CP 要主動回報 Connector 狀態
依 §4.9 與 Errata §3.19,狀態變更時,CP 必須主動送出 StatusNotification,並考慮最短狀態持續時間。
依 Errata §3.22,BootNotification 被 CS 接受後,CP 必須回報 connectorId 0 及所有 Connectors 的目前狀態;connectorId 0 代表 CP 本體。
依 §4.9,若離線期間狀態有改變,重連後應回報目前狀態。但規格沒有要求在狀態完全不變時,每隔固定時間重送所有 Connector 狀態。因此不能只因 06:13 後沒有新通知,就判定 CP 違反「固定週期回報 Connector 狀態」的義務。
### 6.2 Branch 不應只靠 Heartbeat 判定是否仍有通訊
依 §4.6,若 CP 已在 Heartbeat interval 內送出其他 OCPP 訊息,允許省略該次 Heartbeat;CS 應將收到其他訊息視為仍有通訊的證據。
套用到本案,Branch 剛收到 StatusNotification,這也是一份有效通訊證據。修正時應另外保存「最後有效 OCPP 通訊時間」,而不是把它冒充成真正收到 Heartbeat 的時間。
### 6.3 Branch 可以主動要求最新狀態
依 §5.17,CS 可透過 TriggerMessage,要求 CP 送出 StatusNotification。但必須區分:
- CP 回覆 Accepted:只表示接受這次要求。
- Branch 收到後續 StatusNotification:才取得該 Connector 的實際狀態。
設備可能不支援、拒絕或逾時,不能在沒有實際通知時直接把 Connector 改成可用。
原始文件位置:
- `documents/OCPP_1.6_documentation/ocpp-1.6 edition 2.pdf`。
- `documents/OCPP_1.6_documentation/ocpp-1.6-errata-sheet.pdf`。
## 7. 本次要怎麼修改
以下為待確認的實作設計;本單尚未完成程式修改或部署。
### 7.1 增加「最後有效 OCPP 通訊時間」
建議在 `e_charge_points` 新增可為空的 `last_ocpp_message_at` 欄位。
用途是保存 Branch 實際收到有效 OCPP 訊息的時間。例如收到 StatusNotification、MeterValues 或有效的指令回覆時,都能建立新的通訊證據。Heartbeat 欄位仍只記錄真正的 Heartbeat,避免日後查 log 時無法分辨訊息來源。
必須限制證據來源:
- 訊息必須來自該 CP 目前有效的連線。
- 使用 Branch 的接收時間,不使用設備 payload 裡可能過舊或錯誤的時間。
- 舊連線延遲送達的事件、格式錯誤訊息,以及 Branch 自己送出的指令,不能拿來延長 CP 的在線時間。
- 不因收到未知 CP 的訊息而任意建立設備資料。
資料庫變更需提供可重複執行的 migration。既有資料不以部署當下時間回填,避免把離線設備假裝成剛有通訊。
### 7.2 修改 watchdog,防止把新狀態覆寫成不可用
Watchdog 應一起檢查最後 Heartbeat、BootNotification,以及有效 OCPP 通訊時間。
例如:最後 Heartbeat 已超過 330 秒,但兩分鐘前有收到有效 MeterValues,則不能只因 Heartbeat 過舊就判定設備離線。
另外,檢查與寫入之間可能剛好收到新訊息。因此必須保留現有加鎖後重查機制:準備寫入 OFFLINE 前,再確認最新通訊時間,避免使用數秒前的舊查詢結果覆寫新狀態。
真正沒有通訊、或目前連線確實關閉時,仍要正常執行離線處理;既有未完成交易的保護也要保留。
### 7.3 通訊恢復後,重新取得 Connector 的真實狀態
對因離線而被 Branch 標成 UNAVAILABLE 的 Connector,通訊恢復後安排狀態同步。也要涵蓋本次已經卡住的情況:CP 已 ONLINE,但仍有離線處理留下的 UNAVAILABLE。
建議使用標準 TriggerMessage(StatusNotification),請 CP 重新回報。同步動作必須在背景執行,不能卡住接收 OCPP 訊息的執行緒,否則可能連 CP 的回覆都收不到。
收到實際通知後,依原本的狀態處理流程更新:
- CP 回報 AVAILABLE,就更新為可用。
- CP 回報 FAULTED,就保留實際故障。
- CP 回報 CHARGING,就依既有充電/交易規則處理。
- CP 回報 UNAVAILABLE,也應視為設備真的回報不可用,不能為了讓列表恢復而強制改成 AVAILABLE。
此同步只處理狀態,不應開關繼電器、啟停充電、清除交易,或呼叫具有這些副作用的完整設備 Recovery 流程。
### 7.4 同步失敗要有明確結果,不能無限重試
同一 CP/連線只能有一組進行中的同步工作,避免每收到一筆 MeterValues 就再發一次要求。
需要處理:
- CP 不支援 TriggerMessage。
- CP 回覆 Rejected。
- 指令沒有回覆。
- CP 回覆 Accepted,但後續沒有送出 StatusNotification。
- 同步途中再次斷線,之後由新連線接手。
每種情況都要有可追查的 log、逾時與重試上限。尚未取得真實狀態時保留未同步的事實,不假造成功。
## 8. 與 #1436、#1456 的關係,以及本單範圍
| 議題 | 已處理的內容 | 與本單的差別 |
| --- | --- | --- |
| #1436 | Branch 除 Heartbeat 外,也考慮近期 BootNotification,提供重連後的寬限。 | 本案重連時 Boot 被冷卻機制略過,且有效 StatusNotification 尚未納入判斷。 |
| #1456(已關閉) | CP 重連後立即送 Heartbeat,成功後重新回報 Connector 實際狀態。 | 該次明確未修改 Branch watchdog;本單補 Branch 對其他有效 OCPP 通訊的判斷與恢復同步。 |
| 本單 #1517 | 修正 Branch 存活判定與 Connector 狀態恢復,補上本次事故的回歸驗證。 | 同時盤點 A17 各 CP instance 的 #1456 修正版是否確實部署。 |
本單主要修改 `java/ems_branch`。A17 有 CP001、CP002、CP003 多個 instance,部署驗收時要逐一核對,不能只因 CP001 已更新,就推定 CP003 也是同一版本。
本單不包含重寫 MeterValues 佇列,也不承諾解決尚未查明的 pong timeout 底層原因。正式環境的即時復原、服務重啟與部署,要依後續核准的步驟執行;本次診斷尚未做這些操作。
## 9. 修改後應如何驗收
每個案例都要同時核對 OCPP 訊息與後台保存的狀態,不能只看到 HTTP 200、容器執行中,或 CP 為 ONLINE 就算通過。
| 案例 | 測試情境 | 預期結果 |
| --- | --- | --- |
| 近期有狀態回報 | Heartbeat/Boot 已舊,但在離線門檻內收到 StatusNotification。 | 不誤判離線,也不把剛回報的狀態覆寫為 UNAVAILABLE。 |
| 近期有電表回報 | Heartbeat/Boot 已舊,但仍持續收到有效 MeterValues。 | 通訊被視為仍有活動;Connector 狀態不因舊 Heartbeat 被改成不可用。 |
| 真正通訊逾時 | 所有存活證據都超過門檻,或目前連線確實關閉。 | 正常執行離線處理,保留既有交易保護。 |
| 查詢與寫入交錯 | Watchdog 先查到候選設備,但寫入前剛收到新訊息。 | 加鎖後重查可看到新證據,取消過時的離線寫入。 |
| 舊連線或無效訊息 | 重連後舊連線仍有延遲事件,或收到格式錯誤內容。 | 不延長目前連線的存活時間,不污染設備狀態。 |
| 離線後恢復可用 | Branch 已因離線標成 UNAVAILABLE,CP 恢復後回報 AVAILABLE。 | Branch 根據實際回報恢復為可用。 |
| 離線後恢復但仍故障/充電 | CP 恢復後回報 FAULTED 或 CHARGING。 | 真實狀態原樣處理,不一律改成 AVAILABLE,交易不被誤清除。 |
| 設備明確回報不可用 | CP 回報 UNAVAILABLE。 | 保留設備真實不可用,不與後台殘留的離線狀態混淆。 |
| 接受指令但未回報 | TriggerMessage 回覆 Accepted,卻一直沒有後續狀態通知。 | 到期記錄同步未完成,不假裝已恢復。 |
| 不支援、拒絕或無回覆 | NotImplemented、Rejected、timeout。 | 有明確紀錄及有界處理,不無限重試或產生指令風暴。 |
| 同步途中再次重連 | 前一批要求尚未完成,CP 已換新連線。 | 舊連線的回覆/排程不影響新連線,同一時間不重複發起多批同步。 |
| A17 原事故重播 | 重現 Boot cooldown、先回報 AVAILABLE、watchdog 判定、較晚 Heartbeat 的先後順序。 | 修正前能重現問題;修正後不誤覆寫,已離線者也能依實際通知恢復。 |
現場驗收另需確認 A17 CP001/002/003 的版本與行為,並確認狀態同步過程沒有造成繼電器操作、充電啟停或交易異動。
## 10. 測試目錄與執行安排
既有案例需更新:
- `OCP-001`:離線判定及交易保護。
- `OCP-007`:watchdog 存活證據與加鎖後重查。
- `OCP-008`:新舊連線交錯。
- `REL-005`:部署後 OCPP 通訊與狀態驗證。
新增回歸重點:
- P0:Heartbeat 過舊,但近期其他 OCPP 訊息仍能防止誤判。
- P0:watchdog 已先標成離線後,恢復真實 Connector 狀態且不改變供電或交易。
- P1:舊連線/無效訊息不能刷新存活。
- P1:狀態同步不支援、拒絕、逾時或只有 Accepted 時,不假成功也不無限重試。
案例在 OCPP 訊息處理、watchdog、session、重連、恢復同步、資料庫欄位或相關部署變更時執行。發版測試採 Major/OCPP 範圍:所有適用 P0、P1,以及受影響 P2。
目前分支的 `OCP-018` 已用於 #1459 的 Fortune 案例,與 #1456 歷史文件的編號有差異。新增時必須取目前目錄未使用的 ID,不得覆寫既有案例。需要實機或特定廠商的案例,在完成前保留 Gap/Conditional,不以單元測試通過替代。
## 11. 目前進度與交付資料
已完成:
- A17 正式環境唯讀查核與事件時序整理。
- 比對本地 OCPP 1.6 原始規格與 Errata。
- 建立本 ticket,優先級 High。
- 從最新 `origin/main`(`4b0e412`)建立獨立分支 `feature/redmine-1517-ocpp-liveness`。
- 完成實作規劃,列出預計變動檔案、migration、風險及測試設計。
- 既有 E2E 目錄格式檢核通過;這僅表示目錄格式正確,不代表本單修正或測試已通過。
相關文件:
- 查核報告:`test_report/20260926_001_a17_cp003_unavailable_readonly.md`。
- 實作規劃:`documents/20260926/redmine-1517-implementation-plan.md`。
- Ticket 快照:`redmine-tickets/1517.md`。
**2026-09-26 11:33:20 已依 Ken 指示完成 A17 三支的單次現場復原;永久修正的程式修改、整批測試與部署尚未完成。** 本次採 SSH/MySQL 維運方式,依已鎖定的 edge AVAILABLE、最新電表資料與交易/命令檢查結果,直接校正 Branch 三筆 e_connectors;不是 CP 補送 StatusNotification。11:34:16 獨立複查確認三支 Branch/edge 均為 AVAILABLE、無交易,CP003-005 與 CP003-004 原離峰隊列未變動,沒有重啟或操作充電。由於未經應用事件流程,既有畫面可能需要重新整理。本次是現場資料復原,#1517 仍需完成永久修正。復原報告:`test_report/20260926_002_redmine-1517-a17-status-recovery.md`。
Ken 已明確要求本案不使用瀏覽器,未經其同意不得使用 Computer Use。後續實作仍依已列出的規劃確認及測試啟動流程進行。 **尚未完成程式修改、整批測試或部署,A17 現場也尚未由本次工作執行復原。** 目前待確認上述實作規劃;依專案流程,確認後進入程式修改,整批測試與正式部署另依各自的啟動條件進行。
返回