# Redmine #1382 Driver State / Off-Peak Requirements

> Status: Requirement review draft
>
> Date: 2026-07-29
>
> Scope: Branch/HQ API contract and end-to-end LIFF behavior. This document does not authorize implementation yet.

## 1. Goal

`driverState` must describe what the driver is experiencing and what operation is currently possible. It must not be a direct translation of the raw OCPP connector status.

The design must preserve the pre-#1382 user experience while correctly handling:

- PUBLIC and PRIVATE connectors.
- Manual charging and long-term off-peak charging.
- PRIVATE Charge Group slots and queueing.
- Vendor differences between CloudLink and Fortune.
- Asynchronous RemoteStart/StartTransaction and RemoteStop/StopTransaction flows.
- OCPP offline, fault, restart, timeout and late-event recovery.
- Shared WebSocket updates and user-specific command authorization.

## 2. Responsibility Boundaries

### 2.1 Branch

Branch is the source of truth for:

- `driverState`.
- Operation capability exposed in `driverState.actions`.
- Remote operation lifecycle.
- Queue and off-peak scheduling state.
- Authoritative command validation and authorization.
- User-facing operation result and current blocking reason.

Branch derives these values from raw connector status, OCPP connection state, active transactions, queue records, off-peak settings and persisted remote operations.

### 2.2 HQ

HQ must:

- Derive the authenticated driver from the LINE identity.
- Send a trusted Branch user identity to Branch.
- Pass Branch `driverState` and operation results through without deriving a second state machine.
- Publish the same Branch presentation through WebSocket initial data and subsequent connector events.

HQ must not trust an arbitrary `userId` supplied by LIFF.

### 2.3 LIFF

This work will not modify FE source, but the API contract must give FE enough information to implement the intended behavior.

LIFF is responsible for:

- Rendering Branch-provided title, description and actions.
- Hiding the off-peak switch for `areaType=public`.
- Sending commands and displaying structured success/rejection results.
- Treating WebSocket connectivity as a prerequisite for operations.
- Showing confirmation modals where required.

LIFF must not derive charging capability from raw `status` when `driverState` is available.

## 3. Domain Definitions

### 3.1 PUBLIC connector

- Does not participate in PRIVATE rotation queueing.
- Does not expose an off-peak switch.
- May be started by an authorized public user without PRIVATE Charge Group queueing.
- Uses capacity reserved in advance by the Charge Group slot calculation.
- Only the user associated with the active public transaction may stop it; the command API is authoritative.

For a Charge Group with seven physical slots and two PUBLIC connectors, five slots remain available for PRIVATE scheduling. PUBLIC slot reservation is based on configured PUBLIC connector count, not current online or charging status.

### 3.2 PRIVATE connector

- Is associated with one or more Branch users through `e_connector_user`.
- Participates in Charge Group slot allocation and rotation queueing.
- Supports the long-term off-peak setting.
- Any currently linked resident may stop charging, cancel a manual queue, cancel off-peak waiting or cancel a supported PREPARING operation.
- The API must audit the actual actor even when the transaction or off-peak mode was initiated by another linked resident.

The existing term "owner" is misleading. A Connector has linked residents, not a first-class owner role.

### 3.3 Off-peak charging

Off-peak is a long-term preference, not a one-time charging request.

- `enableOffPeak=true` means the connector remains enrolled across future off-peak windows.
- Enabling off-peak must remember the resident who enabled it.
- A queue record is created only when the connector enters an applicable off-peak window and the scheduler evaluates it.
- The long-term preference remains enabled after charging completes, after a temporary fault/offline period, and after the current window is suppressed.
- Disabling off-peak is an explicit user operation.

### 3.4 Off-peak window

One continuous tariff `OFF_PEAK` interval is one window.

- A range crossing midnight remains one continuous window.
- Two disjoint `OFF_PEAK` tariff ranges are two windows.
- Window suppression expires at the end of that continuous range.
- The end of an off-peak window controls automatic start eligibility only. It must not force-stop an already active transaction.

## 4. Core Driver-State Rule

Raw OCPP `PREPARING` does not imply `driverState=PREPARING`.

```text
driverState=PREPARING
only when Branch has dispatched a RemoteStart operation
and that operation is still waiting for StartTransaction.
```

This preserves plug-first behavior:

| Vendor | Plugged in, no RemoteStart | Raw status | Driver state in manual mode |
| --- | --- | --- | --- |
| Fortune | Yes | Usually `PREPARING` | `READY` |
| CloudLink | Yes | Usually `AVAILABLE` | `READY` |

