Understanding CitrineOS - the Linux Foundation Energy Open Source Charging Station Management System
Thu Jan 23 2025In the fast-growing EV infrastructure world, software solutions bridge drivers, charging networks, and utilities. Proprietary Charge Station Management Systems (CSMS) dominate but often bring high costs, vendor lock-in, and limited interoperability. CitrineOS, the Linux Foundation Energy’s open-source CSMS, offers a transformative alternative.
Open-source software fosters collaboration, transparency, and cost-effectiveness by reducing reliance on proprietary stacks and enhancing interoperability across hardware, software, and services. CitrineOS’s community-driven approach enables seamless integration and empowers fleet operators, charging providers, and developers to create scalable, customized solutions. By simplifying infrastructure, lowering barriers, and accelerating innovation, Open-source initiatives like CitrineOS are helping reshape EV charging and advancing the transition to a sustainable energy future.
CitrineOS Architecture
The diagram below, extracted from the CitrineOS documentation, illustrates the CitrineOS architecture. This article will provide a high-level overview of the components and their interactions.
Charging Station
The Open Charge Point Protocol (OCPP) is a standard for communication between EV chargers and backend systems, ensuring interoperability regardless of the hardware or software provider. Commissioning is the process of configuring a charging station to communicate with a CSMS in a specified URL, for example:
- ws://localhost/cp001 (OCPP Security Profile 1, insecure connection, usually for development or testing)
- wss://localhost/cp001 (OCPP Security Profiles 2, backend uses TLS to secure the connection, networking is encrypted)
Once the Charging Station is commissioned, it can establish a WebSocket connection with the CSMS and start sending data and receiving remote commands.
Simulation Tools
The Everest project provides simulation tools that makes it easier to develop CSMS backends, without the need of a physical charging station.
Many simulation scenarios and examples can be found in the everest-demo Github repository. The CitrineOS project provides a convenient command to start an Everest demo:
git clone git@github.com:citrineos/citrineos-core.git
npm install -g cross-env
npm run start-everest
Logs:
> @citrineos/workspace@1.5.0 start-everest
> npm run start-everest --prefix ./Server
> @citrineos/server@1.5.0 start-everest
> cd ./everest && cross-env \
EVEREST_IMAGE_TAG=0.0.16 \
EVEREST_TARGET_URL=ws://host.docker.internal:8081/cp001 \
docker-compose up
The npm run start-everest
command creates a Docker container that will run a Nodered app that simulates a charging station. Nodered is a low-code programming tool for wiring together hardware devices, APIs and online services.
The nodered flows can be opened in the browser at http://localhost:1880, and after deployed, the simulation's UI will
be accessible at http://localhost:1880/ui.
Note
Both the simulation tools and the CitrineOS backend currently does not support to Apple M1-M4 chips. A valid workaround is using Github Codespaces, a cloud-based development environment that provides a full Visual Studio Code experience which supports docker-compose and other development tools.
Nodered Bugfix and Deployment
The current version of everest may require a bugfix to work properly. Follow the steps below to apply the fix:
The Websocket connection
The commissioning URL of this simulation was set via Environment Variable
EVEREST_TARGET_URL=ws://host.docker.internal:8081/cp001
Once started, the simulation will make attempts to establish a WebSocket connection with the CSMS backend that must be listening on the specified URL. The logs for the connection attempts are verbose, but the relevant messages are ilustraded below.
[INFO] All EVSE ready. Starting OCPP2.0.1 service
[INFO] Connecting to plain websocket at uri: ws://host.docker.internal:8081/cp001 with security profile: 1
[ERRO] Failed to connect to websocket server
[INFO] Reconnecting in: 2000ms, attempt: 1
...
[INFO] Reconnecting to plain websocket at uri: ws://host.docker.internal:8081/cp001 with security profile: 1
[ERRO] Failed to connect to websocket server
[INFO] Reconnecting in: 5000ms, attempt: 2
...
[INFO] Reconnecting to plain websocket at uri: ws://host.docker.internal:8081/cp001 with security profile: 1
[ERRO] Failed to connect to websocket server
[INFO] Reconnecting in: 12000ms, attempt: 3
...
[INFO] Reconnecting to plain websocket at uri: ws://host.docker.internal:8081/cp001 with security profile: 1
[ERRO] Failed to connect to websocket server
[INFO] Closing plain websocket.
[ERRO] Error initiating close of plain websocket: invalid state
[WARN] Closed websocket of NetworkConfigurationPriority: 1 which is configurationSlot: 1
Starting the CitrineOS OCPP Server
After following the CitrineOS Getting Started guide, the OCPP server will be running and listening on the specified URL.
cd citrineos-core/Server
docker compose up -d
The relevant message logs in the simulator side are ilustrated below.
manager-1 Reconnecting to plain websocket at uri: ws://host.docker.internal:8081/cp001 with security profile: 1
manager-1 OCPP client successfully connected to plain websocket server
Let's understand how CitrineOS works. Its docker-compose script defines a citrine service:
citrine:
build:
context: ../
dockerfile: ./Server/deploy.Dockerfile
The ./Server/deploy.Dockerfile
defines how the container image is built, and defines the container runs the command when started.
npm run start-docker-cloud
This command is defined in the Server/package.json
:
"start-docker-cloud": "node --inspect=0.0.0.0:9229 dist/index.js"
This, it runs the CitrineOS initialization according to the entrypoint code of the CitrineOS application as defined in https://github.com/citrineos/citrineos-core/blob/main/Server/src/index.ts.
This code reveals the CitrineOS is a Fastify server that wires the components of the architecture:
- An OCPP router that listens for Charging Station connections, and manage their active websocket clients.
- An AMQ Message Broker, that forwards the messages received to the OCPP modules.
- OCPP Modules that consumes the messages published to the message broker, and produces their respective responses.
- a Persistence layer to read/write data into a Postgres database.
- A Directus CSMS backend that can be used for Admin purposes.
- And extensive API that can be used to access data, managing the charging stations, and send remote commands.
This CitrineOS is extensively configurable, and it can be done via environment variables.
The Message Router
By inspecting CitrineOS logs, it's possible to watch the exchange of messages between the simulator and the OCPP backend:
2025-01-19 07:14:09.721
DEBUG /usr/local/apps/citrineos/02_Util/dist/queue/rabbit-mq/sender.js:108
CitrineOS Logger:RabbitMqSender
Publishing to citrineos: {
origin: 'cs',
eventGroup: 'general',
action: 'Heartbeat',
context: {
stationId: 'cp001',
correlationId: 'b3ab0f4c-367b-47ae-b5c4-ffb89715a60e',
tenantId: '',
timestamp: '2025-01-19T07:14:09.720Z'
},
state: 1,
payload: {}
}
2025-01-19 07:14:09.723
DEBUG /usr/local/apps/citrineos/02_Util/dist/queue/rabbit-mq/receiver.js:179
CitrineOS Logger:RabbitMqReceiver
_onMessage:Received message: {
contentType: 'application/json',
contentEncoding: 'utf-8',
headers: {
action: 'Heartbeat',
correlationId: 'b3ab0f4c-367b-47ae-b5c4-ffb89715a60e',
eventGroup: 'general',
origin: 'cs',
state: '1',
stationId: 'cp001',
tenantId: '',
timestamp: '2025-01-19T07:14:09.720Z'
},
}
The grep command below is an easy way to quickly learn what's the module that is handling the message:
% grep -R Heartbeat 03_Modules | grep Handler
03_Modules/Configuration/src/module/module.ts: @AsHandler(CallAction.Heartbeat)
Therefore, the Heartbeat message published into the Message broker will be consumed by the Heartbeat handler defined in Configuration module. The diagram below illustrates the message through the CitrineOS architecture components.
Conclusion
This post covered the first steps of how to test CitrineOS and explained how messages flow from a Charging Station through the CitrineOS building blocks:
- The Charging Station, simulated using an Everest Simulator, sends a Websocket OCPP message to CitrineOS backend
- CitrineOS router is a WebSocket server running on top of a FastAPI instance.
- Messages received by the route from the Charging Station are dispatched to a RabbitMQ broker.
- CitrineOS modules handles messages received on the broker and publishes responses back to to broker.
- The broker then responds back to the router, who builds the OCPP message response and sends it back to the Charging Station.
Want to dive deeper into CitrineOS? Stay tuned for the next post, where we'll explore its database model, API, and backend setup. In the meantime, try setting up the simulator yourself and share your thoughts!