專案

一般

配置概況

Feature #1400

是由 陳國瑋約 24 小時 前更新

## h2. 背景 

 Branch Admin「連接器 Admin 「連接器 → 編輯 → 充電樁狀態」分頁(`react/branch/app/(dashboard)/connector/components/connector-status.tsx`)目前全靠 充電樁狀態」分頁(@react/branch/app/(dashboard)/connector/components/connector-status.tsx@)目前全靠 REST 輪詢取得狀態: 

 * `GET /api/connector/status/{connectorId}` @GET /api/connector/status/{connectorId}@`AVAILABLE`/`PREPARING`/`CHARGING` @AVAILABLE@/@PREPARING@/@CHARGING@ 時每 **2 秒**輪詢一次。 *2 秒*輪詢一次。 
 * 另外為了判斷是否可遠端操作,再打 ChargePoint API 取得 `ocppConnectionStatus`,離線時每 **10 秒**輪詢。 取 @ocppConnectionStatus@,離線時每 *10 秒*輪詢。 
 * 送出 start/stop 後靠 `setTimeout(3000)` @setTimeout(3000)@ 硬等 3 秒才 refetch,用猜的方式等待後端狀態落地。 refetch,用猜的等待後端狀態落地。 

 問題: 

 1. # 後端資料實際上只在 OCPP `StatusNotification`/`MeterValues` 進來時才更新(`MeterValueService`、`TransactionService`、`ChargingOrchestrator`、`ChargePointOfflineService` @StatusNotification@/@MeterValues@ 進來時才更新(@MeterValueService@、@TransactionService@、@ChargingOrchestrator@、@ChargePointOfflineService@ 都會發 connector event)。2 秒輪詢絕大多數拿到同一份資料,是純浪費,卻又不保證比推送快。 
 2. # 同一份 Connector 狀態,LIFF(#1399)已改為由 WebSocket 推送的 `ConnectorForHqDto` @ConnectorForHqDto@ 取得,後台仍是輪詢。同一個真實狀態有兩條取得路徑、兩種時序,出事時無法對齊時間軸。 
 3. 一個畫面同時掛兩條不同節奏的輪詢,`status` # 一個畫面同時掛兩條不同節奏的輪詢,@status@`ocppConnectionStatus` @ocppConnectionStatus@ 可能短暫互相矛盾,已經需要在 FE 使用 `prevOnlineRef` 用 @prevOnlineRef@ 補償。 

 **注意:本案不是要後台照抄 *注意:本案不是要後台照抄 LIFF 的顯示或操作方式。** 的顯示或操作方式。* LIFF 的 `driverState`/`actions[]` 是車主語意(`OFF_PEAK_WAITING`、「取消排隊」),授權綁住戶關聯與 @driverState@/@actions[]@ 是車主語意(@OFF_PEAK_WAITING@、「取消排隊」),授權綁住戶關聯與 transaction owner,管理員不適用也不需要。 

 本案只要求**資料取得管道與來源一致**:同一份 owner,管理員不適用也不需要。本案只要求*資料取得管道與來源一致*:同一份 Connector 狀態、同一個推送時機,後台使用自己的維運語意呈現與操作。 狀態、同一個推送時機,後台用自己的維運語意呈現與操作。 

 ## h2. 目標 

 * 提供後台專用的 Connector 狀態 WebSocket 推送,讓 FE 可移除輪詢與 3 秒硬等。 
 * 後台需要的維運欄位由同一份 payload 一次給齊,不再為了單一欄位另打一支 API。 
 * 後台充電操作維持現行 operator 授權模型,但補上可追蹤性與重送防護。 

 ## h2. A-1. 新增後台專用 Connector WebSocket endpoint(ems_branch) 

 * `ems_branch` @ems_branch@ 目前只有 OCPP 使用的 `/ws/ocpp/*`,沒有供 用的 @/ws/ocpp/*@,沒有供 UI 使用的 WebSocket,需新增。 
 * **不可**沿用 *不可*沿用 HQ 的 `/ws/connector/{buildingId}/{connectorId}`:該 @/ws/connector/{buildingId}/{connectorId}@:該 endpoint 的 `LineIdHandshakeInterceptor` @LineIdHandshakeInterceptor@`xLineId` 查詢 `findByLineIdAndBuildingId` @xLineId@ 查 @findByLineIdAndBuildingId@ 驗證,後台管理員沒有 LINE 身分,也不是該 身分也非該 building 住戶,握手必定被拒。 
 * 握手驗證改用 Branch 既有的 ss3a 身分(JWT),並套用該管理員對此 Connector/Building 的既有權限判斷。授權失敗時的 close code 與原因請明確定義,供 FE 分辨「無權限」與「連線失敗」。 
 * Branch 本身就是 connector event 的 publisher,**直接由 publisher,*直接由 Branch 推送即可,不需要繞 RabbitMQ 再回來**。 再回來*。 
 * 推送時機請與現有 `ConnectorEventPublisher` 的觸發點一致,包括: 

   * 狀態變更 
   * MeterValues 
   * 交易起訖 
   * 離線偵測 
   * remote @ConnectorEventPublisher@ 的觸發點一致(狀態變更、MeterValues、交易起訖、離線偵測、remote operation lifecycle lifecycle)。 
 * 連線建立時需提供一份 **initial snapshot**,FE 連線建立時需給一份 *initial snapshot*,FE 不再另打 detail REST。 

 ## h2. A-2. Payload 欄位需求 

 Base 沿用 `ConnectorForHqDto`(`GET /api/connector/status/{connectorId}` 現在回傳的就是它),後台已使用: @ConnectorForHqDto@(@GET /api/connector/status/{connectorId}@ 現在回的就是它),後台已使用: 
 @id@/@status@/@statusLabel@/@startTime@/@currentPower@/@currentVoltage@/@currentCurrent@/@energyUsage@/@totalEnergy@/@remainTime@/@costPredict@/@currentTariff@ 

 `id`/`status`/`statusLabel`/`startTime`/`currentPower`/`currentVoltage`/`currentCurrent`/`energyUsage`/`totalEnergy`/`remainTime`/`costPredict`/`currentTariff` 

 需要補進同一份 payload 的維運欄位: 

 | |_. 欄位                                         | |_. 用途                                          | |_. 目前如何取得                         | 
 | ---------------------------------------- | ----------------------------------------- | ---------------------------- | 
 | `ocppConnectionStatus`(id @ocppConnectionStatus@(id + label)         | label)| 判斷是否可遠端操作、離線時停用按鈕                           | **另打 *另打 ChargePoint API 並自行輪詢** 並自行輪詢* | 
 | `chargePointId`                            @chargePointId@ | 關聯導頁與 engineer tools                        | 由 FE 從 `ConnectorDto` ConnectorDto 帶入       | 
 | `lastHeartbeatAt`                          @lastHeartbeatAt@ | 判斷離線多久,區分剛斷線與長期失聯                           | 無                              | 
 | `statusChangedAt`                          @statusChangedAt@ | 判斷狀態停滯多久,例如卡在 `PREPARING`                   | 狀態停滯多久(例如卡在 @PREPARING@)|                             | 
 | `errorCode`/`vendorErrorCode`/`vendorId` @errorCode@/@vendorErrorCode@/@vendorId@ | `FAULTED` @FAULTED@ 時的實際故障原因                          | **無,後台目前只顯示「故障/請聯繫技術支援」**      *無,後台目前只顯示「故障/請聯繫技術支援」* | 
 | `currentTransactionId`                     @currentTransactionId@ | 對照充電記錄、engineer tools 追查                    | 無                              | 
 | `currentIdTag`/使用者識別                       @currentIdTag@/使用者識別 | 知道現在是誰在充電,公樁停止前確認                           | 無                              | 
 | `activeOperation`(#1398 已有)                | @activeOperation@(#1398 已有)| 有未終結的 RemoteStart/RemoteStop 時,避免管理員重複送指令 | 無                              | 

 `driverState`(車主語意)在後台不使用;payload 帶著無妨,但後台不得使用它決定顯示或操作。 @driverState@(車主語意)在後台不使用;payload 帶著無妨,但後台不得用它決定顯示或操作。 

 ## h2. A-3. 操作面 API 

 後台維持現行: 

 * `POST /api/tx/charge/start` 
 * `POST /api/tx/charge/stop` 

 以上 API 以 後台維持現行 @POST /api/tx/charge/start@/@POST /api/tx/charge/stop@(以 ss3a 管理員身分帶入 `userId`,**不改用** `POST /api/connector/commands`。 

 管理員身分帶 @userId@),*不改用* @POST /api/connector/commands@ —— 後者的 `authorizeConnector()` 會檢查住戶關聯,公樁停止時還要求操作者是 @authorizeConnector()@ 檢查住戶關聯、公樁停止還要求是 transaction IdTag owner,管理員一律會被 `CONNECTOR_FORBIDDEN` 擋下。 @CONNECTOR_FORBIDDEN@ 擋掉。 

 但需要補齊以下項目: 但需要補齊: 

 1. **回傳 `operationId`** 

    `operationId` 必須與 # *回傳 @operationId@*,與 #1398 的 `e_remote_transaction_log.id` @e_remote_transaction_log.id@ 一致,讓後台操作能與 LIFF、Branch、OCPP log 串起同一條時間軸。目前後台送出的指令在稽核上是斷點。 

 2. **明確的成功/失敗語意** 

    現行回應是 `isSuccess` 
 # *明確的成功/失敗語意*。現行回應是 @isSuccess@ + `message`,FE 只能將訊息原樣顯示給使用者。 

    請提供穩定的 `errorCode`,例如: 

    * 充電樁離線 
    * 已有未終結 operation 
    * 狀態不允許 

    讓 @message@,FE 只能把訊息原樣丟給使用者。請提供穩定的 @errorCode@(如「充電樁離線」「已有未終結 operation」「狀態不允許」),讓 FE 能依錯誤類型分流,而不是比對訊息字串。 

 3. **operation 能分流而不是比對字串。 
 # *operation 結果透過 A-1 的 WebSocket 推送** 

    以 WebSocket 推送 operation 結果,取代 推送*,取代 FE 目前的 `setTimeout(3000)` @setTimeout(3000)@ 硬等。 

 4. **已有未終結 
 # *已有未終結 operation 時明確拒絕** 

    當 `activeOperation` 時明確拒絕*(@activeOperation@ 不為 `null` 時,不得讓管理員再次送出 RemoteStart/RemoteStop。 

    #1398 null),不要讓管理員連送兩筆 RemoteStart/RemoteStop。#1398 §15.2 已規定同一 Connector 不得存在重疊的未完成 operation,後台入口也必須遵守。 

 ## h2. 不在本次範圍 

 * Connector 列表頁的狀態顯示,仍走既有查詢;本案先處理單一 列表頁的狀態顯示(仍走既有查詢;本案先處理單一 Connector 詳細狀態。 詳細狀態)。 
 * Engineer tools(#1380)既有的 OCPP/Modbus 指令面板。 
 * LIFF 端行為(#1397/#1398/#1399)。 

 ## h2. 驗收重點 

 * 後台可透過新 WebSocket endpoint,以管理員身分連線並取得 endpoint 以管理員身分連線並取得 initial snapshot;非授權身分(含 LIFF lineId)連線時會被拒絕,且 lineId)連線被拒且 close code 定義明確。 明確。 
 * Payload 一次帶齊 A-2 的全部欄位,`status` 全部欄位,@status@`ocppConnectionStatus` @ocppConnectionStatus@ 來自同一份資料。 
 * 推送時機涵蓋狀態變更、MeterValues、交易起訖、離線偵測與 remote operation lifecycle。 
 * `FAULTED` 時,payload 帶出實際的 `errorCode`/`vendorErrorCode`。 @FAULTED@ 時 payload 帶出實際 @errorCode@/@vendorErrorCode@。 
 * start/stop 回傳 `operationId` 與穩定的 `errorCode`;已有未終結 @operationId@ 與穩定 @errorCode@;已有未終結 operation 時,操作會被拒絕。 時被拒絕。 
 * 後台送出的充電操作,可使用 `operationId` 後台送出的充電操作可用 @operationId@ 在 Branch log 追蹤到對應的 追到對應的 RemoteStart/RemoteStop。 

 ## h2. 對應 FE ticket 

 FE 實作見 #1401。 

返回