In off-peak mode, the same plug-only connector remains `OFF_PEAK_WAITING` or `QUEUED`, depending on whether a real Priority record has been created.

## 5. Driver-State Codes

| Code | Meaning | Primary driver operation |
| --- | --- | --- |
| `READY` | No active transaction, queue or remote operation; manual start is available | Start charging |
| `OFF_PEAK_WAITING` | Long-term off-peak is enabled, but the connector is not currently dispatchable in the active queue | Disable off-peak / cancel waiting |
| `QUEUED` | A real manual or off-peak queue record is waiting for dispatch | Cancel the correct queue source |
| `PREPARING` | RemoteStart has been sent or accepted and StartTransaction is pending | Cancel start when the vendor supports it |
| `CHARGING` | An active Branch or CP transaction is present | Stop charging |
| `FINISHING` | RemoteStop has been sent or accepted and StopTransaction is pending | No repeated stop |
| `OFFLINE_CHARGING` | OCPP is offline but Branch/CP transaction evidence says charging may still be active | No remote operation |
| `OFFLINE_UNAVAILABLE` | OCPP is offline and there is no active transaction evidence | Disable off-peak remains possible through a connected command channel only |
| `FAULTED` | The connector is faulted and no stronger active-transaction presentation applies | Disable off-peak may remain possible |
| `UNAVAILABLE` | Connector is unavailable for another reason | No charging command |
| `UNKNOWN` | Branch cannot derive a safe state | No charging command |

Internal `ConnectorPriority` states such as `BLOCKED`, `PAUSED`, `ELIGIBLE` and `NOT_PLUGGED` are not automatically public driver-state codes.

## 6. Presentation Precedence

The exact implementation may be expressed as a decision service, but it must preserve these priorities:

1. Active transaction evidence must never be presented as `READY`.
2. An unresolved STOP operation is `FINISHING`.
3. An unresolved START operation is `PREPARING`.
4. OCPP offline without an active transaction is `OFFLINE_UNAVAILABLE`.
5. OCPP offline with active transaction evidence is `OFFLINE_CHARGING`.
6. FAULTED/OFFLINE must not be hidden by `OFF_PEAK_WAITING` or `QUEUED`.
7. A real applicable queue record may project `QUEUED`.
8. Long-term off-peak without an applicable queue record projects `OFF_PEAK_WAITING`.
9. Raw Fortune `PREPARING` without an operation falls back to the applicable manual/off-peak state.

## 7. Actions

The API should expose semantic actions rather than forcing FE to reinterpret `start` and `stop` labels.

Recommended action identifiers:

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

Each action should provide at least:

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

The existing `start` / `stop` fields may be retained temporarily for compatibility, but they are not sufficient to distinguish manual queue cancellation from disabling long-term off-peak.

Shared WebSocket actions describe operational/device capability. They are not proof that the current user is authorized. The command API performs the final user/state check.

## 8. Manual Charging Flow

### 8.1 Immediate start

```text
READY
  -> user requests START_CHARGING
  -> RemoteStart operation SENT/ACCEPTED
  -> PREPARING
  -> StartTransaction
  -> CHARGING
```

### 8.2 PRIVATE slots full

```text
READY
  -> user requests START_CHARGING
  -> PRIVATE slots are full
  -> create MANUAL queue request
  -> QUEUED
```

The queue must preserve:

- `source=MANUAL`
- `requestedByUserId`

When a slot is available, dispatch must use the original requester's IdTag. It must not select the first linked resident.

`CANCEL_MANUAL_START` deletes only the manual queue request. It does not modify `enableOffPeak`.

### 8.3 Manual start failure

- Manual RemoteStart does not auto-retry.
- Rejected, failed or final timeout returns to `READY`.
- The explicit operation failure is returned/pushed separately from the current state.
- A late StartTransaction is accepted as the stronger fact and changes the connector to `CHARGING`.

## 9. PREPARING Behavior

Both operation states below project `PREPARING`:

- `SENT`
- `ACCEPTED`

Vendor cancellation differs:

| Vendor | PREPARING action |
| --- | --- |
| CloudLink | `CANCEL_START` enabled; Branch may use the supported waiting-mode exit flow |
| Fortune | `CANCEL_START` unavailable/disabled with a clear explanation; the driver must unplug or wait for timeout |

If cancellation succeeds:

- Manual source returns to `READY`.
- Off-peak source disables the long-term off-peak mode, removes applicable Priority records and returns to manual mode.

If RemoteStart receives an explicit `Rejected`:

- Manual source returns to `READY`.
- Off-peak source stops retrying for the current window and becomes `OFF_PEAK_WAITING`.
- Off-peak reason is `START_REJECTED_FOR_CURRENT_WINDOW`.
- The next separate off-peak window may schedule again.

