# Redmine #1397 車主狀態與離峰充電需求規格

> 狀態：需求審查草稿
>
> 日期：2026-07-29
>
> 修訂日期：2026-07-31
>
> 需求來源：Redmine #1382
>
> 範圍：Branch／HQ API 契約與 LIFF 端到端行為。本文件尚未授權進入實作。

## 1. 目標

`driverState` 必須描述車主目前實際面對的情況，以及現在可以執行的操作。它不能只是 raw OCPP Connector status 的中文翻譯。

本次設計需要保留 #1382 之前的使用者體驗，同時正確處理：

- 公樁與私樁。
- 手動充電與長期離峰充電。
- 私樁 Charge Group slots 與排隊。
- CloudLink 與 Fortune 的廠牌差異。
- RemoteStart／StartTransaction 與 RemoteStop／StopTransaction 的非同步流程。
- OCPP 離線、故障、服務重啟、逾時與晚到事件。
- 共用 WebSocket 狀態與個別使用者的 API 授權。

## 2. 系統責任邊界

### 2.1 Branch

Branch 是下列資料的唯一判斷來源：

- `driverState`。
- `driverState.actions` 所提供的操作能力。
- Remote operation 的生命週期。
- 手動排隊與離峰排程狀態。
- Command API 最終的狀態檢查與使用者授權。
- 車主可理解的操作結果與目前阻擋原因。

Branch 必須綜合判斷：

- raw Connector status。
- OCPP connection status。
- Branch 與 CP 的 active transaction。
- `e_connector_priority`。
- 長期離峰設定。
- 已持久化的 RemoteStart／RemoteStop operation。

### 2.2 HQ

HQ 必須：

- 從 LINE 身分取得已驗證的車主。
- 將可信任的 Branch user identity 傳給 Branch。
- 原樣傳遞 Branch 的 `driverState` 與 operation result，不建立第二套狀態機。
- WebSocket initial data 與後續 connector event 都使用同一份 Branch presentation。
- 共用 Connector 狀態廣播給所有合法 sessions，但 transient
  `operationResult` 只送給 authenticated Branch user identity 符合 event
  `targetUserId` 的 session。

HQ 不得信任 LIFF 任意傳入的 `userId`。

### 2.3 LIFF

FE source implementation 由 Redmine #1399 負責。本文件定義 #1398 Backend 與
#1399 Frontend 必須共同實作的契約。

LIFF 負責：

- 顯示 Branch 提供的 title、description 與 actions。
- `areaType=public` 時不顯示離峰 switch。
- 發送 command，並顯示結構化的成功或拒絕結果。
- WebSocket 連線正常時才允許操作。
- 在指定情境顯示確認 modal。

當 `driverState` 存在時，LIFF 不得再用 raw `status` 推導是否可以充電。

## 3. 領域名詞

### 3.1 公樁（PUBLIC Connector）

- 不參與私樁 rotation queue。
- 不顯示離峰 switch。
- 合法的公樁使用者不需要經過私樁 Charge Group 排隊即可要求啟動。
- Charge Group 計算 slots 時，已預先保留公樁容量。
- 公樁正在充電時，只有該 Transaction 的使用者可以停止；最終由 Command API 判斷。

範例：

```text
Charge Group 實體總容量 = 7 slots
PUBLIC Connector = 2
PRIVATE 可排程容量 = 5 slots
```

公樁 slot 是依照設定在 Charge Group 內的公樁數量保留，不會因公樁目前離線、故障或未充電而暫時釋放給私樁。

### 3.2 私樁（PRIVATE Connector）

- 透過 `e_connector_user` 與一位或多位 Branch user 關聯。
- 參與 Charge Group slot allocation 與 rotation queue。
- 支援長期離峰設定。
- 目前仍與該 Connector 關聯的任一住戶，都可以停止私樁充電、取消手動排隊、取消離峰等待，或取消廠牌支援的 PREPARING。
- 即使 Transaction 或離峰模式由另一位關聯住戶建立，也必須記錄真正執行停止或取消的人。

現有程式中的 `owner` 用語並不精確。Connector 目前只有「關聯住戶」，沒有正式的 owner role。

### 3.3 離峰充電

離峰充電是長期偏好，不是單次 ChargingRequest。

