feat: port the ocpp gateway from version 4

This commit is contained in:
JianBo He 2023-11-03 16:51:21 +08:00
parent 17544dc410
commit 7cab269e0b
81 changed files with 5254 additions and 4 deletions

View File

@ -56,6 +56,8 @@
-export([mountpoint/0, mountpoint/1, gateway_common_options/0, gateway_schema/1, gateway_names/0]).
-export([ws_listener/2, wss_listener/2]).
namespace() -> gateway.
tags() ->
@ -250,6 +252,137 @@ mountpoint(Default) ->
}
).
ws_listener(DefaultPath, DefaultSubProtocols) when
is_binary(DefaultPath), is_binary(DefaultSubProtocols)
->
[
{acceptors, sc(integer(), #{default => 16, desc => ?DESC(tcp_listener_acceptors)})}
] ++
ws_opts(DefaultPath, DefaultSubProtocols) ++
tcp_opts() ++
proxy_protocol_opts() ++
common_listener_opts().
wss_listener(DefaultPath, DefaultSubProtocols) when
is_binary(DefaultPath), is_binary(DefaultSubProtocols)
->
ws_listener(DefaultPath, DefaultSubProtocols) ++
[
{ssl_options,
sc(
hoconsc:ref(emqx_schema, "listener_wss_opts"),
#{
desc => ?DESC(ssl_listener_options),
validator => fun emqx_schema:validate_server_ssl_opts/1
}
)}
].
ws_opts(DefaultPath, DefaultSubProtocols) ->
[
{"path",
sc(
string(),
#{
default => DefaultPath,
desc => ?DESC(fields_ws_opts_path)
}
)},
{"piggyback",
sc(
hoconsc:enum([single, multiple]),
#{
default => single,
desc => ?DESC(fields_ws_opts_piggyback)
}
)},
{"compress",
sc(
boolean(),
#{
default => false,
desc => ?DESC(fields_ws_opts_compress)
}
)},
{"idle_timeout",
sc(
duration(),
#{
default => <<"7200s">>,
desc => ?DESC(fields_ws_opts_idle_timeout)
}
)},
{"max_frame_size",
sc(
hoconsc:union([infinity, integer()]),
#{
default => infinity,
desc => ?DESC(fields_ws_opts_max_frame_size)
}
)},
{"fail_if_no_subprotocol",
sc(
boolean(),
#{
default => true,
desc => ?DESC(fields_ws_opts_fail_if_no_subprotocol)
}
)},
{"supported_subprotocols",
sc(
comma_separated_list(),
#{
default => DefaultSubProtocols,
desc => ?DESC(fields_ws_opts_supported_subprotocols)
}
)},
{"check_origin_enable",
sc(
boolean(),
#{
default => false,
desc => ?DESC(fields_ws_opts_check_origin_enable)
}
)},
{"allow_origin_absence",
sc(
boolean(),
#{
default => true,
desc => ?DESC(fields_ws_opts_allow_origin_absence)
}
)},
{"check_origins",
sc(
emqx_schema:comma_separated_binary(),
#{
default => <<"http://localhost:18083, http://127.0.0.1:18083">>,
desc => ?DESC(fields_ws_opts_check_origins)
}
)},
{"proxy_address_header",
sc(
string(),
#{
default => <<"x-forwarded-for">>,
desc => ?DESC(fields_ws_opts_proxy_address_header)
}
)},
{"proxy_port_header",
sc(
string(),
#{
default => <<"x-forwarded-port">>,
desc => ?DESC(fields_ws_opts_proxy_port_header)
}
)},
{"deflate_opts",
sc(
ref("deflate_opts"),
#{}
)}
].
common_listener_opts() ->
[
{enable,
@ -328,7 +461,7 @@ proxy_protocol_opts() ->
sc(
duration(),
#{
default => <<"15s">>,
default => <<"3s">>,
desc => ?DESC(tcp_listener_proxy_protocol_timeout)
}
)}
@ -337,7 +470,6 @@ proxy_protocol_opts() ->
%%--------------------------------------------------------------------
%% dynamic schemas
%% FIXME: don't hardcode the gateway names
gateway_schema(Name) ->
case emqx_gateway_utils:find_gateway_definition(Name) of
{ok, #{config_schema_module := SchemaMod}} ->

View File

@ -798,9 +798,11 @@ format(Msg) ->
io_lib:format("~p", [Msg]).
type(_) ->
%% TODO:
gbt32960.
is_message(#frame{}) ->
%% TODO:
true;
is_message(_) ->
false.

23
apps/emqx_gateway_ocpp/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
.rebar3
_*
.eunit
*.o
*.beam
*.plt
*.swp
*.swo
.erlang.cookie
ebin
log
erl_crash.dump
.rebar
logs
_build
.idea
*.iml
rebar3.crashdump
*~
.DS_Store
data/
etc/emqx_ocpp.conf.rendered
rebar.lock

View File

@ -0,0 +1,175 @@
# emqx-ocpp
OCPP-J 1.6 协议的 Central System 实现。
## 客户端信息映射
在 EMQX 4.x 中OCPP-J 网关作为协议插件或协议模块(仅企业版本)进行提供。
所有连接到 OCPP-J 网关的 Charge Point都会被当做一个普通的客户端对待就像 MQTT 客户端一样)。
即可以使用 Charge Point 的唯一标识,在 Dashboard/HTTP-API/CLI 来管理它。
客户端信息的映射关系为:
- Client IDCharge Point 的唯一标识。
- Username从 HTTP Basic 认证中的 Username 解析得来。
- Password从 HTTP Basic 认证中的 Password 解析得来。
### 认证
正如 **ocpp-j-1.6** 规范中提到的Charge Point 可以使用 HTTP Basic 进行认证。
OCPP-J 网关从中提取 Username 和 Password并通过 EMQX 的认证系统获取登录权限。
也就是说OCPP-J 网关使用 EMQX 的认证插件来授权 Charge Point 的登录。
## 消息拓扑
```
+----------------+ upstream publish +---------+
+--------------+ Req/Resp | OCPP-J Gateway | -----------------> | Third |
| Charge Point | <------------> | over | over Topic | Service |
+--------------+ over ws/wss | EMQX | <----------------- | |
+----------------+ dnstream publish +---------+
```
Charge Point 和 OCPP-J 网关通过 OCPP-J 协议定义的规范进行通信。这主要是基于 Websocket 和 Websocket TLS
### Up Stream (emqx-ocpp -> third-services)
OCPP-J 网关将 Charge Point 所有的消息、事件通过 EMQX 进行发布。这个数据流称为 **Up Stream**
其主题配置支持按任意格式进行配置,例如:
```
## 上行默认主题。emqx-ocpp 网关会将所有 Charge Point 的消息发布到该主题上。
##
## 可用占位符为:
## - cid: Charge Point ID
## - action: The Message Name for OCPP
##
ocpp.upstream.topic = ocpp/cp/${cid}/${action}
## 支持按消息名称对默认主题进行重载
##
ocpp.upstream.topic.BootNotification = ocpp/cp/${cid}/Notify/${action}
```
Payload 为固定格式,它包括字段
| Field | Type | Seq | Required | Desc |
| ----------------- | ----------- | --- | -------- | ---- |
| MessageTypeId | MessageType | 1 | R | Define the type of Message, whether it is Call, CallResult or CallError |
| UniqueId | String | 2 | R | This must be the exact same id that is in the call request so that the recipient can match request and result |
| Action | String | 3 | O | The Message Name of OCPP. E.g. Authorize |
| ErrorCode | ErrorType | 4 | O | The string must contain one from ErrorType Table |
| ErrorDescription | String | 5 | O | Detailed Error information |
| Payload | Bytes | 6 | O | Payload field contains the serialized strings of bytes for protobuf format of OCPP message |
例如,一条在 upstream 上的 BootNotifiaction.req 的消息格式为:
```
Topic: ocpp/cp/CP001/Notify/BootNotifiaction
Payload:
{"MessageTypeId": 2,
"UniqueId": "1",
"Payload": {"chargePointVendor":"vendor1","chargePointModel":"model1"}
}
```
同样,对于 Charge Point 发送到 Central System 的 `*.conf` 的应答消息和错误通知,
也可以定制其主题格式:
```
ocpp.upstream.reply_topic = ocpp/cp/Reply/${cid}
ocpp.upstream.error_topic = ocpp/cp/Error/${cid}
```
Up Stream 消息的 QoS 等级固定为 2即最终接收的 QoS 等级取决于订阅者发起订阅时的 QoS 等级。
### Down Stream (third-services -> emqx-ocpp)
OCPP-J 网关通过向 EMQX 订阅主题来接收控制消息,并将它转发的对应的 Charge Point以达到消息下发的效果。
这个数据流被称为 **Down Stream**
其主题配置支持按任意格式进行配置,例如:
```
## 下行主题。网关会为每个连接的 Charge Point 网关自动订阅该主题,
## 以接收下行的控制命令等。
##
## 可用占位符为:
## - cid: Charge Point ID
##
## 注1. 为了区分每个 Charge Point所以 ${cid} 是必须的
## 2. 通配符 `+` 不是必须的,此处仅是一个示例
ocpp.dnstream.topic = ocpp/${cid}/+/+
```
Payload 为固定格式,格式同 upstream。
例如,一条从 Third-Service 发到网关的 BootNotifaction 的应答消息格式为:
```
Topic: ocpp/cp/CP001/Reply/BootNotification
Payload:
{"MessageTypeId": 3,
"UniqueId": "1",
"Payload": {"currentTime": "2022-06-21T14:20:39+00:00", "interval": 300, "status": "Accepted"}
}
```
### 消息收发机制
正如 OCPP-J 协议所说Charge Point 和 Central System 在发送出一条请求消息CALL都必须等待该条消息被应答或者超时后才能发送下一条消息。
网关在实现上,支持严格按照 OCPP-J 定义的通信逻辑执行,也支持不执行该项检查。
```
ocpp.upstream.strit_mode = false
ocpp.dnstream.strit_mode = false
```
`upstream.strit_mode = false` 时,**只要 Charge Point 有新的消息到达,都会被发布到 upsteam 的主题上。**
`dnstream.strit_mode = false` 时,**只要 Third-Party 有新的消息发布到 dnstream都会被里面转发到 Charge Point 上。**
注:当前版本,仅支持 `strit_mode = false`
#### Up Stream (Charge Point -> emqx-ocpp)
`upstream.strit_mode = true` 时, OCPP-J 网关处理 Up Stream 的行为:
- 收到的请求消息会立马发布到 Up Stream 并保存起来,直到 Down Stream 上得到一个该消息的应答、或答超时后才会被移除。但应答和错误消息不会被暂存。
- 如果上一条请求消息没有被应答或超时,后续收到的请求消息都会被 OCPP-J 网关丢弃并回复一个 `SecurityError` 错误。但如果这两条请求消息相同,则会在 Up Stream 上被重新发布。
- 当请求消息被应答或超时后,才会处理下一条请求消息。
- Charge Point 发送的应答和错误消息会立马发布到 Up Stream不会被暂存也不会阻塞下一条应答和错误消息。
相关配置有:
```
# 上行请求消息,最大的应答等待时间
ocpp.upstream.awaiting_timeout = 30s
```
#### Down Stream (Third-services -> emqx-ocpp)
`upstream.strit_mode = true`Down Stream 的行为:
- 下行请求消息会先暂存到网关,直到它被 Charge Point 应答。
- 多条下行请求消息会被暂存到网关的发送队列中,直到上一条请求消息被确认才会发布下一条请求消息。
- 下行的应答和错误消息,会尝试确认 Charge Point 发送的请求消息。无论是否确认成功,该消息都会立马投递到 Charge Point并不会在消息队列里排队。
- 下行的请求消息不会被丢弃,如果等待超时则会重发该请求消息,直到它被确认。
相关配置有:
```
# 下行请求消息重试间隔
ocpp.dnstream.retry_interval = 30s
# 下行请求消息最大队列长度
ocpp.dnstream.max_mqueue_len = 10
```
### 消息格式检查
网关支持通过 Json-Schema 来校验每条消息 Payload 的合法性。
```
## 检查模式
#ocpp.message_format_checking = all
## json-schema 文件夹路径
#ocpp.json_schema_dir = ${application_priv}/schemas
## json-schema 消息前缀
#ocpp.json_schema_id_prefix = urn:OCPP:1.6:2019:12:
```

View File

@ -0,0 +1,94 @@
# emqx-ocpp
OCPP-J 1.6 Gateway for EMQX that implement the Central System for OCPP-J protocol.
## Treat Charge Point as Client of EMQX
In EMQX 4.x, OCPP-J Gateway implement as a protocol Plugin and protocol Module (enterprise only).
All Charge Point connected to OCPP-J Gateway will be treat as a normal Client (like MQTT Client) in EMQX,
you can manage it in Dashboard/HTTP-API/CLI by charge point identity.
The Client Info mapping in OCPP-J Gateway:
- Client ID: presented by charge point identity.
- Username: parsed by the username field for HTTP basic authentication.
- Password: parsed by the password field for HTTP basic authentication.
### Charge Point Authentication
As mentioned in the **ocpp-j-1.6 specification**, Charge Point can use HTTP Basic for
authentication. OCPP-J Gateway extracts the username/password from it and fetches
an approval through EMQX's authentication hooks.
That is, the OCPP-J Gateway uses EMQX's authentication plugin to authorize the Charge Point login.
## Message exchanging among Charge Point, EMQX (Central System) and Third-services
```
+----------------+ upstream publish +---------+
+--------------+ Req/Resp | OCPP-J Gateway | -----------------> | Third |
| Charge Point | <------------> | over | over Topic | Service |
+--------------+ over ws/wss | EMQX | <----------------- | |
+----------------+ dnstream publish +---------+
```
Charge Point and OCPP-J Gateway communicate through the specifications defined by OCPP-J.
It is mainly based on Websocket or Websocket TLS.
The OCPP-J Gateway publishes all Charge point messages through EMQX, which are called **Up Stream**.
It consists of two parts:
- Topic: the default topic structure is `ocpp/${clientid}/up/${type}/${action}/${id}`
* ${clientid}: charge point identity.
* ${type}: enum with `request`, `response`, `error`
* ${action}: enum all message type name defined **ocpp 1.6 edtion 2**. i.e: `BootNotification`.
* ${id}: unique message id parsed by OCPP-J message
- Payload: JSON string defined **ocpp 1.6 edtion 2**. i.e:
```json
{"chargePointVendor":"vendor1","chargePointModel":"model1"}
```
The OCPP-J Gateway receives commands from external services by subscribing to EMQX
topics and routing them down to the Charge Point in the format defined by OCPP-J,
which are called **Down Stream**.
It consists of two parts:
- Topic: the default topic structure is `ocpp/${clientid}/dn/${type}/${action}/${id}`
* The values of these variables are the same as for upstream.
* To receive such messages, OCPP-J Gateway will add a subscription `ocpp/${clientid}/dn/+/+/+`
for each Charge point client.
- Payload: JSON string defined **ocpp 1.6 edtion 2**. i.e:
```json
{"currentTime": "2022-06-21T14:20:39+00:00", "interval": 300, "status": "Accepted"}
```
### Message Re-transmission
TODO
```
ocpp.awaiting_timeout = 30s
ocpp.retry_interval = 30s
```
### Message Format Checking
TODO
```
#ocpp.message_format_checking = all
#ocpp.json_schema_dir = ${application_priv}/schemas
#ocpp.json_schema_id_prefix = urn:OCPP:1.6:2019:12:
```
## Management and Observability
### Manage Clients
### Observe the messaging state

View File

@ -0,0 +1,101 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%% %% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-define(APP, emqx_ocpp).
%% types for ocppj-1.6
-define(OCPP_MSG_TYPE_ID_CALL, 2).
-define(OCPP_MSG_TYPE_ID_CALLRESULT, 3).
-define(OCPP_MSG_TYPE_ID_CALLERROR, 4).
%% actions for ocppj-1.6
-define(OCPP_ACT_Authorize, <<"Authorize">>).
-define(OCPP_ACT_BootNotification, <<"BootNotification">>).
-define(OCPP_ACT_CancelReservation, <<"CancelReservation">>).
-define(OCPP_ACT_ChangeAvailability, <<"ChangeAvailability">>).
-define(OCPP_ACT_ChangeConfiguration, <<"ChangeConfiguration">>).
-define(OCPP_ACT_ClearCache, <<"ClearCache">>).
-define(OCPP_ACT_ClearChargingProfile, <<"ClearChargingProfile">>).
-define(OCPP_ACT_DataTransfer, <<"DataTransfer">>).
-define(OCPP_ACT_DiagnosticsStatusNotification, <<"DiagnosticsStatusNotification">>).
-define(OCPP_ACT_FirmwareStatusNotification, <<"FirmwareStatusNotification">>).
-define(OCPP_ACT_GetCompositeSchedule, <<"GetCompositeSchedule">>).
-define(OCPP_ACT_GetConfiguration, <<"GetConfiguration">>).
-define(OCPP_ACT_GetDiagnostics, <<"GetDiagnostics">>).
-define(OCPP_ACT_GetLocalListVersion, <<"GetLocalListVersion">>).
-define(OCPP_ACT_Heartbeat, <<"Heartbeat">>).
-define(OCPP_ACT_MeterValues, <<"MeterValues">>).
-define(OCPP_ACT_RemoteStartTransaction, <<"RemoteStartTransaction">>).
-define(OCPP_ACT_RemoteStopTransaction, <<"RemoteStopTransaction">>).
-define(OCPP_ACT_ReserveNow, <<"ReserveNow">>).
-define(OCPP_ACT_Reset, <<"Reset">>).
-define(OCPP_ACT_SendLocalList, <<"SendLocalList">>).
-define(OCPP_ACT_SetChargingProfile, <<"SetChargingProfile">>).
-define(OCPP_ACT_StartTransaction, <<"StartTransaction">>).
-define(OCPP_ACT_StatusNotification, <<"StatusNotification">>).
-define(OCPP_ACT_StopTransaction, <<"StopTransaction">>).
-define(OCPP_ACT_TriggerMessage, <<"TriggerMessage">>).
-define(OCPP_ACT_UnlockConnector, <<"UnlockConnector">>).
-define(OCPP_ACT_UpdateFirmware, <<"UpdateFirmware">>).
%% error codes for ocppj-1.6
-define(OCPP_ERR_NotSupported, <<"NotSupported">>).
-define(OCPP_ERR_InternalError, <<"InternalError">>).
-define(OCPP_ERR_ProtocolError, <<"ProtocolError">>).
-define(OCPP_ERR_SecurityError, <<"SecurityError">>).
-define(OCPP_ERR_FormationViolation, <<"FormationViolation">>).
-define(OCPP_ERR_PropertyConstraintViolation, <<"PropertyConstraintViolation">>).
-define(OCPP_ERR_OccurenceConstraintViolation, <<"OccurenceConstraintViolation">>).
-define(OCPP_ERR_TypeConstraintViolation, <<"TypeConstraintViolation">>).
-define(OCPP_ERR_GenericError, <<"GenericError">>).
-type utf8_string() :: unicode:unicode_binary().
-type message_type() :: ?OCPP_MSG_TYPE_ID_CALL..?OCPP_MSG_TYPE_ID_CALLERROR.
%% OCPP_ACT_Authorize..OCPP_ACT_UpdateFirmware
-type action() :: utf8_string().
-type frame() :: #{
type := message_type(),
%% The message ID serves to identify a request.
%% Maximum of 36 characters, to allow for GUIDs
id := utf8_string(),
%% the name of the remote procedure or action.
%% This will be a case-sensitive string.
%% Only presented in ?OCPP_MSG_TYPE_ID_CALL
action => action(),
%% json map decoded by jsx and validated by json schema
payload := null | map()
}.
-define(IS_REQ(F), F = #{type := ?OCPP_MSG_TYPE_ID_CALL}).
-define(IS_REQ(F, Id), F = #{type := ?OCPP_MSG_TYPE_ID_CALL, id := Id}).
-define(IS_RESP(F), F = #{type := ?OCPP_MSG_TYPE_ID_CALLRESULT}).
-define(IS_RESP(F, Id), F = #{type := ?OCPP_MSG_TYPE_ID_CALLRESULT, id := Id}).
-define(IS_ERROR(F), F = #{type := ?OCPP_MSG_TYPE_ID_CALLERROR}).
-define(IS_ERROR(F, Id), F = #{type := ?OCPP_MSG_TYPE_ID_CALLERROR, id := Id}).
-define(IS_BootNotification_RESP(Payload), #{
type := ?OCPP_MSG_TYPE_ID_CALLRESULT,
action := ?OCPP_ACT_BootNotification,
payload := Payload
}).
-define(ERR_FRAME(Id, Code, Desc), #{
id => Id,
type => ?OCPP_MSG_TYPE_ID_CALLERROR,
error_code => Code,
error_desc => Desc,
error_details => null
}).

View File

@ -0,0 +1,16 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:AuthorizeRequest",
"title": "AuthorizeRequest",
"type": "object",
"properties": {
"idTag": {
"type": "string",
"maxLength": 20
}
},
"additionalProperties": false,
"required": [
"idTag"
]
}

View File

@ -0,0 +1,40 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:AuthorizeResponse",
"title": "AuthorizeResponse",
"type": "object",
"properties": {
"idTagInfo": {
"type": "object",
"properties": {
"expiryDate": {
"type": "string",
"format": "date-time"
},
"parentIdTag": {
"type": "string",
"maxLength": 20
},
"status": {
"type": "string",
"additionalProperties": false,
"enum": [
"Accepted",
"Blocked",
"Expired",
"Invalid",
"ConcurrentTx"
]
}
},
"additionalProperties": false,
"required": [
"status"
]
}
},
"additionalProperties": false,
"required": [
"idTagInfo"
]
}

View File

@ -0,0 +1,49 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:BootNotificationRequest",
"title": "BootNotificationRequest",
"type": "object",
"properties": {
"chargePointVendor": {
"type": "string",
"maxLength": 20
},
"chargePointModel": {
"type": "string",
"maxLength": 20
},
"chargePointSerialNumber": {
"type": "string",
"maxLength": 25
},
"chargeBoxSerialNumber": {
"type": "string",
"maxLength": 25
},
"firmwareVersion": {
"type": "string",
"maxLength": 50
},
"iccid": {
"type": "string",
"maxLength": 20
},
"imsi": {
"type": "string",
"maxLength": 20
},
"meterType": {
"type": "string",
"maxLength": 25
},
"meterSerialNumber": {
"type": "string",
"maxLength": 25
}
},
"additionalProperties": false,
"required": [
"chargePointVendor",
"chargePointModel"
]
}

View File

@ -0,0 +1,30 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:BootNotificationResponse",
"title": "BootNotificationResponse",
"type": "object",
"properties": {
"status": {
"type": "string",
"additionalProperties": false,
"enum": [
"Accepted",
"Pending",
"Rejected"
]
},
"currentTime": {
"type": "string",
"format": "date-time"
},
"interval": {
"type": "integer"
}
},
"additionalProperties": false,
"required": [
"status",
"currentTime",
"interval"
]
}