If RemoteStart receives no response:

- Keep `PREPARING` until the vendor-specific start deadline.
- Do not blindly resend while an operation is unresolved.
- A transient off-peak timeout may return to `QUEUED` and retry in a later normal scheduler cycle.
- There must be no overlapping START operation.
- Retry stops at the end of the current off-peak window.

## 10. CHARGING and FINISHING

```text
CHARGING
  -> user confirms STOP_CHARGING
  -> RemoteStop operation SENT/ACCEPTED
  -> FINISHING
  -> StopTransaction
  -> transaction completed
```

While `FINISHING`:

- Start is disabled.
- Stop is disabled.
- Off-peak switch commands are rejected.
- Branch restart must recover the FINISHING state from persistence.

If RemoteStop has no response:

1. Keep `FINISHING` until the stop deadline.
2. Do not automatically resend.
3. If StopTransaction arrives, complete normally.
4. At deadline, re-check the actual transaction.
5. If still active, mark the operation `TIMED_OUT`, return to `CHARGING`, expose the failure and allow a new explicit stop request.
6. If already completed, reconcile as a successful stop.

Off-peak current-window suppression is applied only after the transaction is actually confirmed stopped.

## 11. Long-Term Off-Peak Flow

### 11.1 Enable

After a successful PRIVATE off-peak ON command:

```text
enableOffPeak = true
enabledByUserId = authenticated linked resident
driverState = OFF_PEAK_WAITING
```

This response is immediate. It does not wait for `e_connector_priority`.

### 11.2 Enter applicable window

When the scheduler evaluates the connector in an applicable off-peak window:

```text
create/update OFF_PEAK Priority
driverState = QUEUED
```

Both `OFF_PEAK_WAITING` and OFF_PEAK `QUEUED` provide `DISABLE_OFF_PEAK`, shown to the driver as "取消排隊".

### 11.3 No charging demand

If dispatch cannot establish charging demand:

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

This is neutral, not an error. The description must not imply that the cable is connected incorrectly.

The scheduler may try again in a later normal cycle in the same off-peak window because the vehicle may arrive later.

### 11.4 Active charging

When StartTransaction is received:

```text
driverState = CHARGING
```

`SUSPENDED_EV` and `SUSPENDED_EVSE` remain charging-like because the Transaction is still active. They must not trigger a new RemoteStart.

### 11.5 Driver stops off-peak charging

LIFF must confirm that stopping:

- Ends the active charging transaction.
- Prevents automatic restart for the rest of the current off-peak window.
- Does not disable the long-term off-peak preference.

After confirmed StopTransaction:

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

The driver can re-enter the same window only by switching off-peak OFF and then ON again.

### 11.6 Transaction ends for another reason

OCPP 1.6 has no reliable `FullyCharged` StopReason. A17 also cannot reliably distinguish full charge from unplugging:

- A17 has no SoC MeterValues.
- The CP stop detector may map sustained low current to `EVDisconnected`.

Therefore the presentation must not claim "fully charged" or "unplugged".

If an off-peak Transaction actually started and later receives StopTransaction:

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

The current window is suppressed and the next off-peak window schedules normally.

If the connector reports `SUSPENDED_EV` without StopTransaction, the Transaction remains active and this rule does not apply.

### 11.7 Current-window suppression

Suppression must be persisted because the current scheduler resets non-CHARGING Priority states during later cycles.

Suppression reasons include:

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

Suppression remains until the end of the current continuous off-peak tariff range.

## 12. Off-Peak Switch Command

### 12.1 FE behavior

- PUBLIC: do not render the switch.
- PRIVATE and WebSocket connected: the switch may remain clickable.
- WebSocket disconnected: do not use stale actions or switch state to submit commands.

FE does not maintain a large business-state enable/disable matrix.

### 12.2 API authority

After a switch request, Branch validates current data and returns:

- Success: actual `enableOffPeak`, latest `driverState` and event.
- Rejection: structured error code/message, unchanged actual `enableOffPeak`, latest `driverState`.

The API rejects:

- PUBLIC connector.
- Active `PREPARING`, `CHARGING` or `FINISHING`.
- Enabling off-peak while a MANUAL queue request exists.
- Unauthorized resident.
- Permanent/structural configuration failure.

### 12.3 Fail-fast on enable

Reject ON when:

- Connector is not PRIVATE.
- Authenticated user is not a linked resident.
- Connector has no Charge Group.
- Connector has no valid tariff or no OFF_PEAK range.
- Enabling user has no IdTag.
- Enabling user's IdTag is unauthorized.