- `enableOffPeak=true` 表示 Connector 會持續參加未來的離峰時段。
- 開啟離峰時，必須記錄是哪一位關聯住戶開啟。
- 只有進入適用離峰時段、scheduler 實際評估後，才建立 queue record。
- 完成充電、暫時故障／離線，或目前 window 被抑制時，長期偏好仍維持開啟。
- 關閉離峰必須是明確的使用者操作。

### 3.4 離峰 window

一段連續的 tariff `OFF_PEAK` 區間，就是一個 window。

- 跨越午夜但時間連續，仍屬同一個 window。
- 同一天兩段不連續的 OFF_PEAK 區間，屬於兩個 window。
- Current-window suppression 到該連續 OFF_PEAK 區間結束時失效。
- 離峰時段結束只影響「是否可自動開始」，不能強制停止已經進行中的 Transaction。

## 4. DriverState 核心規則

Raw OCPP `PREPARING` 不等於 `driverState=PREPARING`。

```text
只有 Branch 已送出 RemoteStart，
而且仍在等待 StartTransaction 時，
driverState 才是 PREPARING。
```

這項規則用來保留「先插槍、再開 App」的體驗：

| 廠牌 | 已插槍但未送 RemoteStart | Raw status | 手動模式 driverState |
| --- | --- | --- | --- |
| Fortune | 是 | 通常為 `PREPARING` | `READY` |
| CloudLink | 是 | 通常為 `AVAILABLE` | `READY` |

在離峰模式下，同樣只是插槍但尚未 dispatch 的 Connector，應依 Priority 是否真的存在顯示 `OFF_PEAK_WAITING` 或 `QUEUED`。

## 5. DriverState codes

| Code | 車主語意 | 主要操作 |
| --- | --- | --- |
| `READY` | 沒有 active transaction、queue 或 remote operation，可手動開始 | 開始充電 |
| `OFF_PEAK_WAITING` | 長期離峰已開啟，但目前不在可派發 queue 中 | 關閉離峰／取消等待 |
| `QUEUED` | 已存在真正的手動或離峰 queue record，等待派發 | 取消對應來源的排隊 |
| `PREPARING` | RemoteStart 已送出或已接受，等待 StartTransaction | 廠牌支援時可取消啟動 |
| `CHARGING` | Branch 或 CP 有 active transaction | 停止充電 |
| `FINISHING` | RemoteStop 已送出或已接受，等待 StopTransaction | 不可重複停止 |
| `OFFLINE_CHARGING` | OCPP 離線，但 Branch／CP 證據顯示可能仍在充電 | 不可遠端操作 |
| `OFFLINE_UNAVAILABLE` | OCPP 離線，且沒有 active transaction 證據 | 不可開始或停止 |
| `FAULTED` | Connector 故障，且沒有更強的 active transaction 狀態 | 不可開始充電 |
| `UNAVAILABLE` | Connector 因其他原因不可用 | 不可執行充電 command |
| `UNKNOWN` | Branch 無法安全判斷 | 不可執行充電 command |

`BLOCKED`、`PAUSED`、`ELIGIBLE`、`NOT_PLUGGED` 等 `ConnectorPriority` 內部狀態，不應直接變成公開的 driverState code。

## 6. 狀態優先順序

實作可以使用 decision service 或狀態機，但必須遵守以下規則：

1. 只要存在 active transaction 證據，就不能顯示 `READY`。
2. 未完成的 STOP operation 顯示 `FINISHING`。
3. 未完成的 START operation 顯示 `PREPARING`。
4. OCPP 離線且沒有 active transaction 時，顯示 `OFFLINE_UNAVAILABLE`。
5. OCPP 離線但有 active transaction 證據時，顯示 `OFFLINE_CHARGING`。
6. `FAULTED`／OCPP offline 不得被 `OFF_PEAK_WAITING` 或 `QUEUED` 遮蔽。
7. 真正且適用的 queue record 才可顯示 `QUEUED`。
8. 長期離峰已開啟、但尚無適用 queue record 時，顯示 `OFF_PEAK_WAITING`。
9. Fortune raw `PREPARING` 若沒有 RemoteStart operation，回到對應的手動或離峰狀態。

## 7. Actions

API 應提供具備明確業務語意的 action，FE 不應再從 `start`／`stop` label 猜測用途。

Action code：