View File

@ -0,0 +1,15 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:CancelReservationRequest",
"title": "CancelReservationRequest",
"type": "object",
"properties": {
"reservationId": {
"type": "integer"
}
},
"additionalProperties": false,
"required": [
"reservationId"
]
}

View File

@ -0,0 +1,20 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:CancelReservationResponse",
"title": "CancelReservationResponse",
"type": "object",
"properties": {
"status": {
"type": "string",
"additionalProperties": false,
"enum": [
"Accepted",
"Rejected"
]
}
},
"additionalProperties": false,
"required": [
"status"
]
}

View File

@ -0,0 +1,24 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:ChangeAvailabilityRequest",
"title": "ChangeAvailabilityRequest",
"type": "object",
"properties": {
"connectorId": {
"type": "integer"
},
"type": {
"type": "string",
"additionalProperties": false,
"enum": [
"Inoperative",
"Operative"
]
}
},
"additionalProperties": false,
"required": [
"connectorId",
"type"
]
}

View File

@ -0,0 +1,21 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:ChangeAvailabilityResponse",
"title": "ChangeAvailabilityResponse",
"type": "object",
"properties": {
"status": {
"type": "string",
"additionalProperties": false,
"enum": [
"Accepted",
"Rejected",
"Scheduled"
]
}
},
"additionalProperties": false,
"required": [
"status"
]
}

View File

@ -0,0 +1,21 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:ChangeConfigurationRequest",
"title": "ChangeConfigurationRequest",
"type": "object",
"properties": {
"key": {
"type": "string",
"maxLength": 50
},
"value": {
"type": "string",
"maxLength": 500
}
},
"additionalProperties": false,
"required": [
"key",
"value"
]
}

View File

@ -0,0 +1,22 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:ChangeConfigurationResponse",
"title": "ChangeConfigurationResponse",
"type": "object",
"properties": {
"status": {
"type": "string",
"additionalProperties": false,
"enum": [
"Accepted",
"Rejected",
"RebootRequired",
"NotSupported"
]
}
},
"additionalProperties": false,
"required": [
"status"
]
}

View File

@ -0,0 +1,8 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:ClearCacheRequest",
"title": "ClearCacheRequest",
"type": "object",
"properties": {},
"additionalProperties": false
}

View File

@ -0,0 +1,20 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:ClearCacheResponse",
"title": "ClearCacheResponse",
"type": "object",
"properties": {
"status": {
"type": "string",
"additionalProperties": false,
"enum": [
"Accepted",
"Rejected"
]
}
},
"additionalProperties": false,
"required": [
"status"
]
}

View File

@ -0,0 +1,27 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:ClearChargingProfileRequest",
"title": "ClearChargingProfileRequest",
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"connectorId": {
"type": "integer"
},
"chargingProfilePurpose": {
"type": "string",
"additionalProperties": false,
"enum": [
"ChargePointMaxProfile",
"TxDefaultProfile",
"TxProfile"
]
},
"stackLevel": {
"type": "integer"
}
},
"additionalProperties": false
}

View File

@ -0,0 +1,20 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:ClearChargingProfileResponse",
"title": "ClearChargingProfileResponse",
"type": "object",
"properties": {
"status": {
"type": "string",
"additionalProperties": false,
"enum": [
"Accepted",
"Unknown"
]
}
},
"additionalProperties": false,
"required": [
"status"
]
}

View File

@ -0,0 +1,23 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:DataTransferRequest",
"title": "DataTransferRequest",
"type": "object",
"properties": {
"vendorId": {
"type": "string",
"maxLength": 255
},
"messageId": {
"type": "string",
"maxLength": 50
},
"data": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"vendorId"
]
}

View File

@ -0,0 +1,25 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:DataTransferResponse",
"title": "DataTransferResponse",
"type": "object",
"properties": {
"status": {
"type": "string",
"additionalProperties": false,
"enum": [
"Accepted",
"Rejected",
"UnknownMessageId",
"UnknownVendorId"
]
},
"data": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"status"
]
}

View File

@ -0,0 +1,22 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:DiagnosticsStatusNotificationRequest",
"title": "DiagnosticsStatusNotificationRequest",
"type": "object",
"properties": {
"status": {
"type": "string",
"additionalProperties": false,
"enum": [
"Idle",
"Uploaded",
"UploadFailed",
"Uploading"
]
}
},
"additionalProperties": false,
"required": [
"status"
]
}

View File

@ -0,0 +1,8 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:DiagnosticsStatusNotificationResponse",
"title": "DiagnosticsStatusNotificationResponse",
"type": "object",
"properties": {},
"additionalProperties": false
}

View File

@ -0,0 +1,25 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:FirmwareStatusNotificationRequest",
"title": "FirmwareStatusNotificationRequest",
"type": "object",
"properties": {
"status": {
"type": "string",
"additionalProperties": false,
"enum": [
"Downloaded",
"DownloadFailed",
"Downloading",
"Idle",
"InstallationFailed",
"Installing",
"Installed"
]
}
},
"additionalProperties": false,
"required": [
"status"
]
}

View File

@ -0,0 +1,8 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:FirmwareStatusNotificationResponse",
"title": "FirmwareStatusNotificationResponse",
"type": "object",
"properties": {},
"additionalProperties": false
}

View File

@ -0,0 +1,27 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:GetCompositeScheduleRequest",
"title": "GetCompositeScheduleRequest",
"type": "object",
"properties": {
"connectorId": {
"type": "integer"
},
"duration": {
"type": "integer"
},
"chargingRateUnit": {
"type": "string",
"additionalProperties": false,
"enum": [
"A",
"W"
]
}
},
"additionalProperties": false,
"required": [
"connectorId",
"duration"
]
}

View File

@ -0,0 +1,79 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:GetCompositeScheduleResponse",
"title": "GetCompositeScheduleResponse",
"type": "object",
"properties": {
"status": {
"type": "string",
"additionalProperties": false,
"enum": [
"Accepted",
"Rejected"
]
},
"connectorId": {
"type": "integer"
},
"scheduleStart": {
"type": "string",
"format": "date-time"
},
"chargingSchedule": {
"type": "object",
"properties": {
"duration": {
"type": "integer"
},
"startSchedule": {
"type": "string",
"format": "date-time"
},
"chargingRateUnit": {
"type": "string",
"additionalProperties": false,
"enum": [
"A",
"W"
]
},
"chargingSchedulePeriod": {
"type": "array",
"items": {
"type": "object",
"properties": {
"startPeriod": {
"type": "integer"
},
"limit": {
"type": "number",
"multipleOf" : 0.1
},
"numberPhases": {
"type": "integer"
}
},
"additionalProperties": false,
"required": [
"startPeriod",
"limit"
]
}
},
"minChargingRate": {
"type": "number",
"multipleOf" : 0.1
}
},
"additionalProperties": false,
"required": [
"chargingRateUnit",
"chargingSchedulePeriod"
]
}
},
"additionalProperties": false,
"required": [
"status"
]
}

View File

@ -0,0 +1,16 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:GetConfigurationRequest",
"title": "GetConfigurationRequest",
"type": "object",
"properties": {
"key": {
"type": "array",
"items": {
"type": "string",
"maxLength": 50
}
}
},
"additionalProperties": false
}

View File

@ -0,0 +1,40 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:GetConfigurationResponse",
"title": "GetConfigurationResponse",
"type": "object",
"properties": {
"configurationKey": {
"type": "array",
"items": {
"type": "object",
"properties": {
"key": {
"type": "string",
"maxLength": 50
},
"readonly": {
"type": "boolean"
},
"value": {
"type": "string",
"maxLength": 500
}
},
"additionalProperties": false,
"required": [
"key",
"readonly"
]
}
},
"unknownKey": {
"type": "array",
"items": {
"type": "string",
"maxLength": 50
}
}
},
"additionalProperties": false
}

View File

@ -0,0 +1,30 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:GetDiagnosticsRequest",
"title": "GetDiagnosticsRequest",
"type": "object",
"properties": {
"location": {
"type": "string",
"format": "uri"
},
"retries": {
"type": "integer"
},
"retryInterval": {
"type": "integer"
},
"startTime": {
"type": "string",
"format": "date-time"
},
"stopTime": {
"type": "string",
"format": "date-time"
}
},
"additionalProperties": false,
"required": [
"location"
]
}

View File

@ -0,0 +1,13 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:GetDiagnosticsResponse",
"title": "GetDiagnosticsResponse",
"type": "object",
"properties": {
"fileName": {
"type": "string",
"maxLength": 255
}
},
"additionalProperties": false
}

View File

@ -0,0 +1,8 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:GetLocalListVersionRequest",
"title": "GetLocalListVersionRequest",
"type": "object",
"properties": {},
"additionalProperties": false
}

View File

@ -0,0 +1,15 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:GetLocalListVersionResponse",
"title": "GetLocalListVersionResponse",
"type": "object",
"properties": {
"listVersion": {
"type": "integer"
}
},
"additionalProperties": false,
"required": [
"listVersion"
]
}

View File

@ -0,0 +1,8 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:HeartbeatRequest",
"title": "HeartbeatRequest",
"type": "object",
"properties": {},
"additionalProperties": false
}

View File

@ -0,0 +1,16 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:HeartbeatResponse",
"title": "HeartbeatResponse",
"type": "object",
"properties": {
"currentTime": {
"type": "string",
"format": "date-time"
}
},
"additionalProperties": false,
"required": [
"currentTime"
]
}

View File

@ -0,0 +1,151 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:MeterValuesRequest",
"title": "MeterValuesRequest",
"type": "object",
"properties": {
"connectorId": {
"type": "integer"
},
"transactionId": {
"type": "integer"
},
"meterValue": {
"type": "array",
"items": {
"type": "object",
"properties": {
"timestamp": {
"type": "string",
"format": "date-time"
},
"sampledValue": {
"type": "array",
"items": {
"type": "object",
"properties": {
"value": {
"type": "string"
},
"context": {
"type": "string",
"additionalProperties": false,
"enum": [
"Interruption.Begin",
"Interruption.End",
"Sample.Clock",
"Sample.Periodic",
"Transaction.Begin",
"Transaction.End",
"Trigger",
"Other"
]
},
"format": {
"type": "string",
"additionalProperties": false,
"enum": [
"Raw",
"SignedData"
]
},
"measurand": {
"type": "string",
"additionalProperties": false,
"enum": [
"Energy.Active.Export.Register",
"Energy.Active.Import.Register",
"Energy.Reactive.Export.Register",
"Energy.Reactive.Import.Register",
"Energy.Active.Export.Interval",
"Energy.Active.Import.Interval",
"Energy.Reactive.Export.Interval",
"Energy.Reactive.Import.Interval",
"Power.Active.Export",
"Power.Active.Import",
"Power.Offered",
"Power.Reactive.Export",
"Power.Reactive.Import",
"Power.Factor",
"Current.Import",
"Current.Export",
"Current.Offered",
"Voltage",
"Frequency",
"Temperature",
"SoC",
"RPM"
]
},
"phase": {
"type": "string",
"additionalProperties": false,
"enum": [
"L1",
"L2",
"L3",
"N",
"L1-N",
"L2-N",
"L3-N",
"L1-L2",
"L2-L3",
"L3-L1"
]
},
"location": {
"type": "string",
"additionalProperties": false,
"enum": [
"Cable",
"EV",
"Inlet",
"Outlet",
"Body"
]
},
"unit": {
"type": "string",
"additionalProperties": false,
"enum": [
"Wh",
"kWh",
"varh",
"kvarh",
"W",
"kW",
"VA",
"kVA",
"var",
"kvar",
"A",
"V",
"K",
"Celcius",
"Celsius",
"Fahrenheit",
"Percent"
]
}
},
"additionalProperties": false,
"required": [
"value"
]
}
}
},
"additionalProperties": false,
"required": [
"timestamp",
"sampledValue"
]
}
}
},
"additionalProperties": false,
"required": [
"connectorId",
"meterValue"
]
}