Allow ON despite temporary conditions:

- Currently outside off-peak hours.
- No current slot.
- No vehicle/no charging demand.
- OCPP offline.
- Connector faulted.

### 12.4 Configuration changes after enable

- Enabling user loses Connector association: disable off-peak, clear Priority and actor; do not transfer to another resident.
- Connector has no linked residents: disable off-peak and clear Priority.
- IdTag later becomes invalid while the user remains linked: preserve preference and actor, pause automatic start, show a current reason, resume when authorization is restored.
- Tariff later becomes invalid/disabled: preserve preference, pause, and resume after valid configuration returns.
- Connector changes PRIVATE to PUBLIC: disable off-peak and clear related state.

Charge Group deletion protection is tracked separately in Redmine #1395.
PUBLIC slot configuration validation is tracked separately in Redmine #1396.

## 13. Queue Source and Cancellation

`e_connector_priority` must distinguish:

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

Semantics:

| Queue source | Driver label | Command | Effect |
| --- | --- | --- | --- |
| `MANUAL` | 取消排隊 | `CANCEL_MANUAL_START` | Delete only the manual request |
| `OFF_PEAK` | 取消排隊 | `DISABLE_OFF_PEAK` | Set `enableOffPeak=false`, delete priorities, clear enabled actor and suppression |

The existing ambiguous dequeue behavior must not be used as the source of truth.

A FINISHED or suppressed Priority row must not project `QUEUED` merely because the row still exists.

## 14. Authorization

### 14.1 Shared presentation

WebSocket/MQ `driverState.actions` is shared operational capability. It is not personalized for each resident session.

This avoids coupling MQ status generation to a user-specific authorization matrix.

### 14.2 Command API

The command API re-checks:

- Authenticated user identity.
- Connector area and association.
- Current transaction/request actor.
- Latest operation and driver state.
- Current off-peak and queue state.
- OCPP connectivity and vendor capability.

Expected rejections use structured 403/409 responses.

### 14.3 Stop rules

- PUBLIC: only the transaction user may stop; another user receives `TRANSACTION_OWNED_BY_OTHER_USER`.
- PRIVATE: any currently linked resident may stop; preserve original transaction owner but audit `stopRequestedBy`.
- PRIVATE queue/off-peak/PREPARING cancellation: any currently linked resident may perform it; audit `cancelledBy` or `disabledBy`.

## 15. Persisted Remote Operation

### 15.1 Existing A17 evidence

`e_remote_transaction_log` already persists one row per remote command. A17 validation showed:

- `START/SUCCESS`: 17,508 rows.
- `START/REJECTED`: 761 rows.
- Current `SUCCESS` means RemoteStart.conf `Accepted`, not StartTransaction.
- `transaction_id` remains null even when a later Transaction is created.

Therefore the existing row is suitable to extend, but its current `status` cannot represent the full business lifecycle.

### 15.2 Direction

Extend `e_remote_transaction_log`; do not add a second operation table unless implementation review finds an external compatibility constraint.

Keep the existing command response status and add a separate business lifecycle status.

Recommended new data:

```text
source                  MANUAL / OFF_PEAK
requested_by_user_id    actual authenticated actor
operation_status        SENT / ACCEPTED / STARTED / STARTED_LATE /
                        REJECTED / TIMED_OUT / CANCELLED / FAILED /
                        STOPPED
deadline_at
completed_at
failure_code
transaction_id          populated when StartTransaction is correlated
```

Required query support:

- Latest unresolved START by connector.
- Latest unresolved STOP by connector/transaction.
- Latest relevant operation result for `lastOperation`.
- At most one unresolved START and one unresolved STOP for the applicable connector/transaction.

Ordinary indexes should support these queries. Per repository convention, correctness should be enforced in service transactions rather than adding a database UNIQUE constraint without separate approval.

### 15.3 Lifecycle

RemoteStart:

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

RemoteStop:

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

Branch restart reconstructs `PREPARING` and `FINISHING` from unresolved persisted operations and their deadlines.

Old rows without the new lifecycle fields remain historical. They must not be treated as currently active operations.

## 16. Operation Result Contract

State and operation result are separate concepts.

Example:

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

Rules:

- Manual command responses handle immediate failures.
- Asynchronous off-peak failures are delivered by WebSocket.
- A recovered transient failure must not show a stale modal on a later page opening.
- A permanent unresolved condition remains represented in the current driver state/reason.
- `operationId` supports deduplication and correlation.
- Actual StartTransaction/StopTransaction evidence supersedes an earlier timeout presentation.

## 17. WebSocket Detail-Page Contract

### 17.1 Initial open