- `START_CHARGING`
- `STOP_CHARGING`
- `CANCEL_START`
- `CANCEL_MANUAL_START`
- `ENABLE_OFF_PEAK`
- `DISABLE_OFF_PEAK`

每個 action 至少提供：

```json
{
  "code": "CANCEL_START",
  "enabled": true,
  "label": "取消啟動",
  "disabledMessage": null,
  "requiresConfirmation": true
}
```

新契約只提供 `driverState.actions[]`。舊版 `driverState.start`／
`driverState.stop` 直接移除，不提供相容層。Redmine #1398 與 #1399 必須協調部署。

WebSocket 中的共用 actions 表達設備與流程能力，不代表目前使用者一定有權執行。Command API 仍需做最後授權。

## 8. 手動充電流程

### 8.1 可立即開始

```text
READY
  -> 車主要求 START_CHARGING
  -> RemoteStart operation SENT／ACCEPTED
  -> PREPARING
  -> 收到 StartTransaction
  -> CHARGING
```

### 8.2 私樁 slots 已滿

```text
READY
  -> 車主要求 START_CHARGING
  -> PRIVATE slots 已滿
  -> 建立 MANUAL queue request
  -> QUEUED
```

Queue 必須保存：

```text
source = MANUAL
requestedByUserId = 原始要求開始的車主
```

Slot 釋放後，scheduler 必須使用原始 requester 的 IdTag dispatch，不能再選 Connector 關聯住戶中的第一人。

`CANCEL_MANUAL_START` 只刪除手動 queue，不得修改 `enableOffPeak`。

### 8.3 手動啟動失敗

- 手動 RemoteStart 不自動重試。
- `REJECTED`、`FAILED` 或最終 `TIMED_OUT` 後回 `READY`。
- 失敗結果與目前 driverState 分開回傳。
- 如果 timeout 後仍收到晚到的 StartTransaction，實際 Transaction 優先，driverState 改為 `CHARGING`。

## 9. PREPARING 行為

以下 operation status 都顯示 `PREPARING`：

- `SENT`
- `ACCEPTED`

廠牌取消能力：

| 廠牌 | PREPARING action |
| --- | --- |
| CloudLink | `CANCEL_START` 可用；Branch 可執行既有的 waiting-mode exit 流程 |
| Fortune | `CANCEL_START` 不可用，並提供原因；車主需拔槍或等待 timeout |

取消成功後：

- 手動來源回 `READY`。
- 離峰來源同時關閉長期離峰、移除相關 Priority，回到手動模式。

RemoteStart 明確收到 `Rejected`：

- 手動來源回 `READY`。
- 離峰來源停止本 window 重試，回 `OFF_PEAK_WAITING`。
- reason 使用 `START_REJECTED_FOR_CURRENT_WINDOW`。
- 下一個獨立 off-peak window 可以重新排程。

RemoteStart 沒有回應：

- 保留各廠牌現有的 Charge Point RemoteStart 等待時間。
- Branch 使用相同等待長度作為 operation business deadline，並從
  RemoteStart `Accepted` 開始計時；本需求不修改 Charge Point 等待行為。
- Business deadline 後保留 30 秒訊息 reconciliation grace。Grace 只接收已在
  傳輸中的 StartTransaction／StatusNotification，不延長 Charge Point
  relay 或電流偵測行為。
- Charge Point 等待到期後若回報 AVAILABLE／FAULTED 且沒有 Transaction，
  Branch 可依明確設備結果終結 operation，不必等滿 grace。
- Grace 結束仍沒有 StartTransaction 或明確 terminal device result，才發布
  `TIMED_OUT`。
- Operation 未完成期間不得盲目重送。
- 離峰的 transient timeout 可回 `QUEUED`，在之後正常 scheduler cycle 重試。
- 同一 Connector 不得同時存在重疊 START operation。
- 目前 window 結束後停止重試。

## 10. CHARGING 與 FINISHING

```text
CHARGING
  -> 車主確認 STOP_CHARGING
  -> RemoteStop operation SENT／ACCEPTED
  -> FINISHING
  -> 收到 StopTransaction
  -> Transaction 完成
```

`FINISHING` 期間：

- Start 停用。
- Stop 停用。
- Off-peak switch command 由 API 拒絕。
- Branch 重啟後必須從持久化 operation 還原 FINISHING。

RemoteStop 沒有回應時：