View File

@ -0,0 +1,8 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:MeterValuesResponse",
"title": "MeterValuesResponse",
"type": "object",
"properties": {},
"additionalProperties": false
}

View File

@ -0,0 +1,127 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:RemoteStartTransactionRequest",
"title": "RemoteStartTransactionRequest",
"type": "object",
"properties": {
"connectorId": {
"type": "integer"
},
"idTag": {
"type": "string",
"maxLength": 20
},
"chargingProfile": {
"type": "object",
"properties": {
"chargingProfileId": {
"type": "integer"
},
"transactionId": {
"type": "integer"
},
"stackLevel": {
"type": "integer"
},
"chargingProfilePurpose": {
"type": "string",
"additionalProperties": false,
"enum": [
"ChargePointMaxProfile",
"TxDefaultProfile",
"TxProfile"
]
},
"chargingProfileKind": {
"type": "string",
"additionalProperties": false,
"enum": [
"Absolute",
"Recurring",
"Relative"
]
},
"recurrencyKind": {
"type": "string",
"additionalProperties": false,
"enum": [
"Daily",
"Weekly"
]
},
"validFrom": {
"type": "string",
"format": "date-time"
},
"validTo": {
"type": "string",
"format": "date-time"
},
"chargingSchedule": {
"type": "object",
"properties": {
"duration": {
"type": "integer"
},
"startSchedule": {
"type": "string",
"format": "date-time"
},
"chargingRateUnit": {
"type": "string",
"additionalProperties": false,
"enum": [
"A",
"W"
]
},
"chargingSchedulePeriod": {
"type": "array",
"items": {
"type": "object",
"properties": {
"startPeriod": {
"type": "integer"
},
"limit": {
"type": "number",
"multipleOf" : 0.1
},
"numberPhases": {
"type": "integer"
}
},
"additionalProperties": false,
"required": [
"startPeriod",
"limit"
]
}
},
"minChargingRate": {
"type": "number",
"multipleOf" : 0.1
}
},
"additionalProperties": false,
"required": [
"chargingRateUnit",
"chargingSchedulePeriod"
]
}
},
"additionalProperties": false,
"required": [
"chargingProfileId",
"stackLevel",
"chargingProfilePurpose",
"chargingProfileKind",
"chargingSchedule"
]
}
},
"additionalProperties": false,
"required": [
"idTag"
]
}

View File

@ -0,0 +1,20 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:RemoteStartTransactionResponse",
"title": "RemoteStartTransactionResponse",
"type": "object",
"properties": {
"status": {
"type": "string",
"additionalProperties": false,
"enum": [
"Accepted",
"Rejected"
]
}
},
"additionalProperties": false,
"required": [
"status"
]
}

View File

@ -0,0 +1,15 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:RemoteStopTransactionRequest",
"title": "RemoteStopTransactionRequest",
"type": "object",
"properties": {
"transactionId": {
"type": "integer"
}
},
"additionalProperties": false,
"required": [
"transactionId"
]
}

View File

@ -0,0 +1,20 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:RemoteStopTransactionResponse",
"title": "RemoteStopTransactionResponse",
"type": "object",
"properties": {
"status": {
"type": "string",
"additionalProperties": false,
"enum": [
"Accepted",
"Rejected"
]
}
},
"additionalProperties": false,
"required": [
"status"
]
}

View File

@ -0,0 +1,33 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:ReserveNowRequest",
"title": "ReserveNowRequest",
"type": "object",
"properties": {
"connectorId": {
"type": "integer"
},
"expiryDate": {
"type": "string",
"format": "date-time"
},
"idTag": {
"type": "string",
"maxLength": 20
},
"parentIdTag": {
"type": "string",
"maxLength": 20
},
"reservationId": {
"type": "integer"
}
},
"additionalProperties": false,
"required": [
"connectorId",
"expiryDate",
"idTag",
"reservationId"
]
}

View File

@ -0,0 +1,23 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:ReserveNowResponse",
"title": "ReserveNowResponse",
"type": "object",
"properties": {
"status": {
"type": "string",
"additionalProperties": false,
"enum": [
"Accepted",
"Faulted",
"Occupied",
"Rejected",
"Unavailable"
]
}
},
"additionalProperties": false,
"required": [
"status"
]
}

View File

@ -0,0 +1,20 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:ResetRequest",
"title": "ResetRequest",
"type": "object",
"properties": {
"type": {
"type": "string",
"additionalProperties": false,
"enum": [
"Hard",
"Soft"
]
}
},
"additionalProperties": false,
"required": [
"type"
]
}

View File

@ -0,0 +1,20 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:ResetResponse",
"title": "ResetResponse",
"type": "object",
"properties": {
"status": {
"type": "string",
"additionalProperties": false,
"enum": [
"Accepted",
"Rejected"
]
}
},
"additionalProperties": false,
"required": [
"status"
]
}

View File

@ -0,0 +1,68 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:SendLocalListRequest",
"title": "SendLocalListRequest",
"type": "object",
"properties": {
"listVersion": {
"type": "integer"
},
"localAuthorizationList": {
"type": "array",
"items": {
"type": "object",
"properties": {
"idTag": {
"type": "string",
"maxLength": 20
},
"idTagInfo": {
"type": "object",
"properties": {
"expiryDate": {
"type": "string",
"format": "date-time"
},
"parentIdTag": {
"type": "string",
"maxLength": 20
},
"status": {
"type": "string",
"additionalProperties": false,
"enum": [
"Accepted",
"Blocked",
"Expired",
"Invalid",
"ConcurrentTx"
]
}
},
"additionalProperties": false,
"required": [
"status"
]
}
},
"additionalProperties": false,
"required": [
"idTag"
]
}
},
"updateType": {
"type": "string",
"additionalProperties": false,
"enum": [
"Differential",
"Full"
]
}
},
"additionalProperties": false,
"required": [
"listVersion",
"updateType"
]
}

View File

@ -0,0 +1,22 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:SendLocalListResponse",
"title": "SendLocalListResponse",
"type": "object",
"properties": {
"status": {
"type": "string",
"additionalProperties": false,
"enum": [
"Accepted",
"Failed",
"NotSupported",
"VersionMismatch"
]
}
},
"additionalProperties": false,
"required": [
"status"
]
}

View File

@ -0,0 +1,124 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:SetChargingProfileRequest",
"title": "SetChargingProfileRequest",
"type": "object",
"properties": {
"connectorId": {
"type": "integer"
},
"csChargingProfiles": {
"type": "object",
"properties": {
"chargingProfileId": {
"type": "integer"
},
"transactionId": {
"type": "integer"
},
"stackLevel": {
"type": "integer"
},
"chargingProfilePurpose": {
"type": "string",
"additionalProperties": false,
"enum": [
"ChargePointMaxProfile",
"TxDefaultProfile",
"TxProfile"
]
},
"chargingProfileKind": {
"type": "string",
"additionalProperties": false,
"enum": [
"Absolute",
"Recurring",
"Relative"
]
},
"recurrencyKind": {
"type": "string",
"additionalProperties": false,
"enum": [
"Daily",
"Weekly"
]
},
"validFrom": {
"type": "string",
"format": "date-time"
},
"validTo": {
"type": "string",
"format": "date-time"
},
"chargingSchedule": {
"type": "object",
"properties": {
"duration": {
"type": "integer"
},
"startSchedule": {
"type": "string",
"format": "date-time"
},
"chargingRateUnit": {
"type": "string",
"additionalProperties": false,
"enum": [
"A",
"W"
]
},
"chargingSchedulePeriod": {
"type": "array",
"items": {
"type": "object",
"properties": {
"startPeriod": {
"type": "integer"
},
"limit": {
"type": "number",
"multipleOf" : 0.1
},
"numberPhases": {
"type": "integer"
}
},
"additionalProperties": false,
"required": [
"startPeriod",
"limit"
]
}
},
"minChargingRate": {
"type": "number",
"multipleOf" : 0.1
}
},
"additionalProperties": false,
"required": [
"chargingRateUnit",
"chargingSchedulePeriod"
]
}
},
"additionalProperties": false,
"required": [
"chargingProfileId",
"stackLevel",
"chargingProfilePurpose",
"chargingProfileKind",
"chargingSchedule"
]
}
},
"additionalProperties": false,
"required": [
"connectorId",
"csChargingProfiles"
]
}

View File

@ -0,0 +1,21 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:SetChargingProfileResponse",
"title": "SetChargingProfileResponse",
"type": "object",
"properties": {
"status": {
"type": "string",
"additionalProperties": false,
"enum": [
"Accepted",
"Rejected",
"NotSupported"
]
}
},
"additionalProperties": false,
"required": [
"status"
]
}

View File

@ -0,0 +1,32 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:StartTransactionRequest",
"title": "StartTransactionRequest",
"type": "object",
"properties": {
"connectorId": {
"type": "integer"
},
"idTag": {
"type": "string",
"maxLength": 20
},
"meterStart": {
"type": "integer"
},
"reservationId": {
"type": "integer"
},
"timestamp": {
"type": "string",
"format": "date-time"
}
},
"additionalProperties": false,
"required": [
"connectorId",
"idTag",
"meterStart",
"timestamp"
]
}

View File

@ -0,0 +1,44 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:StartTransactionResponse",
"title": "StartTransactionResponse",
"type": "object",
"properties": {
"idTagInfo": {
"type": "object",
"properties": {
"expiryDate": {
"type": "string",
"format": "date-time"
},
"parentIdTag": {
"type": "string",
"maxLength": 20
},
"status": {
"type": "string",
"additionalProperties": false,
"enum": [
"Accepted",
"Blocked",
"Expired",
"Invalid",
"ConcurrentTx"
]
}
},
"additionalProperties": false,
"required": [
"status"
]
},
"transactionId": {
"type": "integer"
}
},
"additionalProperties": false,
"required": [
"idTagInfo",
"transactionId"
]
}

View File

@ -0,0 +1,70 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:StatusNotificationRequest",
"title": "StatusNotificationRequest",
"type": "object",
"properties": {
"connectorId": {
"type": "integer"
},
"errorCode": {
"type": "string",
"additionalProperties": false,
"enum": [
"ConnectorLockFailure",
"EVCommunicationError",
"GroundFailure",
"HighTemperature",
"InternalError",
"LocalListConflict",
"NoError",
"OtherError",
"OverCurrentFailure",
"PowerMeterFailure",
"PowerSwitchFailure",
"ReaderFailure",
"ResetFailure",
"UnderVoltage",
"OverVoltage",
"WeakSignal"
]
},
"info": {
"type": "string",
"maxLength": 50
},
"status": {
"type": "string",
"additionalProperties": false,
"enum": [
"Available",
"Preparing",
"Charging",
"SuspendedEVSE",
"SuspendedEV",
"Finishing",
"Reserved",
"Unavailable",
"Faulted"
]
},
"timestamp": {
"type": "string",
"format": "date-time"
},
"vendorId": {
"type": "string",
"maxLength": 255
},
"vendorErrorCode": {
"type": "string",
"maxLength": 50
}
},
"additionalProperties": false,
"required": [
"connectorId",
"errorCode",
"status"
]
}

View File

@ -0,0 +1,8 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:StatusNotificationResponse",
"title": "StatusNotificationResponse",
"type": "object",
"properties": {},
"additionalProperties": false
}

View File

@ -0,0 +1,176 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:StopTransactionRequest",
"title": "StopTransactionRequest",
"type": "object",
"properties": {
"idTag": {
"type": "string",
"maxLength": 20
},
"meterStop": {
"type": "integer"
},
"timestamp": {
"type": "string",
"format": "date-time"
},
"transactionId": {
"type": "integer"
},
"reason": {
"type": "string",
"additionalProperties": false,
"enum": [
"EmergencyStop",
"EVDisconnected",
"HardReset",
"Local",
"Other",
"PowerLoss",
"Reboot",
"Remote",
"SoftReset",
"UnlockCommand",
"DeAuthorized"
]
},
"transactionData": {
"type": "array",
"items": {
"type": "object",
"properties": {
"timestamp": {
"type": "string",
"format": "date-time"
},
"sampledValue": {
"type": "array",
"items": {
"type": "object",
"properties": {
"value": {
"type": "string"
},
"context": {
"type": "string",
"additionalProperties": false,
"enum": [
"Interruption.Begin",
"Interruption.End",
"Sample.Clock",
"Sample.Periodic",
"Transaction.Begin",
"Transaction.End",
"Trigger",
"Other"
]
},
"format": {
"type": "string",
"additionalProperties": false,
"enum": [
"Raw",
"SignedData"
]
},
"measurand": {
"type": "string",
"additionalProperties": false,
"enum": [
"Energy.Active.Export.Register",
"Energy.Active.Import.Register",
"Energy.Reactive.Export.Register",
"Energy.Reactive.Import.Register",
"Energy.Active.Export.Interval",
"Energy.Active.Import.Interval",
"Energy.Reactive.Export.Interval",
"Energy.Reactive.Import.Interval",
"Power.Active.Export",
"Power.Active.Import",
"Power.Offered",
"Power.Reactive.Export",
"Power.Reactive.Import",
"Power.Factor",
"Current.Import",
"Current.Export",
"Current.Offered",
"Voltage",
"Frequency",
"Temperature",
"SoC",
"RPM"
]
},
"phase": {
"type": "string",
"additionalProperties": false,
"enum": [
"L1",
"L2",
"L3",
"N",
"L1-N",
"L2-N",
"L3-N",
"L1-L2",
"L2-L3",
"L3-L1"
]
},
"location": {
"type": "string",
"additionalProperties": false,
"enum": [
"Cable",
"EV",
"Inlet",
"Outlet",
"Body"
]
},
"unit": {
"type": "string",
"additionalProperties": false,
"enum": [
"Wh",
"kWh",
"varh",
"kvarh",
"W",
"kW",
"VA",
"kVA",
"var",
"kvar",
"A",
"V",
"K",
"Celcius",
"Fahrenheit",
"Percent"
]
}
},
"additionalProperties": false,
"required": [
"value"
]
}
}
},
"additionalProperties": false,
"required": [
"timestamp",
"sampledValue"
]
}
}
},
"additionalProperties": false,
"required": [
"transactionId",
"timestamp",
"meterStop"
]
}

View File

@ -0,0 +1,37 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:StopTransactionResponse",
"title": "StopTransactionResponse",
"type": "object",
"properties": {
"idTagInfo": {
"type": "object",
"properties": {
"expiryDate": {
"type": "string",
"format": "date-time"
},
"parentIdTag": {
"type": "string",
"maxLength": 20
},
"status": {
"type": "string",
"additionalProperties": false,
"enum": [
"Accepted",
"Blocked",
"Expired",
"Invalid",
"ConcurrentTx"
]
}
},
"additionalProperties": false,
"required": [
"status"
]
}
},
"additionalProperties": false
}

View File

@ -0,0 +1,27 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:TriggerMessageRequest",
"title": "TriggerMessageRequest",
"type": "object",
"properties": {
"requestedMessage": {
"type": "string",
"additionalProperties": false,
"enum": [
"BootNotification",
"DiagnosticsStatusNotification",
"FirmwareStatusNotification",
"Heartbeat",
"MeterValues",
"StatusNotification"
]
},
"connectorId": {
"type": "integer"
}
},
"additionalProperties": false,
"required": [
"requestedMessage"
]
}

View File

@ -0,0 +1,21 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:TriggerMessageResponse",
"title": "TriggerMessageResponse",
"type": "object",
"properties": {
"status": {
"type": "string",
"additionalProperties": false,
"enum": [
"Accepted",
"Rejected",
"NotImplemented"
]
}
},
"additionalProperties": false,
"required": [
"status"
]
}

View File

@ -0,0 +1,15 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:UnlockConnectorRequest",
"title": "UnlockConnectorRequest",
"type": "object",
"properties": {
"connectorId": {
"type": "integer"
}
},
"additionalProperties": false,
"required": [
"connectorId"
]
}

View File

@ -0,0 +1,21 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:UnlockConnectorResponse",
"title": "UnlockConnectorResponse",
"type": "object",
"properties": {
"status": {
"type": "string",
"additionalProperties": false,
"enum": [
"Unlocked",
"UnlockFailed",
"NotSupported"
]
}
},
"additionalProperties": false,
"required": [
"status"
]
}

View File

