# OCPP

> OCPP 驱动参考：内置的中央系统、充电桩证书、充电接口点位，以及 Meter、Status、Transaction 的命名规则。

OCPP 驱动让 Ganter Lab 成为那些说 Open Charge Point Protocol 的电动汽车充电桩的**中央系统**。和 LoRaWAN 一样，方向是反过来的：不是工作站去连充电桩，而是**充电桩通过一条 WebSocket 连到 Ganter Lab**、自行注册，并把它的遥测数据推送进来。

内置的中央系统在第一台已启用的 OCPP 设备被载入正在跑的连接器时启动，在最后一台被移除或禁用时停下：一台充电桩都没配置时，端口上根本没有人在听。加一台就把它带回来，不必重启应用。你配置的每一台设备代表**一台充电桩**，由它连接时所用的那个 id 来标识。

## 准备充电桩 {#prepare-the-charge-point}

是充电桩主动连接本工作站，而它出厂时并不知道工作站在哪里。每台 OCPP 设备的页面上都有一张“**准备充电桩**”卡片，按本工作站写好要填入充电桩 OCPP 设置的内容：

| 要在充电桩上设置的 | 卡片给出的 |
|---|---|
| 中央系统地址 | `wss://<地址>:19521/<充电桩 id>`，先是以本工作站名称写成的地址（内置 OPC UA 服务器配置中的“网络名称”，为空时是计算机名），然后是本工作站在网络上的每个 IPv4 地址各一条，每条带一个复制按钮。会核对服务器名称的充电桩需要按名称写的地址；不核对的充电桩接受其中任何一个，这时用与它同一网络的那个地址即可。 |
| 充电桩标识 | 设备的“**标识符**”；它为空时，就是工作站给设备起的名字。很多充电桩会自己把标识加到地址后面：对这类充电桩，地址只填到端口为止，标识填在它自己的栏位里。 |
| 安全 | 使用客户端证书的 TLS，即 OCPP 所说的安全配置文件 3。没有客户端证书的配置文件 1 和 2 不被接受。充电桩的证书在通用名称或 DNS 备用名称中写有它的标识（见[证书](#certificates)）。 |
| 中央系统的根证书 | 本工作站根证书 `GanterLabConnector-<station>.pem` 的完整路径，充电桩安装它来信任中央系统；“**打开文件夹**”按钮会在 Windows 文件资源管理器中显示这个文件。按钮在工作站自己的窗口里起作用；在其他设备的浏览器里它保持灰色并说明原因。 |

工作站打开以来充电桩还没有连上过时，卡片是展开的；连上之后它会收起，但留在页面上，供下次设置充电桩时使用。证书没有任何 Windows 中已安装的根证书担保的充电桩，首次连接时会被拒绝，并在[现场设备访问](#equipment-access)中等待。

文件在登记了充电桩、中央系统第一次启动时写出，在此之前卡片会说明这一点。本工作站所有充电桩用的是同一个文件，工作站的名称或地址变了，它也不变（见[工作站的根证书](#the-stations-root-certificate)）。

**凡是拿到过旧版本文件的充电桩，都需要装上这个文件一次。** 这个版本第一次启动时，如果工作站上的充电桩可能持有旧文件，卡片上会出现一条通知“**本工作站的文件已更换**”，只在那一次运行中显示。通知只是提醒，不是操作说明本身：对原来就有充电桩的工作站，不论卡片是否显示这条通知，都要在每台充电桩上把新文件装一次。会核对服务器名称的充电桩从此就会接受本工作站。

## 中央系统与端口 {#central-system-and-ports}

| 项目 | 取值 |
|---|---|
| 协议版本 | OCPP **1.6**、**2.0**、**2.0.1** 和 **2.1**（WebSocket 子协议协商；JSON） |
| 监听 | 所有接口，TCP 端口 **19521**，只走 TLS |
| 充电桩地址 | `wss://<station-address>:19521/<charge-point-id>` |
| 身份 | WebSocket 路径的最后一段就是充电桩的 id |

监听永远是 TLS；除了本地回环地址（它的存在只是为了进程内测试）之外，任何地方都拒绝明文 HTTP 的监听。充电桩用它的 BootNotification 自行注册（应答里给的心跳间隔是 60 秒），而每一张出示来做授权的卡都被接受：Ganter Lab 观察充电过程，它不管资费，也不管门禁。

## 证书 {#certificates}

TLS 握手的服务器一侧出示的是由本工作站自己的根证书签发的证书（见[工作站的根证书](#the-stations-root-certificate)）。每一台充电桩都必须出示一份**客户端证书**：

1. 它必须以两种方式之一得到信任：在这台 Windows 机器上构成一条直到其中已安装的根证书的有效证书链，没有任何 TLS 策略错误；或者你已经在这个驱动页面的**现场设备访问**中批准了这份证书本身（见下文）。无论哪种方式，它都必须在有效期之内。检查不通过就拒绝连接，诊断日志记下是哪一项检查没过。
2. 它的主体通用名或者一个 DNS 主体备用名必须**点名它连接时所用的那个充电桩 id**。一份点名了另一台充电桩、而不是连接路径里那一台的证书会被拒绝，即使它已获批准。

没有客户端证书的握手，在任何数据流动之前就被丢掉。

### 工作站的根证书 {#the-stations-root-certificate}

| 项目 | 内容 |
|---|---|
| 根证书 | 每个配置档案只创建一次，在中央系统或 LoRaWAN 网络服务器第一次启动时。它的主题是 `Ganter Lab Connector (<station>)`，其中 `<station>` 是当天的网络名称。它永不过期，也不会自行更换：改计算机名、改网络名称或改工作站的地址，它都不变。 |
| 文件 | `GanterLabConnector-<station>.pem`，只含公开部分，位于数据文件夹的 `ua\pki\station` 下。充电桩和 LoRaWAN 网关用的是同一个文件，网关把它安装为 `tc.trust`。 |
| 中央系统出示的证书 | 由这个根证书签发。它写明工作站的网络名称（内置 OPC UA 服务器配置中的“网络名称”，为空时是计算机名）、`localhost` 和工作站的每个 IPv4 地址，所以会核对服务器名称的充电桩（安全配置文件 3）也接受它。网络名称或某个地址变了，只有这份证书会自动重新签发，充电桩上无需做任何事。 |
| OPC UA 服务器的证书 | 此处不使用。内置的 OPC UA 服务器保留它自己的证书，供 OPC UA 客户端使用。 |

已保存的根证书如果再也打不开（例如是另一个 Windows 账户写入的），会被一个新的根证书取代。这时每台充电桩都需要新文件一次，卡片会说明这一点。

### 现场设备访问 {#equipment-access}

如果一台充电桩的证书是自签名的，或者由制造商自己的证书颁发机构签发，因而没有任何 Windows 中已安装的根证书为它担保，也不需要把那个根证书安装到这台电脑上。它的第一次连接会被拒绝，它的证书在 OCPP 驱动页面末尾的**现场设备访问**里等待你的决定，这和 OPC UA 驱动页面上的是同一张卡片。

| 分组 | 列出的内容 | 你能做的事 |
|---|---|---|
| 等待批准 | 每一份被拒绝的证书，列在它连接时所用的充电桩 id 之下：它的 SHA-256 指纹、最近一次尝试的时间和尝试次数，以及它的详细信息（主体、颁发者、域名、有效期）。 | **批准**从充电桩的下一次连接起接纳这份证书。**阻止**继续拒绝它，且不再把它列到这里。**丢弃所有待处理的证书**清空列表，不批准也不阻止任何东西。 |
| 已批准的现场设备 | 你批准过或导入过的证书，并注明是谁、在什么时候做的决定。 | **撤回批准**关闭充电桩的连接，并把它的证书放回列表。**导入证书**在充电桩第一次连接之前就批准它的公开证书。 |
| 被阻止的现场设备 | 你阻止过的证书。被阻止的证书即使 Windows 信任它也会被拒绝。 | **解除阻止**把证书放回列表，但不批准它。 |

批准的是一份一份的证书，从来不是签发它的证书颁发机构，所以同一制造商的第二台充电桩要等待它自己的批准。批准之前，先把指纹和充电桩报出的指纹核对一遍。批准不会改变这份证书是哪一台充电桩：它仍然只以它点名的那个充电桩 id 被接纳，而且只在有效期之内。Windows 已经信任的证书照旧被接纳，从不出现在这里；没有任何东西会被自动批准。

这些决定保存在工作站数据文件夹的 `ua\pki\ocpp` 下，重启后依然保留。只有可以配置 Connector 的角色才会看到这些命令，而且它们只在工作站自己的窗口里起作用：另一台设备上的浏览器只能读这张卡片，不能修改；也没有任何代理工具能批准证书。列表最多保存 100 份等待批准的证书；列表满了以后，新的证书仍会被拒绝，但不会列出，卡片会说明这一点。

## 连接字段 {#connection-fields}

| 字段 | 它是什么 | 格式 | 默认值 |
|---|---|---|---|
| 标识符 | 这台设备所代表的那个**充电桩 id**：也就是充电桩放在它连接 URL 末尾、并写在它证书里的那个 id。 | 自由文本，比较时忽略大小写。留空就退回到规整过的设备名称。 | 空 |

没有主机、端口或凭据字段：连接的细节握在充电桩手里，本工作站只需要知道这台设备指的是哪一台充电桩。也没有轮询间隔字段。OCPP 是推送式的，充电桩按它自己的节奏发 MeterValues 和 StatusNotification 消息，而点位以固定的一秒周期从最近收到的数据里刷新。

因此，一台还没连上来的充电桩，或者一个还没报告过的充电接口，是**没有读数**，而不是读数失败。它的点位保持空，也不会有失败写进诊断日志：充电桩还没发来的点位，正是协议在正常工作。

### 充电桩的健康状态 {#the-charge-points-health}

充电桩有了点位才发过来，所以光凭点位没有动静，分不清一台还没报告的充电桩和一台已经不在的充电桩。它的健康状态，也就是它在树里那一行上的圆点和驱动页面上的**健康状态**一列，改为跟随它与本工作站中央系统的连接：

| 情况 | 健康状态 |
|---|---|
| 已配置，自工作站打开以来一直没连上来 | **离线** |
| 已连上，还什么都没发 | **在线** |
| 已连上，正在发送 | **在线**；它发来的点位有一部分读不出来时为**部分在线** |
| 掉线了：停了、断电了或者和网络断开了 | **离线**，直到它重新连上 |
| 中央系统没在监听 | **离线** |
| 已禁用 | **已禁用** |

充电桩连着的时候，一个还没报告过的充电接口不会拉低它的状态。充电桩的改动保存之后，或者 Connector 启动之后，最多十秒内它读作**尚未可知**：重新放入设备可能会让中央系统重启，充电桩要过一会儿才能重新连上。

## 点位 {#tags}

OCPP 的点位读的是这台充电桩的一个**充电接口**（OCPP 意义上的 connector：一个插枪位，从 1 开始编号）。在 OCPP 2.0、2.0.1 和 2.1 里，这个编号是 **EVSE** 的编号，因为那里的充电接口在每个 EVSE 内部又从 1 开始编号。存在三种点位，靠**点位名称的后缀**区分开，而这个后缀是要校验的：

| 点位名以什么结尾 | 读的是什么 | 合适的类型 |
|---|---|---|
| `Meter` | 这个充电接口最近报告的那次采样电表值（OCPP MeterValues；在 OCPP 2.x 里，还包括充电过程中 TransactionEvent 带来的值）。单位是充电桩发来的那个，通常是 Wh。服务器保留最近 10 次采样，点位读的是最新那一次。从没报告过的充电接口读作一个空值，不是零。 | Float 或 Double |
| `Status` | 最近一次 StatusNotification 里那个充电接口的状态文字（例如 `Available`、`Faulted`）。在 OCPP 2.x 里，有多个充电接口的 EVSE 读作其中最能说明情况的那个：`Faulted` 优先于 `Occupied`，`Occupied` 优先于 `Reserved`，`Reserved` 优先于 `Available`，`Available` 优先于 `Unavailable`。还没报告过状态的 EVSE 读作一个空值。 | String |
| `Transaction` | 这个充电接口上正在进行的事务的编号，也就是 RemoteStopTransaction 和 GetTransactionStatus 两个命令要的那个编号：OCPP 1.6 里是事务开始时本工作站给它的编号，OCPP 2.x 里是充电站自己的标识，照它发来的原样读出。事务开始时填上，结束时清空，所以没有事务在进行时读到的是空文本。充电桩连上之后、第一次有事务开始或结束之前，它读作空值，因为可能早已有事务在进行。 | String |

这几种后缀都没有的名字会被校验拒绝：请把点位命名成 `Connector1Meter`、`Connector1Status` 和 `Connector1Transaction` 这样。

| 字段 | 作用 | 取值 | 默认值 |
|---|---|---|---|
| Connector | 这个点位读的编号：OCPP 1.6 里是充电接口的编号，OCPP 2.x 里是 EVSE 的编号。 | 1 或更大 | 1 |
| 数据类型 | 这个值对外暴露成的那个 OPC UA 类型。 | Int32、Float、Double、String | Float |

OCPP 的点位是**只读**的：读写字段不提供可写的选项，一次写入会被拒绝而不是被假装成功。给充电桩下命令是它自己的一套命令，下面就讲，不是往点位里写一个值。

## 命令 {#commands}

充电桩不只是被读，也会被下命令。每一台 OCPP 设备都在本工作站自己的 OPC UA 地址空间里，把下面这些命令作为**设备上的方法**带着，所以连接到本工作站的 OPC UA 客户端调用该方法，就能下发一条命令。

每一条命令都以**充电桩 id** 作为它的第一个参数，并把充电桩自己的回答作为结果交回来。

| 命令 | 其余参数 | 作用 |
|---|---|---|
| ChangeAvailability | ConnectorId、Type（`Operative` 或 `Inoperative`） | 把一个充电接口投入或退出运行。 |
| ChangeConfiguration | Key、Value | 在充电桩上设一个配置键（OCPP 1.6）。 |
| ClearCache | 无 | 清掉充电桩的授权缓存。 |
| GetConfiguration | Key | 读一个配置键（OCPP 1.6）。 |
| RemoteStartTransaction | IdTag | 为一个令牌启动一次充电会话。 |
| RemoteStopTransaction | TransactionId | 停掉一次正在进行的会话。 |
| GetTransactionStatus | TransactionId | 问一次事务是不是还在进行（OCPP 2.x）。 |
| Reset | 无 | 重启充电桩。 |
| UnlockConnector | ConnectorId | 从一个充电接口上把电缆解锁放开。 |
| SetChargingProfile | ConnectorId、Limit、NumberOfPhases | 设一个充电功率上限，单位是瓦。ConnectorId 为 0 时限制整台充电桩；其他编号为该充电接口所充的会话设默认上限。 |
| SetVariables | ComponentName、VariableName、AttributeType、AttributeValue | 设某个组件的某一个变量（OCPP 2.x）。 |
| GetVariables | ComponentName、VariableName、AttributeType | 读某个组件的某一个变量（OCPP 2.x）。 |

`AttributeType` 是 `Actual`、`Target`、`MinSet`、`MaxSet` 之一。

**TransactionId 从哪里来。** RemoteStopTransaction 和 GetTransactionStatus 作用于一次事务，用它的编号来指明。这个编号从会话所在充电接口（OCPP 2.x 里是 EVSE）的 `Transaction` 点位读出来，比如 `Connector1Transaction`，读到什么就原样传什么。那里是空文本，就说明这个充电接口上没有事务在进行。

**一份命令表，适用于每个版本。** 不论充电桩讲的是哪个版本，这些命令都沿用上面的名字，工作站按该版本规定的格式发送每一条。在 OCPP 2.0、2.0.1 和 2.1 里，充电接口归在 EVSE 之下，作为 ConnectorId 传入的编号指的是 EVSE。

| 命令 | 在 OCPP 1.6 上 | 在 OCPP 2.0、2.0.1 和 2.1 上 |
|---|---|---|
| ChangeAvailability | 作用于给定的充电接口；0 表示整台充电桩。 | 作用于给定的 EVSE；0 表示整个充电站。 |
| RemoteStartTransaction | 发送 IdTag，由充电桩选择充电接口。 | 以 RequestStartTransaction 发出，IdTag 作为中心令牌，并带上自己的启动编号；由充电站选择 EVSE。 |
| RemoteStopTransaction | TransactionId 是会话开始时本工作站给这次会话的编号。 | 以 RequestStopTransaction 发出；TransactionId 是充电站自己的标识，原样以文本传过去。 |
| Reset | 硬重启。 | 立即重启。 |
| UnlockConnector | 解锁给定的充电接口。 | 解锁给定 EVSE 的第一个充电接口。 |
| SetChargingProfile | 0 设充电桩的最大配置文件；其他充电接口得到一个默认配置文件。 | 0 设充电站的最大配置文件；其他 EVSE 得到一个默认配置文件。 |
| ChangeConfiguration、GetConfiguration | 同上表。 | 不属于这些版本：充电站会回一个错误，命令失败。 |
| GetTransactionStatus、SetVariables、GetVariables | 不属于这个版本：充电桩会回一个错误，命令失败。 | 同上表。 |

**等那个回答。** 工作站把命令发出去，这条命令并不算完。工作站会把这次调用一直挂着，直到充电桩回答的正是它发出去的那一条为止，靠随命令一起走的关联标识符来配对，然后才报告成功并把答复交回来。有三件事会以别的方式把它结束掉，每一种都作为失败连同原因一起报告：

- 充电桩没有连上，或者它的连接没有打开。什么都不会被发出去。
- 充电桩答的是一个 OCPP 错误，例如一条它没有实现的命令。错误码就是原因。
- 过了 **30 秒**还完全没有回答。没有人回答的命令是一次失败，绝不是成功。

为一台某个设备并不代表的、或者没有连上的充电桩下发的命令，会当场被拒绝，而不是排队等着。

## 设备发现 {#discovery}

OCPP 驱动上的**发现设备**列出**此刻连在**中央系统上的那些充电桩，而读取一个候选项的通道，会为这台充电桩到目前为止报告过的每一个充电接口各提议一个 `Meter` 点位、一个 `Status` 点位和一个 `Transaction` 点位。

**第一台充电桩永远是手工配置的。** 中央系统没在听之前什么都连不上，而它只有在这里存在一台 OCPP 设备之后才听，所以在一台充电桩都没配置的安装环境里做一次扫描，什么都找不到，而且它会这么说出来。请用充电桩将要连接时所用的那个 id 把第一台设备添加进来；此后，一台已经连上过的充电桩就能被发现了。一般的设备发现流程见 [Connector](connector) 页面。

这个 id 在证书里、在连接路径里、以及和设备比对时都**忽略大小写**：一台以 `cp-a` 连上来、证书里写着 `CP-A` 的充电桩，和一台标识符写作 `CP-A` 的设备是同一台充电桩。

已经有的充电桩可以在自己的页面上重新读它的充电接口：“点位”卡片标题栏里的**发现点位**列出它连接期间报告过的每个充电接口的 `Meter`、`Status` 和 `Transaction` 点位中设备还没有的那些（见[发现一台设备的点位](connector-discovery#discovering-a-devices-tags)）。