1. Backend 可設定的 30 秒 Stop deadline 以前保持 `FINISHING`。
2. 不自動重送 RemoteStop。
3. 期間收到 StopTransaction 就正常完成。
4. Deadline 到期時重新查實際 Transaction。
5. Transaction 仍為 ACTIVE：operation 標記 `TIMED_OUT`，driverState 回 `CHARGING`，顯示失敗並允許新的明確停止要求。
6. Transaction 已完成：視為晚到或對應延遲，進行成功 reconciliation。
7. Deadline 後收到 late StopTransaction 仍完成 Transaction reconciliation，
   但不重新開啟已終結 operation，也不重播 timeout modal。

離峰 current-window suppression 只有在 Transaction 確認停止後才生效。

## 11. 長期離峰流程

### 11.1 開啟離峰

私樁 off-peak ON 成功後立即：

```text
enableOffPeak = true
enabledByUserId = 已驗證的關聯住戶
driverState = OFF_PEAK_WAITING
```

API 不等待 `e_connector_priority` 建立才回應。

### 11.2 進入適用離峰時段

Scheduler 在適用 window 評估 Connector 後：

```text
建立或更新 source=OFF_PEAK 的 Priority
driverState = QUEUED
```

`OFF_PEAK_WAITING` 與 OFF_PEAK `QUEUED` 都提供 `DISABLE_OFF_PEAK`，LIFF 顯示「取消排隊」。

### 11.3 尚無充電需求

若 dispatch 後沒有建立充電需求：

```text
driverState = QUEUED
reason = NO_CHARGING_DEMAND
```

這是中性狀態，不是錯誤。Description 不得暗示「充電槍沒有正確連接」。

因為車輛可能稍晚才到，scheduler 可以在同一 window 的後續正常 cycle 再試。

### 11.4 已開始充電

收到 StartTransaction：

```text
driverState = CHARGING
```

`SUSPENDED_EV` 與 `SUSPENDED_EVSE` 仍屬 charging-like，因為 Transaction 還是 ACTIVE。此時不得建立另一筆 RemoteStart。

### 11.5 車主停止離峰充電

LIFF modal 必須清楚告知：

- 將停止目前的 active transaction。
- 本次離峰 window 不會再自動啟動。
- 長期離峰偏好不會因此關閉。

收到 StopTransaction、確認停止後：

```text
enableOffPeak = true
driverState = OFF_PEAK_WAITING
reason = USER_CANCELLED_FOR_CURRENT_WINDOW
```

車主若要在同一 window 重新加入排程，必須先將離峰 switch OFF，再切回 ON。

### 11.6 Transaction 因其他原因結束

OCPP 1.6 沒有可靠的 `FullyCharged` StopReason，A17 也無法可靠區分滿電或拔槍：

- A17 MeterValues 沒有 SoC。
- CP stop detector 可能把持續低電流寫成 `EVDisconnected`。

因此 UI 不得宣稱「已充滿」或「已拔槍」。

如果本 off-peak window 內確實建立過 Transaction，之後收到 StopTransaction：

```text
enableOffPeak = true
driverState = OFF_PEAK_WAITING
reason = TRANSACTION_ENDED_FOR_CURRENT_WINDOW
```

本 window 不再自動啟動，下一個 off-peak window 正常重新排程。

若只有 `SUSPENDED_EV`、沒有 StopTransaction，表示 Transaction 尚未結束，不適用本規則。

### 11.7 Current-window suppression

Suppression 必須持久化。原因是現有 scheduler 會在後續 cycle 重設非 CHARGING 的 Priority。

Suppression reason 至少包含：

- `USER_CANCELLED_FOR_CURRENT_WINDOW`
- `START_REJECTED_FOR_CURRENT_WINDOW`
- `TRANSACTION_ENDED_FOR_CURRENT_WINDOW`

Suppression 一直維持到目前連續 OFF_PEAK tariff range 結束。

## 12. Semantic Command API 與離峰 switch

### 12.1 統一 LIFF Command API

LIFF 的所有車主操作都送到同一個 HQ endpoint：

```http
POST /api/connector/commands
```

```json
{
  "buildingId": "BLD000000014",
  "connectorId": "CP001-001",
  "action": "START_CHARGING"
}
```

HQ 從 LINE session 取得 authenticated Branch user。LIFF 不得傳入 `userId`。
HQ 再以相同 semantic command 呼叫 Branch：