- Connector detail does not make a separate REST detail request.
- WebSocket initial data supplies connector information and `driverState`.
- No polling is used.

If initial WebSocket connection fails:

1. Perform one short reconnect attempt.
2. If it still fails, return to the connector list.
3. Show a modal explaining that real-time connector information is unavailable.
4. Do not fall back to REST detail data.

### 17.2 Established connection drops

- Immediately stop using old `driverState.actions`.
- Disable Start, Stop and off-peak operations.
- Perform one reconnect attempt.
- Restore operations only after a new initial snapshot is received.
- If reconnect fails, return to the list and show the failure modal.

## 18. Failure and Retry Classification

### 18.1 Manual

- No automatic retry.
- Explicit result to driver.
- Return to the latest safe current state.

### 18.2 Off-peak recoverable

- No immediate retry loop.
- At most one attempt per normal scheduler cycle.
- No overlapping operation.
- Retry only while the current off-peak window remains eligible.
- `NO_CHARGING_DEMAND` is neutral and may retry in the same window.
- Timeout may retry in a later cycle if no stronger suppression applies.

### 18.3 Off-peak current-window terminal

- Explicit RemoteStart `Rejected`.
- Driver-confirmed stop after StopTransaction.
- A Transaction that started and subsequently ended.

These project `OFF_PEAK_WAITING` with a current-window reason and resume only in the next window or after an explicit OFF -> ON re-entry.

### 18.4 Permanent/configuration pause

- Invalid authorization after enable.
- Invalid/disabled tariff after enable.
- Other structural conditions that cannot succeed by waiting.

Preserve the long-term preference where specified, stop repeated commands and show a current actionable reason.

## 19. Required FE Mapping

FE implementation is outside this Backend work, but the contract must support:

- Render semantic action labels from API.
- "準備中" for operation-backed PREPARING.
- "停止中" for operation-backed FINISHING.
- "取消排隊" for both `OFF_PEAK_WAITING` and OFF_PEAK `QUEUED`.
- Different command semantics for manual queue cancellation and off-peak disable.
- Confirmation before stopping active off-peak charging.
- PUBLIC off-peak switch hidden.
- PRIVATE switch command sent to API without a duplicated FE business matrix.
- WebSocket disconnected state disables all operations.
- Structured command rejection modal without optimistic state drift.

## 20. Required Regression Coverage

This behavior changes user-visible charging, API DTO, schema, queueing, off-peak scheduling, OCPP and WebSocket behavior. E2E catalog impact is `Add` and `Update`.

At minimum, later implementation planning must cover:

1. CloudLink PUBLIC/PRIVATE manual RemoteStart -> PREPARING -> CHARGING.
2. Fortune plug-first raw PREPARING remains READY until RemoteStart.
3. PREPARING cancellation: CloudLink supported, Fortune unavailable.
4. Branch restart recovers PREPARING and FINISHING.
5. Manual slots-full queue preserves requester and supports cancellation.
6. Off-peak ON immediately returns OFF_PEAK_WAITING before Priority exists.
7. Off-peak Priority projects QUEUED and supports DISABLE_OFF_PEAK.
8. NO_CHARGING_DEMAND is neutral and retries later in the same window.
9. Explicit off-peak RemoteStart Rejected suppresses the current window.
10. Driver stop suppresses the current window only after StopTransaction.
11. Transaction-ended suppression prevents A17-style repeated RemoteStart commands.
12. SuspendedEV remains an active transaction and does not start another operation.
13. RemoteStop pending projects FINISHING and blocks duplicate stop.
14. Offline/fault preserves long-term off-peak and blocks unsafe remote operations.
15. PUBLIC transaction stop authorization and PRIVATE linked-resident stop behavior.
16. PRIVATE off-peak switch API success/rejection returns actual state.
17. WebSocket initial failure and established disconnect do not use stale actions.
18. Late StartTransaction/StopTransaction reconciles to actual transaction state.

Release-pack selection and exact P0-P3 assignments belong to the implementation plan gate after this requirement document is approved.

## 21. Out of Scope

- FE source implementation.
- Charge Group deletion guard: Redmine #1395.
- PUBLIC slot configuration guard and runtime `slots==0` defense: Redmine #1396.
- Redesign of the raw OCPP ConnectorStatus enum.
- Claiming that a StopTransaction proves full charge or unplugging.
- New polling behavior.

## 22. Review Gate

Before implementation:

1. Ken reviews this requirement/state contract as one document.
2. Any corrections are applied here.
3. A separate implementation plan lists exact code/schema/test files and E2E catalog impact.
4. Coding starts only after that plan is explicitly approved.