@ -0,0 +1,27 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:UpdateFirmwareRequest",
"title": "UpdateFirmwareRequest",
"type": "object",
"properties": {
"location": {
"type": "string",
"format": "uri"
},
"retries": {
"type": "integer"
},
"retrieveDate": {
"type": "string",
"format": "date-time"
},
"retryInterval": {
"type": "integer"
}
},
"additionalProperties": false,
"required": [
"location",
"retrieveDate"
]
}

View File

@ -0,0 +1,8 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "urn:OCPP:1.6:2019:12:UpdateFirmwareResponse",
"title": "UpdateFirmwareResponse",
"type": "object",
"properties": {},
"additionalProperties": false
}

View File

@ -0,0 +1,3 @@
{deps, [
{jesse, "1.7.0"}
]}.

View File

@ -0,0 +1,9 @@
{application, emqx_gateway_ocpp, [
{description, "OCPP-J 1.6 Gateway for EMQX"},
{vsn, "5.0.0"},
{registered, []},
{applications, [kernel, stdlib, jesse, emqx, emqx_gateway]},
{env, []},
{modules, []},
{links, []}
]}.

View File

@ -0,0 +1,19 @@
%% -*- mode: erlang -*-
{VSN,
[{"4.4.1",[
{load_module,emqx_ocpp_connection,brutal_purge,soft_purge,[]}
]},
{"4.4.0",[
{load_module,emqx_ocpp_connection,brutal_purge,soft_purge,[]}
]},
{<<".*">>, []}
],
[{"4.4.1",[
{load_module,emqx_ocpp_connection,brutal_purge,soft_purge,[]}
]},
{"4.4.0",[
{load_module,emqx_ocpp_connection,brutal_purge,soft_purge,[]}
]},
{<<".*">>, []}
]
}.

View File