```http
POST /api/cp/hq/connector/commands
```

Command code：

- `START_CHARGING`
- `STOP_CHARGING`
- `CANCEL_START`
- `CANCEL_MANUAL_START`
- `ENABLE_OFF_PEAK`
- `DISABLE_OFF_PEAK`

RemoteStart／RemoteStop 完成驗證、持久化 operation 並成功 dispatch OCPP 後，
回 HTTP `202 Accepted`。Response 包含 `operationId`、
`operationStatus=SENT` 與最新 `driverState`，不等待 OCPP CALLRESULT 或
StartTransaction／StopTransaction。

同步完成的取消與離峰偏好 command 回 HTTP `200 OK` 與最新實際狀態。
Dispatch 前的授權或狀態衝突使用結構化 `403`／`409`。

#1398／#1399 協調部署後，移除既有 LIFF-facing `/connector/start`、
`/connector/stop`、`/connector/dequeue` 與 `/connector/offpeak-switch`。
低階 engineer／OCPP endpoint 保留，但 LIFF 不得直接使用。

### 12.2 FE 行為

- PUBLIC：不顯示 switch。
- PRIVATE 且 WebSocket 已連線：switch 可維持可點擊。
- WebSocket 斷線：不得使用舊 actions 或舊 switch state 發送 command。

FE 不維護複雜的 business-state enabled／disabled matrix。

Switch 分別對應統一 command endpoint 的 `ENABLE_OFF_PEAK` 或
`DISABLE_OFF_PEAK`。Primary action 提供的 `DISABLE_OFF_PEAK` 也使用同一 command。

### 12.3 API 是最終判斷者

Switch request 送出後，Branch 以最新資料驗證並回傳：

- 成功：實際 `enableOffPeak`、最新 `driverState`，並發布 event。
- 拒絕：結構化 error code／message、未變更的實際 `enableOffPeak`、最新 `driverState`。

API 拒絕以下操作：

- PUBLIC Connector。
- Active `PREPARING`、`CHARGING` 或 `FINISHING`。
- 存在 MANUAL queue 時要求開啟 off-peak。
- 非合法關聯住戶。
- 永久或結構性的設定錯誤。

### 12.4 開啟時必須立即拒絕的情況

- Connector 不是 PRIVATE。
- 使用者不是目前關聯住戶。
- 沒有 Charge Group。
- 沒有有效 tariff 或 tariff 沒有 OFF_PEAK range。
- 開啟者沒有 IdTag。
- 開啟者的 IdTag 未授權。

### 12.5 不應阻止開啟的暫時情況

- 目前不在離峰時段。
- 目前沒有 slot。
- 沒有車或沒有充電需求。
- OCPP offline。
- Connector faulted。

### 12.6 開啟後資料發生變化

- 開啟者失去 Connector 關聯：關閉離峰、清除 Priority 與 enabled actor，不轉移給其他住戶。
- Connector 沒有任何關聯住戶：關閉離峰並清除 Priority。
- 開啟者 IdTag 之後失效，但仍保持關聯：保留長期偏好與 actor，暫停自動啟動，顯示原因；授權恢復後自動恢復排程。
- Tariff 之後失效或停用：保留長期偏好並暫停；有效設定恢復後自動恢復。
- Connector 從 PRIVATE 改成 PUBLIC：關閉離峰並清除相關狀態。

Charge Group 刪除保護另由 Redmine #1395 處理。

PUBLIC slot 設定 guard 與 runtime `slots==0` 防線另由 Redmine #1396 處理。

## 13. Queue 來源與取消語意

`e_connector_priority` 必須區分：

```text
source = MANUAL | OFF_PEAK
requestedByUserId
```

| Queue 來源 | LIFF 顯示 | Command | 實際效果 |
| --- | --- | --- | --- |
| `MANUAL` | 取消排隊 | `CANCEL_MANUAL_START` | 只刪除手動 request |
| `OFF_PEAK` | 取消排隊 | `DISABLE_OFF_PEAK` | `enableOffPeak=false`、刪除 Priority、清除 enabled actor 與 suppression |

現有語意不明確的 dequeue 行為不能再當作 source of truth。

Priority row 已 `FINISHED` 或已被 current-window suppression 抑制時，不能只因 row 還存在就顯示 `QUEUED`。

## 14. 使用者授權

### 14.1 共用 presentation

