Merge pull request #11885 from HJianBo/ocpp-gw
feat: Port OCPP gateway from v4
This commit is contained in:
commit
6500d21d98
|
@ -380,7 +380,8 @@ fields(Gw) when
|
||||||
Gw == coap;
|
Gw == coap;
|
||||||
Gw == lwm2m;
|
Gw == lwm2m;
|
||||||
Gw == exproto;
|
Gw == exproto;
|
||||||
Gw == gbt32960
|
Gw == gbt32960;
|
||||||
|
Gw == ocpp
|
||||||
->
|
->
|
||||||
[{name, mk(Gw, #{desc => ?DESC(gateway_name)})}] ++
|
[{name, mk(Gw, #{desc => ?DESC(gateway_name)})}] ++
|
||||||
convert_listener_struct(emqx_gateway_schema:gateway_schema(Gw));
|
convert_listener_struct(emqx_gateway_schema:gateway_schema(Gw));
|
||||||
|
@ -390,7 +391,8 @@ fields(Gw) when
|
||||||
Gw == update_coap;
|
Gw == update_coap;
|
||||||
Gw == update_lwm2m;
|
Gw == update_lwm2m;
|
||||||
Gw == update_exproto;
|
Gw == update_exproto;
|
||||||
Gw == update_gbt32960
|
Gw == update_gbt32960;
|
||||||
|
Gw == update_ocpp
|
||||||
->
|
->
|
||||||
"update_" ++ GwStr = atom_to_list(Gw),
|
"update_" ++ GwStr = atom_to_list(Gw),
|
||||||
Gw1 = list_to_existing_atom(GwStr),
|
Gw1 = list_to_existing_atom(GwStr),
|
||||||
|
@ -399,14 +401,18 @@ fields(Listener) when
|
||||||
Listener == tcp_listener;
|
Listener == tcp_listener;
|
||||||
Listener == ssl_listener;
|
Listener == ssl_listener;
|
||||||
Listener == udp_listener;
|
Listener == udp_listener;
|
||||||
Listener == dtls_listener
|
Listener == dtls_listener;
|
||||||
|
Listener == ws_listener;
|
||||||
|
Listener == wss_listener
|
||||||
->
|
->
|
||||||
Type =
|
Type =
|
||||||
case Listener of
|
case Listener of
|
||||||
tcp_listener -> tcp;
|
tcp_listener -> tcp;
|
||||||
ssl_listener -> ssl;
|
ssl_listener -> ssl;
|
||||||
udp_listener -> udp;
|
udp_listener -> udp;
|
||||||
dtls_listener -> dtls
|
dtls_listener -> dtls;
|
||||||
|
ws_listener -> ws;
|
||||||
|
wss_listener -> wss
|
||||||
end,
|
end,
|
||||||
[
|
[
|
||||||
{id,
|
{id,
|
||||||
|
@ -492,14 +498,18 @@ listeners_schema(?R_REF(_Mod, tcp_udp_listeners)) ->
|
||||||
ref(udp_listener),
|
ref(udp_listener),
|
||||||
ref(dtls_listener)
|
ref(dtls_listener)
|
||||||
])
|
])
|
||||||
).
|
);
|
||||||
|
listeners_schema(?R_REF(_Mod, ws_listeners)) ->
|
||||||
|
hoconsc:array(hoconsc:union([ref(ws_listener), ref(wss_listener)])).
|
||||||
|
|
||||||
listener_schema() ->
|
listener_schema() ->
|
||||||
hoconsc:union([
|
hoconsc:union([
|
||||||
ref(?MODULE, tcp_listener),
|
ref(?MODULE, tcp_listener),
|
||||||
ref(?MODULE, ssl_listener),
|
ref(?MODULE, ssl_listener),
|
||||||
ref(?MODULE, udp_listener),
|
ref(?MODULE, udp_listener),
|
||||||
ref(?MODULE, dtls_listener)
|
ref(?MODULE, dtls_listener),
|
||||||
|
ref(?MODULE, ws_listener),
|
||||||
|
ref(?MODULE, wss_listener)
|
||||||
]).
|
]).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
@ -770,6 +780,35 @@ examples_gateway_confs() ->
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
ocpp_gateway =>
|
||||||
|
#{
|
||||||
|
summary => <<"A simple OCPP gateway config">>,
|
||||||
|
vaule =>
|
||||||
|
#{
|
||||||
|
enable => true,
|
||||||
|
name => <<"ocpp">>,
|
||||||
|
enable_stats => true,
|
||||||
|
mountpoint => <<"ocpp/">>,
|
||||||
|
default_heartbeat_interval => <<"60s">>,
|
||||||
|
upstream =>
|
||||||
|
#{
|
||||||
|
topic => <<"cp/${cid}">>,
|
||||||
|
reply_topic => <<"cp/${cid}/reply">>,
|
||||||
|
error_topic => <<"cp/${cid}/error">>
|
||||||
|
},
|
||||||
|
dnstream => #{topic => <<"cp/${cid}">>},
|
||||||
|
message_format_checking => disable,
|
||||||
|
listeners =>
|
||||||
|
[
|
||||||
|
#{
|
||||||
|
type => <<"ws">>,
|
||||||
|
name => <<"default">>,
|
||||||
|
bind => <<"33033">>,
|
||||||
|
max_connections => 1024000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
|
@ -881,5 +920,24 @@ examples_update_gateway_confs() ->
|
||||||
max_retry_times => 3,
|
max_retry_times => 3,
|
||||||
message_queue_len => 10
|
message_queue_len => 10
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
ocpp_gateway =>
|
||||||
|
#{
|
||||||
|
summary => <<"A simple OCPP gateway config">>,
|
||||||
|
vaule =>
|
||||||
|
#{
|
||||||
|
enable => true,
|
||||||
|
enable_stats => true,
|
||||||
|
mountpoint => <<"ocpp/">>,
|
||||||
|
default_heartbeat_interval => <<"60s">>,
|
||||||
|
upstream =>
|
||||||
|
#{
|
||||||
|
topic => <<"cp/${cid}">>,
|
||||||
|
reply_topic => <<"cp/${cid}/reply">>,
|
||||||
|
error_topic => <<"cp/${cid}/error">>
|
||||||
|
},
|
||||||
|
dnstream => #{topic => <<"cp/${cid}">>},
|
||||||
|
message_format_checking => disable
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
|
@ -160,10 +160,10 @@ cluster_gateway_status(GwName) ->
|
||||||
max_connections_count(Config) ->
|
max_connections_count(Config) ->
|
||||||
Listeners = emqx_gateway_utils:normalize_config(Config),
|
Listeners = emqx_gateway_utils:normalize_config(Config),
|
||||||
lists:foldl(
|
lists:foldl(
|
||||||
fun({_, _, _, SocketOpts, _}, Acc) ->
|
fun({_, _, _, Conf0}, Acc) ->
|
||||||
emqx_gateway_utils:plus_max_connections(
|
emqx_gateway_utils:plus_max_connections(
|
||||||
Acc,
|
Acc,
|
||||||
proplists:get_value(max_connections, SocketOpts, 0)
|
maps:get(max_connections, Conf0, 0)
|
||||||
)
|
)
|
||||||
end,
|
end,
|
||||||
0,
|
0,
|
||||||
|
@ -184,7 +184,7 @@ current_connections_count(GwName) ->
|
||||||
get_listeners_status(GwName, Config) ->
|
get_listeners_status(GwName, Config) ->
|
||||||
Listeners = emqx_gateway_utils:normalize_config(Config),
|
Listeners = emqx_gateway_utils:normalize_config(Config),
|
||||||
lists:map(
|
lists:map(
|
||||||
fun({Type, LisName, ListenOn, _, _}) ->
|
fun({Type, LisName, ListenOn, _}) ->
|
||||||
Name0 = listener_id(GwName, Type, LisName),
|
Name0 = listener_id(GwName, Type, LisName),
|
||||||
Name = {Name0, ListenOn},
|
Name = {Name0, ListenOn},
|
||||||
LisO = #{id => Name0, type => Type, name => LisName},
|
LisO = #{id => Name0, type => Type, name => LisName},
|
||||||
|
|
|
@ -56,6 +56,8 @@
|
||||||
|
|
||||||
-export([mountpoint/0, mountpoint/1, gateway_common_options/0, gateway_schema/1, gateway_names/0]).
|
-export([mountpoint/0, mountpoint/1, gateway_common_options/0, gateway_schema/1, gateway_names/0]).
|
||||||
|
|
||||||
|
-export([ws_listener/0, wss_listener/0, ws_opts/2]).
|
||||||
|
|
||||||
namespace() -> gateway.
|
namespace() -> gateway.
|
||||||
|
|
||||||
tags() ->
|
tags() ->
|
||||||
|
@ -127,6 +129,10 @@ fields(ssl_listener) ->
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
];
|
];
|
||||||
|
fields(ws_listener) ->
|
||||||
|
ws_listener() ++ ws_opts(<<>>, <<>>);
|
||||||
|
fields(wss_listener) ->
|
||||||
|
wss_listener() ++ ws_opts(<<>>, <<>>);
|
||||||
fields(udp_listener) ->
|
fields(udp_listener) ->
|
||||||
[
|
[
|
||||||
%% some special configs for udp listener
|
%% some special configs for udp listener
|
||||||
|
@ -250,6 +256,134 @@ mountpoint(Default) ->
|
||||||
}
|
}
|
||||||
).
|
).
|
||||||
|
|
||||||
|
ws_listener() ->
|
||||||
|
[
|
||||||
|
{acceptors, sc(integer(), #{default => 16, desc => ?DESC(tcp_listener_acceptors)})}
|
||||||
|
] ++
|
||||||
|
tcp_opts() ++
|
||||||
|
proxy_protocol_opts() ++
|
||||||
|
common_listener_opts().
|
||||||
|
|
||||||
|
wss_listener() ->
|
||||||
|
ws_listener() ++
|
||||||
|
[
|
||||||
|
{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) when
|
||||||
|
is_binary(DefaultPath), is_binary(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(emqx_schema, "deflate_opts"),
|
||||||
|
#{}
|
||||||
|
)}
|
||||||
|
].
|
||||||
|
|
||||||
common_listener_opts() ->
|
common_listener_opts() ->
|
||||||
[
|
[
|
||||||
{enable,
|
{enable,
|
||||||
|
@ -328,7 +462,7 @@ proxy_protocol_opts() ->
|
||||||
sc(
|
sc(
|
||||||
duration(),
|
duration(),
|
||||||
#{
|
#{
|
||||||
default => <<"15s">>,
|
default => <<"3s">>,
|
||||||
desc => ?DESC(tcp_listener_proxy_protocol_timeout)
|
desc => ?DESC(tcp_listener_proxy_protocol_timeout)
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
|
@ -337,7 +471,6 @@ proxy_protocol_opts() ->
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% dynamic schemas
|
%% dynamic schemas
|
||||||
|
|
||||||
%% FIXME: don't hardcode the gateway names
|
|
||||||
gateway_schema(Name) ->
|
gateway_schema(Name) ->
|
||||||
case emqx_gateway_utils:find_gateway_definition(Name) of
|
case emqx_gateway_utils:find_gateway_definition(Name) of
|
||||||
{ok, #{config_schema_module := SchemaMod}} ->
|
{ok, #{config_schema_module := SchemaMod}} ->
|
||||||
|
|
|
@ -82,6 +82,11 @@
|
||||||
max_mailbox_size => 32000
|
max_mailbox_size => 32000
|
||||||
}).
|
}).
|
||||||
|
|
||||||
|
-define(IS_ESOCKD_LISTENER(T),
|
||||||
|
T == tcp orelse T == ssl orelse T == udp orelse T == dtls
|
||||||
|
).
|
||||||
|
-define(IS_COWBOY_LISTENER(T), T == ws orelse T == wss).
|
||||||
|
|
||||||
-elvis([{elvis_style, god_modules, disable}]).
|
-elvis([{elvis_style, god_modules, disable}]).
|
||||||
|
|
||||||
-spec childspec(supervisor:worker(), Mod :: atom()) ->
|
-spec childspec(supervisor:worker(), Mod :: atom()) ->
|
||||||
|
@ -135,7 +140,7 @@ find_sup_child(Sup, ChildId) ->
|
||||||
{ok, [pid()]}
|
{ok, [pid()]}
|
||||||
| {error, term()}
|
| {error, term()}
|
||||||
when
|
when
|
||||||
ModCfg :: #{frame_mod := atom(), chann_mod := atom()}.
|
ModCfg :: #{frame_mod := atom(), chann_mod := atom(), connection_mod => atom()}.
|
||||||
start_listeners(Listeners, GwName, Ctx, ModCfg) ->
|
start_listeners(Listeners, GwName, Ctx, ModCfg) ->
|
||||||
start_listeners(Listeners, GwName, Ctx, ModCfg, []).
|
start_listeners(Listeners, GwName, Ctx, ModCfg, []).
|
||||||
|
|
||||||
|
@ -167,13 +172,12 @@ start_listeners([L | Ls], GwName, Ctx, ModCfg, Acc) ->
|
||||||
start_listener(
|
start_listener(
|
||||||
GwName,
|
GwName,
|
||||||
Ctx,
|
Ctx,
|
||||||
{Type, LisName, ListenOn, SocketOpts, Cfg},
|
{Type, LisName, ListenOn, Cfg},
|
||||||
ModCfg
|
ModCfg
|
||||||
) ->
|
) ->
|
||||||
ListenOnStr = emqx_listeners:format_bind(ListenOn),
|
ListenOnStr = emqx_listeners:format_bind(ListenOn),
|
||||||
ListenerId = emqx_gateway_utils:listener_id(GwName, Type, LisName),
|
ListenerId = emqx_gateway_utils:listener_id(GwName, Type, LisName),
|
||||||
|
|
||||||
NCfg = maps:merge(Cfg, ModCfg),
|
|
||||||
case
|
case
|
||||||
start_listener(
|
start_listener(
|
||||||
GwName,
|
GwName,
|
||||||
|
@ -181,8 +185,8 @@ start_listener(
|
||||||
Type,
|
Type,
|
||||||
LisName,
|
LisName,
|
||||||
ListenOn,
|
ListenOn,
|
||||||
SocketOpts,
|
Cfg,
|
||||||
NCfg
|
ModCfg
|
||||||
)
|
)
|
||||||
of
|
of
|
||||||
{ok, Pid} ->
|
{ok, Pid} ->
|
||||||
|
@ -199,15 +203,69 @@ start_listener(
|
||||||
emqx_gateway_utils:supervisor_ret({error, Reason})
|
emqx_gateway_utils:supervisor_ret({error, Reason})
|
||||||
end.
|
end.
|
||||||
|
|
||||||
start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) ->
|
start_listener(GwName, Ctx, Type, LisName, ListenOn, Confs, ModCfg) when
|
||||||
|
?IS_ESOCKD_LISTENER(Type)
|
||||||
|
->
|
||||||
Name = emqx_gateway_utils:listener_id(GwName, Type, LisName),
|
Name = emqx_gateway_utils:listener_id(GwName, Type, LisName),
|
||||||
NCfg = Cfg#{
|
SocketOpts = merge_default(Type, esockd_opts(Type, Confs)),
|
||||||
|
HighLevelCfgs0 = filter_out_low_level_opts(Type, Confs),
|
||||||
|
HighLevelCfgs = maps:merge(
|
||||||
|
HighLevelCfgs0,
|
||||||
|
ModCfg#{
|
||||||
ctx => Ctx,
|
ctx => Ctx,
|
||||||
listener => {GwName, Type, LisName}
|
listener => {GwName, Type, LisName}
|
||||||
},
|
}
|
||||||
NSocketOpts = merge_default(Type, SocketOpts),
|
),
|
||||||
MFA = {emqx_gateway_conn, start_link, [NCfg]},
|
ConnMod = maps:get(connection_mod, ModCfg, emqx_gateway_conn),
|
||||||
do_start_listener(Type, Name, ListenOn, NSocketOpts, MFA).
|
MFA = {ConnMod, start_link, [HighLevelCfgs]},
|
||||||
|
do_start_listener(Type, Name, ListenOn, SocketOpts, MFA);
|
||||||
|
start_listener(GwName, Ctx, Type, LisName, ListenOn, Confs, ModCfg) when
|
||||||
|
?IS_COWBOY_LISTENER(Type)
|
||||||
|
->
|
||||||
|
Name = emqx_gateway_utils:listener_id(GwName, Type, LisName),
|
||||||
|
RanchOpts = ranch_opts(Type, ListenOn, Confs),
|
||||||
|
HighLevelCfgs0 = filter_out_low_level_opts(Type, Confs),
|
||||||
|
HighLevelCfgs = maps:merge(
|
||||||
|
HighLevelCfgs0,
|
||||||
|
ModCfg#{
|
||||||
|
ctx => Ctx,
|
||||||
|
listener => {GwName, Type, LisName}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
WsOpts = ws_opts(Confs, HighLevelCfgs),
|
||||||
|
case Type of
|
||||||
|
ws -> cowboy:start_clear(Name, RanchOpts, WsOpts);
|
||||||
|
wss -> cowboy:start_tls(Name, RanchOpts, WsOpts)
|
||||||
|
end.
|
||||||
|
|
||||||
|
filter_out_low_level_opts(Type, RawCfg = #{gw_conf := Conf0}) when ?IS_ESOCKD_LISTENER(Type) ->
|
||||||
|
EsockdKeys = [
|
||||||
|
gw_conf,
|
||||||
|
bind,
|
||||||
|
acceptors,
|
||||||
|
max_connections,
|
||||||
|
max_conn_rate,
|
||||||
|
tcp_options,
|
||||||
|
ssl_options,
|
||||||
|
udp_options,
|
||||||
|
dtls_options
|
||||||
|
],
|
||||||
|
Conf1 = maps:without(EsockdKeys, RawCfg),
|
||||||
|
maps:merge(Conf0, Conf1);
|
||||||
|
filter_out_low_level_opts(Type, RawCfg = #{gw_conf := Conf0}) when ?IS_COWBOY_LISTENER(Type) ->
|
||||||
|
CowboyKeys = [
|
||||||
|
gw_conf,
|
||||||
|
bind,
|
||||||
|
acceptors,
|
||||||
|
max_connections,
|
||||||
|
max_conn_rate,
|
||||||
|
tcp_options,
|
||||||
|
ssl_options,
|
||||||
|
udp_options,
|
||||||
|
dtls_options
|
||||||
|
],
|
||||||
|
Conf1 = maps:without(CowboyKeys, RawCfg),
|
||||||
|
maps:merge(Conf0, Conf1).
|
||||||
|
|
||||||
merge_default(Udp, Options) ->
|
merge_default(Udp, Options) ->
|
||||||
{Key, Default} =
|
{Key, Default} =
|
||||||
|
@ -246,8 +304,8 @@ stop_listeners(GwName, Listeners) ->
|
||||||
lists:foreach(fun(L) -> stop_listener(GwName, L) end, Listeners).
|
lists:foreach(fun(L) -> stop_listener(GwName, L) end, Listeners).
|
||||||
|
|
||||||
-spec stop_listener(GwName :: atom(), Listener :: tuple()) -> ok.
|
-spec stop_listener(GwName :: atom(), Listener :: tuple()) -> ok.
|
||||||
stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) ->
|
stop_listener(GwName, {Type, LisName, ListenOn, Cfg}) ->
|
||||||
StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg),
|
StopRet = stop_listener(GwName, Type, LisName, ListenOn, Cfg),
|
||||||
ListenOnStr = emqx_listeners:format_bind(ListenOn),
|
ListenOnStr = emqx_listeners:format_bind(ListenOn),
|
||||||
case StopRet of
|
case StopRet of
|
||||||
ok ->
|
ok ->
|
||||||
|
@ -263,7 +321,7 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) ->
|
||||||
end,
|
end,
|
||||||
StopRet.
|
StopRet.
|
||||||
|
|
||||||
stop_listener(GwName, Type, LisName, ListenOn, _SocketOpts, _Cfg) ->
|
stop_listener(GwName, Type, LisName, ListenOn, _Cfg) ->
|
||||||
Name = emqx_gateway_utils:listener_id(GwName, Type, LisName),
|
Name = emqx_gateway_utils:listener_id(GwName, Type, LisName),
|
||||||
esockd:close(Name, ListenOn).
|
esockd:close(Name, ListenOn).
|
||||||
|
|
||||||
|
@ -380,8 +438,7 @@ stringfy(T) ->
|
||||||
Type :: udp | tcp | ssl | dtls,
|
Type :: udp | tcp | ssl | dtls,
|
||||||
Name :: atom(),
|
Name :: atom(),
|
||||||
ListenOn :: esockd:listen_on(),
|
ListenOn :: esockd:listen_on(),
|
||||||
SocketOpts :: esockd:option(),
|
RawCfg :: map()
|
||||||
Cfg :: map()
|
|
||||||
}).
|
}).
|
||||||
normalize_config(RawConf) ->
|
normalize_config(RawConf) ->
|
||||||
LisMap = maps:get(listeners, RawConf, #{}),
|
LisMap = maps:get(listeners, RawConf, #{}),
|
||||||
|
@ -393,14 +450,7 @@ normalize_config(RawConf) ->
|
||||||
maps:fold(
|
maps:fold(
|
||||||
fun(Name, Confs, AccIn2) ->
|
fun(Name, Confs, AccIn2) ->
|
||||||
ListenOn = maps:get(bind, Confs),
|
ListenOn = maps:get(bind, Confs),
|
||||||
SocketOpts = esockd_opts(Type, Confs),
|
[{Type, Name, ListenOn, Confs#{gw_conf => Cfg0}} | AccIn2]
|
||||||
RemainCfgs = maps:without(
|
|
||||||
[bind, tcp, ssl, udp, dtls] ++
|
|
||||||
proplists:get_keys(SocketOpts),
|
|
||||||
Confs
|
|
||||||
),
|
|
||||||
Cfg = maps:merge(Cfg0, RemainCfgs),
|
|
||||||
[{Type, Name, ListenOn, SocketOpts, Cfg} | AccIn2]
|
|
||||||
end,
|
end,
|
||||||
[],
|
[],
|
||||||
Liss
|
Liss
|
||||||
|
@ -412,7 +462,7 @@ normalize_config(RawConf) ->
|
||||||
)
|
)
|
||||||
).
|
).
|
||||||
|
|
||||||
esockd_opts(Type, Opts0) ->
|
esockd_opts(Type, Opts0) when ?IS_ESOCKD_LISTENER(Type) ->
|
||||||
Opts1 = maps:with(
|
Opts1 = maps:with(
|
||||||
[
|
[
|
||||||
acceptors,
|
acceptors,
|
||||||
|
@ -427,37 +477,70 @@ esockd_opts(Type, Opts0) ->
|
||||||
maps:to_list(
|
maps:to_list(
|
||||||
case Type of
|
case Type of
|
||||||
tcp ->
|
tcp ->
|
||||||
Opts2#{tcp_options => sock_opts(tcp, Opts0)};
|
Opts2#{tcp_options => sock_opts(tcp_options, Opts0)};
|
||||||
ssl ->
|
ssl ->
|
||||||
Opts2#{
|
Opts2#{
|
||||||
tcp_options => sock_opts(tcp, Opts0),
|
tcp_options => sock_opts(tcp_options, Opts0),
|
||||||
ssl_options => ssl_opts(ssl, Opts0)
|
ssl_options => ssl_opts(ssl_options, Opts0)
|
||||||
};
|
};
|
||||||
udp ->
|
udp ->
|
||||||
Opts2#{udp_options => sock_opts(udp, Opts0)};
|
Opts2#{udp_options => sock_opts(udp_options, Opts0)};
|
||||||
dtls ->
|
dtls ->
|
||||||
Opts2#{
|
Opts2#{
|
||||||
udp_options => sock_opts(udp, Opts0),
|
udp_options => sock_opts(udp_options, Opts0),
|
||||||
dtls_options => ssl_opts(dtls, Opts0)
|
dtls_options => ssl_opts(dtls_options, Opts0)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
).
|
).
|
||||||
|
|
||||||
|
sock_opts(Name, Opts) ->
|
||||||
|
maps:to_list(
|
||||||
|
maps:without(
|
||||||
|
[active_n, keepalive],
|
||||||
|
maps:get(Name, Opts, #{})
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
ssl_opts(Name, Opts) ->
|
ssl_opts(Name, Opts) ->
|
||||||
Type =
|
Type =
|
||||||
case Name of
|
case Name of
|
||||||
ssl -> tls;
|
ssl_options -> tls;
|
||||||
dtls -> dtls
|
dtls_options -> dtls
|
||||||
end,
|
end,
|
||||||
emqx_tls_lib:to_server_opts(Type, maps:get(Name, Opts, #{})).
|
emqx_tls_lib:to_server_opts(Type, maps:get(Name, Opts, #{})).
|
||||||
|
|
||||||
sock_opts(Name, Opts) ->
|
ranch_opts(Type, ListenOn, Opts) ->
|
||||||
maps:to_list(
|
NumAcceptors = maps:get(acceptors, Opts, 4),
|
||||||
maps:without(
|
MaxConnections = maps:get(max_connections, Opts, 1024),
|
||||||
[active_n],
|
SocketOpts1 =
|
||||||
maps:get(Name, Opts, #{})
|
case Type of
|
||||||
)
|
wss ->
|
||||||
).
|
sock_opts(tcp_options, Opts) ++
|
||||||
|
proplists:delete(handshake_timeout, ssl_opts(ssl_options, Opts));
|
||||||
|
ws ->
|
||||||
|
sock_opts(tcp_options, Opts)
|
||||||
|
end,
|
||||||
|
SocketOpts = ip_port(ListenOn) ++ proplists:delete(reuseaddr, SocketOpts1),
|
||||||
|
#{
|
||||||
|
num_acceptors => NumAcceptors,
|
||||||
|
max_connections => MaxConnections,
|
||||||
|
handshake_timeout => maps:get(handshake_timeout, Opts, 15000),
|
||||||
|
socket_opts => SocketOpts
|
||||||
|
}.
|
||||||
|
|
||||||
|
ws_opts(Opts, Conf) ->
|
||||||
|
ConnMod = maps:get(connection_mod, Conf, emqx_gateway_conn),
|
||||||
|
WsPaths = [
|
||||||
|
{emqx_utils_maps:deep_get([websocket, path], Opts, "") ++ "/[...]", ConnMod, Conf}
|
||||||
|
],
|
||||||
|
Dispatch = cowboy_router:compile([{'_', WsPaths}]),
|
||||||
|
ProxyProto = maps:get(proxy_protocol, Opts, false),
|
||||||
|
#{env => #{dispatch => Dispatch}, proxy_header => ProxyProto}.
|
||||||
|
|
||||||
|
ip_port(Port) when is_integer(Port) ->
|
||||||
|
[{port, Port}];
|
||||||
|
ip_port({Addr, Port}) ->
|
||||||
|
[{ip, Addr}, {port, Port}].
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Envs
|
%% Envs
|
||||||
|
@ -665,7 +748,9 @@ ensure_gateway_loaded() ->
|
||||||
emqx_gateway_stomp,
|
emqx_gateway_stomp,
|
||||||
emqx_gateway_coap,
|
emqx_gateway_coap,
|
||||||
emqx_gateway_lwm2m,
|
emqx_gateway_lwm2m,
|
||||||
emqx_gateway_mqttsn
|
emqx_gateway_mqttsn,
|
||||||
|
emqx_gateway_gbt32960,
|
||||||
|
emqx_gateway_ocpp
|
||||||
]
|
]
|
||||||
).
|
).
|
||||||
|
|
||||||
|
|
|
@ -74,13 +74,7 @@ end_per_testcase(_TestCase, _Config) ->
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
t_registered_gateway(_) ->
|
t_registered_gateway(_) ->
|
||||||
[
|
[{coap, #{cbkmod := emqx_gateway_coap}} | _] = emqx_gateway:registered_gateway().
|
||||||
{coap, #{cbkmod := emqx_gateway_coap}},
|
|
||||||
{exproto, #{cbkmod := emqx_gateway_exproto}},
|
|
||||||
{lwm2m, #{cbkmod := emqx_gateway_lwm2m}},
|
|
||||||
{mqttsn, #{cbkmod := emqx_gateway_mqttsn}},
|
|
||||||
{stomp, #{cbkmod := emqx_gateway_stomp}}
|
|
||||||
] = emqx_gateway:registered_gateway().
|
|
||||||
|
|
||||||
t_load_unload_list_lookup(_) ->
|
t_load_unload_list_lookup(_) ->
|
||||||
{ok, _} = emqx_gateway:load(?GWNAME, #{idle_timeout => 1000}),
|
{ok, _} = emqx_gateway:load(?GWNAME, #{idle_timeout => 1000}),
|
||||||
|
|
|
@ -45,7 +45,7 @@
|
||||||
-include_lib("common_test/include/ct.hrl").
|
-include_lib("common_test/include/ct.hrl").
|
||||||
-include_lib("emqx/include/emqx_placeholder.hrl").
|
-include_lib("emqx/include/emqx_placeholder.hrl").
|
||||||
|
|
||||||
-define(CALL(Msg), gen_server:call(?MODULE, {?FUNCTION_NAME, Msg})).
|
-define(CALL(Msg), gen_server:call(?MODULE, {?FUNCTION_NAME, Msg}, 15000)).
|
||||||
|
|
||||||
-define(AUTHN_HTTP_PORT, 37333).
|
-define(AUTHN_HTTP_PORT, 37333).
|
||||||
-define(AUTHN_HTTP_PATH, "/auth").
|
-define(AUTHN_HTTP_PATH, "/auth").
|
||||||
|
|
|
@ -118,14 +118,8 @@ t_gateway_registry_usage(_) ->
|
||||||
|
|
||||||
t_gateway_registry_list(_) ->
|
t_gateway_registry_list(_) ->
|
||||||
emqx_gateway_cli:'gateway-registry'(["list"]),
|
emqx_gateway_cli:'gateway-registry'(["list"]),
|
||||||
?assertEqual(
|
%% TODO: assert it.
|
||||||
"Registered Name: coap, Callback Module: emqx_gateway_coap\n"
|
_ = acc_print().
|
||||||
"Registered Name: exproto, Callback Module: emqx_gateway_exproto\n"
|
|
||||||
"Registered Name: lwm2m, Callback Module: emqx_gateway_lwm2m\n"
|
|
||||||
"Registered Name: mqttsn, Callback Module: emqx_gateway_mqttsn\n"
|
|
||||||
"Registered Name: stomp, Callback Module: emqx_gateway_stomp\n",
|
|
||||||
acc_print()
|
|
||||||
).
|
|
||||||
|
|
||||||
t_gateway_usage(_) ->
|
t_gateway_usage(_) ->
|
||||||
?assertEqual(
|
?assertEqual(
|
||||||
|
@ -142,14 +136,8 @@ t_gateway_usage(_) ->
|
||||||
|
|
||||||
t_gateway_list(_) ->
|
t_gateway_list(_) ->
|
||||||
emqx_gateway_cli:gateway(["list"]),
|
emqx_gateway_cli:gateway(["list"]),
|
||||||
?assertEqual(
|
%% TODO: assert it.
|
||||||
"Gateway(name=coap, status=unloaded)\n"
|
_ = acc_print(),
|
||||||
"Gateway(name=exproto, status=unloaded)\n"
|
|
||||||
"Gateway(name=lwm2m, status=unloaded)\n"
|
|
||||||
"Gateway(name=mqttsn, status=unloaded)\n"
|
|
||||||
"Gateway(name=stomp, status=unloaded)\n",
|
|
||||||
acc_print()
|
|
||||||
),
|
|
||||||
|
|
||||||
emqx_gateway_cli:gateway(["load", "mqttsn", ?CONF_MQTTSN]),
|
emqx_gateway_cli:gateway(["load", "mqttsn", ?CONF_MQTTSN]),
|
||||||
?assertEqual("ok\n", acc_print()),
|
?assertEqual("ok\n", acc_print()),
|
||||||
|
|
|
@ -636,18 +636,18 @@ close({dtls, Sock}) ->
|
||||||
%% Server-Opts
|
%% Server-Opts
|
||||||
|
|
||||||
socketopts(tcp) ->
|
socketopts(tcp) ->
|
||||||
#{tcp => tcp_opts()};
|
#{tcp_options => tcp_opts()};
|
||||||
socketopts(ssl) ->
|
socketopts(ssl) ->
|
||||||
#{
|
#{
|
||||||
tcp => tcp_opts(),
|
tcp_options => tcp_opts(),
|
||||||
ssl => ssl_opts()
|
ssl_options => ssl_opts()
|
||||||
};
|
};
|
||||||
socketopts(udp) ->
|
socketopts(udp) ->
|
||||||
#{udp => udp_opts()};
|
#{udp_options => udp_opts()};
|
||||||
socketopts(dtls) ->
|
socketopts(dtls) ->
|
||||||
#{
|
#{
|
||||||
udp => udp_opts(),
|
udp_options => udp_opts(),
|
||||||
dtls => dtls_opts()
|
dtls_options => dtls_opts()
|
||||||
}.
|
}.
|
||||||
|
|
||||||
tcp_opts() ->
|
tcp_opts() ->
|
||||||
|
|
|
@ -798,9 +798,11 @@ format(Msg) ->
|
||||||
io_lib:format("~p", [Msg]).
|
io_lib:format("~p", [Msg]).
|
||||||
|
|
||||||
type(_) ->
|
type(_) ->
|
||||||
|
%% TODO:
|
||||||
gbt32960.
|
gbt32960.
|
||||||
|
|
||||||
is_message(#frame{}) ->
|
is_message(#frame{}) ->
|
||||||
|
%% TODO:
|
||||||
true;
|
true;
|
||||||
is_message(_) ->
|
is_message(_) ->
|
||||||
false.
|
false.
|
||||||
|
|
|
@ -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
|
|
@ -0,0 +1,94 @@
|
||||||
|
Business Source License 1.1
|
||||||
|
|
||||||
|
Licensor: Hangzhou EMQ Technologies Co., Ltd.
|
||||||
|
Licensed Work: EMQX Enterprise Edition
|
||||||
|
The Licensed Work is (c) 2023
|
||||||
|
Hangzhou EMQ Technologies Co., Ltd.
|
||||||
|
Additional Use Grant: Students and educators are granted right to copy,
|
||||||
|
modify, and create derivative work for research
|
||||||
|
or education.
|
||||||
|
Change Date: 2027-02-01
|
||||||
|
Change License: Apache License, Version 2.0
|
||||||
|
|
||||||
|
For information about alternative licensing arrangements for the Software,
|
||||||
|
please contact Licensor: https://www.emqx.com/en/contact
|
||||||
|
|
||||||
|
Notice
|
||||||
|
|
||||||
|
The Business Source License (this document, or the “License”) is not an Open
|
||||||
|
Source license. However, the Licensed Work will eventually be made available
|
||||||
|
under an Open Source License, as stated in this License.
|
||||||
|
|
||||||
|
License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
|
||||||
|
“Business Source License” is a trademark of MariaDB Corporation Ab.
|
||||||
|
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Business Source License 1.1
|
||||||
|
|
||||||
|
Terms
|
||||||
|
|
||||||
|
The Licensor hereby grants you the right to copy, modify, create derivative
|
||||||
|
works, redistribute, and make non-production use of the Licensed Work. The
|
||||||
|
Licensor may make an Additional Use Grant, above, permitting limited
|
||||||
|
production use.
|
||||||
|
|
||||||
|
Effective on the Change Date, or the fourth anniversary of the first publicly
|
||||||
|
available distribution of a specific version of the Licensed Work under this
|
||||||
|
License, whichever comes first, the Licensor hereby grants you rights under
|
||||||
|
the terms of the Change License, and the rights granted in the paragraph
|
||||||
|
above terminate.
|
||||||
|
|
||||||
|
If your use of the Licensed Work does not comply with the requirements
|
||||||
|
currently in effect as described in this License, you must purchase a
|
||||||
|
commercial license from the Licensor, its affiliated entities, or authorized
|
||||||
|
resellers, or you must refrain from using the Licensed Work.
|
||||||
|
|
||||||
|
All copies of the original and modified Licensed Work, and derivative works
|
||||||
|
of the Licensed Work, are subject to this License. This License applies
|
||||||
|
separately for each version of the Licensed Work and the Change Date may vary
|
||||||
|
for each version of the Licensed Work released by Licensor.
|
||||||
|
|
||||||
|
You must conspicuously display this License on each original or modified copy
|
||||||
|
of the Licensed Work. If you receive the Licensed Work in original or
|
||||||
|
modified form from a third party, the terms and conditions set forth in this
|
||||||
|
License apply to your use of that work.
|
||||||
|
|
||||||
|
Any use of the Licensed Work in violation of this License will automatically
|
||||||
|
terminate your rights under this License for the current and all other
|
||||||
|
versions of the Licensed Work.
|
||||||
|
|
||||||
|
This License does not grant you any right in any trademark or logo of
|
||||||
|
Licensor or its affiliates (provided that you may use a trademark or logo of
|
||||||
|
Licensor as expressly required by this License).
|
||||||
|
|
||||||
|
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
|
||||||
|
AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
|
||||||
|
TITLE.
|
||||||
|
|
||||||
|
MariaDB hereby grants you permission to use this License’s text to license
|
||||||
|
your works, and to refer to it using the trademark “Business Source License”,
|
||||||
|
as long as you comply with the Covenants of Licensor below.
|
||||||
|
|
||||||
|
Covenants of Licensor
|
||||||
|
|
||||||
|
In consideration of the right to use this License’s text and the “Business
|
||||||
|
Source License” name and trademark, Licensor covenants to MariaDB, and to all
|
||||||
|
other recipients of the licensed work to be provided by Licensor:
|
||||||
|
|
||||||
|
1. To specify as the Change License the GPL Version 2.0 or any later version,
|
||||||
|
or a license that is compatible with GPL Version 2.0 or a later version,
|
||||||
|
where “compatible” means that software provided under the Change License can
|
||||||
|
be included in a program with software provided under GPL Version 2.0 or a
|
||||||
|
later version. Licensor may specify additional Change Licenses without
|
||||||
|
limitation.
|
||||||
|
|
||||||
|
2. To either: (a) specify an additional grant of rights to use that does not
|
||||||
|
impose any additional restriction on the right granted in this License, as
|
||||||
|
the Additional Use Grant; or (b) insert the text “None”.
|
||||||
|
|
||||||
|
3. To specify a Change Date.
|
||||||
|
|
||||||
|
4. Not to modify this License in any other way.
|
|
@ -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 ID:Charge 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:
|
||||||
|
```
|
|
@ -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
|
|
@ -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
|
||||||
|
}).
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{deps, [
|
||||||
|
{jesse, "1.7.0"},
|
||||||
|
{emqx, {path, "../../apps/emqx"}},
|
||||||
|
{emqx_utils, {path, "../emqx_utils"}},
|
||||||
|
{emqx_gateway, {path, "../../apps/emqx_gateway"}}
|
||||||
|
]}.
|
|
@ -0,0 +1,9 @@
|
||||||
|
{application, emqx_gateway_ocpp, [
|
||||||
|
{description, "OCPP-J 1.6 Gateway for EMQX"},
|
||||||
|
{vsn, "0.1.0"},
|
||||||
|
{registered, []},
|
||||||
|
{applications, [kernel, stdlib, jesse, emqx, emqx_gateway]},
|
||||||
|
{env, []},
|
||||||
|
{modules, []},
|
||||||
|
{links, []}
|
||||||
|
]}.
|
|
@ -0,0 +1,102 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% 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,
|
||||||
|
connection_mod => emqx_ocpp_connection
|
||||||
|
},
|
||||||
|
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).
|
|
@ -0,0 +1,894 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% 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(map()),
|
||||||
|
%% 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() :: 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{conninfo = ConnInfo}) ->
|
||||||
|
%% XXX:
|
||||||
|
#{
|
||||||
|
created_at => maps:get(connected_at, ConnInfo, undefined),
|
||||||
|
is_persistent => false,
|
||||||
|
subscriptions => #{},
|
||||||
|
upgrade_qos => false,
|
||||||
|
retry_interval => 0,
|
||||||
|
await_rel_timeout => 0
|
||||||
|
};
|
||||||
|
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(ctx, #channel{ctx = Ctx}) ->
|
||||||
|
Ctx;
|
||||||
|
info(timers, #channel{timers = Timers}) ->
|
||||||
|
Timers.
|
||||||
|
|
||||||
|
-spec stats(channel()) -> emqx_types:stats().
|
||||||
|
stats(#channel{mqueue = MQueue}) ->
|
||||||
|
%% XXX:
|
||||||
|
SessionStats = [
|
||||||
|
{subscriptions_cnt, 0},
|
||||||
|
{subscriptions_max, 0},
|
||||||
|
{inflight_cnt, 0},
|
||||||
|
{inflight_max, 0},
|
||||||
|
{mqueue_len, queue:len(MQueue)},
|
||||||
|
{mqueue_max, queue:len(MQueue)},
|
||||||
|
{mqueue_dropped, 0},
|
||||||
|
{next_pkt_id, 0},
|
||||||
|
{awaiting_rel_cnt, 0},
|
||||||
|
{awaiting_rel_max, 0}
|
||||||
|
],
|
||||||
|
lists:append(SessionStats, emqx_pd:get_counters(?CHANNEL_METRICS)).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Init the channel
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-spec init(emqx_types:conninfo(), map()) -> 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,
|
||||||
|
mountpoint := Mountpoint
|
||||||
|
},
|
||||||
|
conninfo = #{proto_ver := ProtoVer}
|
||||||
|
}
|
||||||
|
) when
|
||||||
|
is_map(Frame)
|
||||||
|
->
|
||||||
|
Topic0 = upstream_topic(Frame, Channel),
|
||||||
|
Topic = emqx_mountpoint:mount(Mountpoint, Topic0),
|
||||||
|
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),
|
||||||
|
proc_tmpl(
|
||||||
|
emqx_ocpp_conf:uptopic(Action),
|
||||||
|
Vars#{action => Action}
|
||||||
|
);
|
||||||
|
?OCPP_MSG_TYPE_ID_CALLRESULT ->
|
||||||
|
proc_tmpl(emqx_ocpp_conf:up_reply_topic(), Vars);
|
||||||
|
?OCPP_MSG_TYPE_ID_CALLERROR ->
|
||||||
|
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}),
|
||||||
|
{ok, 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, mountpoint := Mountpoint} = ClientInfo}
|
||||||
|
) ->
|
||||||
|
SubOpts = #{rh => 0, rap => 0, nl => 0, qos => ?QOS_1},
|
||||||
|
Topic0 = proc_tmpl(
|
||||||
|
emqx_ocpp_conf:dntopic(),
|
||||||
|
#{
|
||||||
|
clientid => ClientId,
|
||||||
|
cid => ClientId
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Topic = emqx_mountpoint:mount(Mountpoint, Topic0),
|
||||||
|
ok = emqx_broker:subscribe(Topic, ClientId, SubOpts),
|
||||||
|
ok = emqx_hooks:run('session.subscribed', [ClientInfo, Topic, SubOpts]),
|
||||||
|
Channel.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% 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 = #channel{clientinfo = ClientInfo}) ->
|
||||||
|
emqx_hooks:run('session.terminated', [ClientInfo, Reason, info(session, Channel)]).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% 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
|
||||||
|
}
|
||||||
|
) ->
|
||||||
|
NConnInfo = ConnInfo#{disconnected_at => erlang:system_time(millisecond)},
|
||||||
|
ok = run_hooks('client.disconnected', [ClientInfo, Reason, NConnInfo]),
|
||||||
|
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}.
|
||||||
|
|
||||||
|
proc_tmpl(Tmpl, Vars) ->
|
||||||
|
Tokens = emqx_placeholder:preproc_tmpl(Tmpl),
|
||||||
|
emqx_placeholder:proc_tmpl(Tokens, Vars).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% For CT tests
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
set_field(Name, Value, Channel) ->
|
||||||
|
Pos = emqx_utils:index_of(Name, record_info(fields, channel)),
|
||||||
|
setelement(Pos + 1, Channel, Value).
|
|
@ -0,0 +1,96 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% 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([
|
||||||
|
default_heartbeat_interval/0,
|
||||||
|
heartbeat_checking_times_backoff/0,
|
||||||
|
retry_interval/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(K), [gateway, ocpp, K]).
|
||||||
|
|
||||||
|
conf(K, Default) ->
|
||||||
|
emqx_config:get(?KEY(K), Default).
|
||||||
|
|
||||||
|
-spec default_heartbeat_interval() -> pos_integer().
|
||||||
|
default_heartbeat_interval() ->
|
||||||
|
conf(default_heartbeat_interval, 600).
|
||||||
|
|
||||||
|
-spec heartbeat_checking_times_backoff() -> pos_integer().
|
||||||
|
heartbeat_checking_times_backoff() ->
|
||||||
|
conf(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 message_format_checking() ->
|
||||||
|
all
|
||||||
|
| upstream_only
|
||||||
|
| dnstream_only
|
||||||
|
| disable.
|
||||||
|
message_format_checking() ->
|
||||||
|
conf(message_format_checking, all).
|
||||||
|
|
||||||
|
uptopic(Action) ->
|
||||||
|
Topic = upstream(topic),
|
||||||
|
Mapping = upstream(topic_override_mapping, #{}),
|
||||||
|
maps:get(Action, Mapping, Topic).
|
||||||
|
|
||||||
|
up_reply_topic() ->
|
||||||
|
upstream(reply_topic).
|
||||||
|
|
||||||
|
up_error_topic() ->
|
||||||
|
upstream(error_topic).
|
||||||
|
|
||||||
|
dntopic() ->
|
||||||
|
dnstream(topic).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% internal funcs
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
dnstream(K) ->
|
||||||
|
dnstream(K, undefined).
|
||||||
|
|
||||||
|
dnstream(K, Def) ->
|
||||||
|
emqx_config:get([gateway, ocpp, dnstream, K], Def).
|
||||||
|
|
||||||
|
upstream(K) ->
|
||||||
|
upstream(K, undefined).
|
||||||
|
|
||||||
|
upstream(K, Def) ->
|
||||||
|
emqx_config:get([gateway, ocpp, upstream, K], Def).
|
|
@ -0,0 +1,906 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% 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]}).
|
||||||
|
|
||||||
|
-elvis([
|
||||||
|
{elvis_style, invalid_dynamic_call, #{ignore => [emqx_ocpp_connection]}}
|
||||||
|
]).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% 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 = maps:get(idle_timeout, Opts, 7200000),
|
||||||
|
MaxFrameSize =
|
||||||
|
case maps:get(max_frame_size, Opts, 0) of
|
||||||
|
0 -> infinity;
|
||||||
|
I -> I
|
||||||
|
end,
|
||||||
|
Compress = emqx_utils_maps:deep_get([websocket, 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 = emqx_utils_maps:deep_get([websocket, 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
|
||||||
|
maps:get(proxy_protocol, Opts, false) 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 = emqx_utils_maps:deep_get([websocket, supported_subprotocols], Opts),
|
||||||
|
FailIfNoSubprotocol = emqx_utils_maps:deep_get([websocket, 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 = emqx_utils_maps:deep_get([websocket, 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 emqx_utils_maps:deep_get([websocket, allow_origin_absence], Opts) of
|
||||||
|
true -> ok;
|
||||||
|
false -> {error, origin_header_cannot_be_absent}
|
||||||
|
end;
|
||||||
|
Value ->
|
||||||
|
Origins = emqx_utils_maps:deep_get([websocket, 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 emqx_utils_maps:deep_get([websocket, 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{chann_mod = ChannMod, channel = Channel}) ->
|
||||||
|
Ctx = ChannMod:info(ctx, Channel),
|
||||||
|
ClientId = ChannMod:info(clientid, Channel),
|
||||||
|
emqx_gateway_ctx:insert_channel_info(
|
||||||
|
Ctx,
|
||||||
|
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{
|
||||||
|
chann_mod = ChannMod,
|
||||||
|
channel = Channel,
|
||||||
|
stats_timer = TRef
|
||||||
|
}
|
||||||
|
) ->
|
||||||
|
Ctx = ChannMod:info(ctx, Channel),
|
||||||
|
ClientId = ChannMod:info(clientid, Channel),
|
||||||
|
emqx_gateway_ctx:set_chan_stats(Ctx, 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(
|
||||||
|
emqx_utils_maps:deep_get([websocket, 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(
|
||||||
|
emqx_utils_maps:deep_get([websocket, 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).
|
|
@ -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.
|
|
@ -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}.
|
|
@ -0,0 +1,202 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% 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 => false,
|
||||||
|
desc => ?DESC(heartbeat_checking_times_backoff)
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
{upstream, sc(ref(upstream), #{})},
|
||||||
|
{dnstream, sc(ref(dnstream), #{})},
|
||||||
|
{message_format_checking,
|
||||||
|
sc(
|
||||||
|
hoconsc:union([all, upstream_only, dnstream_only, disable]),
|
||||||
|
#{
|
||||||
|
default => disable,
|
||||||
|
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(ws_listeners), #{})}
|
||||||
|
] ++ emqx_gateway_schema:gateway_common_options();
|
||||||
|
fields(ws_listeners) ->
|
||||||
|
[
|
||||||
|
{ws, sc(map(name, ref(ws_listener)), #{desc => ?DESC(ws)})},
|
||||||
|
{wss, sc(map(name, ref(wss_listener)), #{desc => ?DESC(wss)})}
|
||||||
|
];
|
||||||
|
fields(ws_listener) ->
|
||||||
|
emqx_gateway_schema:ws_listener() ++
|
||||||
|
[{websocket, sc(ref(websocket), #{})}];
|
||||||
|
fields(wss_listener) ->
|
||||||
|
emqx_gateway_schema:wss_listener() ++
|
||||||
|
[{websocket, sc(ref(websocket), #{})}];
|
||||||
|
fields(websocket) ->
|
||||||
|
DefaultPath = <<"/ocpp">>,
|
||||||
|
SubProtocols = <<"ocpp1.6, ocpp2.0">>,
|
||||||
|
emqx_gateway_schema:ws_opts(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(name, 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(upstream) ->
|
||||||
|
"Upload stream topic to notify third-party system what's messages/events reported by "
|
||||||
|
"Charge Point. Available placeholders:\n"
|
||||||
|
"- <code>cid</code>: Charge Point ID\n"
|
||||||
|
"- <code>clientid</code>: Equal to Charge Point ID\n"
|
||||||
|
"- <code>action</code>: Message Name in OCPP";
|
||||||
|
desc(dnstream) ->
|
||||||
|
"Download stream topic to forward the system message to device. Available placeholders:\n"
|
||||||
|
"- <code>cid</code>: Charge Point ID\n"
|
||||||
|
"- <code>clientid</code>: Equal to Charge Point ID\n"
|
||||||
|
"- <code>action</code>: Message Name in OCPP";
|
||||||
|
desc(ws_listeners) ->
|
||||||
|
"Websocket listeners";
|
||||||
|
desc(ws_listener) ->
|
||||||
|
"Websocket listener";
|
||||||
|
desc(wss_listener) ->
|
||||||
|
"Websocket over TLS listener";
|
||||||
|
desc(websocket) ->
|
||||||
|
"Websocket options";
|
||||||
|
desc(_) ->
|
||||||
|
undefined.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% internal functions
|
||||||
|
|
||||||
|
sc(Type, Meta) ->
|
||||||
|
hoconsc:mk(Type, Meta).
|
||||||
|
|
||||||
|
map(Name, Type) ->
|
||||||
|
hoconsc:map(Name, Type).
|
||||||
|
|
||||||
|
ref(Field) ->
|
||||||
|
hoconsc:ref(?MODULE, Field).
|
|
@ -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_config:get([gateway, ocpp, 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_config:get([gateway, ocpp, json_schema_id_prefix]) ++
|
||||||
|
binary_to_list(Action) ++
|
||||||
|
"Request";
|
||||||
|
schema_id(?OCPP_MSG_TYPE_ID_CALLRESULT, Action) when is_binary(Action) ->
|
||||||
|
emqx_config:get([gateway, ocpp, json_schema_id_prefix]) ++
|
||||||
|
binary_to_list(Action) ++
|
||||||
|
"Response".
|
||||||
|
|
||||||
|
format(Fmt, Args) ->
|
||||||
|
lists:flatten(io_lib:format(Fmt, Args)).
|
|
@ -0,0 +1,51 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% 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/include/emqx.hrl").
|
||||||
|
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
-include_lib("common_test/include/ct.hrl").
|
||||||
|
|
||||||
|
all() ->
|
||||||
|
emqx_common_test_helpers:all(?MODULE).
|
||||||
|
|
||||||
|
init_per_suite(Conf) ->
|
||||||
|
emqx_ct_helpers:start_apps([emqx_gateway_ocpp], fun set_special_cfg/1),
|
||||||
|
Conf.
|
||||||
|
|
||||||
|
end_per_suite(_Config) ->
|
||||||
|
emqx_ct_helpers:stop_apps([emqx_gateway_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
|
||||||
|
%%---------------------------------------------------------------------
|
|
@ -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_common_test_helpers:all(?MODULE).
|
||||||
|
|
||||||
|
init_per_suite(Conf) ->
|
||||||
|
Conf.
|
||||||
|
|
||||||
|
end_per_suite(_Conf) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% cases
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
t_load_unload(_) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
t_get_env(_) ->
|
||||||
|
ok.
|
|
@ -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_frame_SUITE).
|
||||||
|
|
||||||
|
-compile(export_all).
|
||||||
|
-compile(nowarn_export_all).
|
||||||
|
|
||||||
|
-include_lib("emqx/include/emqx.hrl").
|
||||||
|
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
-include_lib("common_test/include/ct.hrl").
|
||||||
|
|
||||||
|
all() ->
|
||||||
|
emqx_common_test_helpers:all(?MODULE).
|
||||||
|
|
||||||
|
init_per_suite(Conf) ->
|
||||||
|
Conf.
|
||||||
|
|
||||||
|
end_per_suite(_Config) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% cases
|
||||||
|
%%---------------------------------------------------------------------
|
|
@ -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_common_test_helpers: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)).
|
|
@ -126,7 +126,8 @@
|
||||||
emqx_dashboard_rbac,
|
emqx_dashboard_rbac,
|
||||||
emqx_dashboard_sso,
|
emqx_dashboard_sso,
|
||||||
emqx_audit,
|
emqx_audit,
|
||||||
emqx_gateway_gbt32960
|
emqx_gateway_gbt32960,
|
||||||
|
emqx_gateway_ocpp
|
||||||
],
|
],
|
||||||
%% must always be of type `load'
|
%% must always be of type `load'
|
||||||
ce_business_apps =>
|
ce_business_apps =>
|
||||||
|
|
3
mix.exs
3
mix.exs
|
@ -216,7 +216,8 @@ defmodule EMQXUmbrella.MixProject do
|
||||||
:emqx_dashboard_rbac,
|
:emqx_dashboard_rbac,
|
||||||
:emqx_dashboard_sso,
|
:emqx_dashboard_sso,
|
||||||
:emqx_audit,
|
:emqx_audit,
|
||||||
:emqx_gateway_gbt32960
|
:emqx_gateway_gbt32960,
|
||||||
|
:emqx_gateway_ocpp
|
||||||
])
|
])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -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_dashboard_sso") -> false;
|
||||||
is_community_umbrella_app("apps/emqx_audit") -> 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_gbt32960") -> false;
|
||||||
|
is_community_umbrella_app("apps/emqx_gateway_ocpp") -> false;
|
||||||
is_community_umbrella_app(_) -> true.
|
is_community_umbrella_app(_) -> true.
|
||||||
|
|
||||||
is_jq_supported() ->
|
is_jq_supported() ->
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
##--------------------------------------------------------------------
|
||||||
|
## Gateway GB/T 32960
|
||||||
|
##
|
||||||
|
## Add a GB/T 32960 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.gbt32960 {
|
||||||
|
|
||||||
|
## 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 = "gbt32960/"
|
||||||
|
|
||||||
|
## Re-send time interval
|
||||||
|
retry_interval = "8s"
|
||||||
|
|
||||||
|
## Re-send max times
|
||||||
|
max_retry_times = 3
|
||||||
|
|
||||||
|
## Max message queue length
|
||||||
|
message_queue_len = 10
|
||||||
|
|
||||||
|
listeners.tcp.default {
|
||||||
|
bind = "0.0.0.0:7325"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
websocket.path = "/ocpp"
|
||||||
|
}
|
||||||
|
}
|
|
@ -114,4 +114,84 @@ udp_listener_udp_opts.desc:
|
||||||
udp_listeners.desc:
|
udp_listeners.desc:
|
||||||
"""Settings for the UDP listeners."""
|
"""Settings for the UDP listeners."""
|
||||||
|
|
||||||
|
fields_ws_opts_path.desc:
|
||||||
|
"""WebSocket's MQTT protocol path. So the address of EMQX Broker's WebSocket is:
|
||||||
|
<code>ws://{ip}:{port}/mqtt</code>"""
|
||||||
|
|
||||||
|
fields_ws_opts_path.label:
|
||||||
|
"""WS MQTT Path"""
|
||||||
|
|
||||||
|
fields_ws_opts_piggyback.desc:
|
||||||
|
"""Whether a WebSocket message is allowed to contain multiple MQTT packets."""
|
||||||
|
|
||||||
|
fields_ws_opts_piggyback.label:
|
||||||
|
"""MQTT Piggyback"""
|
||||||
|
|
||||||
|
fields_ws_opts_compress.desc:
|
||||||
|
"""If <code>true</code>, compress WebSocket messages using <code>zlib</code>.<br/>
|
||||||
|
The configuration items under <code>deflate_opts</code> belong to the compression-related parameter configuration."""
|
||||||
|
|
||||||
|
fields_ws_opts_compress.label:
|
||||||
|
"""Ws compress"""
|
||||||
|
|
||||||
|
fields_ws_opts_idle_timeout.desc:
|
||||||
|
"""Close transport-layer connections from the clients that have not sent MQTT CONNECT message within this interval."""
|
||||||
|
|
||||||
|
fields_ws_opts_idle_timeout.label:
|
||||||
|
"""WS idle timeout"""
|
||||||
|
|
||||||
|
fields_ws_opts_max_frame_size.desc:
|
||||||
|
"""The maximum length of a single MQTT packet."""
|
||||||
|
|
||||||
|
fields_ws_opts_max_frame_size.label:
|
||||||
|
"""Max frame size"""
|
||||||
|
|
||||||
|
fields_ws_opts_fail_if_no_subprotocol.desc:
|
||||||
|
"""If <code>true</code>, the server will return an error when
|
||||||
|
the client does not carry the <code>Sec-WebSocket-Protocol</code> field.
|
||||||
|
<br/>Note: WeChat applet needs to disable this verification."""
|
||||||
|
|
||||||
|
fields_ws_opts_fail_if_no_subprotocol.label:
|
||||||
|
"""Fail if no subprotocol"""
|
||||||
|
|
||||||
|
fields_ws_opts_supported_subprotocols.desc:
|
||||||
|
"""Comma-separated list of supported subprotocols."""
|
||||||
|
|
||||||
|
fields_ws_opts_supported_subprotocols.label:
|
||||||
|
"""Supported subprotocols"""
|
||||||
|
|
||||||
|
fields_ws_opts_check_origin_enable.desc:
|
||||||
|
"""If <code>true</code>, <code>origin</code> HTTP header will be
|
||||||
|
validated against the list of allowed origins configured in <code>check_origins</code>
|
||||||
|
parameter."""
|
||||||
|
|
||||||
|
fields_ws_opts_check_origin_enable.label:
|
||||||
|
"""Check origin"""
|
||||||
|
|
||||||
|
fields_ws_opts_allow_origin_absence.desc:
|
||||||
|
"""If <code>false</code> and <code>check_origin_enable</code> is
|
||||||
|
<code>true</code>, the server will reject requests that don't have <code>origin</code>
|
||||||
|
HTTP header."""
|
||||||
|
|
||||||
|
fields_ws_opts_allow_origin_absence.label:
|
||||||
|
"""Allow origin absence"""
|
||||||
|
|
||||||
|
fields_ws_opts_check_origins.desc:
|
||||||
|
"""List of allowed origins.<br/>See <code>check_origin_enable</code>."""
|
||||||
|
|
||||||
|
fields_ws_opts_check_origins.label:
|
||||||
|
"""Allowed origins"""
|
||||||
|
fields_ws_opts_proxy_port_header.desc:
|
||||||
|
"""HTTP header used to pass information about the client port. Relevant when the EMQX cluster is deployed behind a load-balancer."""
|
||||||
|
|
||||||
|
fields_ws_opts_proxy_port_header.label:
|
||||||
|
"""Proxy port header"""
|
||||||
|
|
||||||
|
fields_ws_opts_proxy_address_header.desc:
|
||||||
|
"""HTTP header used to pass information about the client IP address.
|
||||||
|
Relevant when the EMQX cluster is deployed behind a load-balancer."""
|
||||||
|
|
||||||
|
fields_ws_opts_proxy_address_header.label:
|
||||||
|
"""Proxy address header"""
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
emqx_ocpp_schema {
|
||||||
|
|
||||||
|
default_heartbeat_interval.desc:
|
||||||
|
"""The default Heartbeat time interval"""
|
||||||
|
|
||||||
|
heartbeat_checking_times_backoff.desc:
|
||||||
|
"""The backoff for heartbeat checking times"""
|
||||||
|
|
||||||
|
message_format_checking.desc:
|
||||||
|
"""Whether to enable message format legality checking.
|
||||||
|
EMQX checks the message format of the upload stream and download stream against the
|
||||||
|
format defined in json-schema.
|
||||||
|
When the check fails, emqx will reply with a corresponding answer message.
|
||||||
|
|
||||||
|
The checking strategy can be one of the following values:
|
||||||
|
- <code>all</code>: check all messages
|
||||||
|
- <code>upstream_only</code>: check upload stream messages only
|
||||||
|
- <code>dnstream_only</code>: check download stream messages only
|
||||||
|
- <code>disable</code>: don't check any messages"""
|
||||||
|
|
||||||
|
upstream_topic.desc:
|
||||||
|
"""The topic for Upload stream Call Request messages."""
|
||||||
|
|
||||||
|
upstream_topic_override_mapping.desc:
|
||||||
|
"""Upload stream topic override mapping by Message Name."""
|
||||||
|
|
||||||
|
upstream_reply_topic.desc:
|
||||||
|
"""The topic for Upload stream Reply messages."""
|
||||||
|
|
||||||
|
upstream_error_topic.desc:
|
||||||
|
"""The topic for Upload stream error topic."""
|
||||||
|
|
||||||
|
dnstream_topic.desc:
|
||||||
|
"""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."""
|
||||||
|
|
||||||
|
dnstream_max_mqueue_len.desc:
|
||||||
|
"""The maximum message queue length for download stream message delivery."""
|
||||||
|
|
||||||
|
json_schema_dir.desc:
|
||||||
|
"""JSON Schema directory for OCPP message definitions.
|
||||||
|
Default: ${application}/priv/schemas"""
|
||||||
|
|
||||||
|
json_schema_id_prefix.desc:
|
||||||
|
"""The ID prefix for the OCPP message schemas."""
|
||||||
|
|
||||||
|
ws.desc:
|
||||||
|
"""Websocket listener."""
|
||||||
|
|
||||||
|
wss.desc:
|
||||||
|
"""Websocket over TLS listener."""
|
||||||
|
|
||||||
|
}
|
|
@ -289,3 +289,8 @@ Keyspace
|
||||||
OpenTSDB
|
OpenTSDB
|
||||||
saml
|
saml
|
||||||
idp
|
idp
|
||||||
|
ocpp
|
||||||
|
OCPP
|
||||||
|
dnstream
|
||||||
|
upstream
|
||||||
|
priv
|
||||||
|
|
Loading…
Reference in New Issue