@ -0,0 +1,101 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
%% @doc The OCPP Gateway implement
-module(emqx_gateway_ocpp).
-include_lib("emqx/include/logger.hrl").
-include_lib("emqx_gateway/include/emqx_gateway.hrl").
%% define a gateway named ocpp
-gateway(#{
name => ocpp,
callback_module => ?MODULE,
config_schema_module => emqx_ocpp_schema,
edition => ee
}).
%% callback_module must implement the emqx_gateway_impl behaviour
-behaviour(emqx_gateway_impl).
%% callback for emqx_gateway_impl
-export([
on_gateway_load/2,
on_gateway_update/3,
on_gateway_unload/2
]).
-import(
emqx_gateway_utils,
[
normalize_config/1,
start_listeners/4,
stop_listeners/2
]
).
%%--------------------------------------------------------------------
%% emqx_gateway_impl callbacks
%%--------------------------------------------------------------------
on_gateway_load(
_Gateway = #{
name := GwName,
config := Config
},
Ctx
) ->
%% ensure json schema validator is loaded
emqx_ocpp_schemas:load(),
Listeners = normalize_config(Config),
ModCfg = #{
frame_mod => emqx_ocpp_frame,
chann_mod => emqx_ocpp_channel
},
case
start_listeners(
Listeners, GwName, Ctx, ModCfg
)
of
{ok, ListenerPids} ->
%% FIXME: How to throw an exception to interrupt the restart logic ?
%% FIXME: Assign ctx to GwState
{ok, ListenerPids, _GwState = #{ctx => Ctx}};
{error, {Reason, Listener}} ->
throw(
{badconf, #{
key => listeners,
value => Listener,
reason => Reason
}}
)
end.
on_gateway_update(Config, Gateway, GwState = #{ctx := Ctx}) ->
GwName = maps:get(name, Gateway),
try
%% XXX: 1. How hot-upgrade the changes ???
%% XXX: 2. Check the New confs first before destroy old state???
on_gateway_unload(Gateway, GwState),
on_gateway_load(Gateway#{config => Config}, Ctx)
catch
Class:Reason:Stk ->
logger:error(
"Failed to update ~ts; "
"reason: {~0p, ~0p} stacktrace: ~0p",
[GwName, Class, Reason, Stk]
),
{error, Reason}
end.
on_gateway_unload(
_Gateway = #{
name := GwName,
config := Config
},
_GwState
) ->
Listeners = normalize_config(Config),
stop_listeners(GwName, Listeners).

View File

@ -0,0 +1,874 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_ocpp_channel).
-behaviour(emqx_gateway_channel).
-include("emqx_ocpp.hrl").
-include_lib("emqx/include/emqx.hrl").
-include_lib("emqx/include/emqx_mqtt.hrl").
-include_lib("emqx/include/types.hrl").
-include_lib("emqx/include/logger.hrl").
-logger_header("[OCPP-Chann]").
-ifdef(TEST).
-compile(export_all).
-compile(nowarn_export_all).
-endif.
-export([
info/1,
info/2,
stats/1
]).
-export([
init/2,
authenticate/2,
handle_in/2,
handle_deliver/2,
handle_out/3,
handle_timeout/3,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2
]).
%% Exports for CT
-export([set_field/3]).
-export_type([channel/0]).
-record(channel, {
%% Context
ctx :: emqx_gateway_ctx:context(),
%% ConnInfo
conninfo :: emqx_types:conninfo(),
%% ClientInfo
clientinfo :: emqx_types:clientinfo(),
%% Session
session :: maybe(emqx_session:session()),
%% ClientInfo override specs
clientinfo_override :: map(),
%% Keepalive
keepalive :: maybe(emqx_ocpp_keepalive:keepalive()),
%% Stores all unsent messages.
mqueue :: queue:queue(),
%% Timers
timers :: #{atom() => disabled | maybe(reference())},
%% Conn State
conn_state :: conn_state()
}).
-type channel() :: #channel{}.
-type conn_state() :: idle | connecting | connected | disconnected.
-type reply() ::
{outgoing, emqx_ocpp_frame:frame()}
| {outgoing, [emqx_ocpp_frame:frame()]}
| {event, conn_state() | updated}
| {close, Reason :: atom()}.
-type replies() :: emqx_ocpp_frame:frame() | reply() | [reply()].
-define(TIMER_TABLE, #{
alive_timer => keepalive
}).
-define(INFO_KEYS, [
conninfo,
conn_state,
clientinfo,
session
]).
-define(CHANNEL_METRICS, [
recv_pkt,
recv_msg,
'recv_msg.qos0',
'recv_msg.qos1',
'recv_msg.qos2',
'recv_msg.dropped',
'recv_msg.dropped.await_pubrel_timeout',
send_pkt,
send_msg,
'send_msg.qos0',
'send_msg.qos1',
'send_msg.qos2',
'send_msg.dropped',
'send_msg.dropped.expired',
'send_msg.dropped.queue_full',
'send_msg.dropped.too_large'
]).
-define(DEFAULT_OVERRIDE,
%% Generate clientid by default
#{
clientid => <<"">>,
username => <<"">>,
password => <<"">>
}
).
-dialyzer(no_match).
%%--------------------------------------------------------------------
%% Info, Attrs and Caps
%%--------------------------------------------------------------------
%% @doc Get infos of the channel.
-spec info(channel()) -> emqx_types:infos().
info(Channel) ->
maps:from_list(info(?INFO_KEYS, Channel)).
-spec info(list(atom()) | atom(), channel()) -> term().
info(Keys, Channel) when is_list(Keys) ->
[{Key, info(Key, Channel)} || Key <- Keys];
info(conninfo, #channel{conninfo = ConnInfo}) ->
ConnInfo;
info(socktype, #channel{conninfo = ConnInfo}) ->
maps:get(socktype, ConnInfo, undefined);
info(peername, #channel{conninfo = ConnInfo}) ->
maps:get(peername, ConnInfo, undefined);
info(sockname, #channel{conninfo = ConnInfo}) ->
maps:get(sockname, ConnInfo, undefined);
info(proto_name, #channel{conninfo = ConnInfo}) ->
maps:get(proto_name, ConnInfo, undefined);
info(proto_ver, #channel{conninfo = ConnInfo}) ->
maps:get(proto_ver, ConnInfo, undefined);
info(connected_at, #channel{conninfo = ConnInfo}) ->
maps:get(connected_at, ConnInfo, undefined);
info(clientinfo, #channel{clientinfo = ClientInfo}) ->
ClientInfo;
info(zone, #channel{clientinfo = ClientInfo}) ->
maps:get(zone, ClientInfo, undefined);
info(clientid, #channel{clientinfo = ClientInfo}) ->
maps:get(clientid, ClientInfo, undefined);
info(username, #channel{clientinfo = ClientInfo}) ->
maps:get(username, ClientInfo, undefined);
info(session, #channel{session = Session}) ->
emqx_utils:maybe_apply(fun emqx_session:info/1, Session);
info(conn_state, #channel{conn_state = ConnState}) ->
ConnState;
info(keepalive, #channel{keepalive = Keepalive}) ->
emqx_utils:maybe_apply(fun emqx_ocpp_keepalive:info/1, Keepalive);
info(timers, #channel{timers = Timers}) ->
Timers.
-spec stats(channel()) -> emqx_types:stats().
stats(#channel{session = Session}) ->
lists:append(emqx_session:stats(Session), emqx_pd:get_counters(?CHANNEL_METRICS)).
%%--------------------------------------------------------------------
%% Init the channel
%%--------------------------------------------------------------------
-spec init(emqx_types:conninfo(), proplists:proplist()) -> channel().
init(
ConnInfo = #{
peername := {PeerHost, _Port},
sockname := {_Host, SockPort}
},
Options
) ->
Peercert = maps:get(peercert, ConnInfo, undefined),
Mountpoint = maps:get(mountpoint, Options, undefined),
ListenerId =
case maps:get(listener, Options, undefined) of
undefined -> undefined;
{GwName, Type, LisName} -> emqx_gateway_utils:listener_id(GwName, Type, LisName)
end,
EnableAuthn = maps:get(enable_authn, Options, true),
ClientInfo = setting_peercert_infos(
Peercert,
#{
zone => default,
listener => ListenerId,
protocol => ocpp,
peerhost => PeerHost,
sockport => SockPort,
clientid => undefined,
username => undefined,
is_bridge => false,
is_superuser => false,
enalbe_authn => EnableAuthn,
mountpoint => Mountpoint
}
),
ConnInfo1 = ConnInfo#{
keepalive => emqx_ocpp_conf:default_heartbeat_interval()
},
{NClientInfo, NConnInfo} = take_ws_cookie(ClientInfo, ConnInfo1),
Ctx = maps:get(ctx, Options),
Override = maps:merge(
?DEFAULT_OVERRIDE,
maps:get(clientinfo_override, Options, #{})
),
#channel{
ctx = Ctx,
conninfo = NConnInfo,
clientinfo = NClientInfo,
clientinfo_override = Override,
mqueue = queue:new(),
timers = #{},
conn_state = idle
}.
setting_peercert_infos(NoSSL, ClientInfo) when
NoSSL =:= nossl;
NoSSL =:= undefined
->
ClientInfo;
setting_peercert_infos(Peercert, ClientInfo) ->
{DN, CN} = {esockd_peercert:subject(Peercert), esockd_peercert:common_name(Peercert)},
ClientInfo#{dn => DN, cn => CN}.
take_ws_cookie(ClientInfo, ConnInfo) ->
case maps:take(ws_cookie, ConnInfo) of
{WsCookie, NConnInfo} ->
{ClientInfo#{ws_cookie => WsCookie}, NConnInfo};
_ ->
{ClientInfo, ConnInfo}
end.
authenticate(UserInfo, Channel) ->
case
emqx_utils:pipeline(
[
fun enrich_client/2,
fun run_conn_hooks/2,
fun check_banned/2,
fun auth_connect/2
],
UserInfo,
Channel#channel{conn_state = connecting}
)
of
{ok, _, NChannel} ->
{ok, NChannel};
{error, Reason, _NChannel} ->
{error, Reason}
end.
enrich_client(
#{
clientid := ClientId,
username := Username,
proto_name := ProtoName,
proto_ver := ProtoVer
},
Channel = #channel{
conninfo = ConnInfo,
clientinfo = ClientInfo
}
) ->
NConnInfo = ConnInfo#{
clientid => ClientId,
username => Username,
proto_name => ProtoName,
proto_ver => ProtoVer,
clean_start => true,
conn_props => #{},
expiry_interval => 0,
receive_maximum => 1
},
NClientInfo = fix_mountpoint(
ClientInfo#{
clientid => ClientId,
username => Username
}
),
{ok, Channel#channel{conninfo = NConnInfo, clientinfo = NClientInfo}}.
fix_mountpoint(ClientInfo = #{mountpoint := undefined}) ->
ClientInfo;
fix_mountpoint(ClientInfo = #{mountpoint := MountPoint}) ->
MountPoint1 = emqx_mountpoint:replvar(MountPoint, ClientInfo),
ClientInfo#{mountpoint := MountPoint1}.
set_log_meta(#channel{
clientinfo = #{clientid := ClientId},
conninfo = #{peername := Peername}
}) ->
emqx_logger:set_metadata_peername(esockd:format(Peername)),
emqx_logger:set_metadata_clientid(ClientId).
run_conn_hooks(_UserInfo, Channel = #channel{conninfo = ConnInfo}) ->
case run_hooks('client.connect', [ConnInfo], #{}) of
Error = {error, _Reason} -> Error;
_NConnProps -> {ok, Channel}
end.
check_banned(_UserInfo, #channel{clientinfo = ClientInfo}) ->
case emqx_banned:check(ClientInfo) of
true -> {error, banned};
false -> ok
end.
auth_connect(
#{password := Password},
#channel{clientinfo = ClientInfo} = Channel
) ->
#{
clientid := ClientId,
username := Username
} = ClientInfo,
case emqx_access_control:authenticate(ClientInfo#{password => Password}) of
{ok, AuthResult} ->
NClientInfo = maps:merge(ClientInfo, AuthResult),
{ok, Channel#channel{clientinfo = NClientInfo}};
{error, Reason} ->
?SLOG(warning, #{
msg => "client_login_failed",
clientid => ClientId,
username => Username,
reason => Reason
}),
{error, Reason}
end.
publish(
Frame,
Channel = #channel{
clientinfo =
#{
clientid := ClientId,
username := Username,
protocol := Protocol,
peerhost := PeerHost
},
conninfo = #{proto_ver := ProtoVer}
}
) when
is_map(Frame)
->
Topic = upstream_topic(Frame, Channel),
Payload = frame2payload(Frame),
emqx_broker:publish(
emqx_message:make(
ClientId,
?QOS_2,
Topic,
Payload,
#{},
#{
protocol => Protocol,
proto_ver => ProtoVer,
username => Username,
peerhost => PeerHost
}
)
).
upstream_topic(
Frame = #{id := Id, type := Type},
#channel{clientinfo = #{clientid := ClientId}}
) ->
Vars = #{id => Id, type => Type, clientid => ClientId, cid => ClientId},
case Type of
?OCPP_MSG_TYPE_ID_CALL ->
Action = maps:get(action, Frame),
emqx_placeholder:proc_tmpl(
emqx_ocpp_conf:uptopic(Action),
Vars#{action => Action}
);
?OCPP_MSG_TYPE_ID_CALLRESULT ->
emqx_placeholder:proc_tmpl(emqx_ocpp_conf:up_reply_topic(), Vars);
?OCPP_MSG_TYPE_ID_CALLERROR ->
emqx_placeholder:proc_tmpl(emqx_ocpp_conf:up_error_topic(), Vars)
end.
%%--------------------------------------------------------------------
%% Handle incoming packet
%%--------------------------------------------------------------------
-spec handle_in(emqx_ocpp_frame:frame(), channel()) ->
{ok, channel()}
| {ok, replies(), channel()}
| {shutdown, Reason :: term(), channel()}
| {shutdown, Reason :: term(), replies(), channel()}.
handle_in(?IS_REQ(Frame), Channel) ->
%% TODO: strit mode
_ = publish(Frame, Channel),
{ok, Channel};
handle_in(Frame = #{type := Type}, Channel) when
Type == ?OCPP_MSG_TYPE_ID_CALLRESULT;
Type == ?OCPP_MSG_TYPE_ID_CALLERROR
->
_ = publish(Frame, Channel),
try_deliver(Channel);
handle_in({frame_error, {badjson, ReasonStr}}, Channel) ->
shutdown({frame_error, {badjson, iolist_to_binary(ReasonStr)}}, Channel);
handle_in({frame_error, {validation_faliure, Id, ReasonStr}}, Channel) ->
handle_out(
dnstream,
?ERR_FRAME(Id, ?OCPP_ERR_FormationViolation, iolist_to_binary(ReasonStr)),
Channel
);
handle_in(Frame, Channel) ->
?SLOG(error, #{msg => "unexpected_incoming", frame => Frame}),
{ok, Channel}.
%%--------------------------------------------------------------------
%% Process Disconnect
%%--------------------------------------------------------------------
%%--------------------------------------------------------------------
%% Handle Delivers from broker to client
%%--------------------------------------------------------------------
-spec handle_deliver(list(emqx_types:deliver()), channel()) ->
{ok, channel()} | {ok, replies(), channel()}.
handle_deliver(Delivers, Channel) ->
NChannel =
lists:foldl(
fun({deliver, _, Msg}, Acc) ->
enqueue(Msg, Acc)
end,
Channel,
Delivers
),
try_deliver(NChannel).
enqueue(Msg, Channel = #channel{mqueue = MQueue}) ->
case queue:len(MQueue) > emqx_ocpp_conf:max_mqueue_len() of
false ->
try payload2frame(Msg#message.payload) of
Frame ->
Channel#channel{mqueue = queue:in(Frame, MQueue)}
catch
_:_ ->
?SLOG(error, #{msg => "drop_invalid_message", message => Msg}),
Channel
end;
true ->
?SLOG(error, #{msg => "drop_message", message => Msg, reason => message_queue_full}),
Channel
end.
try_deliver(Channel = #channel{mqueue = MQueue}) ->
case queue:is_empty(MQueue) of
false ->
%% TODO: strit_mode
Frames = queue:to_list(MQueue),
handle_out(dnstream, Frames, Channel#channel{mqueue = queue:new()});
true ->
{ok, Channel}
end.
%%--------------------------------------------------------------------
%% Handle outgoing packet
%%--------------------------------------------------------------------
-spec handle_out(atom(), term(), channel()) ->
{ok, channel()}
| {ok, replies(), channel()}
| {shutdown, Reason :: term(), channel()}
| {shutdown, Reason :: term(), replies(), channel()}.
handle_out(dnstream, Frames, Channel) ->
{Outgoings, NChannel} = apply_frame(Frames, Channel),
{ok, [{outgoing, Frames} | Outgoings], NChannel};
handle_out(disconnect, keepalive_timeout, Channel) ->
{shutdown, keepalive_timeout, Channel};
handle_out(Type, Data, Channel) ->
?SLOG(error, #{msg => "unexpected_outgoing", type => Type, data => Data}),
{ok, Channel}.
%%--------------------------------------------------------------------
%% Apply Response frame to channel state machine
%%--------------------------------------------------------------------
apply_frame(Frames, Channel) when is_list(Frames) ->
{Outgoings, NChannel} = lists:foldl(fun apply_frame/2, {[], Channel}, Frames),
{lists:reverse(Outgoings), NChannel};
apply_frame(?IS_BootNotification_RESP(Payload), {Outgoings, Channel}) ->
case maps:get(<<"status">>, Payload) of
<<"Accepted">> ->
Intv = maps:get(<<"interval">>, Payload),
?SLOG(info, #{msg => "adjust_heartbeat_timer", new_interval_s => Intv}),
{[{event, updated} | Outgoings], reset_keepalive(Intv, Channel)};
_ ->
{Outgoings, Channel}
end;
apply_frame(_, Channel) ->
Channel.
%%--------------------------------------------------------------------
%% Handle call
%%--------------------------------------------------------------------
-spec handle_call(Req :: any(), From :: emqx_gateway_channel:gen_server_from(), channel()) ->
{reply, Reply :: any(), channel()}
| {shutdown, Reason :: any(), Reply :: any(), channel()}.
handle_call(kick, _From, Channel) ->
shutdown(kicked, ok, Channel);
handle_call(discard, _From, Channel) ->
shutdown(discarded, ok, Channel);
handle_call(Req, From, Channel) ->
?SLOG(error, #{msg => "unexpected_call", req => Req, from => From}),
reply(ignored, Channel).
%%--------------------------------------------------------------------
%% Handle Cast
%%--------------------------------------------------------------------
-spec handle_cast(Req :: any(), channel()) ->
ok
| {ok, channel()}
| {shutdown, Reason :: term(), channel()}.
handle_cast(Req, Channel) ->
?SLOG(error, #{msg => "unexpected_cast", req => Req}),
{noreply, Channel}.
%%--------------------------------------------------------------------
%% Handle Info
%%--------------------------------------------------------------------
-spec handle_info(Info :: term(), channel()) ->
ok | {ok, channel()} | {shutdown, Reason :: term(), channel()}.
handle_info(after_init, Channel0) ->
set_log_meta(Channel0),
case process_connect(Channel0) of
{ok, Channel} ->
NChannel = ensure_keepalive(
ensure_connected(
ensure_subscribe_dn_topics(Channel)
)
),
{ok, [{event, connected}], NChannel};
{error, Reason} ->
shutdown(Reason, Channel0)
end;
handle_info({sock_closed, Reason}, Channel) ->
NChannel = ensure_disconnected({sock_closed, Reason}, Channel),
shutdown(Reason, NChannel);
handle_info(Info, Channel) ->
?SLOG(error, #{msg => "unexpected_info", info => Info}),
{ok, Channel}.
process_connect(
Channel = #channel{
ctx = Ctx,
conninfo = ConnInfo,
clientinfo = ClientInfo
}
) ->
SessFun = fun(_, _) -> #{} end,
case
emqx_gateway_ctx:open_session(
Ctx,
true,
ClientInfo,
ConnInfo,
SessFun
)
of
{ok, #{session := Session}} ->
NChannel = Channel#channel{session = Session},
{ok, NChannel};
{error, Reason} ->
?SLOG(error, #{msg => "failed_to_open_session", reason => Reason}),
{error, Reason}
end.
ensure_subscribe_dn_topics(
Channel = #channel{
clientinfo = #{clientid := ClientId} = ClientInfo,
session = Session
}
) ->
TopicTokens = emqx_ocpp_conf:dntopic(),
SubOpts = #{rh => 0, rap => 0, nl => 0, qos => ?QOS_1},
Topic = emqx_placeholder:proc_tmpl(
TopicTokens,
#{
clientid => ClientId,
cid => ClientId
}
),
{ok, NSession} = emqx_session:subscribe(
ClientInfo,
Topic,
SubOpts,
Session
),
Channel#channel{session = NSession}.
%%--------------------------------------------------------------------
%% Handle timeout
%%--------------------------------------------------------------------
-spec handle_timeout(reference(), Msg :: term(), channel()) ->
{ok, channel()}
| {ok, replies(), channel()}
| {shutdown, Reason :: term(), channel()}.
handle_timeout(
_TRef,
{keepalive, _StatVal},
Channel = #channel{keepalive = undefined}
) ->
{ok, Channel};
handle_timeout(
_TRef,
{keepalive, _StatVal},
Channel = #channel{conn_state = disconnected}
) ->
{ok, Channel};
handle_timeout(
_TRef,
{keepalive, StatVal},
Channel = #channel{keepalive = Keepalive}
) ->
case emqx_ocpp_keepalive:check(StatVal, Keepalive) of
{ok, NKeepalive} ->
NChannel = Channel#channel{keepalive = NKeepalive},
{ok, reset_timer(alive_timer, NChannel)};
{error, timeout} ->
handle_out(disconnect, keepalive_timeout, Channel)
end;
handle_timeout(_TRef, Msg, Channel) ->
?SLOG(error, #{msg => "unexpected_timeout", timeout_msg => Msg}),
{ok, Channel}.
%%--------------------------------------------------------------------
%% Ensure timers
%%--------------------------------------------------------------------
ensure_timer([Name], Channel) ->
ensure_timer(Name, Channel);
ensure_timer([Name | Rest], Channel) ->
ensure_timer(Rest, ensure_timer(Name, Channel));
ensure_timer(Name, Channel = #channel{timers = Timers}) ->
TRef = maps:get(Name, Timers, undefined),
Time = interval(Name, Channel),
case TRef == undefined andalso Time > 0 of
true -> ensure_timer(Name, Time, Channel);
%% Timer disabled or exists
false -> Channel
end.
ensure_timer(Name, Time, Channel = #channel{timers = Timers}) ->
Msg = maps:get(Name, ?TIMER_TABLE),
TRef = emqx_utils:start_timer(Time, Msg),
Channel#channel{timers = Timers#{Name => TRef}}.
reset_timer(Name, Channel) ->
ensure_timer(Name, clean_timer(Name, Channel)).
clean_timer(Name, Channel = #channel{timers = Timers}) ->
Channel#channel{timers = maps:remove(Name, Timers)}.
interval(alive_timer, #channel{keepalive = KeepAlive}) ->
emqx_ocpp_keepalive:info(interval, KeepAlive).
%%--------------------------------------------------------------------
%% Terminate
%%--------------------------------------------------------------------
-spec terminate(any(), channel()) -> ok.
terminate(_, #channel{conn_state = idle}) ->
ok;
terminate(normal, Channel) ->
run_terminate_hook(normal, Channel);
terminate({shutdown, Reason}, Channel) when
Reason =:= kicked; Reason =:= discarded
->
run_terminate_hook(Reason, Channel);
terminate(Reason, Channel) ->
run_terminate_hook(Reason, Channel).
run_terminate_hook(_Reason, #channel{session = undefined}) ->
ok;
run_terminate_hook(Reason, #channel{clientinfo = ClientInfo, session = Session}) ->
emqx_session:terminate(ClientInfo, Reason, Session).
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
%%--------------------------------------------------------------------
%% Frame
frame2payload(Frame = #{type := ?OCPP_MSG_TYPE_ID_CALL}) ->
emqx_utils_json:encode(
#{
<<"MessageTypeId">> => ?OCPP_MSG_TYPE_ID_CALL,
<<"UniqueId">> => maps:get(id, Frame),
<<"Action">> => maps:get(action, Frame),
<<"Payload">> => maps:get(payload, Frame)
}
);
frame2payload(Frame = #{type := ?OCPP_MSG_TYPE_ID_CALLRESULT}) ->
emqx_utils_json:encode(
#{
<<"MessageTypeId">> => ?OCPP_MSG_TYPE_ID_CALLRESULT,
<<"UniqueId">> => maps:get(id, Frame),
<<"Payload">> => maps:get(payload, Frame)
}
);
frame2payload(Frame = #{type := ?OCPP_MSG_TYPE_ID_CALLERROR}) ->
emqx_utils_json:encode(
#{
<<"MessageTypeId">> => maps:get(type, Frame),
<<"UniqueId">> => maps:get(id, Frame),
<<"ErrorCode">> => maps:get(error_code, Frame),
<<"ErrorDescription">> => maps:get(error_desc, Frame)
}
).
payload2frame(Payload) when is_binary(Payload) ->
payload2frame(emqx_utils_json:decode(Payload, [return_maps]));
payload2frame(#{
<<"MessageTypeId">> := ?OCPP_MSG_TYPE_ID_CALL,
<<"UniqueId">> := Id,
<<"Action">> := Action,
<<"Payload">> := Payload
}) ->
#{
type => ?OCPP_MSG_TYPE_ID_CALL,
id => Id,
action => Action,
payload => Payload
};
payload2frame(
MqttPayload =
#{
<<"MessageTypeId">> := ?OCPP_MSG_TYPE_ID_CALLRESULT,
<<"UniqueId">> := Id,
<<"Payload">> := Payload
}
) ->
Action = maps:get(<<"Action">>, MqttPayload, undefined),
#{
type => ?OCPP_MSG_TYPE_ID_CALLRESULT,
id => Id,
action => Action,
payload => Payload
};
payload2frame(#{
<<"MessageTypeId">> := ?OCPP_MSG_TYPE_ID_CALLERROR,
<<"UniqueId">> := Id,
<<"ErrorCode">> := ErrorCode,
<<"ErrorDescription">> := ErrorDescription
}) ->
#{
type => ?OCPP_MSG_TYPE_ID_CALLERROR,
id => Id,
error_code => ErrorCode,
error_desc => ErrorDescription
}.
%%--------------------------------------------------------------------
%% Ensure connected
ensure_connected(
Channel = #channel{
conninfo = ConnInfo,
clientinfo = ClientInfo
}
) ->
NConnInfo = ConnInfo#{connected_at => erlang:system_time(millisecond)},
ok = run_hooks('client.connected', [ClientInfo, NConnInfo]),
Channel#channel{
conninfo = NConnInfo,
conn_state = connected
}.
ensure_disconnected(
Reason,
Channel = #channel{
conninfo = ConnInfo,
clientinfo = ClientInfo = #{clientid := ClientId}
}
) ->
NConnInfo = ConnInfo#{disconnected_at => erlang:system_time(millisecond)},
ok = run_hooks('client.disconnected', [ClientInfo, Reason, NConnInfo]),
emqx_cm:unregister_channel(ClientId),
Channel#channel{conninfo = NConnInfo, conn_state = disconnected}.
%%--------------------------------------------------------------------
%% Ensure Keepalive
ensure_keepalive(Channel = #channel{conninfo = ConnInfo}) ->
ensure_keepalive_timer(maps:get(keepalive, ConnInfo), Channel).
ensure_keepalive_timer(0, Channel) ->
Channel;
ensure_keepalive_timer(Interval, Channel) ->
Keepalive = emqx_ocpp_keepalive:init(
timer:seconds(Interval),
heartbeat_checking_times_backoff()
),
ensure_timer(alive_timer, Channel#channel{keepalive = Keepalive}).
reset_keepalive(Interval, Channel = #channel{conninfo = ConnInfo, timers = Timers}) ->
case maps:get(alive_timer, Timers, undefined) of
undefined ->
Channel;
TRef ->
NConnInfo = ConnInfo#{keepalive => Interval},
emqx_utils:cancel_timer(TRef),
ensure_keepalive_timer(
Interval,
Channel#channel{
conninfo = NConnInfo,
timers = maps:without([alive_timer], Timers)
}
)
end.
heartbeat_checking_times_backoff() ->
max(0, emqx_ocpp_conf:heartbeat_checking_times_backoff() - 1).
%%--------------------------------------------------------------------
%% Helper functions
%%--------------------------------------------------------------------
-compile({inline, [run_hooks/3]}).
run_hooks(Name, Args) ->
ok = emqx_metrics:inc(Name),
emqx_hooks:run(Name, Args).
run_hooks(Name, Args, Acc) ->
ok = emqx_metrics:inc(Name),
emqx_hooks:run_fold(Name, Args, Acc).
-compile({inline, [reply/2, shutdown/2, shutdown/3]}).
reply(Reply, Channel) ->
{reply, Reply, Channel}.
shutdown(success, Channel) ->
shutdown(normal, Channel);
shutdown(Reason, Channel) ->
{shutdown, Reason, Channel}.
shutdown(success, Reply, Channel) ->
shutdown(normal, Reply, Channel);
shutdown(Reason, Reply, Channel) ->
{shutdown, Reason, Reply, Channel}.
%%--------------------------------------------------------------------
%% For CT tests
%%--------------------------------------------------------------------
set_field(Name, Value, Channel) ->
Pos = emqx_utils:index_of(Name, record_info(fields, channel)),
setelement(Pos + 1, Channel, Value).

View File

@ -0,0 +1,153 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
%% Conf modules for emqx-ocpp gateway
-module(emqx_ocpp_conf).
-export([
load/1,
unload/0,
get_env/1,
get_env/2
]).
-export([
default_heartbeat_interval/0,
heartbeat_checking_times_backoff/0,
retry_interval/0,
awaiting_timeout/0,
message_format_checking/0,
max_mqueue_len/0,
strit_mode/1,
uptopic/1,
up_reply_topic/0,
up_error_topic/0,
dntopic/0
]).
-define(KEY(Key), {?MODULE, Key}).
load(Confs) ->
lists:foreach(fun({K, V}) -> store(K, V) end, Confs).
get_env(K) ->
get_env(K, undefined).
get_env(K, Default) ->
try
persistent_term:get(?KEY(K))
catch
error:badarg ->
Default
end.
-spec default_heartbeat_interval() -> pos_integer().
default_heartbeat_interval() ->
get_env(default_heartbeat_interval, 600).
-spec heartbeat_checking_times_backoff() -> pos_integer().
heartbeat_checking_times_backoff() ->
get_env(heartbeat_checking_times_backoff, 1).
-spec strit_mode(upstream | dnstream) -> boolean().
strit_mode(dnstream) ->
dnstream(strit_mode, false);
strit_mode(upstream) ->
upstream(strit_mode, false).
-spec retry_interval() -> pos_integer().
retry_interval() ->
dnstream(retry_interval, 30).
-spec max_mqueue_len() -> pos_integer().
max_mqueue_len() ->
dnstream(max_mqueue_len, 10).
-spec awaiting_timeout() -> pos_integer().
awaiting_timeout() ->
upstream(awaiting_timeout, 30).
-spec message_format_checking() ->
all
| upstream_only
| dnstream_only
| disable.
message_format_checking() ->
get_env(message_format_checking, all).
uptopic(Action) ->
Topic = upstream(topic),
Mapping = upstream(mapping, #{}),
maps:get(Action, Mapping, Topic).
up_reply_topic() ->
upstream(reply_topic).
up_error_topic() ->
upstream(error_topic).
dntopic() ->
dnstream(topic).
-spec unload() -> ok.
unload() ->
lists:foreach(
fun
({?KEY(K), _}) -> persistent_term:erase(?KEY(K));
(_) -> ok
end,
persistent_term:get()
).
%%--------------------------------------------------------------------
%% internal funcs
%%--------------------------------------------------------------------
dnstream(K) ->
dnstream(K, undefined).
dnstream(K, Def) ->
L = get_env(dnstream, []),
proplists:get_value(K, L, Def).
upstream(K) ->
upstream(K, undefined).
upstream(K, Def) ->
L = get_env(upstream, []),
proplists:get_value(K, L, Def).
store(upstream, L) ->
L1 = preproc([topic, reply_topic, error_topic], L),
Mapping = proplists:get_value(mapping, L1, #{}),
NMappings = maps:map(
fun(_, V) -> emqx_placeholder:preproc_tmpl(V) end,
Mapping
),
L2 = lists:keyreplace(mapping, 1, L1, {mapping, NMappings}),
persistent_term:put(?KEY(upstream), L2);
store(dnstream, L) ->
L1 = preproc([topic], L),
persistent_term:put(?KEY(dnstream), L1);
store(K, V) ->
persistent_term:put(?KEY(K), V).
preproc([], L) ->
L;
preproc([Key | More], L) ->
Val0 = proplists:get_value(Key, L),
Val = emqx_placeholder:preproc_tmpl(Val0),
preproc(More, lists:keyreplace(Key, 1, L, {Key, Val})).

View File

@ -0,0 +1,890 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
%% OCPP/WS|WSS Connection
-module(emqx_ocpp_connection).
-include("emqx_ocpp.hrl").
-include_lib("emqx/include/logger.hrl").
-include_lib("emqx/include/types.hrl").
-logger_header("[OCPP/WS]").
-ifdef(TEST).
-compile(export_all).
-compile(nowarn_export_all).
-endif.
%% API
-export([
info/1,
stats/1
]).
-export([
call/2,
call/3
]).
%% WebSocket callbacks
-export([
init/2,
websocket_init/1,
websocket_handle/2,
websocket_info/2,
websocket_close/2,
terminate/3
]).
%% Export for CT
-export([set_field/3]).
-import(
emqx_utils,
[
maybe_apply/2,
start_timer/2
]
).
-record(state, {
%% Peername of the ws connection
peername :: emqx_types:peername(),
%% Sockname of the ws connection
sockname :: emqx_types:peername(),
%% Sock state
sockstate :: emqx_types:sockstate(),
%% Simulate the active_n opt
active_n :: pos_integer(),
%% Piggyback
piggyback :: single | multiple,
%% Limiter
limiter :: maybe(emqx_limiter:limiter()),
%% Limit Timer
limit_timer :: maybe(reference()),
%% Parse State
parse_state :: emqx_ocpp_frame:parse_state(),
%% Serialize options
serialize :: emqx_ocpp_frame:serialize_opts(),
%% Channel
channel :: emqx_ocpp_channel:channel(),
%% GC State
gc_state :: maybe(emqx_gc:gc_state()),
%% Postponed Packets|Cmds|Events
postponed :: list(emqx_types:packet() | ws_cmd() | tuple()),
%% Stats Timer
stats_timer :: disabled | maybe(reference()),
%% Idle Timeout
idle_timeout :: timeout(),
%%% Idle Timer
idle_timer :: maybe(reference()),
%% OOM Policy
oom_policy :: maybe(emqx_types:oom_policy()),
%% Frame Module
frame_mod :: atom(),
%% Channel Module
chann_mod :: atom(),
%% Listener Tag
listener :: listener() | undefined
}).
-type listener() :: {GwName :: atom(), LisType :: atom(), LisName :: atom()}.
-type state() :: #state{}.
-type ws_cmd() :: {active, boolean()} | close.
-define(ACTIVE_N, 100).
-define(INFO_KEYS, [
socktype,
peername,
sockname,
sockstate,
active_n
]).
-define(SOCK_STATS, [
recv_oct,
recv_cnt,
send_oct,
send_cnt
]).
-define(ENABLED(X), (X =/= undefined)).
-dialyzer({no_match, [info/2]}).
-dialyzer({nowarn_function, [websocket_init/1, postpone/2, classify/4]}).
%%--------------------------------------------------------------------
%% Info, Stats
%%--------------------------------------------------------------------
-spec info(pid() | state()) -> emqx_types:infos().
info(WsPid) when is_pid(WsPid) ->
call(WsPid, info);
info(State = #state{channel = Channel}) ->
ChanInfo = emqx_ocpp_channel:info(Channel),
SockInfo = maps:from_list(
info(?INFO_KEYS, State)
),
ChanInfo#{sockinfo => SockInfo}.
info(Keys, State) when is_list(Keys) ->
[{Key, info(Key, State)} || Key <- Keys];
info(socktype, _State) ->
ws;
info(peername, #state{peername = Peername}) ->
Peername;
info(sockname, #state{sockname = Sockname}) ->
Sockname;
info(sockstate, #state{sockstate = SockSt}) ->
SockSt;
info(active_n, #state{active_n = ActiveN}) ->
ActiveN;
info(channel, #state{chann_mod = ChannMod, channel = Channel}) ->
ChannMod:info(Channel);
info(gc_state, #state{gc_state = GcSt}) ->
maybe_apply(fun emqx_gc:info/1, GcSt);
info(postponed, #state{postponed = Postponed}) ->
Postponed;
info(stats_timer, #state{stats_timer = TRef}) ->
TRef;
info(idle_timeout, #state{idle_timeout = Timeout}) ->
Timeout.
-spec stats(pid() | state()) -> emqx_types:stats().
stats(WsPid) when is_pid(WsPid) ->
call(WsPid, stats);
stats(#state{channel = Channel}) ->
SockStats = emqx_pd:get_counters(?SOCK_STATS),
ChanStats = emqx_ocpp_channel:stats(Channel),
ProcStats = emqx_utils:proc_stats(),
lists:append([SockStats, ChanStats, ProcStats]).
%% kick|discard|takeover
-spec call(pid(), Req :: term()) -> Reply :: term().
call(WsPid, Req) ->
call(WsPid, Req, 5000).
call(WsPid, Req, Timeout) when is_pid(WsPid) ->
Mref = erlang:monitor(process, WsPid),
WsPid ! {call, {self(), Mref}, Req},
receive
{Mref, Reply} ->
erlang:demonitor(Mref, [flush]),
Reply;
{'DOWN', Mref, _, _, Reason} ->
exit(Reason)
after Timeout ->
erlang:demonitor(Mref, [flush]),
exit(timeout)
end.
%%--------------------------------------------------------------------
%% WebSocket callbacks
%%--------------------------------------------------------------------
init(Req, Opts) ->
%% WS Transport Idle Timeout
IdleTimeout = proplists:get_value(idle_timeout, Opts, 7200000),
MaxFrameSize =
case proplists:get_value(max_frame_size, Opts, 0) of
0 -> infinity;
I -> I
end,
Compress = proplists:get_bool(compress, Opts),
WsOpts = #{
compress => Compress,
max_frame_size => MaxFrameSize,
idle_timeout => IdleTimeout
},
case check_origin_header(Req, Opts) of
{error, Message} ->
?SLOG(error, #{msg => "invaild_origin_header", reason => Message}),
{ok, cowboy_req:reply(403, Req), WsOpts};
ok ->
do_init(Req, Opts, WsOpts)
end.
do_init(Req, Opts, WsOpts) ->
case
emqx_utils:pipeline(
[
fun init_state_and_channel/2,
fun parse_sec_websocket_protocol/2,
fun auth_connect/2
],
[Req, Opts, WsOpts],
undefined
)
of
{error, Reason, _State} ->
{ok, cowboy_req:reply(400, #{}, to_bin(Reason), Req), WsOpts};
{ok, [Resp, Opts, WsOpts], NState} ->
{cowboy_websocket, Resp, [Req, Opts, NState], WsOpts}
end.
init_state_and_channel([Req, Opts, _WsOpts], _State = undefined) ->
{Peername, Peercert} = peername_and_cert(Req, Opts),
Sockname = cowboy_req:sock(Req),
WsCookie =
try
cowboy_req:parse_cookies(Req)
catch
error:badarg ->
?SLOG(error, #{msg => "illegal_cookie"}),
undefined;
Error:Reason ->
?SLOG(error, #{
msg => "failed_to_parse_cookie",
error => Error,
reason => Reason
}),
undefined
end,
ConnInfo = #{
socktype => ws,
peername => Peername,
sockname => Sockname,
peercert => Peercert,
ws_cookie => WsCookie,
conn_mod => ?MODULE
},
Limiter = undeined,
ActiveN = emqx_gateway_utils:active_n(Opts),
Piggyback = proplists:get_value(piggyback, Opts, multiple),
ParseState = emqx_ocpp_frame:initial_parse_state(#{}),
Serialize = emqx_ocpp_frame:serialize_opts(),
Channel = emqx_ocpp_channel:init(ConnInfo, Opts),
GcState = emqx_gateway_utils:init_gc_state(Opts),
StatsTimer = emqx_gateway_utils:stats_timer(Opts),
IdleTimeout = emqx_gateway_utils:idle_timeout(Opts),
OomPolicy = emqx_gateway_utils:oom_policy(Opts),
IdleTimer = emqx_utils:start_timer(IdleTimeout, idle_timeout),
emqx_logger:set_metadata_peername(esockd:format(Peername)),
{ok, #state{
peername = Peername,
sockname = Sockname,
sockstate = running,
active_n = ActiveN,
piggyback = Piggyback,
limiter = Limiter,
parse_state = ParseState,
serialize = Serialize,
channel = Channel,
gc_state = GcState,
postponed = [],
stats_timer = StatsTimer,
idle_timeout = IdleTimeout,
idle_timer = IdleTimer,
oom_policy = OomPolicy,
frame_mod = emqx_ocpp_frame,
chann_mod = emqx_ocpp_channel,
listener = maps:get(listener, Opts, undeined)
}}.
peername_and_cert(Req, Opts) ->
case
proplists:get_bool(proxy_protocol, Opts) andalso
maps:get(proxy_header, Req)
of
#{src_address := SrcAddr, src_port := SrcPort, ssl := SSL} ->
SourceName = {SrcAddr, SrcPort},
%% Notice: Only CN is available in Proxy Protocol V2 additional info
SourceSSL =
case maps:get(cn, SSL, undefined) of
undeined -> nossl;
CN -> [{pp2_ssl_cn, CN}]
end,
{SourceName, SourceSSL};
#{src_address := SrcAddr, src_port := SrcPort} ->
SourceName = {SrcAddr, SrcPort},
{SourceName, nossl};
_ ->
{get_peer(Req, Opts), cowboy_req:cert(Req)}
end.
parse_sec_websocket_protocol([Req, Opts, WsOpts], State) ->
SupportedSubprotocols = proplists:get_value(supported_subprotocols, Opts),
FailIfNoSubprotocol = proplists:get_value(fail_if_no_subprotocol, Opts),
case cowboy_req:parse_header(<<"sec-websocket-protocol">>, Req) of
undefined ->
case FailIfNoSubprotocol of
true ->
{error, no_subprotocol};
false ->
Picked = list_to_binary(lists:nth(1, SupportedSubprotocols)),
Resp = cowboy_req:set_resp_header(
<<"sec-websocket-protocol">>,
Picked,
Req
),
{ok, [Resp, Opts, WsOpts], State}
end;
Subprotocols ->
NSupportedSubprotocols = [
list_to_binary(Subprotocol)
|| Subprotocol <- SupportedSubprotocols
],
case pick_subprotocol(Subprotocols, NSupportedSubprotocols) of
{ok, Subprotocol} ->
Resp = cowboy_req:set_resp_header(
<<"sec-websocket-protocol">>,
Subprotocol,
Req
),
{ok, [Resp, Opts, WsOpts], State};
{error, no_supported_subprotocol} ->
{error, no_supported_subprotocol}
end
end.
pick_subprotocol([], _SupportedSubprotocols) ->
{error, no_supported_subprotocol};
pick_subprotocol([Subprotocol | Rest], SupportedSubprotocols) ->
case lists:member(Subprotocol, SupportedSubprotocols) of
true ->
{ok, Subprotocol};
false ->
pick_subprotocol(Rest, SupportedSubprotocols)
end.
auth_connect([Req, Opts, _WsOpts], State = #state{channel = Channel}) ->
{Username, Password} =
try
{basic, Username0, Password0} = cowboy_req:parse_header(<<"authorization">>, Req),
{Username0, Password0}
catch
_:_ -> {undefined, undefined}
end,
{ProtoName, ProtoVer} = parse_protocol_name(
cowboy_req:resp_header(<<"sec-websocket-protocol">>, Req)
),
case parse_clientid(Req, Opts) of
{ok, ClientId} ->
case
emqx_ocpp_channel:authenticate(
#{
clientid => ClientId,
username => Username,
password => Password,
proto_name => ProtoName,
proto_ver => ProtoVer
},
Channel
)
of
{ok, NChannel} ->
{ok, State#state{channel = NChannel}};
{error, Reason} ->
{error, Reason}
end;
{error, Reason2} ->
{error, Reason2}
end.
parse_clientid(Req, Opts) ->
PathPrefix = proplists:get_value(ocpp_path, Opts),
[_, ClientId0] = binary:split(
cowboy_req:path(Req),
iolist_to_binary(PathPrefix ++ "/")
),
case uri_string:percent_decode(ClientId0) of
<<>> ->
{error, clientid_cannot_be_empty};
ClientId ->
%% Client Id can not contains '/', '+', '#'
case re:run(ClientId, "[/#\\+]", [{capture, none}]) of
nomatch ->
{ok, ClientId};
_ ->
{error, unsupported_clientid}
end
end.
parse_protocol_name(<<"ocpp1.6">>) ->
{<<"OCPP">>, <<"1.6">>}.
parse_header_fun_origin(Req, Opts) ->
case cowboy_req:header(<<"origin">>, Req) of
undefined ->
case proplists:get_bool(allow_origin_absence, Opts) of
true -> ok;
false -> {error, origin_header_cannot_be_absent}
end;
Value ->
Origins = proplists:get_value(check_origins, Opts, []),
case lists:member(Value, Origins) of
true -> ok;
false -> {error, {origin_not_allowed, Value}}
end
end.
check_origin_header(Req, Opts) ->
case proplists:get_bool(check_origin_enable, Opts) of
true -> parse_header_fun_origin(Req, Opts);
false -> ok
end.
websocket_init([_Req, _Opts, State]) ->
return(State#state{postponed = [after_init]}).
websocket_handle({text, Data}, State) when is_list(Data) ->
websocket_handle({text, iolist_to_binary(Data)}, State);
websocket_handle({text, Data}, State) ->
?SLOG(debug, #{msg => "raw_bin_received", bin => Data}),
ok = inc_recv_stats(1, iolist_size(Data)),
NState = ensure_stats_timer(State),
return(parse_incoming(Data, NState));
%% Pings should be replied with pongs, cowboy does it automatically
%% Pongs can be safely ignored. Clause here simply prevents crash.
websocket_handle(Frame, State) when Frame =:= ping; Frame =:= pong ->
return(State);
websocket_handle({Frame, _}, State) when Frame =:= ping; Frame =:= pong ->
return(State);
websocket_handle({Frame, _}, State) ->
%% TODO: should not close the ws connection
?SLOG(error, #{msg => "unexpected_frame", frame => Frame}),
shutdown(unexpected_ws_frame, State).
websocket_info({call, From, Req}, State) ->
handle_call(From, Req, State);
websocket_info({cast, rate_limit}, State) ->
Stats = #{
cnt => emqx_pd:reset_counter(incoming_pubs),
oct => emqx_pd:reset_counter(incoming_bytes)
},
NState = postpone({check_gc, Stats}, State),
return(ensure_rate_limit(Stats, NState));
websocket_info({cast, Msg}, State) ->
handle_info(Msg, State);
websocket_info({incoming, Packet}, State) ->
handle_incoming(Packet, State);
websocket_info({outgoing, Packets}, State) ->
return(enqueue(Packets, State));
websocket_info({check_gc, Stats}, State) ->
return(check_oom(run_gc(Stats, State)));
websocket_info(
Deliver = {deliver, _Topic, _Msg},
State = #state{active_n = ActiveN}
) ->
Delivers = [Deliver | emqx_utils:drain_deliver(ActiveN)],
with_channel(handle_deliver, [Delivers], State);
websocket_info(
{timeout, TRef, limit_timeout},
State = #state{limit_timer = TRef}
) ->
NState = State#state{
sockstate = running,
limit_timer = undefined
},
return(enqueue({active, true}, NState));
websocket_info({timeout, TRef, Msg}, State) when is_reference(TRef) ->
handle_timeout(TRef, Msg, State);
websocket_info({shutdown, Reason}, State) ->
shutdown(Reason, State);
websocket_info({stop, Reason}, State) ->
shutdown(Reason, State);
websocket_info(Info, State) ->
handle_info(Info, State).
websocket_close({_, ReasonCode, _Payload}, State) when is_integer(ReasonCode) ->
websocket_close(ReasonCode, State);
websocket_close(Reason, State) ->
?SLOG(debug, #{msg => "websocket_closed", reason => Reason}),
handle_info({sock_closed, Reason}, State).
terminate(Reason, _Req, #state{channel = Channel}) ->
?SLOG(debug, #{msg => "terminated", reason => Reason}),
emqx_ocpp_channel:terminate(Reason, Channel);
terminate(_Reason, _Req, _UnExpectedState) ->
ok.
%%--------------------------------------------------------------------
%% Handle call
%%--------------------------------------------------------------------
handle_call(From, info, State) ->
gen_server:reply(From, info(State)),
return(State);
handle_call(From, stats, State) ->
gen_server:reply(From, stats(State)),
return(State);
handle_call(From, Req, State = #state{channel = Channel}) ->
case emqx_ocpp_channel:handle_call(Req, From, Channel) of
{reply, Reply, NChannel} ->
gen_server:reply(From, Reply),
return(State#state{channel = NChannel});
{shutdown, Reason, Reply, NChannel} ->
gen_server:reply(From, Reply),
shutdown(Reason, State#state{channel = NChannel})
end.
%%--------------------------------------------------------------------
%% Handle Info
%%--------------------------------------------------------------------
handle_info({connack, ConnAck}, State) ->
return(enqueue(ConnAck, State));
handle_info({close, Reason}, State) ->
?SLOG(debug, #{msg => "force_to_close_socket", reason => Reason}),
return(enqueue({close, Reason}, State));
handle_info({event, connected}, State = #state{channel = Channel}) ->
ClientId = emqx_ocpp_channel:info(clientid, Channel),
emqx_cm:insert_channel_info(ClientId, info(State), stats(State)),
return(State);
handle_info({event, disconnected}, State = #state{chann_mod = ChannMod, channel = Channel}) ->
Ctx = ChannMod:info(ctx, Channel),
ClientId = ChannMod:info(clientid, Channel),
emqx_gateway_ctx:set_chan_info(Ctx, ClientId, info(State)),
emqx_gateway_ctx:connection_closed(Ctx, ClientId),
return(State);
handle_info({event, _Other}, State = #state{chann_mod = ChannMod, channel = Channel}) ->
Ctx = ChannMod:info(ctx, Channel),
ClientId = ChannMod:info(clientid, Channel),
emqx_gateway_ctx:set_chan_info(Ctx, ClientId, info(State)),
emqx_gateway_ctx:set_chan_stats(Ctx, ClientId, stats(State)),
return(State);
handle_info(Info, State) ->
with_channel(handle_info, [Info], State).
%%--------------------------------------------------------------------
%% Handle timeout
%%--------------------------------------------------------------------
handle_timeout(TRef, keepalive, State) when is_reference(TRef) ->
RecvOct = emqx_pd:get_counter(recv_oct),
handle_timeout(TRef, {keepalive, RecvOct}, State);
handle_timeout(
TRef,
emit_stats,
State = #state{
channel = Channel,
stats_timer = TRef
}
) ->
ClientId = emqx_ocpp_channel:info(clientid, Channel),
emqx_cm:set_chan_stats(ClientId, stats(State)),
return(State#state{stats_timer = undefined});
handle_timeout(TRef, TMsg, State) ->
with_channel(handle_timeout, [TRef, TMsg], State).
%%--------------------------------------------------------------------
%% Ensure rate limit
%%--------------------------------------------------------------------
ensure_rate_limit(_Stats, State) ->
State.
%%--------------------------------------------------------------------
%% Run GC, Check OOM
%%--------------------------------------------------------------------
run_gc(Stats, State = #state{gc_state = GcSt}) ->
case ?ENABLED(GcSt) andalso emqx_gc:run(Stats, GcSt) of
false -> State;
{_IsGC, GcSt1} -> State#state{gc_state = GcSt1}
end.
check_oom(State = #state{oom_policy = OomPolicy}) ->
case ?ENABLED(OomPolicy) andalso emqx_utils:check_oom(OomPolicy) of
Shutdown = {shutdown, _Reason} ->
postpone(Shutdown, State);
_Other ->
ok
end,
State.
%%--------------------------------------------------------------------
%% Parse incoming data
%%--------------------------------------------------------------------
parse_incoming(<<>>, State) ->
State;
parse_incoming(Data, State = #state{parse_state = ParseState}) ->
try emqx_ocpp_frame:parse(Data, ParseState) of
{ok, Packet, Rest, NParseState} ->
NState = State#state{parse_state = NParseState},
parse_incoming(Rest, postpone({incoming, Packet}, NState))
catch
error:Reason:Stk ->
?SLOG(
error,
#{
msg => "parse_failed",
data => Data,
reason => Reason,
stacktrace => Stk
}
),
FrameError = {frame_error, Reason},
postpone({incoming, FrameError}, State)
end.
%%--------------------------------------------------------------------
%% Handle incoming packet
%%--------------------------------------------------------------------
handle_incoming(Packet, State = #state{active_n = ActiveN}) ->
ok = inc_incoming_stats(Packet),
NState =
case emqx_pd:get_counter(incoming_pubs) > ActiveN of
true -> postpone({cast, rate_limit}, State);
false -> State
end,
with_channel(handle_in, [Packet], NState);
handle_incoming(FrameError, State) ->
with_channel(handle_in, [FrameError], State).
%%--------------------------------------------------------------------
%% With Channel
%%--------------------------------------------------------------------
with_channel(Fun, Args, State = #state{channel = Channel}) ->
case erlang:apply(emqx_ocpp_channel, Fun, Args ++ [Channel]) of
ok ->
return(State);
{ok, NChannel} ->
return(State#state{channel = NChannel});
{ok, Replies, NChannel} ->
return(postpone(Replies, State#state{channel = NChannel}));
{shutdown, Reason, NChannel} ->
shutdown(Reason, State#state{channel = NChannel});
{shutdown, Reason, Packet, NChannel} ->
NState = State#state{channel = NChannel},
shutdown(Reason, postpone(Packet, NState))
end.
%%--------------------------------------------------------------------
%% Handle outgoing packets
%%--------------------------------------------------------------------
handle_outgoing(Packets, State = #state{active_n = ActiveN, piggyback = Piggyback}) ->
IoData = lists:map(serialize_and_inc_stats_fun(State), Packets),
Oct = iolist_size(IoData),
ok = inc_sent_stats(length(Packets), Oct),
NState =
case emqx_pd:get_counter(outgoing_pubs) > ActiveN of
true ->
Stats = #{
cnt => emqx_pd:reset_counter(outgoing_pubs),
oct => emqx_pd:reset_counter(outgoing_bytes)
},
postpone({check_gc, Stats}, State);
false ->
State
end,
{
case Piggyback of
single -> [{text, IoData}];
multiple -> lists:map(fun(Bin) -> {text, Bin} end, IoData)
end,
ensure_stats_timer(NState)
}.
serialize_and_inc_stats_fun(#state{serialize = Serialize}) ->
fun(Packet) ->
case emqx_ocpp_frame:serialize_pkt(Packet, Serialize) of
<<>> ->
?SLOG(
warning,
#{
msg => "discarded_frame",
reason => "message_too_large",
frame => emqx_ocpp_frame:format(Packet)
}
),
ok = inc_outgoing_stats({error, message_too_large}),
<<>>;
Data ->
?SLOG(debug, #{msg => "raw_bin_sent", bin => Data}),
ok = inc_outgoing_stats(Packet),
Data
end
end.
%%--------------------------------------------------------------------
%% Inc incoming/outgoing stats
%%--------------------------------------------------------------------
-compile(
{inline, [
inc_recv_stats/2,
inc_incoming_stats/1,
inc_outgoing_stats/1,
inc_sent_stats/2
]}
).
inc_recv_stats(Cnt, Oct) ->
inc_counter(incoming_bytes, Oct),
inc_counter(recv_cnt, Cnt),
inc_counter(recv_oct, Oct),
emqx_metrics:inc('bytes.received', Oct).
inc_incoming_stats(Packet) ->
_ = emqx_pd:inc_counter(recv_pkt, 1),
%% assert, all OCCP frame are message
true = emqx_ocpp_frame:is_message(Packet),
inc_counter(recv_msg, 1),
inc_counter('recv_msg.qos1', 1),
inc_counter(incoming_pubs, 1).
inc_outgoing_stats({error, message_too_large}) ->
inc_counter('send_msg.dropped', 1),
inc_counter('send_msg.dropped.too_large', 1);
inc_outgoing_stats(Packet) ->
_ = emqx_pd:inc_counter(send_pkt, 1),
%% assert, all OCCP frames are message
true = emqx_ocpp_frame:is_message(Packet),
inc_counter(send_msg, 1),
inc_counter('send_msg.qos1', 1),
inc_counter(outgoing_pubs, 1).
inc_sent_stats(Cnt, Oct) ->
inc_counter(outgoing_bytes, Oct),
inc_counter(send_cnt, Cnt),
inc_counter(send_oct, Oct),
emqx_metrics:inc('bytes.sent', Oct).
inc_counter(Name, Value) ->
_ = emqx_pd:inc_counter(Name, Value),
ok.
%%--------------------------------------------------------------------
%% Helper functions
%%--------------------------------------------------------------------
-compile({inline, [ensure_stats_timer/1]}).
%%--------------------------------------------------------------------
%% Ensure stats timer
ensure_stats_timer(
State = #state{
idle_timeout = Timeout,
stats_timer = undefined
}
) ->
State#state{stats_timer = start_timer(Timeout, emit_stats)};
ensure_stats_timer(State) ->
State.
-compile({inline, [postpone/2, enqueue/2, return/1, shutdown/2]}).
%%--------------------------------------------------------------------
%% Postpone the packet, cmd or event
%% ocpp frame
postpone(Packet, State) when is_map(Packet) ->
enqueue(Packet, State);
postpone(Event, State) when is_tuple(Event) ->
enqueue(Event, State);
postpone(More, State) when is_list(More) ->
lists:foldl(fun postpone/2, State, More).
enqueue([Packet], State = #state{postponed = Postponed}) ->
State#state{postponed = [Packet | Postponed]};
enqueue(Packets, State = #state{postponed = Postponed}) when
is_list(Packets)
->
State#state{postponed = lists:reverse(Packets) ++ Postponed};
enqueue(Other, State = #state{postponed = Postponed}) ->
State#state{postponed = [Other | Postponed]}.
shutdown(Reason, State = #state{postponed = Postponed}) ->
return(State#state{postponed = [{shutdown, Reason} | Postponed]}).
return(State = #state{postponed = []}) ->
{ok, State};
return(State = #state{postponed = Postponed}) ->
{Packets, Cmds, Events} = classify(Postponed, [], [], []),
ok = lists:foreach(fun trigger/1, Events),
State1 = State#state{postponed = []},
case {Packets, Cmds} of
{[], []} ->
{ok, State1};
{[], Cmds} ->
{Cmds, State1};
{Packets, Cmds} ->
{Frames, State2} = handle_outgoing(Packets, State1),
{Frames ++ Cmds, State2}
end.
classify([], Packets, Cmds, Events) ->
{Packets, Cmds, Events};
classify([Packet | More], Packets, Cmds, Events) when
%% ocpp frame
is_map(Packet)
->
classify(More, [Packet | Packets], Cmds, Events);
classify([Cmd = {active, _} | More], Packets, Cmds, Events) ->
classify(More, Packets, [Cmd | Cmds], Events);
classify([Cmd = {shutdown, _Reason} | More], Packets, Cmds, Events) ->
classify(More, Packets, [Cmd | Cmds], Events);
classify([Cmd = close | More], Packets, Cmds, Events) ->
classify(More, Packets, [Cmd | Cmds], Events);
classify([Cmd = {close, _Reason} | More], Packets, Cmds, Events) ->
classify(More, Packets, [Cmd | Cmds], Events);
classify([Event | More], Packets, Cmds, Events) ->
classify(More, Packets, Cmds, [Event | Events]).
trigger(Event) -> erlang:send(self(), Event).
get_peer(Req, Opts) ->
{PeerAddr, PeerPort} = cowboy_req:peer(Req),
AddrHeader = cowboy_req:header(proplists:get_value(proxy_address_header, Opts), Req, <<>>),
ClientAddr =
case string:tokens(binary_to_list(AddrHeader), ", ") of
[] ->
undefined;
AddrList ->
hd(AddrList)
end,
Addr =
case inet:parse_address(ClientAddr) of
{ok, A} ->
A;
_ ->
PeerAddr
end,
PortHeader = cowboy_req:header(proplists:get_value(proxy_port_header, Opts), Req, <<>>),
ClientPort =
case string:tokens(binary_to_list(PortHeader), ", ") of
[] ->
undefined;
PortList ->
hd(PortList)
end,
try
{Addr, list_to_integer(ClientPort)}
catch
_:_ -> {Addr, PeerPort}
end.
to_bin(A) when is_atom(A) -> atom_to_binary(A, utf8);
to_bin(L) when is_list(L) -> list_to_binary(L);
to_bin(B) when is_binary(B) -> B.
%%--------------------------------------------------------------------
%% For CT tests
%%--------------------------------------------------------------------
set_field(Name, Value, State) ->
Pos = emqx_utils:index_of(Name, record_info(fields, state)),
setelement(Pos + 1, State, Value).

View File

@ -0,0 +1,167 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_ocpp_frame).
-behaviour(emqx_gateway_frame).
-include("emqx_ocpp.hrl").
%% emqx_gateway_frame callbacks
-export([
initial_parse_state/1,
serialize_opts/0,
serialize_pkt/2,
parse/2,
format/1,
type/1,
is_message/1
]).
-type parse_state() :: map().
-type parse_result() ::
{ok, frame(), Rest :: binary(), NewState :: parse_state()}.
-export_type([
parse_state/0,
parse_result/0,
frame/0
]).
-dialyzer({nowarn_function, [format/1]}).
-spec initial_parse_state(map()) -> parse_state().
initial_parse_state(_Opts) ->
#{}.
%% No-TCP-Spliting
-spec parse(binary() | list(), parse_state()) -> parse_result().
parse(Bin, Parser) when is_binary(Bin) ->
case emqx_utils_json:safe_decode(Bin, [return_maps]) of
{ok, Json} ->
parse(Json, Parser);
{error, {Position, Reason}} ->
error(
{badjson, io_lib:format("Invalid json at ~w: ~s", [Position, Reason])}
);
{error, Reason} ->
error(
{badjson, io_lib:format("Invalid json: ~p", [Reason])}
)
end;
%% CALL
parse([?OCPP_MSG_TYPE_ID_CALL, Id, Action, Payload], Parser) ->
Frame = #{
type => ?OCPP_MSG_TYPE_ID_CALL,
id => Id,
action => Action,
payload => Payload
},
case emqx_ocpp_schemas:validate(upstream, Frame) of
ok ->
{ok, Frame, <<>>, Parser};
{error, ReasonStr} ->
error({validation_faliure, Id, ReasonStr})
end;
%% CALLRESULT
parse([?OCPP_MSG_TYPE_ID_CALLRESULT, Id, Payload], Parser) ->
Frame = #{
type => ?OCPP_MSG_TYPE_ID_CALLRESULT,
id => Id,
payload => Payload
},
%% TODO: Validate CALLRESULT frame
%%case emqx_ocpp_schemas:validate(upstream, Frame) of
%% ok ->
%% {ok, Frame, <<>>, Parser};
%% {error, ReasonStr} ->
%% error({validation_faliure, Id, ReasonStr})
%%end;
{ok, Frame, <<>>, Parser};
%% CALLERROR
parse(
[
?OCPP_MSG_TYPE_ID_CALLERROR,
Id,
ErrCode,
ErrDesc,
ErrDetails
],
Parser
) ->
{ok,
#{
type => ?OCPP_MSG_TYPE_ID_CALLERROR,
id => Id,
error_code => ErrCode,
error_desc => ErrDesc,
error_details => ErrDetails
},
<<>>, Parser}.
-spec serialize_opts() -> emqx_gateway_frame:serialize_options().
serialize_opts() ->
#{}.
-spec serialize_pkt(frame(), emqx_gateway_frame:serialize_options()) -> iodata().
serialize_pkt(
#{
id := Id,
type := ?OCPP_MSG_TYPE_ID_CALL,
action := Action,
payload := Payload
},
_Opts
) ->
emqx_utils_json:encode([?OCPP_MSG_TYPE_ID_CALL, Id, Action, Payload]);
serialize_pkt(
#{
id := Id,
type := ?OCPP_MSG_TYPE_ID_CALLRESULT,
payload := Payload
},
_Opts
) ->
emqx_utils_json:encode([?OCPP_MSG_TYPE_ID_CALLRESULT, Id, Payload]);
serialize_pkt(
#{
id := Id,
type := Type,
error_code := ErrCode,
error_desc := ErrDesc
} = Frame,
_Opts
) when
Type == ?OCPP_MSG_TYPE_ID_CALLERROR
->
ErrDetails = maps:get(error_details, Frame, #{}),
emqx_utils_json:encode([Type, Id, ErrCode, ErrDesc, ErrDetails]).
-spec format(frame()) -> string().
format(Frame) ->
serialize_pkt(Frame, #{}).
-spec type(frame()) -> atom().
type(_Frame) ->
%% TODO:
todo.
-spec is_message(frame()) -> boolean().
is_message(_Frame) ->
%% TODO:
true.

View File

@ -0,0 +1,118 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2017-2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
%% copied from emqx_keepalive module, but made some broken changes
-module(emqx_ocpp_keepalive).
-export([
init/1,
init/2,
info/1,
info/2,
check/2,
set/3
]).
-export_type([keepalive/0]).
-elvis([{elvis_style, no_if_expression, disable}]).
-record(keepalive, {
interval :: pos_integer(),
statval :: non_neg_integer(),
repeat :: non_neg_integer(),
max_repeat :: non_neg_integer()
}).
-opaque keepalive() :: #keepalive{}.
%% @doc Init keepalive.
-spec init(Interval :: non_neg_integer()) -> keepalive().
init(Interval) when Interval > 0 ->
init(Interval, 1).
-spec init(Interval :: non_neg_integer(), MaxRepeat :: non_neg_integer()) -> keepalive().
init(Interval, MaxRepeat) when
Interval > 0, MaxRepeat >= 0
->
#keepalive{
interval = Interval,
statval = 0,
repeat = 0,
max_repeat = MaxRepeat
}.
%% @doc Get Info of the keepalive.
-spec info(keepalive()) -> emqx_types:infos().
info(#keepalive{
interval = Interval,
statval = StatVal,
repeat = Repeat,
max_repeat = MaxRepeat
}) ->
#{
interval => Interval,
statval => StatVal,
repeat => Repeat,
max_repeat => MaxRepeat
}.
-spec info(interval | statval | repeat, keepalive()) ->
non_neg_integer().
info(interval, #keepalive{interval = Interval}) ->
Interval;
info(statval, #keepalive{statval = StatVal}) ->
StatVal;
info(repeat, #keepalive{repeat = Repeat}) ->
Repeat;
info(max_repeat, #keepalive{max_repeat = MaxRepeat}) ->
MaxRepeat.
%% @doc Check keepalive.
-spec check(non_neg_integer(), keepalive()) ->
{ok, keepalive()} | {error, timeout}.
check(
NewVal,
KeepAlive = #keepalive{
statval = OldVal,
repeat = Repeat,
max_repeat = MaxRepeat
}
) ->
if
NewVal =/= OldVal ->
{ok, KeepAlive#keepalive{statval = NewVal, repeat = 0}};
Repeat < MaxRepeat ->
{ok, KeepAlive#keepalive{repeat = Repeat + 1}};
true ->
{error, timeout}
end.
%% from mqtt-v3.1.1 specific
%% A Keep Alive value of zero (0) has the effect of turning off the keep alive mechanism.
%% This means that, in this case, the Server is not required
%% to disconnect the Client on the grounds of inactivity.
%% Note that a Server is permitted to disconnect a Client that it determines
%% to be inactive or non-responsive at any time,
%% regardless of the Keep Alive value provided by that Client.
%% Non normative comment
%%The actual value of the Keep Alive is application specific;
%% typically this is a few minutes.
%% The maximum value is (65535s) 18 hours 12 minutes and 15 seconds.
%% @doc Update keepalive's interval
-spec set(interval, non_neg_integer(), keepalive()) -> keepalive().
set(interval, Interval, KeepAlive) when Interval >= 0 andalso Interval =< 65535000 ->
KeepAlive#keepalive{interval = Interval}.

View File

@ -0,0 +1,172 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
-module(emqx_ocpp_schema).
-include_lib("hocon/include/hoconsc.hrl").
-include_lib("typerefl/include/types.hrl").
-define(DEFAULT_MOUNTPOINT, <<"ocpp/">>).
%% config schema provides
-export([fields/1, desc/1]).
fields(ocpp) ->
[
{mountpoint, emqx_gateway_schema:mountpoint(?DEFAULT_MOUNTPOINT)},
{default_heartbeat_interval,
sc(
emqx_schema:duration_s(),
#{
default => <<"60s">>,
required => true,
desc => ?DESC(default_heartbeat_interval)
}
)},
{heartbeat_checking_times_backoff,
sc(
integer(),
#{
default => 1,
required => true,
desc => ?DESC(heartbeat_checking_times_backoff)
}
)},
{upstream, sc(ref(upstream), #{desc => ?DESC(upstream)})},
{dnstream, sc(ref(dnstream), #{desc => ?DESC(dnstream)})},
{message_format_checking,
sc(
hoconsc:union([all, upstream_only, dnstream_only, disable]),
#{
default => all,
desc => ?DESC(message_format_checking)
}
)},
{json_schema_dir,
sc(
string(),
#{
default => <<"${application_priv}/schemas">>,
desc => ?DESC(json_schema_dir)
}
)},
{json_schema_id_prefix,
sc(
string(),
#{
default => <<"urn:OCPP:1.6:2019:12:">>,
desc => ?DESC(json_schema_id_prefix)
}
)},
{listeners, sc(ref(listeners), #{desc => ?DESC(listeners)})}
] ++ emqx_gateway_schema:gateway_common_options();
fields(listeners) ->
DefaultPath = <<"/ocpp">>,
SubProtocols = <<"ocpp1.6, ocpp2.0">>,
[
{ws, emqx_gateway_schema:ws_listener(DefaultPath, SubProtocols)},
{wss, emqx_gateway_schema:wss_listener(DefaultPath, SubProtocols)}
];
fields(upstream) ->
[
{topic,
sc(
string(),
#{
required => true,
default => <<"cp/${cid}">>,
desc => ?DESC(upstream_topic)
}
)},
{topic_override_mapping,
sc(
%% XXX: more clearly type defination
hoconsc:map(string(), string()),
#{
required => false,
default => #{},
desc => ?DESC(upstream_topic_override_mapping)
}
)},
{reply_topic,
sc(
string(),
#{
required => true,
default => <<"cp/${cid}/Reply">>,
desc => ?DESC(upstream_reply_topic)
}
)},
{error_topic,
sc(
string(),
#{
required => true,
default => <<"cp/${cid}/Reply">>,
desc => ?DESC(upstream_error_topic)
}
)}
%{awaiting_timeout,
% sc(
% emqx_schema:duration(),
% #{
% required => false,
% default => <<"30s">>,
% desc => ?DESC(upstream_awaiting_timeout)
% }
% )}
];
fields(dnstream) ->
[
{strit_mode,
sc(
boolean(),
#{
required => false,
default => false,
desc => ?DESC(dnstream_strit_mode)
}
)},
{topic,
sc(
string(),
#{
required => true,
default => <<"cs/${cid}">>,
desc => ?DESC(dnstream_topic)
}
)},
%{retry_interval,
% sc(
% emqx_schema:duration(),
% #{
% required => false,
% default => <<"30s">>,
% desc => ?DESC(dnstream_retry_interval)
% }
% )},
{max_mqueue_len,
sc(
integer(),
#{
required => false,
default => 100,
desc => ?DESC(dnstream_max_mqueue_len)
}
)}
].
desc(ocpp) ->
"The OCPP gateway";
desc(_) ->
undefined.
%%--------------------------------------------------------------------
%% internal functions
sc(Type, Meta) ->
hoconsc:mk(Type, Meta).
ref(Field) ->
hoconsc:ref(?MODULE, Field).

View File

@ -0,0 +1,106 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
%% The OCPP messsage validator based on JSON-schema
-module(emqx_ocpp_schemas).
-include("emqx_ocpp.hrl").
-export([
load/0,
validate/2
]).
-spec load() -> ok.
%% @doc The jesse:load_schemas/2 require the caller process to own an ets table.
%% So, please call it in some a long-live process
load() ->
case emqx_ocpp_conf:message_format_checking() of
disable ->
ok;
_ ->
case feedvar(emqx_ocpp_conf:get_env(json_schema_dir)) of
undefined ->
ok;
Dir ->
ok = jesse:load_schemas(Dir, fun emqx_utils_json:decode/1)
end
end.
-spec validate(upstream | dnstream, emqx_ocpp_frame:frame()) ->
ok
| {error, string()}.
%% FIXME: `action` key is absent in OCPP_MSG_TYPE_ID_CALLRESULT frame
validate(Direction, #{type := Type, action := Action, payload := Payload}) when
Type == ?OCPP_MSG_TYPE_ID_CALL;
Type == ?OCPP_MSG_TYPE_ID_CALLRESULT
->
case emqx_ocpp_conf:message_format_checking() of
all ->
do_validate(schema_id(Type, Action), Payload);
upstream_only when Direction == upstream ->
do_validate(schema_id(Type, Action), Payload);
dnstream_only when Direction == dnstream ->
do_validate(schema_id(Type, Action), Payload);
_ ->
ok
end;
validate(_, #{type := ?OCPP_MSG_TYPE_ID_CALLERROR}) ->
ok.
do_validate(SchemaId, Payload) ->
case jesse:validate(SchemaId, Payload) of
{ok, _} ->
ok;
%% jesse_database:error/0
{error, {database_error, Key, Reason}} ->
{error, format("Validation error: ~s ~s", [Key, Reason])};
%% jesse_error:error/0
{error, [{data_invalid, _Schema, Error, _Data, Path} | _]} ->
{error, format("Validation error: ~s ~s", [Path, Error])};
{error, [{schema_invalid, _Schema, Error} | _]} ->
{error, format("Validation error: schema_invalid ~s", [Error])};
{error, Reason} ->
{error, io_lib:format("Validation error: ~0p", [Reason])}
end.
%%--------------------------------------------------------------------
%% internal funcs
%% @doc support vars:
%% - ${application_priv}
feedvar(undefined) ->
undefined;
feedvar(Path) ->
binary_to_list(
emqx_placeholder:proc_tmpl(
emqx_placeholder:preproc_tmpl(Path),
#{application_priv => code:priv_dir(emqx_ocpp)}
)
).
schema_id(?OCPP_MSG_TYPE_ID_CALL, Action) when is_binary(Action) ->
emqx_ocpp_conf:get_env(json_schema_id_prefix) ++
binary_to_list(Action) ++
"Request";
schema_id(?OCPP_MSG_TYPE_ID_CALLRESULT, Action) when is_binary(Action) ->
emqx_ocpp_conf:get_env(json_schema_id_prefix) ++
binary_to_list(Action) ++
"Response".
format(Fmt, Args) ->
lists:flatten(io_lib:format(Fmt, Args)).

View File

@ -0,0 +1,52 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_ocpp_SUITE).
-compile(export_all).
-compile(nowarn_export_all).
-include_lib("emqx_tcp.hrl").
-include_lib("emqx/include/emqx.hrl").
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
all() ->
emqx_ct:all(?MODULE).
init_per_suite(Conf) ->
emqx_ct_helpers:start_apps([emqx_ocpp], fun set_special_cfg/1),
Conf.
end_per_suite(_Config) ->
emqx_ct_helpers:stop_apps([emqx_ocpp]).
set_special_cfg(emqx) ->
application:set_env(emqx, allow_anonymous, true),
application:set_env(emqx, enable_acl_cache, false),
LoadedPluginPath = filename:join(["test", "emqx_SUITE_data", "loaded_plugins"]),
application:set_env(
emqx,
plugins_loaded_file,
emqx_ct_helpers:deps_path(emqx, LoadedPluginPath)
);
set_special_cfg(_App) ->
ok.
%%--------------------------------------------------------------------
%% Testcases
%%---------------------------------------------------------------------

View File

@ -0,0 +1,38 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_ocpp_conf_SUITE).
-compile(export_all).
-compile(nowarn_export_all).
all() -> emqx_ct:all(?MODULE).
init_per_suite(Conf) ->
Conf.
end_per_suite(_Conf) ->
ok.
%%--------------------------------------------------------------------
%% cases
%%--------------------------------------------------------------------
t_load_unload(_) ->
ok.
t_get_env(_) ->
ok.

View File

@ -0,0 +1,39 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_ocpp_frame_SUITE).
-compile(export_all).
-compile(nowarn_export_all).
-include_lib("emqx_tcp.hrl").
-include_lib("emqx/include/emqx.hrl").
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
all() ->
emqx_ct:all(?MODULE).
init_per_suite(Conf) ->
Conf.
end_per_suite(_Config) ->
ok.
%%--------------------------------------------------------------------
%% cases
%%---------------------------------------------------------------------

View File

@ -0,0 +1,61 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_ocpp_keepalive_SUITE).
-compile(export_all).
-compile(nowarn_export_all).
-include_lib("eunit/include/eunit.hrl").
all() -> emqx_ct:all(?MODULE).
t_check(_) ->
Keepalive = emqx_ocpp_keepalive:init(60),
?assertEqual(60, emqx_ocpp_keepalive:info(interval, Keepalive)),
?assertEqual(0, emqx_ocpp_keepalive:info(statval, Keepalive)),
?assertEqual(0, emqx_ocpp_keepalive:info(repeat, Keepalive)),
?assertEqual(1, emqx_ocpp_keepalive:info(max_repeat, Keepalive)),
Info = emqx_ocpp_keepalive:info(Keepalive),
?assertEqual(
#{
interval => 60,
statval => 0,
repeat => 0,
max_repeat => 1
},
Info
),
{ok, Keepalive1} = emqx_ocpp_keepalive:check(1, Keepalive),
?assertEqual(1, emqx_ocpp_keepalive:info(statval, Keepalive1)),
?assertEqual(0, emqx_ocpp_keepalive:info(repeat, Keepalive1)),
{ok, Keepalive2} = emqx_ocpp_keepalive:check(1, Keepalive1),
?assertEqual(1, emqx_ocpp_keepalive:info(statval, Keepalive2)),
?assertEqual(1, emqx_ocpp_keepalive:info(repeat, Keepalive2)),
?assertEqual({error, timeout}, emqx_ocpp_keepalive:check(1, Keepalive2)).
t_check_max_repeat(_) ->
Keepalive = emqx_ocpp_keepalive:init(60, 2),
{ok, Keepalive1} = emqx_ocpp_keepalive:check(1, Keepalive),
?assertEqual(1, emqx_ocpp_keepalive:info(statval, Keepalive1)),
?assertEqual(0, emqx_ocpp_keepalive:info(repeat, Keepalive1)),
{ok, Keepalive2} = emqx_ocpp_keepalive:check(1, Keepalive1),
?assertEqual(1, emqx_ocpp_keepalive:info(statval, Keepalive2)),
?assertEqual(1, emqx_ocpp_keepalive:info(repeat, Keepalive2)),
{ok, Keepalive3} = emqx_ocpp_keepalive:check(1, Keepalive2),
?assertEqual(1, emqx_ocpp_keepalive:info(statval, Keepalive3)),
?assertEqual(2, emqx_ocpp_keepalive:info(repeat, Keepalive3)),
?assertEqual({error, timeout}, emqx_ocpp_keepalive:check(1, Keepalive3)).

View File

@ -126,7 +126,8 @@
emqx_dashboard_rbac,
emqx_dashboard_sso,
emqx_audit,
emqx_gateway_gbt32960
emqx_gateway_gbt32960,
emqx_gateway_ocpp
],
%% must always be of type `load'
ce_business_apps =>

View File

@ -216,7 +216,8 @@ defmodule EMQXUmbrella.MixProject do
:emqx_dashboard_rbac,
:emqx_dashboard_sso,
:emqx_audit,
:emqx_gateway_gbt32960
:emqx_gateway_gbt32960,
:emqx_gateway_ocpp
])
end

View File

@ -112,6 +112,7 @@ is_community_umbrella_app("apps/emqx_dashboard_rbac") -> false;
is_community_umbrella_app("apps/emqx_dashboard_sso") -> false;
is_community_umbrella_app("apps/emqx_audit") -> false;
is_community_umbrella_app("apps/emqx_gateway_gbt32960") -> false;
is_community_umbrella_app("apps/emqx_gateway_ocpp") -> false;
is_community_umbrella_app(_) -> true.
is_jq_supported() ->

View File

@ -0,0 +1,64 @@
##--------------------------------------------------------------------
## Gateway OCPP
##
## Add a OCPP-J gateway
##--------------------------------------------------------------------
## Note: This is an example of how to configure this feature
## you should copy and paste the below data into the emqx.conf for working
gateway.ocpp {
## When publishing or subscribing, prefix all topics with a mountpoint string.
## It's a way that you can use to implement isolation of message routing between different
## gateway protocols
mountpoint = "ocpp/"
## The default Heartbeat time interval
default_heartbeat_interval = "60s"
## The backoff for hearbeat checking times
heartbeat_checking_times_backoff = 1
## Whether to enable message format legality checking.
## EMQX checks the message format of the upstream and dnstream against the
## format defined in json-schema.
## When the check fails, emqx will reply with a corresponding answer message.
##
## Enum with:
## - all: check all messages
## - upstream_only: check upstream messages only
## - dnstream_only: check dnstream messages only
## - disable: don't check any messages
message_format_checking = disable
## Upload stream topic to notify third-party system whats messges/events
## reported by Charge Point
##
## Avaiable placeholders:
## - cid: Charge Point ID
## - clientid: Equal to Charge Point ID
## - action: Message Name in OCPP
upstream {
topic = "cp/${clientid}"
## UpStream topic override mapping by Message Name
topic_override_mapping {
#"BootNotification" = "cp/${clientid}/Notify/BootNotification"
}
reply_topic = "cp/${clientid}/Reply"
error_topic = "cp/${clientid}/Reply"
}
dnstream {
## Download stream topic to receive request/control messages from third-party
## system.
##
## This value is a wildcard topic name that subscribed by every connected Charge
## Point.
topic = "cs/${clientid}"
}
listeners.ws.default {
bind = "0.0.0.0:33033"
path = "/ocpp"
}
}