MQ／WebSocket 的 `driverState.actions` 是共用的設備與流程能力，不依每個 WebSocket session 個人化。

這可以避免 MQ 狀態產生器與個別使用者授權高度耦合。

共用 `driverState`、reason 與 actions 廣播給該 Connector 的所有合法 sessions。
Transient operation result 則定向傳送：

- 手動 command：`targetUserId=requestedByUserId`。
- Scheduler OFF_PEAK command：`targetUserId=enabledByUserId`。
- Branch MQ 內部 payload 提供 `operationResult.targetUserId`。
- HQ 依 session 已驗證的 Branch user identity 篩選，不重新執行 Branch 授權。
- HQ 不把其他使用者的 target identity 暴露給 browser。

### 14.2 Command API

Command API 必須重新檢查：

- 已驗證的使用者。
- Connector area 與住戶關聯。
- 目前 Transaction／request actor。
- 最新 operation 與 driverState。
- 最新 off-peak 與 queue 狀態。
- OCPP connection 與廠牌能力。

不允許的操作使用結構化 403／409 回應。

### 14.3 停止規則

- PUBLIC：只有 Transaction user 可以停止；其他使用者收到 `TRANSACTION_OWNED_BY_OTHER_USER`。
- PRIVATE：任一目前關聯住戶都可以停止；保留原 Transaction owner，並 audit `stopRequestedBy`。
- PRIVATE queue／off-peak／PREPARING：任一目前關聯住戶都可以取消，並 audit `cancelledBy` 或 `disabledBy`。

## 15. Remote operation 持久化

### 15.1 A17 現況

`e_remote_transaction_log` 已經會為每次 remote command 保存一筆資料。

A17 驗證結果：

- `START/SUCCESS`：17,508 筆。
- `START/REJECTED`：761 筆。
- 現有 `SUCCESS` 只代表 RemoteStart.conf `Accepted`，不代表已收到 StartTransaction。
- 即使後續真的建立 Transaction，現有 `transaction_id` 仍可能是 NULL。

因此現有 row 適合擴充，但目前的 `status` 無法表達完整 operation lifecycle。

### 15.2 決定方向

優先擴充 `e_remote_transaction_log`，不新增第二張 operation table；除非 implementation review 發現無法相容的外部限制。

保留現有 command response status，另外新增 business lifecycle status。

API／MQ／WebSocket 的 `operationId` 直接使用既有
`e_remote_transaction_log.id`，不新增 operation-id column。
`ocpp_unique_id` 保持為內部 OCPP correlation；只有完成 Transaction correlation
時才回填 `transaction_id`。

建議新增：

```text
source                  MANUAL / OFF_PEAK
requested_by_user_id    實際已驗證的操作人
operation_status        SENT / ACCEPTED / STARTED / STARTED_LATE /
                        REJECTED / TIMED_OUT / CANCELLED / FAILED /
                        STOPPED
deadline_at
completed_at
failure_code
transaction_id          StartTransaction 對應成功後回填
```

需要支援的查詢：

- Connector 最新未完成 START。
- Connector／Transaction 最新未完成 STOP。
- PREPARING／FINISHING initial snapshot 所需的 active operation。
- 同一 Connector 最多只能有一筆適用的未完成 START；同一 Transaction 最多一筆未完成 STOP。

依 repository 慣例，使用普通 index 支援查詢；正確性由 service transaction 控制。若要新增 DB UNIQUE constraint，必須另外說明並取得同意。

### 15.3 生命週期

RemoteStart：

```text
SENT -> ACCEPTED -> STARTED
  |        |
  |        +-> REJECTED / TIMED_OUT / CANCELLED / FAILED
  +-> FAILED
```

RemoteStop：

```text
SENT -> ACCEPTED -> STOPPED
  |        |
  |        +-> REJECTED / TIMED_OUT / FAILED
  +-> FAILED
```

Branch 重啟後，依未完成 operation 與 deadline 還原 `PREPARING`／`FINISHING`。

舊資料沒有新增 lifecycle 欄位時，只視為歷史紀錄，不能拿來判定目前 operation。

## 16. Operation result API

目前狀態、active operation 與 transient terminal operation result 是三個不同概念。

非同步 command 接受後的 response：

