Blog Miranti

Understanding CitrineOS - the Linux Foundation Energy Open Source Charging Station Management System

In 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.

Continue reading...

Understanding CitrineOS - the Linux Foundation Energy Open Source Charging Station Management System

Thu Jan 23 2025

In 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.

CitrineOS

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.

CitrineOS

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:

CitrineOS CitrineOS

The Websocket connection

The commissioning URL of this simulation was set via Environment Variable

EVEREST_TARGET_URL=ws://host.docker.internal:8081/cp001
CitrineOS

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.

CitrineOS

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!

Social
About

I'm an engineer with a bachelor's in Computer Engineering from ITA (2004).
Driven by a passion for fullstack development, I became a polyglot programmer (Java, Ruby on Rails, Python, Typescript, React, and Next.js).
My professional experience includes developing software for the aviation industry, payments startups, and serving as CTO at Donorbox, driving growth from 2017 to 2021.
Currently at 7GEN, building software for electric vehicle fleets, integrating Telematics APIs and OCPP-based charging systems to advance sustainable transportation.