diff --git a/doc/connectivity/networking/api/ocpp.rst b/doc/connectivity/networking/api/ocpp.rst
new file mode 100644
index 0000000000000..d4f403997fbe3
--- /dev/null
+++ b/doc/connectivity/networking/api/ocpp.rst
@@ -0,0 +1,110 @@
+.. _ocpp_interface:
+
+Open Charge Point Protocol (OCPP)
+#################################
+
+.. contents::
+ :local:
+ :depth: 2
+
+Overview
+********
+
+Open Charge Point Protocol (OCPP) is an application protocol for communication
+between Charge Points (Electric vehicle (EV) charging stations) and a central
+management system, also known as a charging station network. OCPP is a
+`standard `_
+defined by The Open Charge Alliance and goal is to offer a uniform solution for
+the method of communication between charge point and central system. With this
+protocol it is possible to connect any central system with any charge point,
+regardless of the vendor
+
+Zephyr provides an OCPP Charge Point (CP) library built on top of websocket API
+with payload in json format. The library can be enabled with
+:kconfig:option:`CONFIG_OCPP` Kconfig option. Currently OCPP 1.6 with basic
+core profile is supported.
+
+OCPP charge point (CP) require a Central System (CS) server to connect, an open
+source SteVe server shall be setup locally for devlopment purpose, See
+`SteVe server `_
+for more information about the setup.
+
+The Zephyr OCPP CP library implements the following items:
+
+* engine to process socket connectivity and events
+* OCPP core functions to frame/parse payload, user notification for OCPP events,
+ heartbeat notification
+
+Sample usage
+************
+
+Init ocpp library with overall CP and CS information. Prior to init an OCPP
+library, a network interface should be ready using ethernet or wifi or modem.
+A filled CP, CS structure and user callback needs to be passed in ocpp_init.
+
+.. code-block:: c
+
+ static int user_notify_cb(ocpp_notify_reason_t reason,
+ ocpp_io_value_t *io,
+ void *user_data)
+ {
+
+ switch (reason) {
+ case OCPP_USR_GET_METER_VALUE:
+ ...
+ break;
+
+ case OCPP_USR_START_CHARGING:
+ ...
+ break;
+
+ ...
+ ...
+ }
+
+ /* OCPP configuration */
+ ocpp_cp_info_t cpi = { "basic", "zephyr", .num_of_con = 1};
+ ocpp_cs_info_t csi = {"192.168.1.3", /* ip address */
+ "/steve/websocket/CentralSystemService/zephyr",
+ 8180,
+ AF_INET};
+
+ ret = ocpp_init(&cpi, &csi, user_notify_cb, NULL);
+
+A unique session must open for each physical connector before any ocpp
+transcation API call.
+
+.. code-block:: c
+
+ ocpp_session_handle_t sh = NULL;
+ ret = ocpp_session_open(&sh);
+
+idtag is EV user's authentication token which should match with list on CS.
+Authorize request must call to ensure validity of idtag (if charging request
+originate from local CP) before start energy transfer to EV.
+
+.. code-block:: c
+
+ ocpp_auth_status_t status;
+ ret = ocpp_authorize(sh, idtag, &status, 500);
+
+On successful, authorization status is available in status.
+
+Apart from local CP, charging request may originate from CS is notified to user
+in callback with OCPP_USR_START_CHARGING, here authorize request call is
+optional. When the CS is ready to provide power to EV, a start transaction
+is notified to CS with meter reading and connector id using ocpp_start_transaction.
+
+.. code-block:: c
+
+ const int idcon = 1;
+ const int mval = 25; //meter reading in wh
+ ret = ocpp_start_transaction(sh, mval, idcon, 200);
+
+Once the start transaction is success, user callback is invoked to get meter
+readings from the library. callback should be not be hold for longer time.
+
+API Reference
+*************
+
+.. doxygengroup:: ocpp_api
diff --git a/doc/connectivity/networking/api/protocols.rst b/doc/connectivity/networking/api/protocols.rst
index f4f3b1f88c1af..86e312d5c5eef 100644
--- a/doc/connectivity/networking/api/protocols.rst
+++ b/doc/connectivity/networking/api/protocols.rst
@@ -15,5 +15,6 @@ Protocols
lwm2m
mqtt
mqtt_sn
+ ocpp
ptp
tftp
diff --git a/include/zephyr/net/ocpp.h b/include/zephyr/net/ocpp.h
new file mode 100644
index 0000000000000..bb6221778e065
--- /dev/null
+++ b/include/zephyr/net/ocpp.h
@@ -0,0 +1,255 @@
+/*
+ * Copyright (c) 2024 Linumiz
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * @file ocpp.h
+ *
+ * @defgroup ocpp_api OCPP library
+ * @ingroup networking
+ * @{
+ * @brief OCPP Charge Point Implementation
+ *
+ * @note The implementation assumes Websocket module is enabled.
+ *
+ * @note By default the implementation uses OCPP version 1.6.
+ */
+
+#ifndef ZEPHYR_INCLUDE_NET_OCPP_H_
+#define ZEPHYR_INCLUDE_NET_OCPP_H_
+
+#include
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** Max length of string literals e.g idtag */
+#define CISTR50 50
+
+/**
+ * @brief OCPP IdTag authorization status in result to ocpp request
+ * authorization
+ */
+enum ocpp_auth_status {
+ OCPP_AUTH_INVALID, /*< IdTag not valid */
+ OCPP_AUTH_ACCEPTED, /*< accepted, allowed to charge */
+ OCPP_AUTH_BLOCKED, /*< blocked to charge */
+ OCPP_AUTH_EXPIRED, /*< IdTag expired, not allowed to charge */
+ OCPP_AUTH_CONCURRENT_TX /*< Parallel access of same IdTag */
+};
+
+enum ocpp_notify_reason {
+ /** User must fill the current reading */
+ OCPP_USR_GET_METER_VALUE,
+
+ /** Process the start charging request as like idtag received from local
+ * e.g authorize etc
+ */
+ OCPP_USR_START_CHARGING,
+
+ /** Process the stop charging sequence */
+ OCPP_USR_STOP_CHARGING,
+
+ /** Unlock mechanical connector of CP */
+ OCPP_USR_UNLOCK_CONNECTOR,
+};
+
+/** @brief OCPP meter readings to be filled on user callback request from
+ * library
+ */
+enum ocpp_meter_measurand {
+ OCPP_OMM_CURRENT_FROM_EV, /*< current from EV, in A */
+ OCPP_OMM_CURRENT_TO_EV, /*< current to EV, in A */
+ OCPP_OMM_CURRENT_MAX_OFFERED_TO_EV, /*< maximum current offered to EV, in A */
+ OCPP_OMM_ACTIVE_ENERGY_FROM_EV, /*< active energy from EV, in Wh */
+ OCPP_OMM_ACTIVE_ENERGY_TO_EV, /*< active energy to EV, in Wh */
+ OCPP_OMM_REACTIVE_ENERGY_FROM_EV, /*< reactive energy from EV, in varh */
+ OCPP_OMM_REACTIVE_ENERGY_TO_EV, /*< reactive energy to EV, in varh */
+ OCPP_OMM_ACTIVE_POWER_FROM_EV, /*< active power from EV, in W */
+ OCPP_OMM_ACTIVE_POWER_TO_EV, /*< active power to EV, in W */
+ OCPP_OMM_REACTIVE_POWER_FROM_EV, /*< reactive power from EV, in var */
+ OCPP_OMM_REACTIVE_POWER_TO_EV, /*< reactive power to EV, in var */
+ OCPP_OMM_POWERLINE_FREQ, /*< powerline frequency, in Hz */
+ OCPP_OMM_POWER_FACTOR, /*< power factor of supply */
+ OCPP_OMM_POWER_MAX_OFFERED_TO_EV, /*< maximum power offered to EV, in W */
+ OCPP_OMM_FAN_SPEED, /*< fan speed, in rpm */
+ OCPP_OMM_CHARGING_PERCENT, /*< charging percentage */
+ OCPP_OMM_TEMPERATURE, /*< temperature inside charge point, in Celsius */
+ OCPP_OMM_VOLTAGE_AC_RMS, /*< AC RMS supply voltage, in V */
+
+ OCPP_OMM_END
+};
+
+/** @brief OCPP user callback notification/request of input/output values
+ * union member should be accessed with enum value ocpp_notify_reason
+ * correspondingly.
+ * e.g. callback reason is OCPP_USR_GET_METER_VALUE, struct meter_val
+ * is valid
+ */
+union ocpp_io_value {
+
+ struct {
+ /** Input to user, requested connector_id or 0 - main meter */
+ int id_con;
+
+ /** Input to user, measurand */
+ enum ocpp_meter_measurand mes;
+
+ /** To be filled by user, value as string */
+ char val[CISTR50];
+ } meter_val;
+
+ struct {
+ char idtag[CISTR50]; /**< Input to user */
+
+ /** Input to user(optional). connector id -1 means invalid */
+ int id_con;
+ } start_charge;
+
+ struct {
+ int id_con; /**< Input to user, to stop charging connector */
+ } stop_charge;
+
+ struct {
+ int id_con; /**< Input to user, to unlock connector id. */
+ } unlock_con;
+};
+
+/** @brief Parameters for ocpp_init information about Charge Point (CP)
+ * all are string literal except num_of_con
+ */
+struct ocpp_cp_info {
+ char *model; /**< Charge Point (CP) model */
+ char *vendor; /**< CP vendor */
+
+ int num_of_con; /**< Max. number of connector supports */
+
+ /** optional fields */
+ char *sl_no; /**< CP serial number */
+ char *box_sl_no; /**< Box serial number */
+ char *fw_ver; /**< Firmware version */
+ char *iccid; /**< ICC ID */
+ char *imsi; /**< IMSI */
+ char *meter_sl_no; /**< Main power meter serial number */
+ char *meter_type; /**< Main power meter type */
+};
+
+/** @brief Parameters for ocpp_init information about central system (CS) */
+struct ocpp_cs_info {
+ char *cs_ip; /**< Central system IP address */
+ char *ws_url; /**< Websocket url exclude ipaddr & port */
+ int port; /**< Central system port number */
+ sa_family_t sa_family; /**< IP protocol family type 4/6 */
+};
+
+/** @brief Parameters opaque session handle for ocpp_* API */
+typedef void *ocpp_session_handle_t;
+
+/**
+ * @brief Asynchronous event notification callback registered by the
+ * application. advised callback should not be hold for longer time
+ * to unblock the ocpp protocol stack/lib.
+ *
+ * @param[in] reason for callback invoked.
+ * @param[in] io reffered corresponding to reason.
+ * @param[in] user_data passed on ocpp_init.
+ *
+ * @return 0 or a negative error code (errno.h)
+ */
+typedef int (*ocpp_user_notify_callback_t)(enum ocpp_notify_reason reason,
+ union ocpp_io_value *io,
+ void *user_data);
+
+/**
+ * @brief OCPP library init.
+ *
+ * @param[in] cpi Charge Point information
+ * @param[in] csi Central System information
+ * @param[in] cb user register callback
+ * @param[in] user_data same reference will be passed on callback
+ *
+ * @return 0 on success or a negative error code (errno.h) indicating reason of failure
+ *
+ * @note Must be called before any other ocpp API
+ */
+int ocpp_init(struct ocpp_cp_info *cpi,
+ struct ocpp_cs_info *csi,
+ ocpp_user_notify_callback_t cb,
+ void *user_data);
+
+/**
+ * @brief API to request a new Session
+ *
+ * @param[out] hndl a valid opaque handle
+ *
+ * @return 0 on success or a negative error code (errno.h) indicating reason of failure
+ *
+ * @note Each connector should open unique session after ocpp_init and
+ * prior to anyother ocpp_* request message api
+ */
+int ocpp_session_open(ocpp_session_handle_t *hndl);
+
+/**
+ * @brief API to close a Session
+ *
+ * @param[in] hndl a handle received from session open
+ */
+void ocpp_session_close(ocpp_session_handle_t hndl);
+
+/**
+ * @brief Authorize request call to CS to get validity of idtag
+ *
+ * @param[in] hndl session handle
+ * @param[in] idtag (string literal) to get authorize validity
+ * @param[out] status authorization status
+ * @param[in] timeout_ms in msec
+ *
+ * @return 0 on success or a negative error code (errno.h) indicating reason of failure
+ */
+int ocpp_authorize(ocpp_session_handle_t hndl,
+ char *idtag,
+ enum ocpp_auth_status *status,
+ uint32_t timeout_ms);
+
+/**
+ * @brief Notify transaction start to CS
+ *
+ * @param[in] hndl session handle
+ * @param[in] Wh energy meter reading of this connector
+ * @param[in] conn_id connector id should be > 0 and sequential number
+ * @param[in] timeout_ms timeout in msec
+ *
+ * @return: 0 on success
+ * EACCES - not authorized, should follow the stop charging process
+ * a negative error code (errno.h) indicating reason of failure
+ */
+int ocpp_start_transaction(ocpp_session_handle_t hndl,
+ int Wh,
+ uint8_t conn_id,
+ uint32_t timeout_ms);
+
+/**
+ * @brief Notify transaction stopped to CS
+ *
+ * @param[in] hndl session handle
+ * @param[in] Wh energy meter reading of this connector
+ * @param[in] timeout_ms timeout in msec
+ *
+ * @return 0 on success or a negative error code (errno.h) indicating reason of failure
+ */
+int ocpp_stop_transaction(ocpp_session_handle_t hndl,
+ int Wh,
+ uint32_t timeout_ms);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* ZEPHYR_INCLUDE_NET_OCPP_H_ */
+
+/**@} */
diff --git a/samples/net/ocpp/CMakeLists.txt b/samples/net/ocpp/CMakeLists.txt
new file mode 100644
index 0000000000000..b7e93067df31f
--- /dev/null
+++ b/samples/net/ocpp/CMakeLists.txt
@@ -0,0 +1,9 @@
+# SPDX-License-Identifier: Apache-2.0
+
+cmake_minimum_required(VERSION 3.20.0)
+
+find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
+project(ocpp)
+
+include(${ZEPHYR_BASE}/samples/net/common/common.cmake)
+target_sources(app PRIVATE src/main.c)
diff --git a/samples/net/ocpp/Kconfig b/samples/net/ocpp/Kconfig
new file mode 100644
index 0000000000000..3117b73e76791
--- /dev/null
+++ b/samples/net/ocpp/Kconfig
@@ -0,0 +1,22 @@
+# Config options for OCPP sample application
+
+# Copyright (c) 2025 Linumiz GmbH
+# SPDX-License-Identifier: Apache-2.0
+
+mainmenu "OCPP sample application"
+config NET_SAMPLE_OCPP_SERVER
+ string "OCPP server ip"
+ help
+ OCPP central system server ip address
+
+config NET_SAMPLE_OCPP_PORT
+ int "OCPP server port"
+ help
+ OCPP central system server port
+
+config NET_SAMPLE_SNTP_SERVER
+ string "SNTP server ip"
+ help
+ SNTP server ip to get the time from network
+
+source "Kconfig.zephyr"
diff --git a/samples/net/ocpp/README.rst b/samples/net/ocpp/README.rst
new file mode 100644
index 0000000000000..0b9bb7707463f
--- /dev/null
+++ b/samples/net/ocpp/README.rst
@@ -0,0 +1,69 @@
+.. zephyr:code-sample:: ocpp
+ :name: OCPP charge point
+ :relevant-api: ocpp_api
+
+ Implement an OCPP charge point that connects to a Central System server and
+ simulates the meter readings.
+
+Overview
+********
+
+Open Charge Point Protocol (OCPP) is an application protocol for communication
+between Charge Points (Electric vehicle (EV) charging stations) and a central
+management system, also known as a charging station network.
+
+This ocpp sample application for Zephyr implements the ocpp library
+and establishes a connection to an Central System server using the web socket
+
+The source code for this sample application can be found at:
+:zephyr_file:`samples/net/ocpp`.
+
+Requirements
+************
+
+- Linux machine
+- STEM32 Discovery kit (32F769IDISCOVERY) or any network interface device
+- SteVe Demo Server ()
+- LAN for testing purposes (Ethernet)
+
+Building and Running
+********************
+
+Build the ocpp sample application like this:
+
+.. zephyr-app-commands::
+ :zephyr-app: samples/net/ocpp
+ :board:
+ :goals: build
+ :compact:
+
+The sample application is to built and tested on
+
+.. code-block:: console
+
+ west build -b stm32f769i_disco
+ west flash
+
+The output of sample is:
+
+.. code-block:: console
+
+ *** Booting Zephyr OS build v3.6.0-rc1-37-g8c035d8f24cf ***
+ OCPP sample stm32f769i_disco
+ [00:00:02.642,000] net_dhcpv4: Received: 192.168.1.101
+ [00:00:02.642,000] main: net mgr cb
+ [00:00:02.642,000] main: Your address: 192.168.1.101
+ [00:00:02.642,000] main: Lease time: 86400 seconds
+ [00:00:02.642,000] main: Subnet: 255.255.255.0
+ [00:00:02.643,000] main: Router: 192.168.1.1
+ [00:00:07.011,000] main: cs server 122.165.245.213 8180
+ [00:00:07.011,000] main: IPv4 Address 122.165.245.213
+ [00:00:07.024,000] main: sntp succ since Epoch: 1707890823
+ [00:00:07.024,000] ocpp: upstream init
+ [00:00:07.025,000] ocpp: ocpp init success
+ [00:00:17.066,000] main: ocpp auth 0> idcon 1 status 1
+ [00:00:17.101,000] main: ocpp auth 0> idcon 2 status 1
+ [00:00:17.197,000] main: ocpp start charging connector id 1
+ [00:00:17.255,000] main: ocpp start charging connector id 2
+ [00:01:07.064,000] main: ocpp stop charging connector id 1
+ [00:01:08.063,000] main: ocpp stop charging connector id 2
diff --git a/samples/net/ocpp/prj.conf b/samples/net/ocpp/prj.conf
new file mode 100644
index 0000000000000..3897041e6c96a
--- /dev/null
+++ b/samples/net/ocpp/prj.conf
@@ -0,0 +1,50 @@
+# HTTP & Websocket
+CONFIG_HTTP_CLIENT=y
+CONFIG_WEBSOCKET_CLIENT=y
+
+# Networking config
+CONFIG_NETWORKING=y
+CONFIG_NET_IPV4=y
+CONFIG_NET_IPV6=n
+CONFIG_NET_CONNECTION_MANAGER=y
+CONFIG_NET_CONFIG_SETTINGS=y
+CONFIG_NET_TCP=y
+CONFIG_NET_SOCKETS=y
+CONFIG_NET_SOCKETS_POLL_MAX=4
+CONFIG_NET_L2_ETHERNET=y
+CONFIG_ETH_DRIVER=y
+CONFIG_LOG=y
+CONFIG_NET_LOG=y
+
+# OCPP
+CONFIG_OCPP=y
+
+# Please set the server addresses when compiling the sample
+CONFIG_NET_SAMPLE_SNTP_SERVER=""
+CONFIG_NET_SAMPLE_OCPP_SERVER=""
+CONFIG_NET_SAMPLE_OCPP_PORT=8180
+
+CONFIG_MAIN_STACK_SIZE=4096
+CONFIG_HEAP_MEM_POOL_SIZE=15000
+
+CONFIG_JSON_LIBRARY=y
+CONFIG_PICOLIBC=y
+CONFIG_NET_ARP=y
+CONFIG_NET_UDP=y
+CONFIG_NET_DHCPV4=y
+CONFIG_NET_DHCPV4_OPTION_CALLBACKS=y
+CONFIG_TEST_RANDOM_GENERATOR=y
+
+CONFIG_NET_TX_STACK_SIZE=2048
+CONFIG_NET_RX_STACK_SIZE=2048
+CONFIG_NET_PKT_RX_COUNT=28
+CONFIG_NET_BUF_RX_COUNT=60
+CONFIG_NET_MGMT=y
+CONFIG_NET_MGMT_EVENT=y
+
+CONFIG_SNTP=y
+
+CONFIG_ZBUS=y
+CONFIG_ZBUS_RUNTIME_OBSERVERS=y
+CONFIG_POSIX_API=y
+CONFIG_POSIX_CLOCK_SELECTION=y
diff --git a/samples/net/ocpp/sample.yaml b/samples/net/ocpp/sample.yaml
new file mode 100644
index 0000000000000..591ba7d6d24dc
--- /dev/null
+++ b/samples/net/ocpp/sample.yaml
@@ -0,0 +1,16 @@
+sample:
+ description: OCPP sample application
+ name: OCPP charge point
+common:
+ harness: net
+ tags:
+ - net
+ - ocpp
+
+tests:
+ sample.net.ocpp:
+ platform_allow:
+ - stm32f769i_disco
+ tags:
+ - net
+ - ocpp
diff --git a/samples/net/ocpp/src/main.c b/samples/net/ocpp/src/main.c
new file mode 100644
index 0000000000000..be784f20800a2
--- /dev/null
+++ b/samples/net/ocpp/src/main.c
@@ -0,0 +1,346 @@
+/*
+ * Copyright (c) 2025 Linumiz GmbH
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "net_sample_common.h"
+
+#if __POSIX_VISIBLE < 200809
+char *strdup(const char *);
+#endif
+
+LOG_MODULE_REGISTER(main, LOG_LEVEL_INF);
+
+#define NO_OF_CONN 2
+K_KERNEL_STACK_ARRAY_DEFINE(cp_stk, NO_OF_CONN, 2 * 1024);
+
+static struct k_thread tinfo[NO_OF_CONN];
+static k_tid_t tid[NO_OF_CONN];
+static char idtag[NO_OF_CONN][25];
+
+static int ocpp_get_time_from_sntp(void)
+{
+ struct sntp_ctx ctx;
+ struct sntp_time stime;
+ struct sockaddr_in addr;
+ struct timespec tv;
+ int ret;
+
+ /* ipv4 */
+ memset(&addr, 0, sizeof(addr));
+ addr.sin_family = AF_INET;
+ addr.sin_port = htons(123);
+ inet_pton(AF_INET, CONFIG_NET_SAMPLE_SNTP_SERVER, &addr.sin_addr);
+
+ ret = sntp_init(&ctx, (struct sockaddr *) &addr,
+ sizeof(struct sockaddr_in));
+ if (ret < 0) {
+ LOG_ERR("Failed to init SNTP IPv4 ctx: %d", ret);
+ return ret;
+ }
+
+ ret = sntp_query(&ctx, 60, &stime);
+ if (ret < 0) {
+ LOG_ERR("SNTP IPv4 request failed: %d", ret);
+ return ret;
+ }
+
+ LOG_INF("sntp succ since Epoch: %llu\n", stime.seconds);
+ tv.tv_sec = stime.seconds;
+ clock_settime(CLOCK_REALTIME, &tv);
+ sntp_close(&ctx);
+ return 0;
+}
+
+ZBUS_CHAN_DEFINE(ch_event, /* Name */
+ union ocpp_io_value,
+ NULL, /* Validator */
+ NULL, /* User data */
+ ZBUS_OBSERVERS_EMPTY, /* observers */
+ ZBUS_MSG_INIT(0) /* Initial value {0} */
+);
+
+ZBUS_SUBSCRIBER_DEFINE(cp_thread0, 5);
+ZBUS_SUBSCRIBER_DEFINE(cp_thread1, 5);
+struct zbus_observer *obs[NO_OF_CONN] = {(struct zbus_observer *)&cp_thread0,
+ (struct zbus_observer *)&cp_thread1};
+
+static void ocpp_cp_entry(void *p1, void *p2, void *p3);
+static int user_notify_cb(enum ocpp_notify_reason reason,
+ union ocpp_io_value *io,
+ void *user_data)
+{
+ static int wh = 6 + NO_OF_CONN;
+ int idx;
+ int i;
+
+ switch (reason) {
+ case OCPP_USR_GET_METER_VALUE:
+ if (OCPP_OMM_ACTIVE_ENERGY_TO_EV == io->meter_val.mes) {
+ snprintf(io->meter_val.val, CISTR50, "%u",
+ wh + io->meter_val.id_con);
+
+ wh++;
+ LOG_DBG("mtr reading val %s con %d", io->meter_val.val,
+ io->meter_val.id_con);
+
+ return 0;
+ }
+ break;
+
+ case OCPP_USR_START_CHARGING:
+ if (io->start_charge.id_con < 0) {
+ for (i = 0; i < NO_OF_CONN; i++) {
+ if (!tid[i]) {
+ break;
+ }
+ }
+
+ if (i >= NO_OF_CONN) {
+ return -EBUSY;
+ }
+ idx = i;
+ } else {
+ idx = io->start_charge.id_con - 1;
+ }
+
+ if (!tid[idx]) {
+ LOG_INF("Remote start charging idtag %s connector %d\n",
+ idtag[idx], idx + 1);
+
+ strncpy(idtag[idx], io->start_charge.idtag,
+ sizeof(idtag[0]));
+
+ tid[idx] = k_thread_create(&tinfo[idx], cp_stk[idx],
+ sizeof(cp_stk[idx]), ocpp_cp_entry,
+ (void *)(idx + 1), idtag[idx],
+ obs[idx], 7, 0, K_NO_WAIT);
+
+ return 0;
+ }
+ break;
+
+ case OCPP_USR_STOP_CHARGING:
+ zbus_chan_pub(&ch_event, io, K_MSEC(100));
+ return 0;
+
+ case OCPP_USR_UNLOCK_CONNECTOR:
+ LOG_INF("unlock connector %d\n", io->unlock_con.id_con);
+ return 0;
+ }
+
+ return -ENOTSUP;
+}
+
+static void ocpp_cp_entry(void *p1, void *p2, void *p3)
+{
+ int ret;
+ int idcon = (uint32_t)p1;
+ char *idtag = (char *)p2;
+ struct zbus_observer *obs = (struct zbus_observer *)p3;
+ ocpp_session_handle_t sh = NULL;
+ enum ocpp_auth_status status;
+ const uint32_t timeout_ms = 500;
+
+ ret = ocpp_session_open(&sh);
+ if (ret) {
+ LOG_ERR("ocpp open ses idcon %d> res %d\n", idcon, ret);
+ return;
+ }
+
+ while (1) {
+ /* Avoid quick retry since authorization request is possible only
+ * after Bootnotification process (handled in lib) completed.
+ */
+
+ k_sleep(K_SECONDS(5));
+ ret = ocpp_authorize(sh,
+ idtag,
+ &status,
+ timeout_ms);
+ if (ret) {
+ LOG_ERR("ocpp auth %d> idcon %d status %d\n",
+ ret, idcon, status);
+ } else {
+ LOG_INF("ocpp auth %d> idcon %d status %d\n",
+ ret, idcon, status);
+ break;
+ }
+ }
+
+ if (status != OCPP_AUTH_ACCEPTED) {
+ LOG_ERR("ocpp start idcon %d> not authorized status %d\n",
+ idcon, status);
+ return;
+ }
+
+ ret = ocpp_start_transaction(sh, sys_rand32_get(), idcon, timeout_ms);
+ if (!ret) {
+ const struct zbus_channel *chan;
+ union ocpp_io_value io;
+
+ LOG_INF("ocpp start charging connector id %d\n", idcon);
+ memset(&io, 0xff, sizeof(io));
+
+ /* wait for stop charging event from main or remote CS */
+ zbus_chan_add_obs(&ch_event, obs, K_SECONDS(1));
+ do {
+ zbus_sub_wait(obs, &chan, K_FOREVER);
+ zbus_chan_read(chan, &io, K_SECONDS(1));
+
+ if (io.stop_charge.id_con == idcon) {
+ break;
+ }
+
+ } while (1);
+ }
+
+ ret = ocpp_stop_transaction(sh, sys_rand32_get(), timeout_ms);
+ if (ret) {
+ LOG_ERR("ocpp stop txn idcon %d> %d\n", idcon, ret);
+ return;
+ }
+
+ LOG_INF("ocpp stop charging connector id %d\n", idcon);
+ k_sleep(K_SECONDS(1));
+ ocpp_session_close(sh);
+ tid[idcon - 1] = NULL;
+ k_sleep(K_SECONDS(1));
+ k_thread_abort(k_current_get());
+}
+
+static int ocpp_getaddrinfo(char *server, int port, char **ip)
+{
+ int ret;
+ uint8_t retry = 5;
+ char addr_str[INET_ADDRSTRLEN];
+ struct sockaddr_storage b;
+ struct addrinfo *result = NULL;
+ struct addrinfo *addr;
+ struct addrinfo hints = {
+ .ai_family = AF_INET,
+ .ai_socktype = SOCK_STREAM
+ };
+
+ LOG_INF("cs server %s %d", server, port);
+ do {
+ ret = getaddrinfo(server, NULL, &hints, &result);
+ if (ret == -EAGAIN) {
+ LOG_ERR("ERROR: getaddrinfo %d, rebind", ret);
+ k_sleep(K_SECONDS(1));
+ } else if (ret) {
+ LOG_ERR("ERROR: getaddrinfo failed %d", ret);
+ return ret;
+ }
+ } while (--retry && ret);
+
+ addr = result;
+ while (addr != NULL) {
+ /* IPv4 Address. */
+ if (addr->ai_addrlen == sizeof(struct sockaddr_in)) {
+ struct sockaddr_in *broker =
+ ((struct sockaddr_in *)&b);
+
+ broker->sin_addr.s_addr =
+ ((struct sockaddr_in *)addr->ai_addr)
+ ->sin_addr.s_addr;
+ broker->sin_family = AF_INET;
+ broker->sin_port = htons(port);
+
+ inet_ntop(AF_INET, &broker->sin_addr, addr_str,
+ sizeof(addr_str));
+
+ *ip = strdup(addr_str);
+ LOG_INF("IPv4 Address %s", addr_str);
+ break;
+ }
+
+ LOG_ERR("error: ai_addrlen = %u should be %u or %u",
+ (unsigned int)addr->ai_addrlen,
+ (unsigned int)sizeof(struct sockaddr_in),
+ (unsigned int)sizeof(struct sockaddr_in6));
+
+ addr = addr->ai_next;
+ }
+
+ /* Free the address. */
+ freeaddrinfo(result);
+
+ return 0;
+}
+
+int main(void)
+{
+ int ret;
+ int i;
+ char *ip = NULL;
+
+ struct ocpp_cp_info cpi = { "basic", "zephyr", .num_of_con = NO_OF_CONN };
+ struct ocpp_cs_info csi = { NULL,
+ "/steve/websocket/CentralSystemService/zephyr",
+ CONFIG_NET_SAMPLE_OCPP_PORT,
+ AF_INET };
+
+ printk("OCPP sample %s\n", CONFIG_BOARD);
+
+ wait_for_network();
+
+ ret = ocpp_getaddrinfo(CONFIG_NET_SAMPLE_OCPP_SERVER, CONFIG_NET_SAMPLE_OCPP_PORT, &ip);
+ if (ret < 0) {
+ return ret;
+ }
+
+ csi.cs_ip = ip;
+
+ ocpp_get_time_from_sntp();
+
+ ret = ocpp_init(&cpi,
+ &csi,
+ user_notify_cb,
+ NULL);
+ if (ret) {
+ LOG_ERR("ocpp init failed %d\n", ret);
+ return ret;
+ }
+
+ /* Spawn threads for each connector */
+ for (i = 0; i < NO_OF_CONN; i++) {
+ snprintf(idtag[i], sizeof(idtag[0]), "ZepId%02d", i);
+
+ tid[i] = k_thread_create(&tinfo[i], cp_stk[i],
+ sizeof(cp_stk[i]),
+ ocpp_cp_entry, (void *)(i + 1),
+ idtag[i], obs[i], 7, 0, K_NO_WAIT);
+ }
+
+ /* Active charging session */
+ k_sleep(K_SECONDS(30));
+
+ /* Send stop charging to thread */
+ for (i = 0; i < NO_OF_CONN; i++) {
+ union ocpp_io_value io = {0};
+
+ io.stop_charge.id_con = i + 1;
+
+ zbus_chan_pub(&ch_event, &io, K_MSEC(100));
+ k_sleep(K_SECONDS(1));
+ }
+
+ /* User could trigger remote start/stop transcation from CS server */
+ k_sleep(K_SECONDS(1200));
+
+ return 0;
+}
diff --git a/subsys/net/lib/CMakeLists.txt b/subsys/net/lib/CMakeLists.txt
index ee23de2ab027d..3f1c4b31e05d8 100644
--- a/subsys/net/lib/CMakeLists.txt
+++ b/subsys/net/lib/CMakeLists.txt
@@ -19,6 +19,7 @@ add_subdirectory_ifdef(CONFIG_NET_TRICKLE trickle)
add_subdirectory_ifdef(CONFIG_NET_DHCPV6 dhcpv6)
add_subdirectory_ifdef(CONFIG_PROMETHEUS prometheus)
add_subdirectory_ifdef(CONFIG_WIFI_CREDENTIALS wifi_credentials)
+add_subdirectory_ifdef(CONFIG_OCPP ocpp)
if (CONFIG_NET_DHCPV4 OR CONFIG_NET_DHCPV4_SERVER)
add_subdirectory(dhcpv4)
diff --git a/subsys/net/lib/Kconfig b/subsys/net/lib/Kconfig
index e530877125afa..ca960102e7b63 100644
--- a/subsys/net/lib/Kconfig
+++ b/subsys/net/lib/Kconfig
@@ -25,6 +25,8 @@ source "subsys/net/lib/socks/Kconfig"
source "subsys/net/lib/sntp/Kconfig"
+source "subsys/net/lib/ocpp/Kconfig"
+
endmenu
menu "Network Libraries"
diff --git a/subsys/net/lib/ocpp/CMakeLists.txt b/subsys/net/lib/ocpp/CMakeLists.txt
new file mode 100644
index 0000000000000..5d7a237bd3bf6
--- /dev/null
+++ b/subsys/net/lib/ocpp/CMakeLists.txt
@@ -0,0 +1,17 @@
+# SPDX-License-Identifier: Apache-2.0
+
+zephyr_include_directories(.)
+
+zephyr_interface_library_named(ocpp)
+
+zephyr_library()
+
+zephyr_library_sources(
+ ocpp.c
+ core.c
+ key_mgmt.c
+ ocpp_j.c
+ ocpp_wamp_rpc.c
+)
+
+zephyr_library_link_libraries(ocpp)
diff --git a/subsys/net/lib/ocpp/Kconfig b/subsys/net/lib/ocpp/Kconfig
new file mode 100644
index 0000000000000..ffa835b876790
--- /dev/null
+++ b/subsys/net/lib/ocpp/Kconfig
@@ -0,0 +1,83 @@
+# Copyright (c) 2025 Linumiz GmbH
+# SPDX-License-Identifier: Apache-2.0
+
+config OCPP
+ bool "Open Charge Point Protocol [EXPERIMENTAL]"
+ depends on JSON_LIBRARY
+ depends on WEBSOCKET_CLIENT
+ select EXPERIMENTAL
+ help
+ This option enables the open charge point protocol library
+
+if OCPP
+
+module=OCPP
+module-dep=NET_LOG
+module-str=Log level for OCPP
+module-help=Enables ocpp debug messages.
+source "subsys/net/Kconfig.template.log_config.net"
+
+config OCPP_INT_THREAD_STACKSIZE
+ int "OCPP internal thread stacksize"
+ default 4096
+ help
+ OCPP internal thread stacksize
+
+config OCPP_WSREADER_THREAD_STACKSIZE
+ int "OCPP websocket reader thread stacksize"
+ default 4096
+ help
+ OCPP websocket reader thread stacksize
+
+config OCPP_RECV_BUFFER_SIZE
+ int "OCPP websocket recive buffer size"
+ default 2048
+ help
+ OCPP websocket recive buffer size
+
+config OCPP_INTERNAL_MSGQ_CNT
+ int "OCPP internal message queue count"
+ default 10
+ help
+ Messages communication between websocket reader and internal
+ process thread
+
+config OCPP_MSG_JSON
+ bool "PDU message format as JSON"
+ default y
+ help
+ If enabled y, then the Charge Point and Central System PDU message
+ format as JSON
+
+config OCPP_PROFILE_SMART_CHARGE
+ bool "OCPP profile smart charging"
+ help
+ Enables the OCPP library to support the Smart Charging profile
+ functionality. Charge Point may support this profile as optional.
+
+config OCPP_PROFILE_REMOTE_TRIG
+ bool "OCPP profile remote trigger"
+ help
+ Enables the OCPP library to support the Remote Trigger
+ functionality. Charge Point may support this profile as optional.
+
+config OCPP_PROFILE_RESERVATION
+ bool "OCPP profile reservation"
+ help
+ Enables the OCPP library to support the Profile Reservation
+ functionality. Charge Point may support this profile as optional.
+
+config OCPP_PROFILE_LOCAL_AUTH_LIST
+ bool "OCPP profile local authorization list"
+ help
+ Enables the OCPP library to support the Local Authorization List
+ functionality. Stores IdTag and its validity information in the
+ persistent stroage. Charge Point may support this as optional.
+
+config OCPP_PROFILE_FIRMWARE_MGNT
+ bool "OCPP profile firmware management"
+ help
+ Enables the OCPP library to support the Firmware Management of
+ Charge Point as optional profile.
+
+endif # OCPP
diff --git a/subsys/net/lib/ocpp/core.c b/subsys/net/lib/ocpp/core.c
new file mode 100644
index 0000000000000..54f40bcd811f3
--- /dev/null
+++ b/subsys/net/lib/ocpp/core.c
@@ -0,0 +1,500 @@
+/*
+ * Copyright (c) 2025 Linumiz GmbH
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include "ocpp_i.h"
+
+#define OCPP_PDU_TIMEOUT 2
+#define OCPP_USER_REQ_PDU_BUF 350
+#define FILL_METER_TABLE(_mes, _smes, _unit)[_mes] = {.mes = _mes, \
+ .smes = _smes, .unit = _unit}
+
+static struct {
+ enum ocpp_meter_measurand mes;
+ const char * const smes;
+ const char * const unit;
+} mtr_ref_table[] = {
+ FILL_METER_TABLE(OCPP_OMM_CURRENT_FROM_EV,
+ "Current.Export", "A"),
+ FILL_METER_TABLE(OCPP_OMM_CURRENT_TO_EV,
+ "Current.Import", "A"),
+ FILL_METER_TABLE(OCPP_OMM_CURRENT_MAX_OFFERED_TO_EV,
+ "Current.OfferedMaximum", "A"),
+ FILL_METER_TABLE(OCPP_OMM_ACTIVE_ENERGY_FROM_EV,
+ "Energy.Active.Export.Register", "Wh"),
+ FILL_METER_TABLE(OCPP_OMM_ACTIVE_ENERGY_TO_EV,
+ "Energy.Active.Import.Register", "Wh"),
+ FILL_METER_TABLE(OCPP_OMM_REACTIVE_ENERGY_FROM_EV,
+ "Energy.Reactive.Export.Register", "varh"),
+ FILL_METER_TABLE(OCPP_OMM_REACTIVE_ENERGY_TO_EV,
+ "Energy.Reactive.Import.Register", "varh"),
+ FILL_METER_TABLE(OCPP_OMM_ACTIVE_POWER_FROM_EV,
+ "Power.Active.Export", "W"),
+ FILL_METER_TABLE(OCPP_OMM_ACTIVE_POWER_TO_EV,
+ "Power.Active.Import", "W"),
+ FILL_METER_TABLE(OCPP_OMM_REACTIVE_POWER_FROM_EV,
+ "Power.Reactive.Export", "var"),
+ FILL_METER_TABLE(OCPP_OMM_REACTIVE_POWER_TO_EV,
+ "Power.Reactive.Import", "var"),
+ FILL_METER_TABLE(OCPP_OMM_POWERLINE_FREQ,
+ "Frequency", NULL),
+ FILL_METER_TABLE(OCPP_OMM_POWER_FACTOR,
+ "Power.Factor", NULL),
+ FILL_METER_TABLE(OCPP_OMM_POWER_MAX_OFFERED_TO_EV,
+ "Power.Offered", NULL),
+ FILL_METER_TABLE(OCPP_OMM_FAN_SPEED,
+ "RPM", "rpm"),
+ FILL_METER_TABLE(OCPP_OMM_CHARGING_PERCENT,
+ "SoCState", "Percent"),
+ FILL_METER_TABLE(OCPP_OMM_TEMPERATURE,
+ "Temperature", "Celsius"),
+ FILL_METER_TABLE(OCPP_OMM_VOLTAGE_AC_RMS,
+ "Voltage", "V")
+};
+
+int ocpp_boot_notification(ocpp_session_handle_t hndl,
+ struct ocpp_cp_info *cpi)
+{
+ int ret;
+ struct ocpp_session *sh = (struct ocpp_session *)hndl;
+ struct ocpp_info *ctx = sh->ctx;
+ struct ocpp_upstream_info *ui = &ctx->ui;
+ char *buf = ctx->pdu_buf;
+ ocpp_msg_fp_t fn;
+ struct ocpp_wamp_rpc_msg rmsg = {0};
+
+ fn = ctx->cfn[PDU_BOOTNOTIFICATION];
+ sh->uid = fn(buf, sizeof(ctx->pdu_buf), sh, cpi);
+
+ rmsg.ctx = ctx;
+ rmsg.msg = buf;
+ rmsg.msg_len = strlen(buf);
+ rmsg.sndlock = &ui->ws_sndlock;
+ rmsg.rspsig = &ui->ws_rspsig;
+
+ ret = ocpp_send_to_server(&rmsg, K_SECONDS(OCPP_PDU_TIMEOUT));
+ if (!ret && sh->resp_status != BOOT_ACCEPTED) {
+ ret = -EAGAIN;
+ }
+
+ return ret;
+}
+
+int ocpp_get_configuration(enum ocpp_key key, struct ocpp_info *ctx, char *uid)
+{
+ int ret;
+ char tmp[32];
+ char *sval = NULL;
+ char *buf = ctx->pdu_buf;
+ struct ocpp_wamp_rpc_msg rmsg = {0};
+ union ocpp_keyval *kval;
+ ocpp_msg_fp_t fn;
+ bool is_rw;
+ enum ocpp_key_type ktype;
+
+ if (key >= OCPP_CFG_END) {
+ return -EINVAL;
+ }
+
+ ktype = ocpp_get_keyval_type(key);
+ is_rw = ocpp_is_key_rw(key);
+ kval = ocpp_get_key_val(key);
+
+ if (ktype < KEY_TYPE_STR) {
+ sval = tmp;
+ snprintk(tmp, sizeof(tmp), "%d", kval->ival);
+ } else {
+ sval = kval->str;
+ }
+
+ fn = ctx->cfn[PDU_GET_CONFIGURATION];
+ fn(buf, sizeof(ctx->pdu_buf), ocpp_get_key_literal(key), sval, is_rw, uid);
+
+ rmsg.ctx = ctx;
+ rmsg.msg = buf;
+ rmsg.msg_len = strlen(buf);
+
+ ret = ocpp_send_to_server(&rmsg, K_SECONDS(OCPP_PDU_TIMEOUT));
+ return ret;
+}
+
+int ocpp_change_configuration(char *skey, struct ocpp_info *ctx,
+ char *sval, char *uid)
+{
+ int ret = -EINVAL;
+ char *buf = ctx->pdu_buf;
+ struct ocpp_wamp_rpc_msg rmsg = {0};
+ union ocpp_keyval kval;
+ ocpp_msg_fp_t fn;
+ enum ocpp_key key;
+ const char *res = "Accepted";
+
+ key = ocpp_key_to_cfg(skey);
+ if (key < OCPP_CFG_END) {
+ enum ocpp_key_type ktype;
+
+ ktype = ocpp_get_keyval_type(key);
+
+ if (ktype < KEY_TYPE_STR) {
+ kval.ival = atoi(sval);
+ } else {
+ kval.str = sval;
+ }
+
+ ret = ocpp_update_cfg_val(key, &kval);
+ if (ret) {
+ res = "Rejected";
+ }
+ } else {
+ res = "NotSupported";
+ }
+
+ if (ret == 0) {
+ switch (key) {
+ case CFG_MTR_VAL_SAMPLE_INTERVAL:
+ if (atomic_get(&ctx->mtr_timer_ref_cnt) <= 0) {
+ break;
+ }
+
+ k_timer_start(&ctx->mtr_timer,
+ K_SECONDS(kval.ival),
+ K_SECONDS(kval.ival));
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ fn = ctx->cfn[PDU_CHANGE_CONFIGURATION];
+ fn(buf, sizeof(ctx->pdu_buf), res, uid);
+
+ rmsg.ctx = ctx;
+ rmsg.msg = buf;
+ rmsg.msg_len = strlen(buf);
+
+ ret = ocpp_send_to_server(&rmsg, K_SECONDS(OCPP_PDU_TIMEOUT));
+
+ return ret;
+}
+
+int ocpp_authorize(ocpp_session_handle_t hndl, char *idtag,
+ enum ocpp_auth_status *status,
+ uint32_t timeout_ms)
+{
+ int ret;
+ struct ocpp_session *sh = (struct ocpp_session *)hndl;
+ struct ocpp_info *ctx;
+ struct ocpp_upstream_info *ui;
+ char buf[OCPP_USER_REQ_PDU_BUF];
+ union ocpp_keyval *val;
+ struct ocpp_wamp_rpc_msg rmsg = {0};
+ ocpp_msg_fp_t fn;
+
+ if (!idtag || !status || !ocpp_session_is_valid(sh)) {
+ return -EINVAL;
+ }
+
+ ctx = sh->ctx;
+ if (ctx->state < CP_STATE_READY) {
+ return -EAGAIN;
+ }
+
+ if (ctx->is_cs_offline) {
+ val = ocpp_get_key_val(CFG_LOCAL_AUTH_OFFLINE);
+ if (val && !val->ival) {
+ return -EAGAIN;
+ }
+ }
+
+ strncpy(sh->idtag, idtag, sizeof(sh->idtag));
+ ui = &ctx->ui;
+ fn = ctx->cfn[PDU_AUTHORIZE];
+ sh->uid = fn(buf, sizeof(buf), sh);
+
+ rmsg.ctx = ctx;
+ rmsg.msg = buf;
+ rmsg.msg_len = strlen(buf);
+ rmsg.sndlock = &ui->ws_sndlock;
+ rmsg.rspsig = &ui->ws_rspsig;
+ ret = ocpp_send_to_server(&rmsg, K_MSEC(timeout_ms));
+ if (ret < 0) {
+ return ret;
+ }
+
+ *status = sh->resp_status;
+ if (sh->resp_status == OCPP_AUTH_ACCEPTED) {
+ sh->is_active = true;
+ }
+
+ return 0;
+}
+
+int ocpp_heartbeat(ocpp_session_handle_t hndl)
+{
+ int ret;
+ struct ocpp_session *sh = (struct ocpp_session *)hndl;
+ struct ocpp_info *ctx = sh->ctx;
+ struct ocpp_upstream_info *ui = &ctx->ui;
+ ocpp_msg_fp_t fn;
+ struct ocpp_wamp_rpc_msg rmsg = {0};
+ char *buf = ctx->pdu_buf;
+
+ fn = ctx->cfn[PDU_HEARTBEAT];
+ sh->uid = fn(buf, sizeof(ctx->pdu_buf), sh);
+
+ rmsg.ctx = ctx;
+ rmsg.msg = buf;
+ rmsg.msg_len = strlen(buf);
+ rmsg.sndlock = &ui->ws_sndlock;
+ rmsg.rspsig = &ui->ws_rspsig;
+
+ ret = ocpp_send_to_server(&rmsg, K_SECONDS(OCPP_PDU_TIMEOUT));
+ if (!ret && sh->resp_status != BOOT_ACCEPTED) {
+ ret = -EAGAIN;
+ }
+
+ return ret;
+}
+
+int ocpp_start_transaction(ocpp_session_handle_t hndl,
+ int Wh,
+ uint8_t conn_id,
+ uint32_t timeout_ms)
+{
+ struct ocpp_session *sh = (struct ocpp_session *)hndl;
+ struct ocpp_info *ctx;
+ struct ocpp_upstream_info *ui;
+ char buf[OCPP_USER_REQ_PDU_BUF];
+ struct ocpp_wamp_rpc_msg rmsg = {0};
+ ocpp_msg_fp_t fn;
+ char utc[CISTR25] = {0};
+ int ret;
+
+ if (!ocpp_session_is_valid(sh) ||
+ conn_id <= 0) {
+ return -EINVAL;
+ }
+ ctx = sh->ctx;
+ sh->idcon = conn_id;
+ if (ctx->state < CP_STATE_READY) {
+ return -EAGAIN;
+ }
+
+ if (ctx->is_cs_offline) {
+ /* todo: fill meter reading
+ * server offline, accept start txn and save it in Q.
+ */
+ return 0;
+ }
+
+ fn = ctx->cfn[PDU_START_TRANSACTION];
+ ocpp_get_utc_now(utc);
+ sh->uid = fn(buf, sizeof(buf), sh, Wh, -1, utc);
+
+ ui = &ctx->ui;
+ rmsg.ctx = ctx;
+ rmsg.msg = buf;
+ rmsg.msg_len = strlen(buf);
+ rmsg.sndlock = &ui->ws_sndlock;
+ rmsg.rspsig = &ui->ws_rspsig;
+ ret = ocpp_send_to_server(&rmsg, K_MSEC(timeout_ms));
+ if (ret < 0) {
+ return ret;
+ }
+
+ if (sh->resp_status != OCPP_AUTH_ACCEPTED) {
+ sh->is_active = false;
+ ret = -EACCES;
+ } else {
+ union ocpp_keyval *keyval;
+
+ keyval = ocpp_get_key_val(CFG_MTR_VAL_SAMPLE_INTERVAL);
+ if (!atomic_inc(&ctx->mtr_timer_ref_cnt)) {
+ k_timer_start(&ctx->mtr_timer,
+ K_SECONDS(keyval->ival),
+ K_SECONDS(keyval->ival));
+ }
+ }
+
+ return ret;
+}
+
+int ocpp_stop_transaction(ocpp_session_handle_t hndl,
+ int Wh,
+ uint32_t timeout_ms)
+{
+ struct ocpp_session *sh = (struct ocpp_session *)hndl;
+ struct ocpp_info *ctx;
+ struct ocpp_upstream_info *ui;
+ char buf[OCPP_USER_REQ_PDU_BUF];
+ char utc[CISTR25] = {0};
+ struct ocpp_wamp_rpc_msg rmsg = {0};
+ ocpp_msg_fp_t fn;
+ int ret;
+
+ if (!ocpp_session_is_valid(sh)) {
+ return -EINVAL;
+ }
+
+ ctx = sh->ctx;
+ if (ctx->state < CP_STATE_READY) {
+ return -EAGAIN;
+ }
+
+ sh->is_active = false;
+ if (atomic_dec(&ctx->mtr_timer_ref_cnt) <= 1) {
+ k_timer_stop(&ctx->mtr_timer);
+ }
+
+ if (ctx->is_cs_offline) {
+ /* todo: fill meter reading
+ * server offline, accept start txn and save it in Q.
+ */
+ return 0;
+ }
+
+ fn = ctx->cfn[PDU_STOP_TRANSACTION];
+ ocpp_get_utc_now(utc);
+ sh->uid = fn(buf, sizeof(buf), sh, Wh, NULL, utc);
+
+ ui = &ctx->ui;
+ rmsg.ctx = ctx;
+ rmsg.msg = buf;
+ rmsg.msg_len = strlen(buf);
+ rmsg.sndlock = &ui->ws_sndlock;
+ rmsg.rspsig = &ui->ws_rspsig;
+ ret = ocpp_send_to_server(&rmsg, K_MSEC(timeout_ms));
+
+ return ret;
+}
+
+int ocpp_remote_start_transaction(struct ocpp_info *ctx,
+ struct internal_msg *msg,
+ char *uid)
+{
+ char *buf = ctx->pdu_buf;
+ const char *resp = "Rejected";
+ struct ocpp_wamp_rpc_msg rmsg = {0};
+ ocpp_msg_fp_t fn;
+ int ret;
+
+ ret = k_msgq_put(ctx->msgq, msg, K_MSEC(100));
+ if (ret == 0) {
+ resp = "Accepted";
+ }
+
+ fn = ctx->cfn[PDU_REMOTE_START_TRANSACTION];
+ fn(buf, sizeof(ctx->pdu_buf), resp, uid);
+
+ rmsg.ctx = ctx;
+ rmsg.msg = buf;
+ rmsg.msg_len = strlen(buf);
+ ret = ocpp_send_to_server(&rmsg, K_SECONDS(OCPP_PDU_TIMEOUT));
+
+ return ret;
+}
+
+int ocpp_remote_stop_transaction(struct ocpp_info *ctx,
+ struct internal_msg *msg,
+ int idtxn, char *uid)
+{
+ char *buf = ctx->pdu_buf;
+ const char *resp = "Rejected";
+ struct ocpp_wamp_rpc_msg rmsg = {0};
+ ocpp_msg_fp_t fn;
+ sys_snode_t *node;
+ struct ocpp_session *sh;
+ bool is_found = false;
+ int ret;
+
+ k_mutex_lock(&ctx->ilock, K_FOREVER);
+ SYS_SLIST_FOR_EACH_NODE(&ctx->slist, node) {
+ sh = to_session(node);
+ if (sh->is_active &&
+ sh->idtxn == idtxn) {
+ is_found = true;
+ break;
+ }
+ }
+ k_mutex_unlock(&ctx->ilock);
+
+ if (is_found) {
+ msg->usr.stop_charge.id_con = sh->idcon;
+ ret = k_msgq_put(ctx->msgq, msg, K_MSEC(100));
+ if (ret == 0) {
+ resp = "Accepted";
+ }
+ }
+
+ fn = ctx->cfn[PDU_REMOTE_STOP_TRANSACTION];
+ fn(buf, sizeof(ctx->pdu_buf), resp, uid);
+
+ rmsg.ctx = ctx;
+ rmsg.msg = buf;
+ rmsg.msg_len = strlen(buf);
+ ret = ocpp_send_to_server(&rmsg, K_SECONDS(OCPP_PDU_TIMEOUT));
+
+ return ret;
+}
+
+int ocpp_unlock_connector(struct ocpp_info *ctx,
+ struct internal_msg *msg,
+ char *uid)
+{
+ char *buf = ctx->pdu_buf;
+ char *resp = "UnlockFailed";
+ struct ocpp_wamp_rpc_msg rmsg = {0};
+ ocpp_msg_fp_t fn;
+ int ret;
+
+ ret = k_msgq_put(ctx->msgq, msg, K_MSEC(100));
+ if (ret == 0) {
+ resp = "Unlocked";
+ }
+
+ fn = ctx->cfn[PDU_UNLOCK_CONNECTOR];
+ fn(buf, sizeof(ctx->pdu_buf), resp, uid);
+
+ rmsg.ctx = ctx;
+ rmsg.msg = buf;
+ rmsg.msg_len = strlen(buf);
+ ret = ocpp_send_to_server(&rmsg, K_SECONDS(OCPP_PDU_TIMEOUT));
+
+ return ret;
+}
+
+int ocpp_meter_values(ocpp_session_handle_t hndl,
+ enum ocpp_meter_measurand mes,
+ char *sval)
+{
+ struct ocpp_session *sh = (struct ocpp_session *)hndl;
+ struct ocpp_info *ctx = sh->ctx;
+ struct ocpp_upstream_info *ui = &ctx->ui;
+ char *buf = ctx->pdu_buf;
+ char utc[CISTR25] = {0};
+ struct ocpp_wamp_rpc_msg rmsg = {0};
+ ocpp_msg_fp_t fn;
+ int ret;
+
+ if (ctx->is_cs_offline) {
+ return -EAGAIN;
+ }
+
+ fn = ctx->cfn[PDU_METER_VALUES];
+ ocpp_get_utc_now(utc);
+ sh->uid = fn(buf, sizeof(ctx->pdu_buf), sh, utc, sval,
+ mtr_ref_table[mes].smes,
+ mtr_ref_table[mes].unit);
+
+ rmsg.ctx = ctx;
+ rmsg.msg = buf;
+ rmsg.msg_len = strlen(buf);
+ rmsg.sndlock = &ui->ws_sndlock;
+ rmsg.rspsig = &ui->ws_rspsig;
+ ret = ocpp_send_to_server(&rmsg, K_SECONDS(OCPP_PDU_TIMEOUT));
+
+ return ret;
+}
diff --git a/subsys/net/lib/ocpp/key_mgmt.c b/subsys/net/lib/ocpp/key_mgmt.c
new file mode 100644
index 0000000000000..c9b3a2b71c705
--- /dev/null
+++ b/subsys/net/lib/ocpp/key_mgmt.c
@@ -0,0 +1,191 @@
+/*
+ * Copyright (c) 2025 Linumiz GmbH
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include "ocpp_i.h"
+
+#if defined(CONFIG_OCPP_PROFILE_SMART_CHARGE)
+#define CP_SC ",SmartCharging"
+#else
+#define CP_SC ""
+#endif
+
+#if defined(CONFIG_OCPP_PROFILE_REMOTE_TRIG)
+#define CP_RT ",RemoteTrigger"
+#else
+#define CP_RT ""
+#endif
+
+#if defined(CONFIG_OCPP_PROFILE_RESERVATION)
+#define CP_RE ",Reservation"
+#else
+#define CP_RE ""
+#endif
+
+#if defined(CONFIG_OCPP_PROFILE_LOCAL_AUTH_LIST)
+#define CP_LAL ",LocalAuthListManagement"
+#else
+#define CP_LAL ""
+#endif
+
+#if defined(CONFIG_OCPP_PROFILE_FIRMWARE_MGNT)
+#define CP_FM ",FirmwareManagement"
+#else
+#define CP_FM ""
+#endif
+
+#define CP_CORE "Core"
+
+struct ocpp_cfg_info {
+ enum ocpp_key_type type;
+ bool is_rw;
+ enum ocpp_key key;
+ char *skey;
+ union ocpp_keyval val;
+};
+
+#define FILL_KEY_TABLE(_type, _rw, _key, _skey, _val)[_key] = \
+ {.type = _type, .is_rw = _rw, .key = _key, .skey = _skey, .val = {_val}}
+
+static struct ocpp_cfg_info cached_key_cfg[OCPP_CFG_END] = {
+ FILL_KEY_TABLE(KEY_TYPE_BOOL, 1, CFG_ALLOW_OFFLINE_TX_FOR_UNKN_ID,
+ "AllowOfflineTxForUnknownId", false),
+ FILL_KEY_TABLE(KEY_TYPE_BOOL, 1, CFG_AUTH_CACHE_ENABLED,
+ "AuthorizationCacheEnabled", false),
+ FILL_KEY_TABLE(KEY_TYPE_BOOL, 0, CFG_AUTH_REMOTE_TX_REQ,
+ "AuthorizeRemoteTxRequests", true),
+ FILL_KEY_TABLE(KEY_TYPE_INT, 1, CFG_BLINK_REPEAT, "BlinkRepeat", 0),
+ FILL_KEY_TABLE(KEY_TYPE_INT, 1, CFG_CLK_ALIGN_DATA_INTERVAL,
+ "ClockAlignedDataInterval", 0),
+ FILL_KEY_TABLE(KEY_TYPE_INT, 1, CFG_CONN_TIMEOUT,
+ "ConnectionTimeOut", 0),
+ FILL_KEY_TABLE(KEY_TYPE_INT, 0, CFG_GETCFG_MAX_KEY,
+ "GetConfigurationMaxKeys", 1),
+ FILL_KEY_TABLE(KEY_TYPE_INT, 1, CFG_HEARTBEAT_INTERVAL,
+ "HeartbeatInterval", 0),
+ FILL_KEY_TABLE(KEY_TYPE_INT, 1, CFG_LIGHT_INTENSITY,
+ "LightIntensity", 0),
+ FILL_KEY_TABLE(KEY_TYPE_BOOL, 1, CFG_LOCAL_AUTH_OFFLINE,
+ "LocalAuthorizeOffline", 0),
+ FILL_KEY_TABLE(KEY_TYPE_BOOL, 1, CFG_LOCAL_PREAUTH,
+ "LocalPreAuthorize", 0),
+ FILL_KEY_TABLE(KEY_TYPE_INT, 1, CFG_MAX_ENERGYON_INVL_ID,
+ "MaxEnergyOnInvalidId", 0),
+ FILL_KEY_TABLE(KEY_TYPE_CSL, 1, CFG_MTR_VAL_ALGIN_DATA,
+ "MeterValuesAlignedData", (uint32_t)"0"),
+ FILL_KEY_TABLE(KEY_TYPE_INT, 0, CFG_MTR_VAL_ALGIN_DATA_MAXLEN,
+ "MeterValuesAlignedDataMaxLength", 1),
+ FILL_KEY_TABLE(KEY_TYPE_CSL, 1, CFG_MTR_VAL_SAMPLED_DATA,
+ "MeterValuesSampledData", (uint32_t)"0"),
+ FILL_KEY_TABLE(KEY_TYPE_INT, 0, CFG_MTR_VAL_SAMPLED_DATA_MAXLEN,
+ "MeterValuesSampledDataMaxLength", 1),
+ FILL_KEY_TABLE(KEY_TYPE_INT, 1, CFG_MTR_VAL_SAMPLE_INTERVAL,
+ "MeterValueSampleInterval", 5),
+ FILL_KEY_TABLE(KEY_TYPE_INT, 1, CFG_MIN_STATUS_DURATION,
+ "MinimumStatusDuration", 0),
+ FILL_KEY_TABLE(KEY_TYPE_INT, 0, CFG_NO_OF_CONNECTORS,
+ "NumberOfConnectors", 0),
+ FILL_KEY_TABLE(KEY_TYPE_INT, 1, CFG_REST_RETRIES,
+ "ResetRetries", 0),
+ FILL_KEY_TABLE(KEY_TYPE_CSL, 1, CFG_CONN_PHASE_ROT,
+ "ConnectorPhaseRotation", (uint32_t)"0"),
+ FILL_KEY_TABLE(KEY_TYPE_INT, 0, CFG_CONN_PHASE_ROT_MAXLEN,
+ "ConnectorPhaseRotationMaxLength", 1),
+ FILL_KEY_TABLE(KEY_TYPE_BOOL, 1, CFG_STOP_TXN_ON_EVSIDE_DISCON,
+ "StopTransactionOnEVSideDisconnect", 0),
+ FILL_KEY_TABLE(KEY_TYPE_BOOL, 1, CFG_STOP_TXN_ON_INVL_ID,
+ "StopTransactionOnInvalidId", 0),
+ FILL_KEY_TABLE(KEY_TYPE_CSL, 1, CFG_STOP_TXN_ALIGNED_DATA,
+ "StopTxnAlignedData", (uint32_t)"0"),
+ FILL_KEY_TABLE(KEY_TYPE_INT, 0, CFG_STOP_TXN_ALIGNED_DATA_MAXLEN,
+ "StopTxnAlignedDataMaxLength", 1),
+ FILL_KEY_TABLE(KEY_TYPE_CSL, 0, CFG_SUPPORTED_FEATURE_PROFILE,
+ "SupportedFeatureProfiles",
+ (uint32_t) CP_CORE CP_SC CP_RT CP_RE CP_LAL CP_FM),
+ FILL_KEY_TABLE(KEY_TYPE_INT, 0, CFG_SUPPORTED_FEATURE_PROFILE_MAXLEN,
+ "SupportedFeatureProfilesMaxLength", 6),
+ FILL_KEY_TABLE(KEY_TYPE_INT, 1, CFG_TXN_MSG_ATTEMPTS,
+ "TransactionMessageAttempts", 0),
+ FILL_KEY_TABLE(KEY_TYPE_INT, 1, CFG_TXN_MSG_RETRY_INTERVAL,
+ "TransactionMessageRetryInterval", 0),
+ FILL_KEY_TABLE(KEY_TYPE_BOOL, 1, CFG_UNLOCK_CONN_ON_EVSIDE_DISCON,
+ "UnlockConnectorOnEVSideDisconnect", 0),
+ FILL_KEY_TABLE(KEY_TYPE_INT, 1, CFG_WEBSOCK_PING_INTERVAL,
+ "WebSocketPingInterval", 0),
+
+ /* optional */
+};
+
+enum ocpp_key_type ocpp_get_keyval_type(enum ocpp_key key)
+{
+ return cached_key_cfg[key].type;
+}
+
+enum ocpp_key ocpp_key_to_cfg(const char *skey)
+{
+ int i;
+ struct ocpp_cfg_info *cfg = cached_key_cfg;
+
+ for (i = 0; i < OCPP_CFG_END; i++) {
+ if (!strncmp(cfg[i].skey, skey, strlen(cfg[i].skey))) {
+ break;
+ }
+ }
+
+ return i;
+}
+
+bool ocpp_is_key_rw(enum ocpp_key key)
+{
+ return cached_key_cfg[key].is_rw;
+}
+
+union ocpp_keyval *ocpp_get_key_val(enum ocpp_key key)
+{
+ if (key >= OCPP_CFG_END) {
+ return NULL;
+ }
+
+ return &cached_key_cfg[key].val;
+}
+
+char *ocpp_get_key_literal(enum ocpp_key key)
+{
+ if (key >= OCPP_CFG_END) {
+ return NULL;
+ }
+
+ return cached_key_cfg[key].skey;
+}
+
+int ocpp_set_cfg_val(enum ocpp_key key, union ocpp_keyval *val)
+{
+ enum ocpp_key_type type;
+ struct ocpp_cfg_info *key_cfg;
+
+ key_cfg = &cached_key_cfg[key];
+ type = ocpp_get_keyval_type(key);
+ if (type < KEY_TYPE_STR) {
+ key_cfg->val.ival = val->ival;
+ } else {
+ if (key_cfg->val.str) {
+ free(key_cfg->val.str);
+ }
+ key_cfg->val.str = strdup(val->str);
+ }
+
+ return 0;
+}
+
+int ocpp_update_cfg_val(enum ocpp_key key, union ocpp_keyval *val)
+{
+
+ if (key >= OCPP_CFG_END ||
+ !cached_key_cfg[key].is_rw) {
+ return -EINVAL;
+ }
+
+ return ocpp_set_cfg_val(key, val);
+}
diff --git a/subsys/net/lib/ocpp/ocpp.c b/subsys/net/lib/ocpp/ocpp.c
new file mode 100644
index 0000000000000..885721beb8e5c
--- /dev/null
+++ b/subsys/net/lib/ocpp/ocpp.c
@@ -0,0 +1,737 @@
+/*
+ * Copyright (c) 2025 Linumiz GmbH
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include "ocpp_i.h"
+#include
+
+LOG_MODULE_REGISTER(ocpp, CONFIG_OCPP_LOG_LEVEL);
+
+#define OCPP_UPSTREAM_PRIORITY 7
+#define OCPP_WS_TIMEOUT 5000
+#define OCPP_INTERNAL_MSG_SIZE (sizeof(struct internal_msg))
+#define FILL_PDU_TABLE(_pdu, _spdu)[_pdu] = {_pdu, _spdu, }
+
+struct ocpp_msg_table {
+ enum ocpp_pdu_msg pdu;
+ char *spdu;
+};
+
+static K_THREAD_STACK_DEFINE(ocpp_int_handler_stack, CONFIG_OCPP_INT_THREAD_STACKSIZE);
+static K_THREAD_STACK_DEFINE(ocpp_wsreader_stack, CONFIG_OCPP_WSREADER_THREAD_STACKSIZE);
+
+K_MSGQ_DEFINE(ocpp_iq, OCPP_INTERNAL_MSG_SIZE, CONFIG_OCPP_INTERNAL_MSGQ_CNT, sizeof(uint32_t));
+
+struct ocpp_info *gctx;
+
+static struct ocpp_msg_table pdu_msg_table[] = {
+ FILL_PDU_TABLE(PDU_BOOTNOTIFICATION, "BootNotification"),
+ FILL_PDU_TABLE(PDU_AUTHORIZE, "Authorize"),
+ FILL_PDU_TABLE(PDU_START_TRANSACTION, "StartTransaction"),
+ FILL_PDU_TABLE(PDU_STOP_TRANSACTION, "StopTransaction"),
+ FILL_PDU_TABLE(PDU_HEARTBEAT, "Heartbeat"),
+ FILL_PDU_TABLE(PDU_METER_VALUES, "MeterValues"),
+ FILL_PDU_TABLE(PDU_CLEAR_CACHE, "ClearCache"),
+ FILL_PDU_TABLE(PDU_REMOTE_START_TRANSACTION, "RemoteStartTransaction"),
+ FILL_PDU_TABLE(PDU_REMOTE_STOP_TRANSACTION, "RemoteStopTransaction"),
+ FILL_PDU_TABLE(PDU_GET_CONFIGURATION, "GetConfiguration"),
+ FILL_PDU_TABLE(PDU_CHANGE_CONFIGURATION, "ChangeConfiguration"),
+ FILL_PDU_TABLE(PDU_CHANGE_AVAILABILITY, "ChangeAvailability"),
+ FILL_PDU_TABLE(PDU_UNLOCK_CONNECTOR, "UnlockConnector"),
+ FILL_PDU_TABLE(PDU_RESET, "Reset"),
+};
+
+const char *ocpp_get_pdu_literal(enum ocpp_pdu_msg pdu)
+{
+ if (pdu >= PDU_MSG_END) {
+ return "";
+ }
+
+ return pdu_msg_table[pdu].spdu;
+}
+
+int ocpp_find_pdu_from_literal(const char *msg)
+{
+ struct ocpp_msg_table *mt = pdu_msg_table;
+ int i;
+
+ for (i = 0; i < PDU_MSG_END; i++) {
+ if (!strncmp(mt[i].spdu, msg, strlen(mt[i].spdu))) {
+ break;
+ }
+ }
+
+ return i;
+}
+
+void ocpp_get_utc_now(char utc[CISTR25])
+{
+ struct timeval tv;
+ struct tm htime = {0};
+
+ gettimeofday(&tv, NULL);
+ gmtime_r(&tv.tv_sec, &htime);
+
+ snprintk(utc, CISTR25, "%04hu-%02hu-%02huT%02hu:%02hu:%02huZ",
+ htime.tm_year + 1900,
+ htime.tm_mon + 1,
+ htime.tm_mday,
+ htime.tm_hour,
+ htime.tm_min,
+ htime.tm_sec);
+}
+
+bool ocpp_session_is_valid(struct ocpp_session *sh)
+{
+ sys_snode_t *node;
+ struct ocpp_session *lsh;
+ bool is_found = false;
+
+ if (!sh) {
+ return is_found;
+ }
+
+ k_mutex_lock(&gctx->ilock, K_FOREVER);
+ SYS_SLIST_FOR_EACH_NODE(&gctx->slist, node) {
+ lsh = to_session(node);
+ if (lsh == sh) {
+ is_found = true;
+ break;
+ }
+ }
+ k_mutex_unlock(&gctx->ilock);
+
+ return is_found;
+}
+
+static int ocpp_ws_connect_cb(int ws_sock, struct http_request *req,
+ void *user_data)
+{
+ struct ocpp_info *ctx = user_data;
+
+ ctx->is_cs_offline = false;
+ return 0;
+}
+
+static int ocpp_connect_to_cs(struct ocpp_info *ctx)
+{
+ int ret;
+ struct websocket_request config = {0};
+ struct ocpp_upstream_info *ui = &ctx->ui;
+ struct sockaddr addr_buf;
+ struct sockaddr *addr = &addr_buf;
+ int addr_size;
+
+ if (ui->csi.sa_family == AF_INET) {
+#if defined(CONFIG_NET_IPV4)
+ addr_size = sizeof(addr_buf);
+ addr->sa_family = ui->csi.sa_family;
+ net_sin(addr)->sin_port = htons(ui->csi.port);
+ zsock_inet_pton(addr->sa_family, ui->csi.cs_ip,
+ &net_sin(addr)->sin_addr);
+#endif
+ } else {
+#if defined(CONFIG_NET_IPV6)
+ addr_size = sizeof(struct sockaddr_in6);
+ addr->sa_family = ui->csi.sa_family;
+ net_sin6(addr)->sin6_port = htons(ui->csi.port);
+ zsock_inet_pton(addr->sa_family, ui->csi.cs_ip,
+ &net_sin6(addr)->sin6_addr);
+#endif
+ }
+
+ ret = zsock_connect(ui->tcpsock, addr, addr_size);
+ if (ret < 0 && errno != EALREADY && errno != EISCONN) {
+ LOG_ERR("tcp socket connect fail %d %d", ret, errno);
+ return ret;
+ }
+
+ if (ui->wssock >= 0) {
+ websocket_disconnect(ui->wssock);
+ ui->wssock = -1;
+ }
+
+ if (ui->wssock < 0) {
+ char buf[128];
+ char const *optional_hdr[] = {
+ "Sec-WebSocket-Protocol: ocpp1.6\r\n",
+ NULL };
+
+ snprintk(buf, sizeof(buf), "%s:%u", ui->csi.cs_ip, ui->csi.port);
+ config.url = ui->csi.ws_url;
+ config.host = buf;
+ config.tmp_buf = ui->wsrecv_buf;
+ config.tmp_buf_len = sizeof(ui->wsrecv_buf);
+ config.cb = ocpp_ws_connect_cb;
+ config.optional_headers = optional_hdr;
+
+ ret = websocket_connect(ui->tcpsock, &config,
+ OCPP_WS_TIMEOUT, ctx);
+ if (ret < 0) {
+ LOG_ERR("Websocket connect fail %d", ret);
+ return ret;
+ }
+ ui->wssock = ret;
+ }
+
+ LOG_DBG("WS connect success %d", ui->wssock);
+ return 0;
+}
+
+static inline void bootnotification_free_resource(struct ocpp_cp_info *cpi)
+{
+ free(cpi->model);
+ free(cpi->vendor);
+ free(cpi->sl_no);
+ free(cpi->box_sl_no);
+ free(cpi->fw_ver);
+ free(cpi->iccid);
+ free(cpi->imsi);
+ free(cpi->meter_sl_no);
+ free(cpi->meter_type);
+ k_free(cpi);
+}
+
+static void ocpp_internal_handler(void *p1, void *p2, void *p3)
+{
+ struct ocpp_info *ctx = p1;
+ struct ocpp_cp_info *cpi = p2;
+ ocpp_session_handle_t sh;
+ struct internal_msg msg = {0};
+ int ret;
+ int i;
+
+ /* open internal session */
+ ocpp_session_open(&sh);
+
+ while (!k_msgq_get(ctx->msgq, &msg, K_FOREVER)) {
+ switch (msg.msgtype) {
+ case PDU_BOOTNOTIFICATION:
+
+ if (!ctx->is_cs_offline && cpi) {
+ ret = ocpp_boot_notification(sh, cpi);
+ if (!ret) {
+ bootnotification_free_resource(cpi);
+ cpi = NULL;
+ ctx->state = CP_STATE_READY;
+ }
+ }
+
+ k_timer_start(&ctx->hb_timer,
+ K_SECONDS(ctx->hb_sec),
+ K_NO_WAIT);
+ break;
+
+ case PDU_METER_VALUES:
+ union ocpp_io_value io;
+ sys_snode_t *node;
+ struct ocpp_session *lsh;
+
+ /* list of all active session */
+ k_mutex_lock(&ctx->ilock, K_FOREVER);
+ SYS_SLIST_FOR_EACH_NODE(&ctx->slist, node) {
+ lsh = to_session(node);
+ if (lsh == sh ||
+ !lsh->is_active) {
+ continue;
+ }
+
+ k_mutex_lock(&lsh->slock, K_FOREVER);
+ k_mutex_unlock(&ctx->ilock);
+ io.meter_val.id_con = lsh->idcon;
+ for (i = 0; i < OCPP_OMM_END; i++) {
+ io.meter_val.mes = i;
+ ret = ctx->cb(OCPP_USR_GET_METER_VALUE,
+ &io, ctx->user_data);
+ if (ret) {
+ continue;
+ }
+ ocpp_meter_values(lsh, i,
+ io.meter_val.val);
+ }
+ k_mutex_lock(&ctx->ilock, K_FOREVER);
+ k_mutex_unlock(&lsh->slock);
+ }
+ k_mutex_unlock(&ctx->ilock);
+ break;
+
+ case PDU_HEARTBEAT:
+ ocpp_heartbeat(sh);
+ /* adjust local time with cs time ! */
+ k_timer_start(&ctx->hb_timer,
+ K_SECONDS(ctx->hb_sec),
+ K_NO_WAIT);
+ break;
+
+ case PDU_CS_ONLINE:
+ /* check offline msg and do nothing on empty
+ * else read msg and send to server
+ */
+ break;
+
+ case PDU_REMOTE_START_TRANSACTION:
+ msg.msgtype = OCPP_USR_START_CHARGING;
+ ctx->cb(msg.msgtype, &msg.usr, ctx->user_data);
+ break;
+
+ case PDU_REMOTE_STOP_TRANSACTION:
+ msg.msgtype = OCPP_USR_STOP_CHARGING;
+ ctx->cb(msg.msgtype, &msg.usr, ctx->user_data);
+ break;
+
+ case PDU_UNLOCK_CONNECTOR:
+ msg.msgtype = OCPP_USR_UNLOCK_CONNECTOR;
+ ctx->cb(msg.msgtype, &msg.usr, ctx->user_data);
+ break;
+
+ default:
+ break;
+ }
+ }
+}
+
+static int ocpp_process_server_msg(struct ocpp_info *ctx)
+{
+ ocpp_msg_fp_t fn;
+ struct ocpp_session *sh = NULL;
+ struct ocpp_upstream_info *ui = &ctx->ui;
+ char *buf, *tmp;
+ char uid[128];
+ int ret, idtxn, pdu, i;
+ bool is_rsp;
+ struct internal_msg msg;
+
+ ret = parse_rpc_msg(ui->recv_buf, sizeof(ui->recv_buf),
+ uid, sizeof(uid), &pdu, &is_rsp);
+ if (ret) {
+ return ret;
+ }
+
+ if (is_rsp) {
+ buf = strtok_r(uid, "-", &tmp);
+ sh = (struct ocpp_session *) atoi(buf);
+
+ buf = strtok_r(NULL, "-", &tmp);
+ pdu = atoi(buf);
+
+ if (!ocpp_session_is_valid(sh)) {
+ sh = NULL;
+ }
+ }
+
+ if (pdu >= PDU_MSG_END) {
+ return -EINVAL;
+ }
+
+ fn = ctx->pfn[pdu];
+
+ switch (pdu) {
+ char skey[CISTR50];
+
+ case PDU_BOOTNOTIFICATION:
+ struct boot_notif binfo;
+
+ ret = fn(ui->recv_buf, &binfo);
+ if (!ret && sh) {
+ sh->resp_status = binfo.status;
+ ctx->hb_sec = binfo.interval;
+ }
+ break;
+
+ case PDU_AUTHORIZE:
+ case PDU_STOP_TRANSACTION:
+ struct ocpp_idtag_info idinfo = {0};
+
+ if (!sh) {
+ break;
+ }
+
+ ret = fn(ui->recv_buf, &idinfo);
+ if (!ret) {
+ sh->resp_status = idinfo.auth_status;
+ }
+ break;
+
+ case PDU_START_TRANSACTION:
+ if (!sh) {
+ break;
+ }
+
+ ret = fn(ui->recv_buf, &idtxn, &idinfo);
+ if (!ret) {
+ sh->idtxn = idtxn;
+ sh->resp_status = idinfo.auth_status;
+ }
+ break;
+
+ case PDU_GET_CONFIGURATION:
+ memset(skey, 0, sizeof(skey));
+
+ ret = fn(ui->recv_buf, skey);
+ if (ret) {
+ break;
+ }
+
+ if (*skey) {
+ enum ocpp_key key;
+
+ key = ocpp_key_to_cfg(skey);
+ ocpp_get_configuration(key, ctx, uid);
+ } else {
+ for (i = 0; i < OCPP_CFG_END; i++) {
+ ocpp_get_configuration(i, ctx, uid);
+ }
+ }
+
+ break;
+
+ case PDU_CHANGE_CONFIGURATION:
+ char sval[CISTR500] = {0};
+
+ memset(skey, 0, sizeof(skey));
+ ret = fn(ui->recv_buf, skey, sval);
+ if (ret) {
+ break;
+ }
+
+ ocpp_change_configuration(skey, ctx, sval, uid);
+ break;
+
+ case PDU_HEARTBEAT:
+ /* todo : sync time */
+ break;
+
+ case PDU_REMOTE_START_TRANSACTION:
+ memset(&msg, 0, OCPP_INTERNAL_MSG_SIZE);
+ msg.msgtype = PDU_REMOTE_START_TRANSACTION;
+
+ ret = fn(ui->recv_buf, &msg.usr.start_charge.id_con,
+ msg.usr.start_charge.idtag);
+ if (ret) {
+ break;
+ }
+
+ ocpp_remote_start_transaction(ctx, &msg, uid);
+ break;
+
+ case PDU_REMOTE_STOP_TRANSACTION:
+ memset(&msg, 0, OCPP_INTERNAL_MSG_SIZE);
+ msg.msgtype = PDU_REMOTE_STOP_TRANSACTION;
+
+ ret = fn(ui->recv_buf, &idtxn);
+ if (ret) {
+ break;
+ }
+
+ ocpp_remote_stop_transaction(ctx, &msg, idtxn, uid);
+ break;
+
+ case PDU_UNLOCK_CONNECTOR:
+ memset(&msg, 0, OCPP_INTERNAL_MSG_SIZE);
+ msg.msgtype = PDU_UNLOCK_CONNECTOR;
+
+ ret = fn(ui->recv_buf, &msg.usr.unlock_con.id_con);
+ if (ret) {
+ break;
+ }
+
+ ocpp_unlock_connector(ctx, &msg, uid);
+ break;
+
+ case PDU_METER_VALUES:
+ default:
+ break;
+ }
+
+ if (is_rsp) {
+ k_poll_signal_raise(&ui->ws_rspsig, 0);
+ }
+ return 0;
+}
+
+#define TCP_CONNECT_AFTER 20 /* fixme arbitrary */
+static void ocpp_wsreader(void *p1, void *p2, void *p3)
+{
+ struct ocpp_info *ctx = p1;
+ struct ocpp_upstream_info *ui = &ctx->ui;
+ struct zsock_pollfd tcpfd;
+ uint8_t ret;
+ uint8_t retry_cnt = 0;
+
+ ctx->is_cs_offline = true;
+ tcpfd.fd = ui->tcpsock;
+ tcpfd.events = ZSOCK_POLLIN | ZSOCK_POLLERR
+ | ZSOCK_POLLHUP | ZSOCK_POLLNVAL;
+
+ while (1) {
+
+ if (ctx->is_cs_offline &&
+ !(retry_cnt++ % TCP_CONNECT_AFTER)) {
+
+ k_mutex_lock(&ctx->ilock, K_FOREVER);
+ ocpp_connect_to_cs(ctx);
+ k_mutex_unlock(&ctx->ilock);
+ }
+
+ ret = zsock_poll(&tcpfd, 1, 200);
+ if (ret <= 0) {
+ continue;
+ }
+
+ if ((tcpfd.revents & ZSOCK_POLLERR) ||
+ (tcpfd.revents & ZSOCK_POLLHUP) ||
+ (tcpfd.revents & ZSOCK_POLLNVAL)) {
+ LOG_ERR("poll err %d", tcpfd.revents);
+ ctx->is_cs_offline = true;
+ continue;
+ }
+
+ if (tcpfd.revents & ZSOCK_POLLIN) {
+ struct ocpp_wamp_rpc_msg rcv = {0};
+ uint32_t msg_type;
+
+ rcv.ctx = ctx;
+ rcv.msg = ui->recv_buf;
+ rcv.sndlock = &ui->ws_sndlock;
+ rcv.msg_len = sizeof(ui->recv_buf);
+
+ memset(ui->recv_buf, 0, rcv.msg_len);
+ ret = ocpp_receive_from_server(&rcv, &msg_type, 200);
+
+ if (ret < 0) {
+ if (ret == -ENOTCONN) {
+ ctx->is_cs_offline = true;
+ }
+ continue;
+ }
+
+ if (msg_type & WEBSOCKET_FLAG_PING) {
+ websocket_send_msg(ctx->ui.wssock, NULL, 0,
+ WEBSOCKET_OPCODE_PONG, true,
+ true, 100);
+ } else if (msg_type & WEBSOCKET_FLAG_CLOSE) {
+ ctx->is_cs_offline = true;
+ } else {
+ ocpp_process_server_msg(ctx);
+ }
+
+ }
+ }
+}
+
+int ocpp_upstream_init(struct ocpp_info *ctx, struct ocpp_cs_info *csi)
+{
+ struct ocpp_upstream_info *ui = &ctx->ui;
+
+ LOG_INF("upstream init");
+
+ ui->csi.ws_url = strdup(csi->ws_url);
+ ui->csi.cs_ip = strdup(csi->cs_ip);
+ ui->csi.port = csi->port;
+ ui->csi.sa_family = csi->sa_family;
+ ui->tcpsock = zsock_socket(csi->sa_family, SOCK_STREAM,
+ IPPROTO_TCP);
+ if (ui->tcpsock < 0) {
+ return -errno;
+ }
+
+ k_mutex_init(&ui->ws_sndlock);
+ k_poll_signal_init(&ui->ws_rspsig);
+ ui->wssock = -1;
+
+ websocket_init();
+
+ k_thread_create(&ui->tinfo, ocpp_wsreader_stack,
+ CONFIG_OCPP_WSREADER_THREAD_STACKSIZE,
+ ocpp_wsreader, ctx, NULL, NULL,
+ OCPP_UPSTREAM_PRIORITY, 0, K_MSEC(100));
+
+ return 0;
+}
+
+static void timer_heartbeat_cb(struct k_timer *t)
+{
+ struct ocpp_info *ctx;
+ struct internal_msg msg = {PDU_HEARTBEAT};
+
+ ctx = k_timer_user_data_get(t);
+ if (ctx->state == CP_STATE_BOOTNOTIF) {
+ msg.msgtype = PDU_BOOTNOTIFICATION;
+ }
+
+ k_msgq_put(ctx->msgq, &msg, K_NO_WAIT);
+}
+
+static void timer_meter_cb(struct k_timer *t)
+{
+ struct ocpp_info *ctx;
+ struct internal_msg msg = {PDU_METER_VALUES};
+
+ ctx = k_timer_user_data_get(t);
+
+ k_msgq_put(ctx->msgq, &msg, K_NO_WAIT);
+}
+
+static inline void bootnotification_fill_resource(struct ocpp_cp_info *cp,
+ struct ocpp_cp_info *cpi,
+ struct ocpp_info *ctx)
+{
+ struct k_timer *hbt = &ctx->hb_timer;
+
+ cp->model = strdup(cpi->model);
+ cp->vendor = strdup(cpi->vendor);
+ if (cp->sl_no) {
+ cp->sl_no = strdup(cpi->sl_no);
+ }
+
+ if (cp->box_sl_no) {
+ cp->box_sl_no = strdup(cpi->box_sl_no);
+ }
+
+ if (cp->fw_ver) {
+ cp->fw_ver = strdup(cpi->fw_ver);
+ }
+
+ if (cp->iccid) {
+ cp->iccid = strdup(cpi->iccid);
+ }
+
+ if (cp->imsi) {
+ cp->imsi = strdup(cpi->imsi);
+ }
+
+ if (cp->meter_sl_no) {
+ cp->meter_sl_no = strdup(cpi->meter_sl_no);
+ }
+
+ if (cp->meter_type) {
+ cp->meter_type = strdup(cpi->meter_type);
+ }
+
+ ctx->state = CP_STATE_BOOTNOTIF;
+ ctx->hb_sec = 10;
+ k_timer_start(hbt, K_SECONDS(1), K_NO_WAIT);
+}
+
+int ocpp_session_open(ocpp_session_handle_t *hndl)
+{
+ struct ocpp_session *sh;
+
+ if (!hndl) {
+ return -EINVAL;
+ }
+
+ sh = (struct ocpp_session *)k_calloc(1, sizeof(struct ocpp_session));
+ if (!sh) {
+ return -ENOMEM;
+ }
+
+ sh->is_active = false;
+ sh->idcon = INVALID_CONN_ID;
+ sh->idtxn = INVALID_TXN_ID;
+ sh->ctx = gctx;
+ k_mutex_init(&sh->slock);
+
+ k_mutex_lock(&gctx->ilock, K_FOREVER);
+ sys_slist_append(&gctx->slist, &sh->node);
+ k_mutex_unlock(&gctx->ilock);
+
+ *hndl = (void *)sh;
+
+ return 0;
+}
+
+void ocpp_session_close(ocpp_session_handle_t hndl)
+{
+ bool is_removed;
+ struct ocpp_session *sh = (struct ocpp_session *)hndl;
+
+ if (!sh) {
+ return;
+ }
+
+ k_mutex_lock(&gctx->ilock, K_FOREVER);
+ is_removed = sys_slist_find_and_remove(&gctx->slist, &sh->node);
+ k_mutex_unlock(&gctx->ilock);
+
+ if (is_removed) {
+ k_mutex_lock(&sh->slock, K_FOREVER);
+ k_free(sh);
+ }
+}
+
+int ocpp_init(struct ocpp_cp_info *cpi,
+ struct ocpp_cs_info *csi,
+ ocpp_user_notify_callback_t cb,
+ void *user_data)
+{
+ struct ocpp_info *ctx;
+ struct ocpp_cp_info *cp;
+ union ocpp_keyval val;
+ int ret;
+
+ if (!cpi ||
+ !cpi->model ||
+ !cpi->vendor ||
+ !csi ||
+ !csi->cs_ip ||
+ !csi->ws_url ||
+ !cb ||
+ cpi->num_of_con < 1) {
+ return -EINVAL;
+ }
+
+ ctx = (struct ocpp_info *)k_calloc(1, sizeof(struct ocpp_info));
+ if (!ctx) {
+ return -ENOMEM;
+ }
+
+ gctx = ctx;
+ k_mutex_init(&ctx->ilock);
+ sys_slist_init(&ctx->slist);
+ ocpp_parser_init(&ctx->cfn, &ctx->pfn);
+
+ ctx->state = CP_STATE_INIT;
+ ctx->msgq = &ocpp_iq;
+
+ k_timer_init(&ctx->hb_timer, timer_heartbeat_cb, NULL);
+ k_timer_user_data_set(&ctx->hb_timer, ctx);
+ k_timer_init(&ctx->mtr_timer, timer_meter_cb, NULL);
+ k_timer_user_data_set(&ctx->mtr_timer, ctx);
+ atomic_set(&ctx->mtr_timer_ref_cnt, 0);
+
+ ctx->user_data = user_data;
+ ctx->cb = cb;
+ val.ival = cpi->num_of_con;
+ ocpp_set_cfg_val(CFG_NO_OF_CONNECTORS, &val);
+
+ ret = ocpp_upstream_init(ctx, csi);
+ if (ret) {
+ LOG_ERR("ocpp upstream init fail %d", ret);
+ goto out;
+ }
+
+ /* free after success of bootnotification to CS */
+ cp = (struct ocpp_cp_info *)k_calloc(1, sizeof(struct ocpp_cp_info));
+ if (!cp) {
+ ret = -ENOMEM;
+ goto out;
+ }
+
+ bootnotification_fill_resource(cp, cpi, ctx);
+
+ k_thread_create(&ctx->tinfo, ocpp_int_handler_stack,
+ CONFIG_OCPP_INT_THREAD_STACKSIZE,
+ ocpp_internal_handler, ctx,
+ cp, NULL, OCPP_UPSTREAM_PRIORITY,
+ 0, K_NO_WAIT);
+
+ LOG_INF("ocpp init success");
+ return 0;
+
+out:
+ k_free(ctx);
+ return ret;
+}
diff --git a/subsys/net/lib/ocpp/ocpp_i.h b/subsys/net/lib/ocpp/ocpp_i.h
new file mode 100644
index 0000000000000..8c74407b9c0cf
--- /dev/null
+++ b/subsys/net/lib/ocpp/ocpp_i.h
@@ -0,0 +1,233 @@
+/*
+ * Copyright (c) 2025 Linumiz GmbH
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#ifndef __OCPP_I_
+#define __OCPP_I_
+
+#include "string.h"
+#include
+#include
+#include
+#include
+#include
+
+#if __POSIX_VISIBLE < 200809
+char *strdup(const char *);
+#endif
+
+/* case-insensitive */
+#define CISTR20 20
+#define CISTR25 25
+#define CISTR500 500
+
+#define INVALID_CONN_ID ((uint8_t) -1)
+#define INVALID_TXN_ID ((int) -1)
+
+#define to_session(ptr) CONTAINER_OF(ptr, struct ocpp_session, node)
+
+enum ocpp_pdu_msg {
+ PDU_BOOTNOTIFICATION,
+ PDU_AUTHORIZE,
+ PDU_START_TRANSACTION,
+ PDU_STOP_TRANSACTION,
+ PDU_HEARTBEAT,
+ PDU_METER_VALUES,
+ PDU_CLEAR_CACHE,
+ PDU_REMOTE_START_TRANSACTION,
+ PDU_REMOTE_STOP_TRANSACTION,
+ PDU_GET_CONFIGURATION,
+ PDU_CHANGE_CONFIGURATION,
+ PDU_CHANGE_AVAILABILITY,
+ PDU_UNLOCK_CONNECTOR,
+ PDU_RESET,
+
+ PDU_MSG_END,
+
+ /* internal msg */
+ PDU_CS_ONLINE,
+};
+
+enum ocpp_key_type {
+ KEY_TYPE_BOOL = 1,
+ KEY_TYPE_INT = sizeof(int),
+ KEY_TYPE_STR,
+ KEY_TYPE_CSL,
+};
+
+enum boot_status {
+ BOOT_ACCEPTED,
+ BOOT_PENDING,
+ BOOT_REJECTED
+};
+
+enum ocpp_key {
+ /* core mandatory */
+ CFG_ALLOW_OFFLINE_TX_FOR_UNKN_ID,
+ CFG_AUTH_CACHE_ENABLED,
+ CFG_AUTH_REMOTE_TX_REQ,
+ CFG_BLINK_REPEAT,
+ CFG_CLK_ALIGN_DATA_INTERVAL,
+ CFG_CONN_TIMEOUT,
+ CFG_GETCFG_MAX_KEY,
+ CFG_HEARTBEAT_INTERVAL,
+ CFG_LIGHT_INTENSITY,
+ CFG_LOCAL_AUTH_OFFLINE,
+ CFG_LOCAL_PREAUTH,
+ CFG_MAX_ENERGYON_INVL_ID,
+ CFG_MTR_VAL_ALGIN_DATA,
+ CFG_MTR_VAL_ALGIN_DATA_MAXLEN,
+ CFG_MTR_VAL_SAMPLED_DATA,
+ CFG_MTR_VAL_SAMPLED_DATA_MAXLEN,
+ CFG_MTR_VAL_SAMPLE_INTERVAL,
+ CFG_MIN_STATUS_DURATION,
+ CFG_NO_OF_CONNECTORS,
+ CFG_REST_RETRIES,
+ CFG_CONN_PHASE_ROT,
+ CFG_CONN_PHASE_ROT_MAXLEN,
+ CFG_STOP_TXN_ON_EVSIDE_DISCON,
+ CFG_STOP_TXN_ON_INVL_ID,
+ CFG_STOP_TXN_ALIGNED_DATA,
+ CFG_STOP_TXN_ALIGNED_DATA_MAXLEN,
+ CFG_SUPPORTED_FEATURE_PROFILE,
+ CFG_SUPPORTED_FEATURE_PROFILE_MAXLEN,
+ CFG_TXN_MSG_ATTEMPTS,
+ CFG_TXN_MSG_RETRY_INTERVAL,
+ CFG_UNLOCK_CONN_ON_EVSIDE_DISCON,
+ CFG_WEBSOCK_PING_INTERVAL,
+
+ /* optional */
+
+ OCPP_CFG_END
+};
+
+enum ocpp_cp_state {
+ CP_STATE_INIT,
+ CP_STATE_BOOTNOTIF,
+ CP_STATE_READY,
+};
+
+enum ocpp_wamp_rpc {
+ OCPP_WAMP_RPC_REQ = '2',
+ OCPP_WAMP_RPC_RESP = '3',
+ OCPP_WAMP_RPC_ERR = '4'
+};
+
+typedef int (*ocpp_msg_fp_t)(char *buf, ...);
+
+struct boot_notif {
+ enum boot_status status;
+ int interval;
+ struct timeval date;
+};
+
+struct ocpp_idtag_info {
+ char idtag[CISTR20];
+ char p_idtag[CISTR20];
+ enum ocpp_auth_status auth_status;
+ char exptime[CISTR25];
+};
+
+struct ocpp_upstream_info {
+ struct k_mutex ws_sndlock; /* to server */
+ struct k_poll_signal ws_rspsig; /* wait for resp parsed */
+ int tcpsock;
+ int wssock;
+ struct k_thread tinfo;
+ char recv_buf[CONFIG_OCPP_RECV_BUFFER_SIZE];
+ char wsrecv_buf[CONFIG_OCPP_RECV_BUFFER_SIZE * 2];
+
+ struct ocpp_cs_info csi;
+};
+
+struct ocpp_info {
+ struct k_mutex ilock; /* internal to lib */
+ sys_slist_t slist; /* session list */
+ ocpp_msg_fp_t *cfn;
+ ocpp_msg_fp_t *pfn;
+ bool is_cs_offline;
+ struct k_timer hb_timer;
+ struct k_timer mtr_timer;
+ atomic_t mtr_timer_ref_cnt;
+ int hb_sec; /* heartbeat interval */
+ struct k_msgq *msgq;
+ struct k_thread tinfo;
+ struct ocpp_upstream_info ui;
+ bool is_cs_connected; /* central system */
+
+ ocpp_user_notify_callback_t cb;
+ void *user_data;
+ enum ocpp_cp_state state;
+ /* only for pdu message from internal thread (ocpp_internal_handler) */
+ char pdu_buf[512];
+};
+
+struct ocpp_session {
+ struct k_mutex slock; /* session lock */
+ char idtag[CISTR20];
+ bool is_active;
+ uint8_t idcon;
+ int idtxn;
+ int resp_status;
+ int uid;
+ sys_snode_t node;
+ struct ocpp_info *ctx;
+};
+
+union ocpp_keyval {
+ int ival;
+ char *str;
+};
+
+struct ocpp_wamp_rpc_msg {
+ char *msg;
+ size_t msg_len;
+ struct ocpp_info *ctx;
+ struct k_mutex *sndlock;
+ struct k_poll_signal *rspsig;
+};
+
+struct internal_msg {
+ enum ocpp_pdu_msg msgtype;
+ union ocpp_io_value usr;
+};
+
+void ocpp_parser_init(ocpp_msg_fp_t **cfn, ocpp_msg_fp_t **pfn);
+int parse_rpc_msg(char *msg, int msglen, char *uid, int uidlen,
+ int *pdu, bool *is_rsp);
+int ocpp_send_to_server(struct ocpp_wamp_rpc_msg *snd, k_timeout_t timeout);
+int ocpp_receive_from_server(struct ocpp_wamp_rpc_msg *rcv, uint32_t *msg_type,
+ uint32_t timeout);
+
+int ocpp_unlock_connector(struct ocpp_info *ctx,
+ struct internal_msg *msg, char *uid);
+int ocpp_get_configuration(enum ocpp_key key, struct ocpp_info *ctx, char *uid);
+int ocpp_boot_notification(ocpp_session_handle_t hndl,
+ struct ocpp_cp_info *cpi);
+int ocpp_heartbeat(ocpp_session_handle_t hndl);
+void ocpp_get_utc_now(char utc[CISTR25]);
+bool ocpp_session_is_valid(struct ocpp_session *sh);
+int ocpp_remote_start_transaction(struct ocpp_info *ctx,
+ struct internal_msg *msg,
+ char *uid);
+int ocpp_remote_stop_transaction(struct ocpp_info *ctx,
+ struct internal_msg *msg,
+ int idtxn, char *uid);
+int ocpp_change_configuration(char *skey, struct ocpp_info *ctx,
+ char *sval, char *uid);
+int ocpp_meter_values(ocpp_session_handle_t hndl,
+ enum ocpp_meter_measurand mes,
+ char *sval);
+
+union ocpp_keyval *ocpp_get_key_val(enum ocpp_key key);
+enum ocpp_key ocpp_key_to_cfg(const char *skey);
+enum ocpp_key_type ocpp_get_keyval_type(enum ocpp_key key);
+char *ocpp_get_key_literal(enum ocpp_key key);
+bool ocpp_is_key_rw(enum ocpp_key key);
+int ocpp_set_cfg_val(enum ocpp_key key, union ocpp_keyval *val);
+int ocpp_update_cfg_val(enum ocpp_key key, union ocpp_keyval *val);
+int ocpp_find_pdu_from_literal(const char *msg);
+const char *ocpp_get_pdu_literal(enum ocpp_pdu_msg pdu);
+#endif /* __OCPP_I_ */
diff --git a/subsys/net/lib/ocpp/ocpp_j.c b/subsys/net/lib/ocpp/ocpp_j.c
new file mode 100644
index 0000000000000..4b075e23a5bc6
--- /dev/null
+++ b/subsys/net/lib/ocpp/ocpp_j.c
@@ -0,0 +1,883 @@
+/*
+ * Copyright (c) 2025 Linumiz GmbH
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include "ocpp_i.h"
+#include "ocpp_j.h"
+#include
+#include
+
+static int extract_string_field(char *out_buf, int outlen, char *token)
+{
+ char *end;
+
+ if (!out_buf || !token) {
+ return -EINVAL;
+ }
+
+ strncpy(out_buf, token + 1, outlen - 1);
+ end = strchr(out_buf, '"');
+ if (end) {
+ *end = '\0';
+ }
+
+ return 0;
+}
+
+static int extract_payload(char *msg, int msglen)
+{
+ size_t len;
+ char *start = strchr(msg, '{');
+ char *end = strrchr(msg, '}');
+
+ if (!start || !end || end < start) {
+ return -EINVAL;
+ }
+
+ len = end - start + 1;
+ if (len >= msglen) {
+ return -ENOMEM;
+ }
+
+ memmove(msg, start, len);
+ msg[len] = '\0';
+
+ return 0;
+}
+
+static int frame_rpc_call_req(char *rpcbuf, int len, int pdu,
+ uint32_t ses, char *pdumsg)
+{
+ int ret;
+ char uid[JSON_MSG_BUF_128];
+ const char *action;
+ uint32_t rnd = sys_rand32_get();
+
+ snprintk(uid, sizeof(uid), "%u-%d-%u", ses, pdu, rnd);
+
+ action = ocpp_get_pdu_literal(pdu);
+ if (!action) {
+ return -EINVAL;
+ }
+
+ /* Encode OCPP Call Request msg: [2,"","",] */
+ ret = snprintk(rpcbuf, len,
+ "[2,\"%s\",\"%s\",%s]",
+ uid, action, pdumsg);
+
+ if (ret < 0 || ret >= len) {
+ return -ENOMEM;
+ }
+
+ return 0;
+}
+
+static int frame_rpc_call_res(char *rpcbuf, int len,
+ char *uid, char *pdumsg)
+{
+ int ret;
+
+ /* Encode OCPP Call Result msg: [3,"",] */
+ ret = snprintk(rpcbuf, len, "[3,\"%s\",%s]", uid, pdumsg);
+
+ if (ret < 0 || ret >= len) {
+ return -ENOMEM;
+ }
+
+ return 0;
+}
+
+static int frame_authorize_msg(char *buf, int len,
+ struct ocpp_session *ses)
+{
+ int ret;
+ char auth_obj[JSON_MSG_BUF_128];
+
+ struct json_obj_descr authorize_descr[] = {
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_common_payload_field_str, "idTag",
+ val1, JSON_TOK_STRING),
+ };
+
+ struct json_common_payload_field_str payload = {
+ .val1 = ses->idtag,
+ };
+
+ ret = json_obj_encode_buf(authorize_descr,
+ ARRAY_SIZE(authorize_descr),
+ &payload,
+ auth_obj,
+ sizeof(auth_obj));
+ if (ret < 0) {
+ return ret;
+ }
+
+ ret = frame_rpc_call_req(buf, len, PDU_AUTHORIZE,
+ (uint32_t)ses, auth_obj);
+ if (ret < 0) {
+ return ret;
+ }
+
+ return 0;
+}
+
+static int frame_heartbeat_msg(char *buf, int len, struct ocpp_session *ses)
+{
+ int ret;
+ char tmp_buf[8] = "{}";
+
+ ret = frame_rpc_call_req(buf, len, PDU_HEARTBEAT,
+ (uint32_t)ses, tmp_buf);
+ if (ret < 0) {
+ return ret;
+ }
+
+ return 0;
+}
+
+static int frame_bootnotif_msg(char *buf, int len,
+ struct ocpp_session *ses,
+ struct ocpp_cp_info *cpi)
+{
+ int ret;
+ uint8_t descr_count = BOOTNOTIF_MIN_FIELDS;
+ char tmp_buf[JSON_MSG_BUF_512];
+
+ struct json_ocpp_bootnotif_msg msg = {
+ .charge_point_model = cpi->model,
+ .charge_point_vendor = cpi->vendor,
+ .charge_box_serial_number = cpi->box_sl_no ? cpi->box_sl_no : NULL,
+ .charge_point_serial_number = cpi->sl_no ? cpi->sl_no : NULL,
+ .firmware_version = cpi->fw_ver ? cpi->fw_ver : NULL,
+ .iccid = cpi->iccid ? cpi->iccid : NULL,
+ .imsi = cpi->imsi ? cpi->imsi : NULL,
+ .meter_serial_number = cpi->meter_sl_no ? cpi->meter_sl_no : NULL,
+ .meter_type = cpi->meter_type ? cpi->meter_type : NULL,
+ };
+
+ struct json_obj_descr bootnotif_descr[BOOTNOTIF_MAX_FIELDS] = {
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_bootnotif_msg, "chargePointModel",
+ charge_point_model, JSON_TOK_STRING),
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_bootnotif_msg, "chargePointVendor",
+ charge_point_vendor, JSON_TOK_STRING),
+ };
+
+ if (msg.charge_box_serial_number != NULL) {
+ bootnotif_descr[descr_count++] = (struct json_obj_descr)
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_bootnotif_msg, "chargeBoxSerialNumber",
+ charge_box_serial_number, JSON_TOK_STRING);
+ }
+
+ if (msg.charge_point_serial_number != NULL) {
+ bootnotif_descr[descr_count++] = (struct json_obj_descr)
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_bootnotif_msg, "chargePointSerialNumber",
+ charge_point_serial_number, JSON_TOK_STRING);
+ }
+
+ if (msg.firmware_version != NULL) {
+ bootnotif_descr[descr_count++] = (struct json_obj_descr)
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_bootnotif_msg, "firmwareVersion",
+ firmware_version, JSON_TOK_STRING);
+ }
+
+ if (msg.iccid != NULL) {
+ bootnotif_descr[descr_count++] = (struct json_obj_descr)
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_bootnotif_msg, "iccid",
+ iccid, JSON_TOK_STRING);
+ }
+
+ if (msg.imsi != NULL) {
+ bootnotif_descr[descr_count++] = (struct json_obj_descr)
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_bootnotif_msg, "imsi",
+ imsi, JSON_TOK_STRING);
+ }
+
+ if (msg.meter_serial_number != NULL) {
+ bootnotif_descr[descr_count++] = (struct json_obj_descr)
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_bootnotif_msg, "meterSerialNumber",
+ meter_serial_number, JSON_TOK_STRING);
+ }
+
+ if (msg.meter_type != NULL) {
+ bootnotif_descr[descr_count++] = (struct json_obj_descr)
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_bootnotif_msg, "meterType",
+ meter_type, JSON_TOK_STRING);
+ }
+
+ ret = json_obj_encode_buf(bootnotif_descr, descr_count,
+ &msg, tmp_buf, sizeof(tmp_buf));
+ if (ret < 0) {
+ return ret;
+ }
+
+ ret = frame_rpc_call_req(buf, len, PDU_BOOTNOTIFICATION,
+ (uint32_t)ses, tmp_buf);
+ if (ret < 0) {
+ return ret;
+ }
+
+ return 0;
+}
+
+static int frame_meter_val_msg(char *buf, int len, struct ocpp_session *ses, char *timestamp,
+ char *val, char *measurand, char *unit)
+{
+ int ret = 0;
+ char tmp_buf[JSON_MSG_BUF_512];
+ uint8_t descr_count = SAMPLED_VALUE_MIN_FIELDS;
+
+ struct json_ocpp_meter_val_msg msg = {
+ .connector_id = ses ? ses->idcon : 0,
+ .transaction_id = ses ? ses->idtxn : 0,
+ .meter_value = {{
+ .timestamp = timestamp,
+ .sampled_value = {{
+ .measurand = measurand,
+ .value = val,
+ .unit = unit ? unit : NULL,
+ }},
+ .sampled_value_len = 1,
+ }},
+ .meter_value_len = 1,
+ };
+
+ struct json_obj_descr sampled_value_descr[SAMPLED_VALUE_MAX_FIELDS] = {
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_sample_val, "measurand",
+ measurand, JSON_TOK_STRING),
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_sample_val, "value",
+ value, JSON_TOK_STRING),
+ };
+
+ if (msg.meter_value[0].sampled_value[0].unit != NULL) {
+ sampled_value_descr[descr_count++] = (struct json_obj_descr)
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_sample_val, "unit",
+ unit, JSON_TOK_STRING);
+ }
+
+ struct json_obj_descr meter_value_descr[] = {
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_meter_val, "timestamp",
+ timestamp, JSON_TOK_STRING),
+ JSON_OBJ_DESCR_OBJ_ARRAY_NAMED(struct json_ocpp_meter_val, "sampledValue",
+ sampled_value, 1, sampled_value_len,
+ sampled_value_descr,
+ descr_count),
+ };
+
+ struct json_obj_descr meter_val_msg_descr[] = {
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_meter_val_msg, "connectorId",
+ connector_id, JSON_TOK_NUMBER),
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_meter_val_msg, "transactionId",
+ transaction_id, JSON_TOK_NUMBER),
+ JSON_OBJ_DESCR_OBJ_ARRAY_NAMED(struct json_ocpp_meter_val_msg, "meterValue",
+ meter_value, 1, meter_value_len,
+ meter_value_descr,
+ ARRAY_SIZE(meter_value_descr)),
+ };
+
+ ret = json_obj_encode_buf(meter_val_msg_descr,
+ ARRAY_SIZE(meter_val_msg_descr),
+ &msg, tmp_buf, sizeof(tmp_buf));
+ if (ret < 0) {
+ return ret;
+ }
+
+ ret = frame_rpc_call_req(buf, len, PDU_METER_VALUES,
+ (uint32_t)ses, tmp_buf);
+ if (ret < 0) {
+ return ret;
+ }
+
+ return 0;
+}
+
+static int frame_stop_txn_msg(char *buf, int len, struct ocpp_session *ses,
+ int Wh, char *reason, char *timestamp)
+{
+ int ret = 0;
+ char tmp_buf[JSON_MSG_BUF_256];
+ uint8_t descr_count = STOP_TXN_MIN_FIELDS;
+
+ struct json_ocpp_stop_txn_msg msg = {
+ .transaction_id = ses->idtxn,
+ .meter_stop = Wh,
+ .timestamp = timestamp,
+ .reason = reason ? reason : NULL,
+ .id_tag = ses->idtag[0] ? ses->idtag : NULL,
+ };
+
+ struct json_obj_descr descr[STOP_TXN_MAX_FIELDS] = {
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_stop_txn_msg, "transactionId",
+ transaction_id, JSON_TOK_NUMBER),
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_stop_txn_msg, "meterStop",
+ meter_stop, JSON_TOK_NUMBER),
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_stop_txn_msg, "timestamp",
+ timestamp, JSON_TOK_STRING),
+ };
+
+ if (msg.reason != NULL) {
+ descr[descr_count++] = (struct json_obj_descr)
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_stop_txn_msg, "reason",
+ reason, JSON_TOK_STRING);
+ }
+
+ if (msg.id_tag != NULL) {
+ descr[descr_count++] = (struct json_obj_descr)
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_stop_txn_msg, "idTag",
+ id_tag, JSON_TOK_STRING);
+ }
+
+ ret = json_obj_encode_buf(descr, descr_count, &msg,
+ tmp_buf, sizeof(tmp_buf));
+ if (ret < 0) {
+ return ret;
+ }
+
+ ret = frame_rpc_call_req(buf, len, PDU_STOP_TRANSACTION,
+ (uint32_t)ses, tmp_buf);
+ if (ret < 0) {
+ return ret;
+ }
+
+ return 0;
+}
+
+static int frame_start_txn_msg(char *buf, int len, struct ocpp_session *ses,
+ int Wh, int reserv_id, char *timestamp)
+{
+ int ret = 0;
+ char tmp_buf[JSON_MSG_BUF_256];
+ uint8_t descr_count = START_TXN_MIN_FIELDS;
+
+ struct json_ocpp_start_txn_msg msg = {
+ .connector_id = ses->idcon,
+ .id_tag = ses->idtag,
+ .meter_start = Wh,
+ .timestamp = timestamp,
+ .reservation_id = (reserv_id >= 0) ? reserv_id : -1,
+ };
+
+ struct json_obj_descr descr[START_TXN_MAX_FIELDS] = {
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_start_txn_msg, "connectorId",
+ connector_id, JSON_TOK_NUMBER),
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_start_txn_msg, "idTag",
+ id_tag, JSON_TOK_STRING),
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_start_txn_msg, "meterStart",
+ meter_start, JSON_TOK_NUMBER),
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_start_txn_msg, "timestamp",
+ timestamp, JSON_TOK_STRING),
+ };
+
+ if (msg.reservation_id != -1) {
+ descr[descr_count++] = (struct json_obj_descr)
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_start_txn_msg, "reservationId",
+ reservation_id, JSON_TOK_NUMBER);
+ }
+
+ ret = json_obj_encode_buf(descr, descr_count, &msg,
+ tmp_buf, sizeof(tmp_buf));
+ if (ret < 0) {
+ return ret;
+ }
+
+ ret = frame_rpc_call_req(buf, len, PDU_START_TRANSACTION,
+ (uint32_t)ses, tmp_buf);
+ if (ret < 0) {
+ return ret;
+ }
+
+ return 0;
+}
+
+static int frame_getconfig_msg(char *buf, int len, char *key, char *val,
+ bool is_rw, char *uid)
+{
+ int ret = 0;
+ char tmp_buf[JSON_MSG_BUF_128];
+
+ struct json_ocpp_getconfig_msg msg = { 0 };
+
+ if (val) {
+ msg.configuration_key[0].key = key;
+ msg.configuration_key[0].readonly = !is_rw;
+ msg.configuration_key[0].value = val;
+ msg.configuration_key_len = 1;
+ } else {
+ msg.unknown_key = key;
+ }
+
+ struct json_obj_descr keyval_descr[] = {
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_key_val, "key",
+ key, JSON_TOK_STRING),
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_key_val, "readonly",
+ readonly, JSON_TOK_NUMBER),
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_key_val, "value",
+ value, JSON_TOK_STRING),
+ };
+
+ struct json_obj_descr config_descr[GET_CFG_MAX_FIELDS] = {
+ JSON_OBJ_DESCR_OBJ_ARRAY_NAMED(struct json_ocpp_getconfig_msg, "configurationKey",
+ configuration_key, 1, configuration_key_len,
+ keyval_descr, ARRAY_SIZE(keyval_descr))
+ };
+
+ if (!val) {
+ config_descr[0] = (struct json_obj_descr)
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_getconfig_msg, "unknownKey",
+ unknown_key, JSON_TOK_STRING);
+ }
+
+ ret = json_obj_encode_buf(config_descr, GET_CFG_MAX_FIELDS, &msg,
+ tmp_buf, sizeof(tmp_buf));
+ if (ret < 0) {
+ return ret;
+ }
+
+ ret = frame_rpc_call_res(buf, len, uid, tmp_buf);
+ if (ret < 0) {
+ return ret;
+ }
+
+ return 0;
+}
+
+static int frame_status_resp_msg(char *buf, int len, char *res, char *uid)
+{
+ int ret = 0;
+ char tmp_buf[JSON_MSG_BUF_128];
+
+ struct json_obj_descr descr[] = {
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_common_payload_field_str, "status",
+ val1, JSON_TOK_STRING),
+ };
+
+ struct json_common_payload_field_str msg = {
+ .val1 = res,
+ };
+
+ ret = json_obj_encode_buf(descr, ARRAY_SIZE(descr), &msg,
+ tmp_buf, sizeof(tmp_buf));
+ if (ret < 0) {
+ return ret;
+ }
+
+ ret = frame_rpc_call_res(buf, len, uid, tmp_buf);
+ if (ret < 0) {
+ return ret;
+ }
+
+ return 0;
+}
+
+/* parse msg from server */
+int parse_rpc_msg(char *msg, int msglen, char *uid, int uidlen,
+ int *pdu, bool *is_rsp)
+{
+ int ret = 0;
+ char local_buf[JSON_MSG_BUF_512];
+ char action[JSON_MSG_BUF_128];
+ char *token;
+ int rpc_id = -1;
+
+ if (!msg || !uid || !pdu || !is_rsp) {
+ return -EINVAL;
+ }
+
+ memcpy(local_buf, msg + 1, sizeof(local_buf) - 1);
+ local_buf[sizeof(local_buf) - 1] = '\0';
+
+ token = strtok(local_buf, ",");
+ if (!token) {
+ return -EINVAL;
+ }
+
+ rpc_id = *token - '0';
+
+ token = strtok(NULL, ",");
+ if (!token) {
+ return -EINVAL;
+ }
+
+ ret = extract_string_field(uid, uidlen, token);
+ if (ret < 0) {
+ return ret;
+ }
+
+ switch (rpc_id + '0') {
+ case OCPP_WAMP_RPC_REQ:
+ token = strtok(NULL, ",");
+ if (!token) {
+ return -EINVAL;
+ }
+
+ ret = extract_string_field(action, sizeof(action), token);
+ if (ret < 0) {
+ return ret;
+ }
+ *pdu = ocpp_find_pdu_from_literal(action);
+ /* fall through */
+ __fallthrough;
+
+ case OCPP_WAMP_RPC_RESP:
+ *is_rsp = rpc_id - 2;
+ ret = extract_payload(msg, msglen);
+ if (ret < 0) {
+ return ret;
+ }
+ break;
+
+ case OCPP_WAMP_RPC_ERR:
+ /* fall through */
+ __fallthrough;
+
+ default:
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int parse_idtag_info(char *json, struct ocpp_idtag_info *idtag_info)
+{
+ int ret = 0;
+ char *status;
+
+ struct json_obj_descr inner_descr[] = {
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_idtag_info_root, "status",
+ json_id_tag_info.status, JSON_TOK_STRING),
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_idtag_info_root, "parentIdTag",
+ json_id_tag_info.parent_id_tag, JSON_TOK_STRING),
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_idtag_info_root, "expiryDate",
+ json_id_tag_info.expiry_date, JSON_TOK_STRING),
+ };
+
+ struct json_obj_descr root_descr[] = {
+ JSON_OBJ_DESCR_OBJECT_NAMED(struct json_idtag_info_root, "idTagInfo",
+ json_id_tag_info, inner_descr),
+ };
+
+ struct json_idtag_info_root parsed = { 0 };
+
+ ret = json_obj_parse(json, strlen(json), root_descr,
+ ARRAY_SIZE(root_descr), &parsed);
+ if (ret < 0) {
+ return ret;
+ }
+
+ status = parsed.json_id_tag_info.status;
+ if (!status) {
+ return -EINVAL;
+ }
+
+ switch (*status) {
+ case 'A':
+ idtag_info->auth_status = OCPP_AUTH_ACCEPTED;
+ break;
+ case 'B':
+ idtag_info->auth_status = OCPP_AUTH_BLOCKED;
+ break;
+ case 'E':
+ idtag_info->auth_status = OCPP_AUTH_EXPIRED;
+ break;
+ case 'I':
+ idtag_info->auth_status = OCPP_AUTH_INVALID;
+ break;
+ case 'C':
+ idtag_info->auth_status = OCPP_AUTH_CONCURRENT_TX;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ if (parsed.json_id_tag_info.parent_id_tag) {
+ strncpy(idtag_info->p_idtag, parsed.json_id_tag_info.parent_id_tag,
+ sizeof(idtag_info->p_idtag));
+ }
+
+ if (parsed.json_id_tag_info.expiry_date) {
+ strncpy(idtag_info->exptime, parsed.json_id_tag_info.expiry_date,
+ sizeof(idtag_info->exptime));
+ }
+
+ return 0;
+}
+
+static int parse_heartbeat_msg(char *json, struct timeval *date)
+{
+ int ret = 0;
+
+ struct json_obj_descr descr[] = {
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_common_payload_field_str, "currentTime",
+ val1, JSON_TOK_STRING),
+ };
+
+ struct json_common_payload_field_str heartbeat = {0};
+
+ ret = json_obj_parse(json, strlen(json), descr,
+ ARRAY_SIZE(descr), &heartbeat);
+
+ /* todo: convert civil time to epoch and update local time */
+
+ if (ret < 0) {
+ return ret;
+ }
+
+ return 0;
+}
+
+static int parse_authorize_msg(char *json, struct ocpp_idtag_info *idtag_info)
+{
+ int ret = 0;
+
+ ret = parse_idtag_info(json, idtag_info);
+ if (ret < 0) {
+ return ret;
+ }
+
+ return 0;
+}
+
+static int parse_bootnotification_msg(char *json, struct boot_notif *binfo)
+{
+ int ret = 0;
+
+ struct json_obj_descr descr[] = {
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_bootnotif_payload, "status",
+ status, JSON_TOK_STRING),
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_bootnotif_payload, "interval",
+ interval, JSON_TOK_NUMBER),
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_bootnotif_payload, "currentTime",
+ current_time, JSON_TOK_STRING),
+ };
+
+ struct json_bootnotif_payload msg = { 0 };
+
+ ret = json_obj_parse(json, strlen(json), descr, ARRAY_SIZE(descr), &msg);
+ if (ret < 0) {
+ return ret;
+ }
+
+ if (!msg.status) {
+ return -EINVAL;
+ }
+
+ switch (*msg.status) {
+ case 'A': /* accepted */
+ binfo->status = BOOT_ACCEPTED;
+ break;
+ case 'P': /* pending */
+ binfo->status = BOOT_PENDING;
+ break;
+ case 'R': /* rejected */
+ binfo->status = BOOT_REJECTED;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ if (!msg.interval) {
+ return -EINVAL;
+ }
+
+ binfo->interval = msg.interval;
+
+ if (!msg.current_time) {
+ return -EINVAL;
+ }
+
+ /* todo: convert civil time to epoch and update local time */
+ (void)binfo->date;
+
+ return 0;
+}
+
+static int parse_start_txn_msg(char *json,
+ int *idtxn,
+ struct ocpp_idtag_info *idtag_info)
+{
+ int ret = 0;
+
+ struct json_obj_descr descr[] = {
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_common_payload_field, "transactionId",
+ val1, JSON_TOK_NUMBER),
+ };
+
+ struct json_common_payload_field payload = { 0 };
+
+ ret = json_obj_parse(json, strlen(json), descr, ARRAY_SIZE(descr), &payload);
+ if (ret < 0) {
+ return ret;
+ }
+
+ *idtxn = payload.val1;
+
+ ret = parse_idtag_info(json, idtag_info);
+ if (ret < 0) {
+ return ret;
+ }
+
+ return 0;
+}
+
+static int parse_getconfig_msg(char *json, char *key)
+{
+ int ret = 0;
+
+ struct json_obj_descr descr[] = {
+ JSON_OBJ_DESCR_ARRAY_NAMED(struct json_getconfig_payload, "key",
+ key, 1, key_len, JSON_TOK_STRING),
+ };
+
+ struct json_getconfig_payload payload = { 0 };
+
+ ret = json_obj_parse(json, strlen(json), descr,
+ ARRAY_SIZE(descr), &payload);
+ if (ret < 0) {
+ return ret;
+ }
+
+ /* key is optional so return success*/
+ if (payload.key[0] != NULL) {
+ strcpy(key, payload.key[0]);
+ }
+
+ return 0;
+}
+
+static int parse_changeconfig_msg(char *json, char *key, char *val)
+{
+ int ret = 0;
+
+ struct json_obj_descr descr[] = {
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_common_payload_field_str, "key",
+ val1, JSON_TOK_STRING),
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_common_payload_field_str, "value",
+ val2, JSON_TOK_STRING),
+ };
+
+ struct json_common_payload_field_str payload = { 0 };
+
+ ret = json_obj_parse(json, strlen(json), descr, ARRAY_SIZE(descr), &payload);
+ if (ret < 0) {
+ return ret;
+ }
+
+ if (!payload.val1 || !payload.val2) {
+ return -EINVAL;
+ }
+
+ strncpy(key, payload.val1, CISTR50);
+ strncpy(val, payload.val2, CISTR500);
+
+ return 0;
+}
+
+static int parse_remote_start_txn_msg(char *json,
+ int *idcon,
+ char *idtag)
+{
+ int ret = 0;
+
+ struct json_obj_descr descr[] = {
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_common_payload_field, "connectorId",
+ val1, JSON_TOK_NUMBER),
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_common_payload_field, "idTag",
+ val2, JSON_TOK_STRING),
+ };
+
+ struct json_common_payload_field payload = { 0 };
+
+ ret = json_obj_parse(json, strlen(json), descr, ARRAY_SIZE(descr), &payload);
+ if (ret < 0) {
+ return ret;
+ }
+
+ if (!payload.val2) {
+ return -EINVAL;
+ }
+
+ strncpy(idtag, payload.val2, CISTR50);
+ *idcon = payload.val1;
+
+ return 0;
+}
+
+static int parse_remote_stop_txn_msg(char *json, int *idtxn)
+{
+ int ret = 0;
+
+ struct json_obj_descr descr[] = {
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_common_payload_field, "transactionId",
+ val1, JSON_TOK_NUMBER),
+ };
+
+ struct json_common_payload_field payload = { 0 };
+
+ ret = json_obj_parse(json, strlen(json), descr, ARRAY_SIZE(descr), &payload);
+ if (ret < 0) {
+ return ret;
+ }
+
+ *idtxn = payload.val1;
+
+ return 0;
+}
+
+static int parse_unlock_connectormsg(char *json, int *idcon)
+{
+ int ret = 0;
+
+ struct json_obj_descr descr[] = {
+ JSON_OBJ_DESCR_PRIM_NAMED(struct json_common_payload_field, "connectorId",
+ val1, JSON_TOK_NUMBER),
+ };
+
+ struct json_common_payload_field payload = { 0 };
+
+ ret = json_obj_parse(json, strlen(json), descr, ARRAY_SIZE(descr), &payload);
+ if (ret < 0) {
+ return ret;
+ }
+
+ if (payload.val1 == 0) {
+ return -EINVAL;
+ }
+
+ *idcon = payload.val1;
+
+ return 0;
+}
+
+static ocpp_msg_fp_t ocpp_json_parser[PDU_MSG_END] = {
+ [PDU_BOOTNOTIFICATION] = (ocpp_msg_fp_t)parse_bootnotification_msg,
+ [PDU_AUTHORIZE] = (ocpp_msg_fp_t)parse_authorize_msg,
+ [PDU_START_TRANSACTION] = (ocpp_msg_fp_t)parse_start_txn_msg,
+ [PDU_STOP_TRANSACTION] = (ocpp_msg_fp_t)parse_authorize_msg,
+ [PDU_METER_VALUES] = NULL,
+ [PDU_HEARTBEAT] = (ocpp_msg_fp_t)parse_heartbeat_msg,
+ [PDU_GET_CONFIGURATION] = (ocpp_msg_fp_t)parse_getconfig_msg,
+ [PDU_CHANGE_CONFIGURATION] = (ocpp_msg_fp_t)parse_changeconfig_msg,
+ [PDU_REMOTE_START_TRANSACTION] = (ocpp_msg_fp_t)parse_remote_start_txn_msg,
+ [PDU_REMOTE_STOP_TRANSACTION] = (ocpp_msg_fp_t)parse_remote_stop_txn_msg,
+ [PDU_UNLOCK_CONNECTOR] = (ocpp_msg_fp_t)parse_unlock_connectormsg,
+};
+
+static ocpp_msg_fp_t ocpp_json_frame[PDU_MSG_END] = {
+ [PDU_BOOTNOTIFICATION] = (ocpp_msg_fp_t)frame_bootnotif_msg,
+ [PDU_AUTHORIZE] = (ocpp_msg_fp_t)frame_authorize_msg,
+ [PDU_START_TRANSACTION] = (ocpp_msg_fp_t)frame_start_txn_msg,
+ [PDU_STOP_TRANSACTION] = (ocpp_msg_fp_t)frame_stop_txn_msg,
+ [PDU_METER_VALUES] = (ocpp_msg_fp_t)frame_meter_val_msg,
+ [PDU_HEARTBEAT] = (ocpp_msg_fp_t)frame_heartbeat_msg,
+ [PDU_GET_CONFIGURATION] = (ocpp_msg_fp_t)frame_getconfig_msg,
+ [PDU_CHANGE_CONFIGURATION] (ocpp_msg_fp_t)frame_status_resp_msg,
+ [PDU_REMOTE_START_TRANSACTION] = (ocpp_msg_fp_t)frame_status_resp_msg,
+ [PDU_REMOTE_STOP_TRANSACTION] = (ocpp_msg_fp_t)frame_status_resp_msg,
+ [PDU_UNLOCK_CONNECTOR] = (ocpp_msg_fp_t)frame_status_resp_msg,
+};
+
+void ocpp_parser_init(ocpp_msg_fp_t **cfn, ocpp_msg_fp_t **pfn)
+{
+ *pfn = ocpp_json_parser;
+ *cfn = ocpp_json_frame;
+}
diff --git a/subsys/net/lib/ocpp/ocpp_j.h b/subsys/net/lib/ocpp/ocpp_j.h
new file mode 100644
index 0000000000000..71306d915052c
--- /dev/null
+++ b/subsys/net/lib/ocpp/ocpp_j.h
@@ -0,0 +1,114 @@
+/*
+ * Copyright (c) 2025 Linumiz GmbH
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#ifndef __OCPP_J_
+#define __OCPP_J_
+
+#define JSON_MSG_BUF_128 128
+#define JSON_MSG_BUF_256 256
+#define JSON_MSG_BUF_512 512
+
+#define BOOTNOTIF_MIN_FIELDS 2
+#define BOOTNOTIF_MAX_FIELDS 9
+
+#define STOP_TXN_MIN_FIELDS 3
+#define STOP_TXN_MAX_FIELDS 5
+
+#define START_TXN_MIN_FIELDS 4
+#define START_TXN_MAX_FIELDS 5
+
+#define GET_CFG_MAX_FIELDS 1
+
+#define SAMPLED_VALUE_MIN_FIELDS 2
+#define SAMPLED_VALUE_MAX_FIELDS 3
+
+struct json_common_payload_field {
+ int val1;
+ char *val2;
+};
+
+struct json_common_payload_field_str {
+ char *val1;
+ char *val2;
+};
+
+struct json_ocpp_bootnotif_msg {
+ char *charge_point_model;
+ char *charge_point_vendor;
+ char *charge_box_serial_number;
+ char *charge_point_serial_number;
+ char *firmware_version;
+ char *iccid;
+ char *imsi;
+ char *meter_serial_number;
+ char *meter_type;
+};
+
+struct json_ocpp_meter_val_msg {
+ int connector_id;
+ int transaction_id;
+
+ struct json_ocpp_meter_val {
+ char *timestamp;
+
+ struct json_ocpp_sample_val {
+ char *measurand;
+ char *value;
+ char *unit;
+ } sampled_value[1];
+ size_t sampled_value_len;
+ } meter_value[1];
+
+ size_t meter_value_len;
+};
+
+struct json_ocpp_stop_txn_msg {
+ int transaction_id;
+ int meter_stop;
+ char *timestamp;
+ char *reason;
+ char *id_tag;
+};
+
+struct json_ocpp_start_txn_msg {
+ int connector_id;
+ char *id_tag;
+ int meter_start;
+ char *timestamp;
+ int reservation_id;
+};
+
+struct json_ocpp_getconfig_msg {
+ struct json_ocpp_key_val {
+ char *key;
+ int readonly;
+ char *value;
+ } configuration_key[1];
+
+ size_t configuration_key_len;
+ char *unknown_key;
+};
+
+struct json_idtag_info_root {
+ struct {
+ char *status;
+ char *parent_id_tag;
+ char *expiry_date;
+ } json_id_tag_info;
+};
+
+struct json_bootnotif_payload {
+ char *status;
+ int interval;
+ char *current_time;
+};
+
+struct json_getconfig_payload {
+ char *key[1];
+ size_t key_len;
+};
+
+#endif /* __OCPP_J_ */
diff --git a/subsys/net/lib/ocpp/ocpp_wamp_rpc.c b/subsys/net/lib/ocpp/ocpp_wamp_rpc.c
new file mode 100644
index 0000000000000..713364229f27d
--- /dev/null
+++ b/subsys/net/lib/ocpp/ocpp_wamp_rpc.c
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2025 Linumiz GmbH
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include "ocpp_i.h"
+
+LOG_MODULE_REGISTER(ocpp_rpc, CONFIG_OCPP_LOG_LEVEL);
+
+#define OCPP_WAMP_RPC_TYPE_IDX 1
+
+int ocpp_send_to_server(struct ocpp_wamp_rpc_msg *snd, k_timeout_t timeout)
+{
+ int ret;
+ int sock;
+ struct ocpp_info *ctx = snd->ctx;
+
+ switch (snd->msg[OCPP_WAMP_RPC_TYPE_IDX]) {
+ case OCPP_WAMP_RPC_REQ:
+ /* ocpp spec - allow only one active call at a time
+ * release lock on response received from CS or timeout
+ */
+ ret = k_mutex_lock(snd->sndlock, timeout);
+ if (ret) {
+ return ret;
+ }
+ k_poll_signal_reset(snd->rspsig);
+ break;
+
+ case OCPP_WAMP_RPC_RESP:
+ case OCPP_WAMP_RPC_ERR:
+ break;
+
+ default:
+ return -EINVAL;
+ }
+
+ k_mutex_lock(&ctx->ilock, K_FOREVER);
+ sock = ctx->ui.wssock;
+ k_mutex_unlock(&ctx->ilock);
+ if (sock < 0) {
+ ret = -EAGAIN;
+ goto unlock;
+ }
+
+ ret = websocket_send_msg(sock, snd->msg, snd->msg_len,
+ WEBSOCKET_OPCODE_DATA_TEXT, true,
+ true, 5000); /* fixme timeout */
+ if (ret < 0) {
+ goto unlock;
+ }
+
+ if (snd->rspsig) {
+ struct k_poll_event events[1] = {
+ K_POLL_EVENT_INITIALIZER(K_POLL_TYPE_SIGNAL,
+ K_POLL_MODE_NOTIFY_ONLY,
+ snd->rspsig),
+ };
+
+ ret = k_poll(events, 1, timeout);
+ goto unlock;
+ }
+
+ return 0;
+
+unlock:
+ if (snd->sndlock) {
+ k_mutex_unlock(snd->sndlock);
+ }
+
+ return ret;
+}
+
+int ocpp_receive_from_server(struct ocpp_wamp_rpc_msg *rcv, uint32_t *msg_type,
+ uint32_t timeout)
+{
+ int ret;
+ int sock;
+ uint64_t remaining = 0;
+ struct ocpp_info *ctx = rcv->ctx;
+
+ k_mutex_lock(&ctx->ilock, K_MSEC(timeout));
+ sock = ctx->ui.wssock;
+ k_mutex_unlock(&ctx->ilock);
+ if (sock < 0) {
+ return -EAGAIN;
+ }
+
+ ret = websocket_recv_msg(sock, rcv->msg,
+ rcv->msg_len,
+ msg_type,
+ &remaining,
+ timeout);
+
+ return ret;
+}
diff --git a/tests/net/lib/ocpp/CMakeLists.txt b/tests/net/lib/ocpp/CMakeLists.txt
new file mode 100644
index 0000000000000..50a0f7dd9670e
--- /dev/null
+++ b/tests/net/lib/ocpp/CMakeLists.txt
@@ -0,0 +1,12 @@
+# SPDX-License-Identifier: Apache-2.0
+
+cmake_minimum_required(VERSION 3.20.0)
+
+find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
+project(ocpp_test)
+
+target_include_directories(app PRIVATE
+ ${ZEPHYR_BASE}/subsys/net/ip
+ )
+FILE(GLOB app_sources src/*.c)
+target_sources(app PRIVATE ${app_sources})
diff --git a/tests/net/lib/ocpp/prj.conf b/tests/net/lib/ocpp/prj.conf
new file mode 100644
index 0000000000000..b949422a4c020
--- /dev/null
+++ b/tests/net/lib/ocpp/prj.conf
@@ -0,0 +1,43 @@
+# HTTP & Websocket
+CONFIG_HTTP_CLIENT=y
+CONFIG_WEBSOCKET_CLIENT=y
+
+# Networking config
+CONFIG_NETWORKING=y
+CONFIG_NET_IPV4=y
+CONFIG_NET_IPV6=n
+CONFIG_NET_TCP=y
+CONFIG_NET_ARP=y
+CONFIG_NET_UDP=y
+CONFIG_NET_SOCKETS=y
+CONFIG_NET_SOCKETS_POLL_MAX=4
+CONFIG_NET_L2_ETHERNET=y
+CONFIG_ETH_DRIVER=y
+CONFIG_LOG=y
+CONFIG_NET_LOG=y
+CONFIG_LOG_DEFAULT_LEVEL=3
+CONFIG_NET_DHCPV4=y
+
+# OCPP
+CONFIG_OCPP=y
+CONFIG_TEST_EXTRA_STACK_SIZE=2048
+CONFIG_MAIN_STACK_SIZE=2048
+CONFIG_HEAP_MEM_POOL_SIZE=15000
+
+CONFIG_JSON_LIBRARY=y
+CONFIG_PICOLIBC=y
+
+CONFIG_NET_TX_STACK_SIZE=2048
+CONFIG_NET_RX_STACK_SIZE=2048
+CONFIG_NET_PKT_RX_COUNT=28
+CONFIG_NET_BUF_RX_COUNT=60
+CONFIG_NET_MGMT=y
+CONFIG_NET_MGMT_EVENT=y
+
+CONFIG_ENTROPY_GENERATOR=y
+CONFIG_TEST_RANDOM_GENERATOR=y
+
+CONFIG_POSIX_API=y
+CONFIG_POSIX_CLOCK_SELECTION=y
+
+CONFIG_ZTEST=y
diff --git a/tests/net/lib/ocpp/src/main.c b/tests/net/lib/ocpp/src/main.c
new file mode 100644
index 0000000000000..29d7bb0b56841
--- /dev/null
+++ b/tests/net/lib/ocpp/src/main.c
@@ -0,0 +1,121 @@
+/*
+ * Copyright (c) 2024 Linumiz
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include
+#include
+#include
+#include
+
+static void test_ocpp_charge_cycle(ocpp_session_handle_t hndl)
+{
+ int ret;
+ int retry = 3;
+ enum ocpp_auth_status status;
+ const uint32_t timeout_ms = 500;
+
+ while (retry--) {
+ ret = ocpp_authorize(hndl, "ZepId00", &status, timeout_ms);
+
+ TC_PRINT("auth req ret %d status %d", ret, status);
+ k_sleep(K_SECONDS(1));
+
+ if (!ret) {
+ break;
+ }
+ }
+ zassert_equal(ret, 0, "CP authorize fail %d");
+ zassert_equal(status, OCPP_AUTH_ACCEPTED, "idtag not authorized");
+
+ ret = ocpp_start_transaction(hndl, sys_rand32_get(), 1, timeout_ms);
+ zassert_equal(ret, 0, "start transaction fail");
+
+ /* Active charging session */
+ k_sleep(K_SECONDS(20));
+ ret = ocpp_stop_transaction(hndl, sys_rand32_get(), timeout_ms);
+ zassert_equal(ret, 0, "stop transaction fail");
+}
+
+static int test_ocpp_user_notify_cb(enum ocpp_notify_reason reason,
+ union ocpp_io_value *io,
+ void *user_data)
+{
+ switch (reason) {
+ case OCPP_USR_GET_METER_VALUE:
+ if (OCPP_OMM_ACTIVE_ENERGY_TO_EV == io->meter_val.mes) {
+ snprintf(io->meter_val.val, CISTR50, "%u",
+ sys_rand32_get());
+
+ TC_PRINT("mtr reading val %s con %d",
+ io->meter_val.val,
+ io->meter_val.id_con);
+ return 0;
+ }
+ break;
+
+ case OCPP_USR_START_CHARGING:
+ TC_PRINT("start charging idtag %s connector %d\n",
+ io->start_charge.idtag,
+ io->stop_charge.id_con);
+ return 0;
+
+ case OCPP_USR_STOP_CHARGING:
+ TC_PRINT("stop charging connector %d\n", io->stop_charge.id_con);
+ return 0;
+
+ case OCPP_USR_UNLOCK_CONNECTOR:
+ TC_PRINT("unlock connector %d\n", io->unlock_con.id_con);
+ return 0;
+ }
+
+ return -ENOTSUP;
+}
+
+int test_ocpp_init(void)
+{
+ int ret;
+
+ struct ocpp_cp_info cpi = { "basic", "zephyr", .num_of_con = 1 };
+ struct ocpp_cs_info csi = { "122.165.245.213", /* ssh.linumiz.com */
+ "/steve/websocket/CentralSystemService/zephyr",
+ 8180,
+ AF_INET };
+
+ net_dhcpv4_start(net_if_get_default());
+
+ /* wait for device dhcp ip recive */
+ k_sleep(K_SECONDS(3));
+
+ ret = ocpp_init(&cpi,
+ &csi,
+ test_ocpp_user_notify_cb,
+ NULL);
+ if (ret) {
+ TC_PRINT("ocpp init failed %d\n", ret);
+ return ret;
+ }
+
+ return 0;
+}
+
+ZTEST_EXPECT_FAIL(net_ocpp, test_ocpp_chargepoint);
+ZTEST(net_ocpp, test_ocpp_chargepoint)
+{
+ int ret;
+ ocpp_session_handle_t hndl = NULL;
+
+ ret = test_ocpp_init();
+ zassert_equal(ret, 0, "ocpp init failed %d", ret);
+
+ ret = ocpp_session_open(&hndl);
+ zassert_equal(ret, 0, "session open failed %d", ret);
+
+ k_sleep(K_SECONDS(2));
+ test_ocpp_charge_cycle(hndl);
+
+ ocpp_session_close(hndl);
+}
+
+ZTEST_SUITE(net_ocpp, NULL, NULL, NULL, NULL, NULL);
diff --git a/tests/net/lib/ocpp/testcase.yaml b/tests/net/lib/ocpp/testcase.yaml
new file mode 100644
index 0000000000000..fabee74523ee8
--- /dev/null
+++ b/tests/net/lib/ocpp/testcase.yaml
@@ -0,0 +1,9 @@
+tests:
+ net.ocpp:
+ depends_on: netif
+ platform_allow:
+ - native_sim/native
+ min_ram: 16
+ tags:
+ - net
+ - ocpp