```json
{
  "action": "START_CHARGING",
  "operationId": "18456",
  "operationStatus": "SENT",
  "driverState": {
    "code": "PREPARING",
    "title": "準備中",
    "description": "正在等待充電樁開始交易",
    "actions": []
  }
}
```

後續 targeted WebSocket event 可以包含：

```json
{
  "driverState": {
    "code": "READY",
    "title": "可開始充電",
    "description": "充電樁已待命，可以開始充電",
    "actions": [
      {
        "code": "START_CHARGING",
        "enabled": true,
        "label": "開始充電",
        "disabledMessage": null,
        "requiresConfirmation": false
      }
    ]
  },
  "operationResult": {
    "operationId": "18456",
    "type": "START_CHARGING",
    "source": "MANUAL",
    "status": "REJECTED",
    "errorCode": "REMOTE_START_REJECTED",
    "message": "充電樁未接受啟動指令",
    "retryable": true,
    "occurredAt": "2026-07-29T14:27:41",
    "nextRetryAt": null
  }
}
```

規則：

- Dispatch 前即可判定的失敗由 HTTP `403`／`409` response 表達。
- 非同步 terminal result 只在狀態轉換發生時推送一次。
- 手動 result 的 target 是 `requestedByUserId`；scheduler OFF_PEAK result 的
  target 是 `enabledByUserId`。
- FE 以 `operationId` 去重，modal 只顯示一次。
- WebSocket initial snapshot 不重播 terminal `operationResult`。
- Initial snapshot 只提供尚未終結的 `activeOperation`，用於把
  PREPARING／FINISHING 與 `operationId` 對應。
- 尚未解除的永久問題留在目前 `driverState.reason` 與 description，不靠 modal
  重播。
- 實際 StartTransaction／StopTransaction 的事實優先於先前 timeout
  presentation。

## 17. WebSocket Connector 詳細頁

### 17.1 第一次開啟

- Connector 詳細頁不另外呼叫 REST detail API。
- Connector 資訊與 `driverState` 由 WebSocket initial data 提供。
- Initial data 只在 START／STOP 尚未終結時提供 `activeOperation`。
- Initial data 不包含 terminal transient `operationResult`。
- 不使用 polling。

WebSocket initial connection 失敗時：

1. 進行一次短時間 reconnect。
2. 再次失敗就返回 Connector list。
3. 顯示 modal，告知目前無法取得即時 Connector 資訊。
4. 不使用 REST detail fallback。

### 17.2 已連線後斷線

- 立即停止使用舊 `driverState.actions`。
- 停用 Start、Stop 與 off-peak 操作。
- 嘗試 reconnect 一次。
- 只有收到新的 initial snapshot 後才恢復操作。
- Reconnect 失敗就返回 list 並顯示 modal。

### 17.3 Targeted result 傳送

- 共用 Connector presentation 仍送給所有合法 Connector sessions。
- HQ 只把 `operationResult` 傳給 authenticated target session。
- 非 target 住戶收到共用狀態變更，但不顯示該次 operation modal。
- FE 以 `operationId` 處理 reconnect race 去重；WebSocket 斷線期間不得使用 stale
  actions 執行 command。

## 18. 失敗與重試分類

### 18.1 手動來源

- 不自動重試。
- 明確通知車主操作結果。
- 回到最新且安全的 driverState。

### 18.2 可恢復的離峰失敗

- 不建立 immediate retry loop。
- 每個正常 scheduler cycle 最多嘗試一次。
- 不允許重疊 operation。
- 只有目前 off-peak window 仍適用時才可重試。
- `NO_CHARGING_DEMAND` 是中性狀態，可在同一 window 後續重試。
- Timeout 在沒有 suppression 時，可於之後 cycle 重試。

### 18.3 本 window 終止

- RemoteStart 明確 `Rejected`。
- 車主要求停止，且已收到 StopTransaction。
- 本 window 已建立 Transaction，之後 Transaction 結束。

以上狀態顯示 `OFF_PEAK_WAITING`，附帶 current-window reason；只有下一個 window 或車主明確 OFF -> ON 才重新加入。

### 18.4 永久／設定問題暫停

- 開啟後 IdTag 授權失效。
- 開啟後 tariff 失效或停用。
- 其他只靠等待不會成功的結構性問題。

依已確認規則保留長期偏好、停止重複 command，並提供目前可理解的 reason。

## 19. FE 必須能完成的畫面行為

雖然 FE 實作不屬於本 Backend 工作，但 API 必須支援：

- 依 semantic action 顯示 label。
- Operation-backed PREPARING 顯示「準備中」。
- Operation-backed FINISHING 顯示「停止中」。
- `OFF_PEAK_WAITING` 與 OFF_PEAK `QUEUED` 都顯示「取消排隊」。
- 手動 queue cancellation 與 off-peak disable 使用不同 command。
- 停止 active off-peak charging 前顯示確認 modal。
- PUBLIC 不顯示 off-peak switch。
- PRIVATE switch command 交由 API 判斷，不在 FE 複製 business matrix。
- WebSocket 斷線時停用所有 operation。
- Command 被拒絕時顯示結構化 modal，且 switch／driverState 不做錯誤的 optimistic update。
- 所有操作都將 semantic code 送到 `/api/connector/commands`。
- 非同步 `202` response 使用 `operationId` 對應後續 targeted result。
- Terminal operation result 只顯示一次，initial snapshot 不重播。

## 20. 必要回歸測試範圍

本需求涉及使用者可見行為、API DTO、schema、queue、off-peak scheduler、OCPP 與 WebSocket。E2E Catalog impact 為 `Add` 與 `Update`。

後續 implementation plan 至少必須涵蓋：

1. CloudLink 公／私樁手動 RemoteStart -> PREPARING -> CHARGING。
2. Fortune 先插槍的 raw PREPARING，在 RemoteStart 前仍顯示 READY。
3. PREPARING 取消：CloudLink 支援、Fortune 不支援。
4. Branch 重啟後可還原 PREPARING 與 FINISHING。
5. 私樁 slots full 的手動 queue 保存 requester 並可取消。
6. Off-peak ON 在 Priority 建立前立即回 OFF_PEAK_WAITING。
7. Off-peak Priority 顯示 QUEUED 並可執行 DISABLE_OFF_PEAK。
8. NO_CHARGING_DEMAND 使用中性文案，且可在同一 window 後續重試。
9. Off-peak RemoteStart 明確 Rejected 後抑制目前 window。
10. 車主停止離峰，只有 StopTransaction 後才抑制目前 window。
11. Transaction 結束後不再發生 A17 每 10 分鐘重送 RemoteStart。
12. SuspendedEV 保持 active transaction，不建立另一筆 Start。
13. RemoteStop pending 顯示 FINISHING 並阻止重複 Stop。
14. Offline／fault 保留長期 off-peak，並阻止不安全 remote operation。
15. PUBLIC Transaction stop 授權與 PRIVATE 關聯住戶 stop 行為。
16. PRIVATE off-peak switch API 成功／拒絕都回實際狀態。
17. WebSocket initial failure 與 established disconnect 不使用 stale actions。
18. 晚到 StartTransaction／StopTransaction 以實際 Transaction 狀態完成 reconciliation。
19. 統一 semantic command 能區分 CANCEL_START、CANCEL_MANUAL_START 與
    DISABLE_OFF_PEAK，不再使用模糊 dequeue。
20. 非同步 Start／Stop 回 `202` 與 operationId；同步 command 回 `200`。
21. 共用狀態送給所有合法 sessions，但 operationResult 只送給
    requestedBy／enabledBy target 並完成去重。
22. RemoteStart 保留既有廠牌等待時間、使用一致的 business deadline 與 30 秒
    reconciliation grace。
23. RemoteStop 使用可設定的 30 秒 deadline；timeout 後允許明確重試，且能
    reconciliation late StopTransaction。

Release pack 與 P0-P3 分級，等需求文件確認後在 implementation plan gate 決定。

## 21. 不在本次範圍

- Backend ticket #1398 內的 FE source implementation；FE 另由 Redmine #1399 處理。
- Charge Group 刪除 guard：Redmine #1395。
- PUBLIC slot 設定 guard 與 runtime `slots==0` 防線：Redmine #1396。
- 重新設計 raw OCPP ConnectorStatus enum。
- 宣稱 StopTransaction 能證明滿電或拔槍。
- 新增 polling。

## 22. Review gate

進入實作前：

1. Ken 以本文件一次審查需求與狀態契約。
2. 所有修正先更新至本文件。
3. 另外產出 implementation plan，列出確切程式、schema、測試檔案與 E2E impact。
4. Implementation plan 明確同意後才開始 coding。
