developing virtual ocpp stations with node.js & react in

89
Developing virtual OCPP stations with Node.js & React in TypeScript Hieu Cao Bachelor’s Thesis Degree Programme in Busi- ness Information Technology 2020

Upload: others

Post on 28-Jul-2022

5 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Developing virtual OCPP stations with Node.js & React in

Developing virtual OCPP stations with Node.js & React in

TypeScript

Hieu Cao

Bachelor’s Thesis

Degree Programme in Busi-

ness Information Technology

2020

Page 2: Developing virtual OCPP stations with Node.js & React in

Abstract

22 November 2020

Author(s) Hieu Cao

Degree programme Business Information Technology

Report/thesis title Developing virtual OCPP stations with Node.js & React in Type-Script

Number of pages and appendix pages 74 + 11

The author wrote the thesis as a product thesis for Virta Ltd, a company that specialises in electric vehicle charging solutions. The work consists of applications that act as virtual charging stations. The product includes a user interface written in React and TypeScript, a server written with Node.js and NestJS and a deployment pipeline using Kubernetes with Google Cloud Platform. The theory part of the thesis cover on various topics related to the product: OCPP, HTTP & WebSocket, JavaScript and TypeScript, Node.js, NestJS, ReactJS and DevOps. The first part of the product represented how to develop the user interface for the server by utilising ReactJS with TypeScript alongside a mock server. The second part emphasises on having a Node.js server that acts as virtual stations that can connect to a central sys-tem. The server communicates with a MySQL database. The product’s final part lays out how to deploy the applications with Kubernetes, CI pipeline on Google Cloud Platform. The product describes comprehensive details of the technical development of each appli-cation. It can be used as examples for learning the relevant technologies. Furthermore, if one is interested in electric vehicle charging, the thesis offers relatable technology knowledge in the industry. The last part concludes the thesis with discussion thoughts from the author regarding en-countered problems, pros & cons of the product as well as the lessons learned during the development process.

Keywords CI/CD, Docker, GCP, Kubernetes, NestJS, Node.js, OCPP, ReactJS, TypeScript

Page 3: Developing virtual OCPP stations with Node.js & React in

Table of contents

List of Abbreviations .......................................................................................................... 1

1 Introduction ................................................................................................................... 2

1.1 Thesis structure .................................................................................................... 2

1.2 About the company ............................................................................................... 3

1.3 Objectives and scope ............................................................................................ 3

2 Theoretical framework ................................................................................................... 4

2.1 OCPP ................................................................................................................... 4

2.2 HTTP and WebSocket .......................................................................................... 5

2.2.1 HTTP ......................................................................................................... 5

2.2.2 WebSocket................................................................................................. 6

2.3 JavaScript and TypeScript .................................................................................... 7

2.3.1 JavaScript .................................................................................................. 7

2.3.2 TypeScript .................................................................................................. 8

2.4 Node.js ................................................................................................................. 8

2.4.1 The event loop ........................................................................................... 9

2.4.2 Node package manager (npm) ................................................................. 10

2.5 NestJS ................................................................................................................ 10

2.5.1 Modules ................................................................................................... 10

2.5.2 Controllers................................................................................................ 11

2.6 ReactJS .............................................................................................................. 12

2.6.1 JSX .......................................................................................................... 12

2.6.2 React Components and Props ................................................................. 13

2.6.3 React State .............................................................................................. 13

2.6.4 More Hooks .............................................................................................. 14

2.6.5 React Context .......................................................................................... 15

2.6.6 Create-react-app ...................................................................................... 16

2.6.7 Material UI................................................................................................ 16

2.7 DevOps ............................................................................................................... 17

2.7.1 Docker ..................................................................................................... 17

2.7.2 Kubernetes............................................................................................... 18

2.7.3 Kubernetes Objects .................................................................................. 19

2.7.4 Google Cloud Platform (GCP) .................................................................. 21

3 Product ....................................................................................................................... 22

3.1 User Interface ..................................................................................................... 22

3.1.1 Creating project with create-react-app ...................................................... 22

3.1.2 Creating application’s home page ............................................................ 22

3.1.3 Creating mock server ............................................................................... 24

Page 4: Developing virtual OCPP stations with Node.js & React in

3.1.4 Fetching and populating StationList component ....................................... 25

3.1.5 Using React Context for API request ........................................................ 27

3.1.6 Station Information display with StationContext ........................................ 29

3.1.7 Mock server and operations ..................................................................... 30

3.1.8 Use OperationContext for managing operation across components ......... 31

3.1.9 ControlCenter component ........................................................................ 31

3.2 Node.js server with Nest.js framework ................................................................ 34

3.2.1 Setting up project with nest-cli .................................................................. 34

3.2.2 Database migration with TypeORM & database connection with Node.js

app 35

3.2.3 Setting up station entity, controller, service, and repository ...................... 36

3.2.4 Setting up WebSocket connection for the stations.................................... 39

3.2.5 OCPP operations for stations ................................................................... 45

3.2.6 Handle incoming messages from Central System .................................... 50

3.3 Deployment to GCP ............................................................................................ 54

3.3.1 Setting up project on internal Bitbucket .................................................... 54

3.3.2 Set up Dockerfile for the UI repository ...................................................... 54

3.3.3 Architecture of all components inside Kubernetes .................................... 55

3.3.4 Creating Kubernetes cluster & service account ........................................ 57

3.3.5 Creating configurations files for deployment ............................................. 57

3.3.6 Setting up BitBucket pipeline .................................................................... 63

4 Discussion ................................................................................................................... 67

4.1 Problems encountered ........................................................................................ 67

4.2 Pros and cons of the product .............................................................................. 67

4.2.1 Pros ......................................................................................................... 67

4.2.2 Cons ........................................................................................................ 68

4.3 Lessons learned .................................................................................................. 68

References ...................................................................................................................... 70

Tables of figures .............................................................................................................. 73

Appendices ...................................................................................................................... 75

Appendix 1. Complete code of StationContext ............................................................ 75

Appendix 2. The user interface flow for request a StartTransaction to the server ........ 78

Appendix 3. Complete set-up of server with docker-compose.yml for virtual-ocpp-j-

server .......................................................................................................................... 80

Appendix 4. StationTable migration file ....................................................................... 82

Appendix 5. docker-compose.yml file for both projects in Virta’s internal repository .... 84

Page 5: Developing virtual OCPP stations with Node.js & React in

1

List of Abbreviations

API Application Programming Interface

B2B Business-to-business

B2C Business-to-consumer

CLI Command-line interface

CI/CD Continuous Integration / Continuous Delivery

CS Central System

CSS Cascading Style Sheets

DB Database

DTO Data Transfer Object

EV Electric Vehicle

GCP Google Cloud Platform

HTML Hypertext Markup Language

HTTP Hypertext Transfer Protocol

HTTPS Hypertext Transfer Protocol Secure

npm Node.js package manager

OCPP Open Charge Point Protocol

ORM Object Relational Mapper

PubSub Publish & Subscribe

rRPC routed Remote Procedure Calls

SQL Structured Query Language

UI User Interface

WAMP Web Application Messaging Protocol

WS WebSocket

Page 6: Developing virtual OCPP stations with Node.js & React in

2

1 Introduction

This thesis is a product thesis to develop applications that act as virtual stations for Virta

Ltd. The idea for the product was initiated within the author’s team working at Virta Ltd.

The company needs something that simulates all the messages coming from a Web-

Socket OCPP (Open Charge Point Protocol) station to our system. Additionally, a user in-

terface would allow other personnel at the company to utilise the product.

The applications were developed mainly with JavaScript/TypeScript as the primary lan-

guage. JavaScript is beginner-friendly while TypeScript ensures that we keep the code

and architecture organised. The deployment pipeline was set up to deploy the applications

with Kubernetes on Google Cloud Platform (GCP).

On a technical level, the thesis gives great insights on how to develop full-stack applica-

tions from front-end to back-end and DevOps. The thesis’s writing style allows readers to

follow along with the code written for the product. Any interested party in any part of the

product will be able to gain some valuable knowledge from the author’s work and experi-

ence from the development.

Besides the benefits this product brings to Virta, the product can also be useful for any

party that is interested in the industry of electric vehicle charging. OCPP is available for

anyone to use, and we hope the product brings benefits to the industry.

This chapter goes through the thesis structure, a brief introduction of the commissioning

company as well as the scope and objectives of the project.

1.1 Thesis structure

The thesis starts with a framework that covers the theories on the technologies used in

the project. Due to limited space and changing nature of technology, it is recommended

that readers also update themselves on the latest changes.

The second part concentrates on all the small steps in developing the applications. It con-

sists of 3 subchapters that represent important pieces of the product: the user interface,

the back-end server, and the deployment pipeline. Each subchapter goes into details of

the technical implementation and explanations for those choices.

Page 7: Developing virtual OCPP stations with Node.js & React in

3

In the end, the author concluded the thesis with a discussion on the encountered issues

during the process, the pros & cons of the product and the lessons learned after writing

the thesis.

Beside the appendix and references, the thesis also includes a list of abbreviations and a

table of figures to help the readers navigate between technical terms & examples.

1.2 About the company

Virta Ltd offers charging solutions to both B2B and B2C customers. At Virta, we:

- Provide smart electric vehicle (EV) charging stations - Connect EV drivers to charging stations - Enable intelligent EV charging networks

As of now, Virta employs more than 100 people with offices in Finland, Sweden, France,

Germany & UK.

At the moment, the author works at Device Communications team which is responsible for

handling communication between Virta’s Central System (CS) and a vast network of

charging stations.

1.3 Objectives and scope

As this is a product-oriented thesis, the focus of this thesis would be on the implementa-

tions of all the product. Theories are included to support the choice as well as the actions

made during the development phase. Even though the applications would be open-

sourced, the author does not describe certain information and processes to preserve con-

fidentiality of the company and the security of Virta’s system. With that in mind, the pri-

mary objectives of the thesis are:

- Having a server that can act as multiple virtual OCPP stations (which connect to Virta’s Central System Service).

- Having a UI to allow users to perform specific OCPP actions on a chosen station. As a result of this requirement, the server also needs to publish API endpoints to support these actions.

- Having an automated deployment pipeline. Any other developers must be able to understand what is included in the deployment from the repository itself.

The product, however, does not support all kind of OCPP messages. It only includes the

most used ones that we predetermined.

Page 8: Developing virtual OCPP stations with Node.js & React in

4

2 Theoretical framework

This chapter comprises of the theories of the technologies used in this project. We mostly

cover what the technologies are, their benefits as well as some of their core characteris-

tics. We will not, however, go into details about how those technologies work as they are

widely available in the official documentation and Internet blog posts.

2.1 OCPP

OCPP (Open Charge Point Protocol) is a widely used protocol for vendors of charging sta-

tions for communication between charging stations and their central system software. The

initiative was led by the Open Charge Alliance to encourage innovation and collaboration.

The protocol is vendor-independent so that infrastructure operators can freely choose the

EV charging stations provider. Additionally, the protocol makes it effortless for EV charg-

ing stations to supply to any operators. (Virta Ltd 2020.)

Figure 1. EV charging ecosystem (Virta Ltd 2020)

To illustrate this point further, figure 1 above shows the interdependence between Charg-

ing Point Management System and Charging Stations. By adopting OCPP standards,

Page 9: Developing virtual OCPP stations with Node.js & React in

5

business decision-makers can have their choices of Charging Point Management System

or Charging Point Vendors if OCPP is supported.

Having been deployed in 2010, OCPP is now extensively adopted by a large number of

charging station vendors and considered as the de-facto standard (Virta Ltd 2020).

As of September 2020, the three main versions of OCPP are (OCA 2020):

- Version 1.5 was deployed in 2012 supporting only SOAP messages - Version 1.6 was introduced in 2015 supporting both SOAP & JSON - Version 2.0 was introduced in 2018 supporting only JSON messages

In this thesis, the focus is on the JSON version of version 1.6, which utilises WebSocket.

2.2 HTTP and WebSocket

“The WebSocket API is an advanced technology that makes it possible to open a two-way

interactive communication session between the user's browser and a server”. It allows the

client to send messages to the server and at the same time receives responses without

polling for a reply. (Mozilla 2020c.) Discussing WebSocket is, however, not complete with-

out mentioning HTTP.

2.2.1 HTTP

The first version of HTTP was developed as a consequence of the birth of the World Wide

Web (Lombardi 2015, Chapter 8).

“HTTP is a protocol which allows the fetching of resources, such as HTML documents”.

Most of the data exchange on the Web is based on this foundation. The clients initiate and

send messages which are regarded as requests. After that, the servers reply with mes-

sages which are called responses. (Mozilla 2020a.)

HTTP is stateless and consequently allows each request to be unique. The advantage is

that the server does not need to store data related to each client. Nevertheless, the disad-

vantage is that the client must send some similar information in each request, such as in-

formation in the request headers. (Wang, Moskovits, Pye & Salim 2013, Chapter 1.)

Page 10: Developing virtual OCPP stations with Node.js & React in

6

HTTP was inefficient due to its unidirectional nature. Other workarounds such as Comet

and long-polling tried to solve the inefficiency but also brought additional costs on compu-

ting resources. WebSocket took place as a result of the popularity of Ajax and increasing

demand for real-time updates. (Lombardi 2015, Chapter 1.)

2.2.2 WebSocket

WebSocket provides full-duplex communication between clients and servers. It grants

both parties the abilities to send messages at any point in time. By using WebSocket, we

gain the benefits of reducing data latency and keeping lightweight connection without sac-

rificing performance. Furthermore, we can open and close the connection anytime we

want. To be able to use WebSocket, the API must be implemented on both client and

server-side. Most modern browsers and servers nowadays support WebSocket API,

which makes it easy for developers to implement the technology. (Chopra 2015, Chapter

2.)

An example of WebSocket is a chat application which requires constant communication

between the client and server. The users of the application would want the messages to

be sent and received in real-time and WebSocket is the only efficient solution.

Chopra (2015, Chapter 2) explains how WebSocket works in the following steps:

1. The client initiates an HTTP call 2. Server authenticates the request and sends back a response 3. The connection is now open, and data can be sent between the client and server 4. Data is sent using TCP protocol in small packets

WebSocket connection begins with a handshake. The first request coming from the client

to a server is an HTTP request which should include a header Connection: Upgrade. If the

server accepts the request, a header named Sec-WebSocket-Accept is returned along-

side the header Sec-WebSocket-Key. (Lombardi 2015, Chapter 8.)

The client and the server must negotiate with each other on the subprotocols to be used

during their communication. The negotiation also happens in the HTTP header Sec-Web-

Socket-Protocol of the initial upgrade request. The server should select the subprotocol it

agrees with and returns it to the client. Subprotocol is a way to enhance higher-level com-

munication channel in addition to the WebSocket protocol. (Lombardi 2015, Chapter 8.)

An example of the Sec-WebSocket-Protocol header in request and response

Sec-WebSocket-Protocol: ocpp1.6, ocpp1.5 (Request)

Page 11: Developing virtual OCPP stations with Node.js & React in

7

Sec-WebSocket-Protocol: ocpp1.6 (Response)

“WAMP (Web Application Messaging Protocol) is a routed protocol, with all components

connecting to a WAMP Router, where the WAMP Router performs message routing be-

tween the components, and provides two messaging patterns in one Web native protocol:

Publish & Subscribe (PubSub) and routed Remote Procedure Calls (rRPC)”.

Even though WebSocket bring open possibilities on the Web, the API is defined at the

message level, which requires the protocol users to define their semantics. The Web Ap-

plication Messaging Protocol (WAMP) is introduced to provide developers with a more

convenient way to communicate between components. (Crossbar.io Technologies GmbH

2020).

There are two messaging patterns provided by WAMP: Publish and Subscribe (PubSub)

and routed Remote Procedure Calls (rRPC). We will focus on rRPC as OCPP1.6 JSON

uses it. In rRPC, “a component, the Callee, announces to the router that it provides a cer-

tain procedure, identified by a procedure name. Other components, Callers, can then call

the procedure, with the router invoking the procedure on the Callee, receiving the proce-

dure’s result, and then forwarding this result back to the Caller.” (Crossbar.io Technolo-

gies GmbH 2020).

2.3 JavaScript and TypeScript

As the product of this thesis utilises JavaScript and TypeScript heavily, we talk shortly

about them in this subchapter. Additionally, we will go through the advantages of Type-

Script and understand why they would be the perfect choice for protocol messages like

OCPP. A program written in TypeScript works best when it knows the types of everything

within it (Cherny 2019, Chapter 2).

2.3.1 JavaScript

JavaScript is the language that is used for most kinds of interactive things that happen in

the browsers (Haverbeke 2011, Chapter Introduction). While it was dominantly used on

the browsers, JavaScript has extended itself to other environments such as Node.js,

Apache CouchDB and Adobe Acrobat (Mozilla 2020b).

JavaScript is highly flexible, although some people may despise it for the same reason,

especially if they come from a language that is strictly typed. Haverbeke (2011, Chapter

Page 12: Developing virtual OCPP stations with Node.js & React in

8

Introduction), however, point out that it is also an advantage of JavaScript as it allows

some techniques that would not be possible in other languages.

2.3.2 TypeScript

“TypeScript is an open-source language which builds on JavaScript, one of the world’s

most used tools, by adding static type definitions”. With TypeScript, we can easily de-

scribe the properties of an object, have better documentation, and allow early error detec-

tion. (Microsoft 2020.)

JavaScript is weakly-typed, and it helps the programmer even when he/she is doing

something wrong. TypeScript, however, will output warnings in the compiler as soon as it

notices something invalid. (Cherny 2019, Chapter 2.) Figure 2 and Figure 3 below show

the difference when a programmer does something wrong in JavaScript and TypeScript.

Figure 2. No error is shown in JavaScript (adapted from Cherny 2019, Chapter 2)

Figure 3. Error is shown in TypeScript (adapted from Cherny 2019, Chapter 2)

The TypeScript compiler processes TypeScript files and output JavaScript that can be run

by a runtime such as a browser or Node.js (Freeman 2019, Chapter 2).

2.4 Node.js

Node.js is “an asynchronous event-driven JavaScript runtime” which is “designed to build

scalable network applications” (OpenJS Foundation 2020). Additionally, with its non-block-

ing I/O model, Node.js is lightweight and efficient. The Node.js package manager, also

known as npm, has the most open-source libraries compared to others. (Patel 2018.)

Thanks to Node.js, JavaScript has become one of the most significant languages for dif-

ferent types of software development. The arrival of ECMAScript 2015 or ES6 helped

solve crucial issues of JavaScript. Node.js has its advantages by having its operations

Page 13: Developing virtual OCPP stations with Node.js & React in

9

based on the V8 JavaScript engine. On the other hands, other innovative JavaScript tech-

nologies such as React, React Native and Electron pave the way for the language to be

everywhere. (Meck, Young & Cantelon 2017, Chapter 1.)

2.4.1 The event loop

Despite being single-threaded, Node.js manage to perform non-blocking operations by uti-

lising event loop. Figure 4 details the steps in the event loop.

Figure 4. A graphical explanation of the event loop (Borrelli 2019)

Borrelli (2019), detailed each step in the event-loop as follows:

1. During the first step, Node checks for operations with a timer (e.g. setTimeout(), setInterval() and executes the callbacks passed to it if the timer has expired.

2. In the second step, Node looks for pending OS tasks and checks execute the ready callback functions. (e.g. reading a file)

3. In the third step, Node pauses its execution to retrieve new I/O events. 4. In this step, we execute callbacks from setImmediate() functions 5. Close callback is managed in this step. An example would be a WebSocket clos-

ing connection.

Page 14: Developing virtual OCPP stations with Node.js & React in

10

2.4.2 Node package manager (npm)

“Npm is the world’s largest software registry”. Developers used npm to share and borrow

open-source packages. Additionally, organisations can also use npm to manage their pri-

vate packages. The npm CLI is installed alongside with Node.js and can be used to exe-

cute its actions. (npm, Inc 2020.)

Even though Node.js ships with a lot of useful built-in libraries, developers can browse

freely on npm for more packages that may tailor to their needs without reinventing the

wheel.

2.5 NestJS

“A progressive Node.js framework for building efficient, reliable and scalable server-side

applications.” NestJS is built with TypeScript and uses progressive JavaScript. Further-

more, it combines different elements from Object-Oriented Programming, Functional Pro-

gramming as well as Functional Reactive Programming together. With its out-of-the-box

application architecture, Nest facilitates the development of “highly testable, scalable,

loosely coupled, and easily maintainable applications”. (Mysliwiec 2020)

Heavily inspired by Angular, “Nest provides an out-of-the-box application architecture

which allows developers and teams to create highly testable, scalable, loosely coupled,

and easily maintainable applications” (Mysliwiec 2020).

NestJS provides the developer with many built-in tools to be used out-of-the-box. Using

this framework saves developers time by not reinventing the wheels. Additionally, its na-

tive TypeScript support also contributes to its strength.

2.5.1 Modules

One of the unique features of NestJS is modules. Modules provide an efficient way of or-

ganising the code structure. An application’s logic can be split into smaller modules and

thus easier to manage. Each module is responsible for itself and has its capabilities. Mod-

ules ensure the separation of concerns principle. Figure 5 provides the example of a mod-

ule in NestJS. (Mysliwiec 2020.)

Page 15: Developing virtual OCPP stations with Node.js & React in

11

Figure 5. Example of module structure in NestJS

Mysliwiec (2020) describe the components of module as below:

- Providers: The provider/service classes to be shared across the modules - Controllers: The controller class to handle request/response - Imports: A list of imported modules - Exports: A list of exported modules to be used by other modules

One good thing to note is that not all components must be defined. However, a provider or

controller is at least needed for the module to have some capabilities. In the next sub-

chapter, we will discuss how controllers support the handling of requests.

2.5.2 Controllers

A controller is a class that handles incoming requests to the server. In NestJS, a controller

can do different things like routing, validation, exception handling as well as response

parsing. Mysliwiec (2020) said that the routing mechanism would help the server choose

the controller for each request. Figure 6 below shows how to create a controller in NestJS.

import { Controller, Get } from '@nestjs/common';

@Controller('cats')

export class CatsController {

@Get()

findAll(): string {

return 'This action returns all cats';

}

} Figure 6. Controller example in NestJS

Page 16: Developing virtual OCPP stations with Node.js & React in

12

NestJS utilises heavily decorators (such as @Get & @Controller from the example

above). Those decorators provide NestJS with necessary metadata for its operation. The

@Controller(‘cats’) decorator helps it understand that this is, in fact, a controller with route

“/cats”. Consequently, any request starting with “/cats” will go through this controller. The

@Get decorator helps it understand that this is a GET operation.

The controller helper is just a straightforward example of how NestJS simplify things for

the developers. There are other convenient things that it provides, such as Middleware,

Exception Filters, Validation, Guards, Object-Relational Mapping integration, etc. that a

developer can use depending on their needs. Next, we are going through ReactJS which

is a fundamental front-end library used for the product in this thesis.

2.6 ReactJS

Single Page Application (SPA) have been becoming quite popular in recent years. SPA

frameworks made it easier to build web applications with advanced JavaScript. React was

released by Facebook in 2013 as another solution to SPAs. Since the introduction of

SPAs, the clients (which usually is the browser) has helped to bear the burden of loading

the pages. In a SPA, the server sends the client an HTML file with JavaScript. The JavaS-

cript code, in turn, do everything else such as transitioning between pages, requesting

data from the server, and adding user interaction behaviour. (Wieruch 2020, 2.) Facebook

Inc (2020f) says that React lets you compose complex UIs from small and isolated pieces

of code called components.

2.6.1 JSX

JSX “is a syntax extension to JavaScript” and it is used with React to form the User Inter-

face (UI). JSX enhances React components to be able to contain both markup and logic in

the same file. After being compiled, JSX expressions are regular function calls. As a re-

sult, JSX can be put inside if or for statements. (Facebook Inc 2020e.) Figure 7 shows an

example of JSX in an if clause.

function getGreeting(user) {

if (user) {

return <h1>Hello, {formatName(user)}!</h1>;

}

return <h1>Hello, Stranger.</h1>;

} Figure 7. Example of if clause with JSX (Facebook Inc 2020e)

Page 17: Developing virtual OCPP stations with Node.js & React in

13

2.6.2 React Components and Props

React has undoubtedly contributed to the rise of using components. All elements of the

web like HTML, CSS & JavaScript are included in each component. A hierarchy of compo-

nents is defined together to form an entire application. (Wieruch 2020, 2.)

Components enable the developers to split their logic into independent and reusable

pieces of code. Components are conceptually similar to JavaScript functions. They take in

inputs which are called props and return what should be displayed in the UI. (Facebook

Inc 2020a.)

As components can refer to other components, this lets developers reuse the same com-

ponent abstraction. In a React app, any small details like a button, a form, a dialog are ex-

pressed as React components. (Facebook Inc 2020a.)

2.6.3 React State

While props allow the developers to pass information down the component tree, state

helps make the application interactive. Mostly we depend on the component’s state to

change its appearance or modify its interacting behaviour. (Wieruch 2020, 39.)

Hooks were introduced recently in React version 16.8.0 (the latest version as of writing

this is 17.0.1). Hooks enable the using of state and other React features without the needs

of a class. The hook useState is one of the basic hooks of React that allows the developer

to manage state in a functional component. (Facebook Inc 2020d.)

An example of useState can be seen in figure 8 below.

function Example() {

const [count, setCount] = useState(0);

return (

<div>

<p>You clicked {count} times</p>

<button onClick={() => setCount(count + 1)}>

Click me

</button>

</div>

);

} Figure 8. Example of useState (Facebook Inc 2020d)

Page 18: Developing virtual OCPP stations with Node.js & React in

14

The useState hook returns an array with the state and a method to update the state. The

above example displays how the state can be shown and how the setCount method can

be called to update the state. Whenever the button is clicked, the state is updated to a

higher number than the current one. As the state is updated, the component will automati-

cally get re-rendered, and a new “count” state is displayed in the paragraph element.

2.6.4 More Hooks

As hooks were introduced briefly in the previous subchapter, this subchapter goes further

into other common hooks that can be used in a React application.

Sometimes, the developers want to add “side effects” for the components such as chang-

ing the DOM or API request. The useEffect hook ensures that the component has been

rendered before the effect is run. (Facebook Inc 2020d.) An example of useEffect can be

found in figure 9.

useEffect(() => {

function handleStatusChange(status) {

setIsOnline(status.isOnline);

}

ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);

return () => {

ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleSta-

tusChange);

};

}); Figure 9. Example of useEffect

After the component is rendered, the function inside useEffect is called to subscribe to a

friend’s status in this case. Furthermore, by returning a clean-up function at the end, Re-

act will understand that the function should be called when clean-up is required. From the

official documentation, React performs clean-up whenever a component unmounts. (Fa-

cebook Inc 2020d.)

To manage a component state more effectively, Facebook introduces the useReducer

hook that accepts a reducer function (state, action) => newState and the initial

state. The hook will return the state with its dispatch method. (Facebook Inc 2020d.) Fig-

ure 10 shows the same way to update a “count” with useReducer.

Page 19: Developing virtual OCPP stations with Node.js & React in

15

const initialState = {count: 0};

function reducer(state, action) {

switch (action.type) {

case 'increment':

return {count: state.count + 1};

case 'decrement':

return {count: state.count - 1};

default:

throw new Error();

}

}

function Counter() {

const [state, dispatch] = useReducer(reducer, initialState);

return (

<>

Count: {state.count}

<button onClick={() => dispatch({type: 'decrement'})}>-</button>

<button onClick={() => dispatch({type: 'increment'})}>+</button>

</>

);

} Figure 10. Updating component’s count with useReducer (Facebook Inc 2020d)

The useReducer is almost similar to what Redux (a popular state management React li-

brary) has. However, now it has been built inside React itself and can be used inside any

functional component. By having useReducer, more complex behaviour can be defined to

change component’s state depending on the type of input passed on through the dispatch

method as seen above.

2.6.5 React Context

As passing down props can be troublesome in a React application when the props are

passed down too many times. Some props like language or UI theme are required in al-

most all components.

As a solution for this, Facebook introduces Context that allows the passing of props

through the component tree without having to manually pass them down at each level.

Context should be used to share “global” data inside the components tree. (Facebook Inc

2020b.)

The context can be created by using the createContext built-in method.

Page 20: Developing virtual OCPP stations with Node.js & React in

16

const MyContext = React.createContext(defaultValue);

After that, when a component subscribes to this context, it will read the value from the

context provider in the tree. (Facebook Inc 2020b.)

Every context subject requires a provider itself. With the provider providing the value, the subscriber components can have access to the provided context value. A provider can be initiated as <MyContext.Provider value={/* some value */}>

As a result, the consumers of such provider re-render themselves whenever the value of the context is changed. (Facebook Inc 2020b.) There are different ways of consuming a context. The most common way is using a Con-text consumer. Facebook Inc (2020b) shows how to use the consumer in figure 11 below:

<MyContext.Consumer>

{value => /* render something based on the context value */}

</MyContext.Consumer> Figure 11. Consuming the context value (Facebook Inc 2020b)

2.6.6 Create-react-app

A React can be added to a web application by adding a script tag that includes the React

library. However, it usually requires more than that for the structure to be organised. There

are different toolchains available, but create-react-app is the most popular as it is simple

to use and supported by Facebook.

Create-react-app helps set up the development environment. Developers can use the lat-

est JavaScript features as it has a build pipeline that can transpile the code to an older

version of JavaScript. Furthermore, it provides pleasant developer experience with fea-

tures such as hot loading. Last, the built-in build script will optimise the code and create a

build of the application ready for deployment. (Facebook Inc 2020c).

2.6.7 Material UI

To speed up the development process, there are different types of React UI framework a

developer can choose from. Boduch (2019, Preface) said that Material UI is the most pop-

ular React framework.

Material-UI is a combination of two of the best front-end technologies: Google’s Material

Design and Facebook’s React. Material UI is a bridge between the two. It simplifies build-

ing React applications with beautiful design. (Boduch 2019, Preface.)

Page 21: Developing virtual OCPP stations with Node.js & React in

17

2.7 DevOps

DevOps (Development Operations) is a term introduced by Patrick Debois, Gene Kim and

John Willis between 2007 – 2009. It is a set of practices that help reduce the barriers be-

tween developers and operations. By utilising those practices, collaboration is facilitated,

and communication is improved between both sides. DevOps accelerates the speed of

deployment by employing continuous integration / continuous delivery (CI/CD). The accel-

eration is helped by creating and updating infrastructure into code, also known as (Infra-

structure as Code). (Krief 2019, Chapter Getting started with DevOps.)

There are many tools that strengthen the culture of DevOps. In this project, Docker, Ku-

bernetes are used as the tools to facilitate development and deployment. Additionally,

Google Cloud Platform is chosen as the cloud service provider.

2.7.1 Docker

Docker is a tool used for containerisation. It helps isolate the runtime environment of the

application to its host system. This means that the developers can have confidence that

the container running the application in their development environment will be identical to

the production environment. (Krief 2019, Chapter Containerizing Your Application with

Docker.)

Fong (2018), also listed the benefits of using containers compared to the old-school virtual

machines:

- Containers can be deployed quickly and maintain its immutable infrastructure - Applications can be deployed anywhere (virtual machines, bare metal, etc.) - Containers can be integrated easily with existing IT system/process - It saves companies on license for VMs

Figure 12 shows an example of a Dockerfile that can be used to build a runtime for a

Node application.

FROM node

WORKDIR /app

COPY package.json .

RUN npm install

EXPOSE 8080

COPY . .

CMD [ "npm", "start" ] Figure 12. Example of Dockerfile

Page 22: Developing virtual OCPP stations with Node.js & React in

18

With the above Dockerfile and Docker installed in any environment, the Node.js applica-

tion can be built and run without any further installation of dependencies. With such poten-

tial advantages from Docker and containerisation, let us have a look at Kubernetes and

how it can help the organisation in deploying containerised applications.

2.7.2 Kubernetes

Kubernetes is an open-source system from Google. It is used for automating deployment,

scaling, and managing containerised applications (The Kubernetes Authors 2020a).

Whenever we say we use Kubernetes, it means that we have a cluster that holds a set of

worker machines (or nodes) that run containerised applications. Inside each node, we can

find the pods that run the applications. A production environment will usually have multiple

nodes to ensure high availability and fault-tolerance. (The Kubernetes Authors 2020b.)

Figure 13 below shows an overview of the Kubernetes cluster and its components.

Figure 13. Components of Kubernetes (The Kubernetes Authors 2020b)

The control plane is responsible for managing the cluster with tasks like scheduling or re-

sponding to cluster events (for example when a new deployment configuration changes).

Other nodes can communicate with the control plane by the exposed API. (The Kuber-

netes Authors 2020b.)

The kubelet inside each node is an agent that ensures that the containers are running in

each Pod. The specs of the pods are passed down to the kubelet from a configuration file.

Page 23: Developing virtual OCPP stations with Node.js & React in

19

From the specs, Kubernetes assures that the conditions are always established. The

kube-proxy maintains the network part of the node. (The Kubernetes Authors 2020b.)

In the next subchapter, Kubernetes are explained to see how they work in a Kubernetes

cluster.

2.7.3 Kubernetes Objects

Kubernetes Objects are entities in the system to represent the state of the cluster. They

can describe what containerised applications to run, what resources to be used for those

applications and policies for running them. Kubernetes system relies on our definition of

the object to understand what we want. It continually monitors and works to ensure that

the cluster stays at the desired state. (The Kubernetes Authors 2020d.)

Deployment is one of the most used objects which represents an application running in-

side the cluster (The Kubernetes Authors 2020d). Figure 14 shows an example of a De-

ployment object in a yaml file.

apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2

kind: Deployment

metadata:

name: nginx-deployment

spec:

selector:

matchLabels:

app: nginx

replicas: 2 # tells deployment to run 2 pods matching the template

template:

metadata:

labels:

app: nginx

spec:

containers:

- name: nginx

image: nginx:1.14.2

ports:

- containerPort: 80

Figure 14. Example of Deployment object (The Kubernetes Authors 2020d)

The apiVersion specifies which version of the Kubernetes API we want to use to create

this object. The kind is the type of object we want to make (in this case Deployment). Meta

is defined to give a unique identifier to the object. Last, the spec defines what the desired

state we want from the object is. (The Kubernetes Authors 2020d.)

Page 24: Developing virtual OCPP stations with Node.js & React in

20

Another necessary type of object in Kubernetes is Service. It defines a logical set of Pods

and rules on how to access them. Service is used to help Pods communicate with each

other as well as with the outside world. (The Kubernetes Authors 2020c.) Figure 15 shows

an example of a Service object.

apiVersion: v1

kind: Service

metadata:

name: my-service

spec:

selector:

app: MyApp

ports:

- protocol: TCP

port: 80

targetPort: 9376 Figure 15. Example of Service

The selector means that the service will be used for pods that match that metadata. The

ports defined a set of ports that we want to expose or target. In this example, Kuber-

netes’s load balancer will route any traffic coming to port 80 of the service to port 9376 of

MyApp pods.

There are multiple service types, but the default is ClusterIP which is used for internal

communication inside the cluster, and it is also the only type used in this thesis. While

ClusterIP provides a tool for communicating between pods, Ingress helps clients outside

the cluster have a communication channel with services inside (The Kubernetes Authors

2020c). Figure 16 below shows the link between Ingress and Service.

Figure 16. Link between Ingress and Service (The Kubernetes Authors 2020c)

Page 25: Developing virtual OCPP stations with Node.js & React in

21

In the next subchapter, we have a look at the chosen cloud provider for the deployment of

applications in this thesis: Google Cloud Platform.

2.7.4 Google Cloud Platform (GCP)

Google Cloud Platform is a cloud service provider similar to the famous Amazon Web Ser-

vices or Microsoft Azure. They are the third-largest cloud provider in the world. GCP pro-

vides many well-known cloud services such as app engine, compute engine, cloud SQL,

etc.

Fulton III (2019) mentioned the three strengths of GCP compared to its competitors:

- Automation of application deployment: Google is proactive in finding solutions for deployment automation

- Creative cost control - Friendlier for beginners: Google offers many step-by-step examples along with its

extensive documentation.

Additionally, Google is the originator of Kubernetes, and they, as a result, provide better

management panel and monitoring system for Kubernetes cluster inside their console.

With the theories used inside the thesis’ product covered, the next chapter goes into de-

tails of all the implementations of the product.

Page 26: Developing virtual OCPP stations with Node.js & React in

22

3 Product

The product is considered finished when the three parts below are ready:

- User Interface - Server that contains WebSocket clients (virtual stations) - Deployment of both the user interface and the server

The first phase starts with the user interface, which drives the development to visualise on

crucial components for the application first. After the User Interface is completed, the core

of the application which is the back-end server is next. Finally, the software application will

be completed by the deployment stage.

3.1 User Interface

The user interface was decided to be the first component as it helps set out the necessary

API needed for the front-end. Besides, having a mock interface in hands allow the author

to draft the essentials of the whole product.

3.1.1 Creating project with create-react-app

The project was initiated using create-react-app with TypeScript template. The template

gives us a frame of React that integrate TypeScript with all the necessary configurations.

npx create-react-app my-app --template typescript

npm install --save typescript @types/node @types/react @types/react-

dom @types/jest

Material UI is used for styling as they hold a large amount of quality pre-made React Com-

ponents that suits the simplicity needed for the project.

npm install @material-ui/core

3.1.2 Creating application’s home page

We start by designing mock-up for the product. The mock-up was drafted using Balsamiq

Wireframes. The mock-up assists us with the visualising all the components needed in the

app. From the mock-up (see figure 17 below), we can see that our user interface consists

of three primary parts: header, left-side container, and right-side container:

• The header is simple and contains only the name of the application.

• The left side container comprises a button to create a new station, a search bar, and a list of all stations available in the server.

• The right-side container has three small tabs which allow the user to control the station, to start a manual flow and to see the station’s information.

Page 27: Developing virtual OCPP stations with Node.js & React in

23

Figure 17. Mock-up of the application’s home page

Now we put all the main components (Header, SideContainer & Control Container) inside

the main App component. Each component will contain its child components. For exam-

ple, our SideContainer component comprises of a button, a text field for searching and a

list of stations (figure 18 below):

const SideContainer = () => {

const classes = useStyles();

return (

<Paper className={classes.root}>

<div className={classes.buttonContainer}>

<Button className={classes.button} variant="contained" color="pri-

mary">

Create New

</Button>

</div>

Page 28: Developing virtual OCPP stations with Node.js & React in

24

<TextField

label="Search For Station"

id="outlined-margin-dense"

defaultValue=""

className={classes.textField}

margin="dense"

variant="outlined"

/>

<StationList />

</Paper>

);

}; Figure 18. Implementations of SideContainer

From the code above, we can see that styling is inserted to our component by the us-

eStyles method, which utilises makeStyles built-in method from Material Ui. The function

returns an object of CSS properties and its desired values to be inserted to our React

components. Figure 19 below is an example of inserting the style for our root component.

import { makeStyles} from '@material-ui/core';

const useStyles = makeStyles((theme) => ({

root: {

flex: 1,

paddingTop: theme.spacing(1),

paddingLeft: theme.spacing(1),

color: theme.palette.text.secondary,

minHeight: '100vh',

}

} Figure 19. Using useStyles for styling

Moving on to the control container, the Navigation Tabs component provided by Material

UI was used. It gives the page a convenient way to switch between tabs.

3.1.3 Creating mock server

Now the application has a StationList component that requires a list of stations to be pop-

ulated, which we can provide as an array containing multiple Station object. However, a

mock server will be used to return an array of station objects to enable easier transitioning

when we have our back-end server functioning.

Page 29: Developing virtual OCPP stations with Node.js & React in

25

To achieve this, we will create another npm project that installs a package called json-

server:

npm init -y

npm i json-server axios

Axios is an HTTP client package for Node.js. It is used to together with a script to create

seed data for the app. After installing the necessary packages, a db.json file is needed for

storing data. In the file, a stations property is added to allow the client to perform multiple

CRUD operations on the endpoint /stations. The file’s content should look like this:

{

"stations": []

}

A start script to the project package.json is needed to run the mock server on port 3004:

"start": "json-server --watch db.json --port 3004"

After this, we can start our project by npm start. Once the server starts up on port 3004,

we created seed data with a Node.js script as seen below in figure 20.

const axios = require('axios');

const url = 'http://localhost:3004'

const createNewStation = (identity) => {

axios.post(`${url}/stations`, {

identity

})

}

for (let i = 1; i <= 50; i++) {

const identity = `VIRTUAL-OCPPJ-STATION-${i}`

createNewStation(identity)

} Figure 20. Script for creating seed data

The above script will populate 50 stations to the db.json database. json-server also up-

dates each record with an auto-incrementing id property which can be used as a key in

React list element in the StationList component.

3.1.4 Fetching and populating StationList component

First, we added the environment variable needed for the action to the .env file. React au-

tomatically imported this variable to be used in the process.env variable:

REACT_APP_SERVER_URL=http://localhost:3004

Page 30: Developing virtual OCPP stations with Node.js & React in

26

We applied the useEffect hook which allowed us to perform a data fetching operation after

the component has been rendered (Figure 21):

const [stations, setStations] = useState([]);

useEffect(() => {

fetch(`${process.env.REACT_APP_SERVER_URL}/stations`)

.then((data) => data.json())

.then((result) => setStations(result));

}, []); Figure 21. Using useEffect for fetching stations

Additionally, useState hook was also used so that the component re-renders itself when-

ever a new list of stations is received from the API. After the response is received, the ren-

dering of a list of stations is done by utilising the Array.map function (Figure 22):

const stationsList = stations.map((station: any) => (

<li className={classes.listElement} key={station.id}>

<Button variant="outlined" color="primary">

{station.identity}

</Button>

</li>

)); Figure 22. Rendering list of stations with Array.map

Figure 23 shows the real-life progress so far of the application up until this point:

Page 31: Developing virtual OCPP stations with Node.js & React in

27

Figure 23. Home page of the application

The next subchapter focuses on how we can use React Context for managing app-wide

information.

3.1.5 Using React Context for API request

As the application needs to keep track of the selected station across components, we can

use React Context for passing data through without passing props manually at all children

components.

We are going to create a context provider which is called StationContext. In our provider,

we will handle different actions relating to stations within our apps. Those include fetching

all stations, selecting one station for the control tabs as shown in figure 23 above. At the

end of our module, we will export two important components:

- StationContext which can be used by components within the app - StationContextProvider which will allow its children to have access to the Station-

Context

Page 32: Developing virtual OCPP stations with Node.js & React in

28

Within our StationContext provider, we are going to utilise useReducer from React API

which allows us to manage complex state logic. Inside the provider, we can define actions

that will affect the state of our components.

Figure 24 shows an example of an action that requests from an API endpoint the infor-

mation of a particular station. Upon a successful request, we will receive a response with

information regarding the station.

const selectStation = async (id: number) => {

try {

const data = await fetch(

`${process.env.REACT_APP_SERVER_URL}/stations/${id}`

);

if (!data.ok) {

return dispatchRequestError(

`Error fetching station (id: ${id}) info (${data.statusText})`

);

}

const station = await data.json();

dispatch({

type: Actions.SELECT_STATION,

payload: { station },

});

} catch (error) {

dispatchRequestError(

`Error fetching station (id: ${id}) info (${error.message})`

);

}

}; Figure 24. Example of an API request action within context provider component

After defining our necessary state to be managed in the context, we passed them through

the value props in our provider as below in figure 25. By passing the necessary state and

callback, any component that utilises the StationContext within our app can share the

same information and method. The next chapter shows and explains how we are going to

utilise StationContext across components.

return (

<StationContext.Provider value={{ state, selectStation, getStations }}>

{children}

</StationContext.Provider>

); Figure 25. Return value of StationContextProvider

Page 33: Developing virtual OCPP stations with Node.js & React in

29

Finally, to provide the StationContext across our application, we wrap our HomeCompo-

nent inside the StationContextProvider as shown in figure 26. By doing that, we ensure

that all children components have access to the provided context.

<StationContextProvider>

<Home />

</StationContextProvider> Figure 26. Wrapping Home component inside StationContextProvider

3.1.6 Station Information display with StationContext

Now that we have a way to select a particular station from our station list as shown in the

previous chapter. Inside our StationList component, we can access the value from the

StationContext by utilising the useContext hook (figure 27).

const {

selectStation,

getStations,

state: { stations, error },

} = useContext(StationContext); Figure 27. Accessing context value with useContext hook

Now we can add the function selectStation to the onClick event in each of the rendered

station Button (Chapter 3.1.4). On the other hands, more information has been added to

our mock server data seeder to render random data for the station using faker npm library.

Complete code of StationContext.tsx can be found at appendix 1.

Going to our StationInformation component, we can use the same useContext hook to ac-

cess the selectedStation property from the context state. By having access to the select-

edStation state, we can display the information as shown in figure 28.

Page 34: Developing virtual OCPP stations with Node.js & React in

30

Figure 28. Station information acquired from StationContext

3.1.7 Mock server and operations

The core part of the UI is the ability for a user to request our server to send particular

charge point operations to the Central System as defined in OCPP 1.6. To do that, we

need to support an API endpoint /station/operations/:action where :action is any action to

be defined. Besides, we also want to add a delay to our response to replicate real-life use

cases. In our application, we added a 1-second delay by adding middleware to our newly

defined server (figure 29). The full code of the mock server can be found at

https://github.com/qhieu45/virtual-ocpp-j-mock-server.

server.use(function (req, res, next) {

setTimeout(next, 1000);

}); Figure 29. Middleware to add delay of 1000ms to our response

Page 35: Developing virtual OCPP stations with Node.js & React in

31

3.1.8 Use OperationContext for managing operation across components

Similar to what we did with StationContext, we will define an OperationContextProvider

with all the actions needed to send API requests to the mock server. The provider shall

have the state to keep track of request payload, response, error, and selected operation

(Figure 30).

const initialState: OperationContextState = {

requestPayload: {},

responsePayload: {},

operation: ChargePointOperations.Unknown,

error: '',

}; Figure 30. Initiate state for OperationContext

In addition to our state, actions need to be defined to send API request and manipulate in-

ternal state within the provider. The value props for OperationContextProvider are shown

in Figure 31.

return (

<OperationContext.Provider

value={{

state,

setCurrentOperation,

setRequestPayload,

sendOperationRequest,

}}

>

{children}

</OperationContext.Provider>

); Figure 31. Return value of OperationContextProvider

3.1.9 ControlCenter component

The ControlCenter component is our main component for rendering supported charge

point operations. They are predefined in another model component called ChargePoin-

tOperations. We import the module and populate them to ControlCenter component, as

shown in Figure 32.

const operationButtons = operations.map((operation) => {

const { name, editable } = operation;

return (

Page 36: Developing virtual OCPP stations with Node.js & React in

32

<div key={name} className={classes.buttonContainer}>

<Button

onClick={() => onOperationClick(operation)}

className={classes.button}

variant="outlined"

color="primary"

>

{name}

</Button>

{editable ? (

<IconButton

aria-label="send customized message"

color="primary"

onClick={() => onOperationClick(operation, true)}

>

<EditIcon />

</IconButton>

) : null}

</div>

);

}); Figure 32. Rendering operation buttons

It is important to note we have an editable property in each operation. Having that prop-

erty allows the component to conditionally render an additional EditIcon if the user wants

to edit the payload. The onOperationClick method, which is bound to the onClick event will

set the context state to be provided to other consumers of OperationContext.

Inside the ControlCenter component lies the FormDialog component which is the parent

element that will render dynamically based on the selected operation. By utilising a switch

statement, the FormDialog component determines the rendered content depending on the

charge point operation (figure 33).

const getDialogContent = () => {

let dialogContent;

switch (operation) {

case ChargePointOperations.StartTransaction:

dialogContent = <StartTransactionDialogContent />;

break;

// other cases here

}

return dialogContent;

}; Figure 33. Dynamic rendering of DialogContent with switch statement

Page 37: Developing virtual OCPP stations with Node.js & React in

33

By reusing the same FormDialog in all operations, we only have to make minimal changes

to each DialogContent based on their request payload. Moreover, due to the simplicity of

our payload, figure 34 shows how we can have one simple function to bind to all our in-

puts. Provided that the input has the right name property for the request payload, the func-

tion will append the property name with its value to the requestPayload state of our pro-

vider using the setRequestPayload method provided by OperationContext.

const onChangeText: ChangeTextEventFunc = ({ target: { name, value } }) => {

setRequestPayload({ ...requestPayload, [name]: value });

}; Figure 34. Dynamic text change event to be bound to input element

Finally, upon clicking the send button inside the FormDialog, an API request is sent to the

server based on the input provided by the user in each dialog content. Appendix 2 demon-

strates the user interface flow for request a StartTransaction to server. With the UI now

ready, we will go through the developing of the Node.js server in chapter 3.2.

Page 38: Developing virtual OCPP stations with Node.js & React in

34

3.2 Node.js server with Nest.js framework

The goal of this Node.js server is to act as a centralised back-end for our virtual stations.

The server should operate as a middleman between the UI and the Central System Ser-

vice (Virta in this case). Whenever the user wants to send a message to the Central Sys-

tem, our server would take the request, look for the station information in the database,

use the station info to send the requested message and return the response to the UI.

3.2.1 Setting up project with nest-cli

Like React, NestJS provides us with a simple way to create a new project with boilerplate

files:

npm i -g @nestjs/cli

nest new virtual-ocpp-j-server

Additionally, we are going to install typeorm and mysql npm package to the project. Type-

ORM is one of the supported packages for Object Relational Mapper (ORM). With an

ORM installed, we can query from DB by using the provided model/entity by ORM without

writing actual SQL queries.

npm install --save @nestjs/typeorm typeorm mysql

We are going to use Docker for setting up the project as it allows other developers to

quickly start the project without installing all the dependencies (MySQL in this case). First,

we set up Dockerfile for the Node.js server (Figure 35).

FROM node:12.19-alpine

WORKDIR /app

COPY . .

RUN npm install

# Compile

RUN npm run build

EXPOSE 8080 Figure 35. Dockerfile for the application

Next, we are going to set up a docker-compose file which will include both the image from

the above Dockerfile and the official MySQL image. The full docker-compose.yml setup

can be found in appendix 3. Some explanations for the docker-compose files:

Page 39: Developing virtual OCPP stations with Node.js & React in

35

- env_file parameter tells Docker to pick up our predefined environment variable and put it to Docker. For example, our server can access the DB_HOST variable with process.env.DB_HOST.

- command: by using the wait-for.sh developed by Eficode (a DevOps consultant company), we tell Docker not to start up our server until MySQL is ready.

- ports: exposing specific ports of the Docker container to our local machine. With this set-up, we can access our server at http://localhost:8080

Last but not least, we add our environment variable .env file, add the .env file to .gitignore,

add unnecessary folders to .dockerignore to speed up build time and set up .prettierrc for

auto-formatting.

3.2.2 Database migration with TypeORM & database connection with Node.js app

Migrations are critical for the application to synchronize changes to our database. A mi-

gration file is a file which contains the SQL queries used to update a database schema.

Additionally, the migration file should contain another query for the application to roll back

the changes should it need to.

Before creating the migrations, the app needs to have a connection to the MySQL data-

base. This can be done by defining all the necessary variables for TypeORM as Figure 36

below:

const typeOrmConfig: TypeOrmModuleOptions = {

type: 'mysql',

host: process.env.DB_HOST,

port: parseInt(process.env.DB_PORT),

username: process.env.DB_USER,

password: process.env.DB_PASSWORD,

database: process.env.DB_DATABASE,

entities: [__dirname + './../**/*.entity{.ts,.js}'],

migrations: ['dist/migrations/*{.ts,.js}'],

migrationsTableName: 'migrations',

migrationsRun: false,

synchronize: false,

}; Figure 36. typeOrmConfig of the application

All the host, port, username, password as well as the database are populated from the en-

vironment variables. This benefits us on the later deployment stage since we will not need

to make any changes to the codebase.

Page 40: Developing virtual OCPP stations with Node.js & React in

36

After having the config, we followed the instructions to set up TypeORM from NestJS in

our app module. We also added some helper scripts for TypeORM to package.json so

they can be used later. Now a migration can be added by using TypeORM cli:

npx typeorm migration:generate -n StationTable

The command above will generate a boilerplate file for us where we can fill what we want

to do with the migration. A full StationTable migration file can be found in appendix 4.

There we can see all the defined fields for a station record, their data type and their de-

fault value if needed.

After having defined the properties in the table, we then need to run the migration so that

the queries will be run against our SQL server. Since the runtime environment is in

Docker, we would want to run the command inside Docker. Figure 37 shows the com-

mand to run as well as the success logs of the migration:

Figure 37. Migration command and success logs

Now the table is ready in MySQL, and we can go to the next step where we set up the

controllers, service & repository to create, update as well as get station(s).

3.2.3 Setting up station entity, controller, service, and repository

An entity is a model where we describe the properties of the correlating database table.

TypeORM will also append an additional method to the entity to help us save/update the

record whenever necessary. Figure 38 shows an example of how properties are defined in

station.entity.ts

@Entity()

export class Station extends BaseEntity {

@PrimaryGeneratedColumn()

id: number;

@Column()

identity: string;

// other properties here

Page 41: Developing virtual OCPP stations with Node.js & React in

37

} Figure 38. Station entity definition

Figure 39 shows how we added a POST endpoint to create a station in the controller.

NestJS recommends the use of Data Transfer Object (DTO) where we can define the re-

quest payload in advance and pass through many layers of the application. In the exam-

ple below, we can see the createStationDto is passed to the createStation method of the

stationsService. Additionally, by adding the ValidationPipe here, NestJS will validate the

request payload upon receiving the request body.

@Post()

@UsePipes(new ValidationPipe({ skipMissingProperties: true }))

createStation(

@Body() createStationDto: CreateOrUpdateStationDto,

): Promise<Station> {

return this.stationsService.createStation(createStationDto);

} Figure 39. Adding POST endpoint

The next step is creating the station service. Usually, the service class contains business

logic. However, in this case, the use case is still simple, so we have not yet had any logic

besides calling the createStation method of the stationRepository (figure 40).

async createStation(createStationDto: CreateOrUpdateStationDto) {

return this.stationRepository.createStation(createStationDto);

} Figure 40. Method createStation inside stationsService

The final step is defining stationRepository class. The createStation method of the reposi-

tory will try to parse the data from the DTO and insert the record to the database. The

method will also use some of our environment variables as default value if it were not de-

fined in the request payload (Figure 41).

async createStation(createStationDto: CreateOrUpdateStationDto) {

const { identity, centralSystemUrl, meterValue, currentCharg-

ingPower } = createStationDto;

let latestStation: Station = null;

if (!identity) {

latestStation = await this.getLatestStation();

}

const station = this.create();

Page 42: Developing virtual OCPP stations with Node.js & React in

38

station.identity = identity ?? `${process.env.DEFAULT_IDEN-

TITY_NAME}${(latestStation?.id ?? 0) + 1}`;

station.centralSystemUrl = centralSystemUrl ?? `${process.env.DEFAULT_CEN-

TRAL_SYSTEM_URL}`;

station.meterValue = meterValue ?? 0;

station.currentChargingPower = currentChargingPower ?? 11000;

await station.save();

return station;

} Figure 41. createStation method inside stationRepository

Now everything is ready to go. We are going to use an application for calling our API

called Insomnia to test the endpoint. Figure 42 shows the successful result of our request.

The response contains all information about a station is returned to the API client.

Figure 42. Testing POST /stations with Insomnia

Finally, we are adding unit tests for each of our components including entity, controller,

service and repository. By adding unit tests, we ensure that the core functionality is well-

maintained whenever we make changes to the application. This thesis does not cover the

code of unit tests as they are quite abundant to be included. Figure 43 shows an example

of test coverage when we run the command npm run test:cov

Page 43: Developing virtual OCPP stations with Node.js & React in

39

Figure 43. Test coverage of station module

3.2.4 Setting up WebSocket connection for the stations

Now station’s record can be created. The server needs to read the station’s data from our

database and make WebSocket (WS) connections to the Central System Service (as de-

fined by OCPP1.6). To do that, a wrapper class is needed to open a WebSocket connec-

tion. We are going to use the npm package ws in this server. The package provides API

for both WS clients and server. However, in this project, we will only use the client-side of

the package.

Firstly, after defining a class named StationWebSocketClient, we extend the class with the

base ws package, as seen in figure 44.

import * as WebSocket from 'ws';

export class StationWebSocketClient extends WebSocket {

private _lastMessageId: number = 0;

public stationIdentity: string = '';

public connectedTime: Date = null;

public heartbeatInterval: NodeJS.Timeout = null;

public meterValueInterval: NodeJS.Timeout = null;

Page 44: Developing virtual OCPP stations with Node.js & React in

40

public callResultMessageFromCS?: string = null;

public expectingCallResult: boolean = false;

private _callMessageOperationFromStation: string = '';

// other setters & getters

} Figure 44. StationWebSocketClient class

By extending/inheriting the base package, we can easily add our implementation of the

package. Each additionally defined property is used for a later purpose:

- _lastMessageId: is used alongside with its setter & getter to retrieve and create a unique messageId for each request. As OCPP utilises rRPC protocol as defined in chapter 2.2.2, a unique identifier is included in each message so that the sender can recognise if a received message is a response to the previous message.

- stationIdentity: is used as a unique identifier for each station, this is used to per-form OCPP operations later (chapter 3.2.5)

- connectedTime: is used for debugging purpose to calculate how long the connec-tion lasts

- heartbeatInterval & meterValueInterval: are used to create an interval message to be sent to CS as defined in OCPP 1.6

- callResultMessageFromCS & _callMessageOperationFromStation: is used to re-port if the client has received a message (to be explained further in chapter 3.2.5)

- expectingCallResult: is used to define if a response is expected from the Central System. There are cases where a response is not needed for any further pro-cessing.

Now that the base class is defined, we need a service for creating WS clients. The Sta-

tionWebSocketService is responsible for all actions related to the client. We start by defin-

ing the dependency of the service. For now, the service relies on two other providers: the

StationRepository and ByChargePointOperationMessageGenerator. The repository is

used to perform database action related to the station, and the ByChargePointOperation-

MessageGenerator is used in generating OCPP messages based on the info provided.

More information regarding the message generator is covered in chapter 3.2.5. On the

other hand, a function is also needed to create WS clients as well as its binding method.

The constructor and such method are displayed in figure 45.

constructor(

@InjectRepository(StationRepository)

private stationRepository: StationRepository,

private byChargePointOperationMessageGenerator: ByChargePointOperation-

MessageGenerator,

) { }

public createStationWebSocket = (station: Station): StationWebSocketCli-

ent => {

let wsClient: StationWebSocketClient;

const protocols = 'ocpp1.6';

Page 45: Developing virtual OCPP stations with Node.js & React in

41

try {

wsClient = new StationWebSocketClient(`${station.centralSys-

temUrl}/${station.identity}`, protocols);

} catch (error) {

this.logger.log(`Error connecting for station ${station.iden-

tity}: ${error?.message ?? ''}`);

return null;

}

wsClient.on('open', () => this.onConnectionOpen(wsClient, station));

wsClient.on('message', (data: string) => this.onMessage(wsClient, sta-

tion, data));

wsClient.on('error', error => this.onError(error));

wsClient.on('close', (code: number, reason: string) => this.onConnec-

tionClosed(wsClient, station, code, reason));

return wsClient;

}; Figure 45. Creating new WS connection

As seen from above, a WebSocket connection is opened by creating a new instance of

the StationWebSocketClient with a URL and the protocols of the client. In this case, we

have a predefined protocol which is ‘ocpp1.6’ and the URL is taken from the station’s cen-

tralSystemUrl property upon creation as seen from Figure 41.

Upon opening a new connection, a client binds specific events to the action that it wants

to perform once the event happens. It is comparable to binding a click event in a JavaS-

cript DOM. Here, the four main events ‘open’, ‘message’, ‘error’ & ‘close’ are bound to

their corresponding event-handler methods. Figure 46 shows the action performed upon

opening a new connection:

public onConnectionOpen = (wsClient: StationWebSocketClient, station: Sta-

tion) => {

this.logger.log(`connection opened for station ${station.iden-

tity}, sending Boot`);

wsClient.stationIdentity = station.identity;

wsClient.connectedTime = new Date();

const bootMessage = this.byChargePointOperationMessageGenerator.cre-

ateMessage(

'BootNotification',

station,

wsClient.getMessageIdForCall(),

);

this.sendMessageToCS(wsClient, bootMessage, 'BootNotification');

this.createHeartbeatInterval(wsClient, station);

Page 46: Developing virtual OCPP stations with Node.js & React in

42

if (station.chargeInProgress) {

this.createMeterValueInterval(wsClient, station);

}

}; Figure 46. Implementation of onConnectionOpen

As seen from above, upon creating a new connection, the stationIdentity is set right away

so that the client can be found later if needed. Afterwards, a BootNotification message is

created and sent to the CS to inform that the station is now online. As mentioned earlier, a

heartbeat interval and meter value interval are created so that the client automatically

sends those messages to the Central System every interval (which is set as one minute in

this server). Heartbeat is sent to inform the CS if the station is still online. Meter Value is

used to notify the CS of the energy used during a charging session. That is the reason

why the meter value interval is only created if the charge is in progress. Next, we will have

a look at the onConectionClosed method in figure 47 below.

public onConnectionClosed = (wsClient: StationWebSocketClient, sta-

tion: Station, code: number, reason: string) => {

clearInterval(wsClient.heartbeatInterval);

clearInterval(wsClient.meterValueInterval);

if (wsClient?.connectedTime) {

const connectedDurationInSeconds = (new Date().getTime() - wsCli-

ent.connectedTime.getTime()) / 1000;

const connectedMinutes = Math.floor(connectedDurationInSeconds / 60);

const extraConnectedSeconds = connectedDurationInSeconds % 60;

this.logger.log(`Duration of the connection: ${connected-

Minutes} minutes & ${extraConnectedSeconds} seconds.

Closing connection ${station.identity}. Code: ${code}. Reason: ${reason}.`);

}

}; Figure 47. Implementation of onConnectionClosed

As the intervals are created when connections are open, we need to clear them when the

client no longer has any connection to the CS. This helps to reduce the resources on the

server to maintain unnecessary interval activities. Additionally, the duration of the connec-

tion is calculated and logged alongside with the WebSocket close codes and reason for

debugging purpose. As each binding is separated in each own method, it facilitates easier

unit testing. Figure 48 shows how the event listener for closing connection is tested.

Page 47: Developing virtual OCPP stations with Node.js & React in

43

it('test onClose function', () => {

jest.useFakeTimers();

stationWebSocketClient.connectedTime = new Date();

stationWebSocketClient.heartbeatInterval = setInter-

val(() => {}, 1000);

stationWebSocketClient.meterValueInterval = setInter-

val(() => {}, 1000);

stationWebSocketService.onConnectionClosed(stationWebSocketCli-

ent, station, 1005, 'needs to be closed');

expect(clearInterval).toHaveBeenCalledWith(stationWebSocketCli-

ent.heartbeatInterval);

expect(clearInterval).toHaveBeenCalledWith(stationWebSocketClient.me-

terValueInterval);

}); Figure 48. Example of unit test for onConnectionClosed

By using the useFakeTimers from Jest, we ensure that no unnecessary interval is created

during a test. The test here makes sure that the intervals are cleared correctly upon con-

nection closure.

With a class to create WS connection, we can now add functionalities to the Station-

Service as defined in chapter 3.2.3. First, we define a method to use the StationWebSock-

etService class to connect a station. Second, we define another method to fetch all the

records from the database, filter out already connected stations and establish connections

for the rest (figure 49).

connectStationToCentralSystem(station: Station) {

const newStationWebSocketClient = this.stationWebSocketService.cre-

ateStationWebSocket(station);

this.connectedStationsClients.add(newStationWebSocketClient);

}

async connectAllStationsToCentralSystem() {

let dbStations: Station[] = [];

try {

dbStations = await this.getStations({});

} catch (error) {

this.logger.error(`Error fetching stations information`, er-

ror?.stack ?? '');

}

// remove closing / closed sockets

this.connectedStationsClients.forEach(client => {

if (client.readyState !== WebSocketReadyStates.CONNECTING && cli-

ent.readyState !== WebSocketReadyStates.OPEN) {

Page 48: Developing virtual OCPP stations with Node.js & React in

44

this.connectedStationsClients.delete(client);

}

});

const connectedStationsIdentity = [...this.connectedStationsCli-

ents].map(client => client.stationIdentity);

const unconnectedStations = dbStations.filter(dbStation => !connected-

StationsIdentity.includes(dbStation.identity));

unconnectedStations.forEach(station => this.connectStationToCentralSys-

tem(station));

} Figure 49. Methods to establish connections for station

To fetch all the stations inside connectAllStationsToCentralSystem, we reuse getStations

that we have defined before for our controllers. Additionally, we will want to catch the error

and log it for now. Since this method is supposed to be an interval method, we filter out

the stations that are already connected. This can be checked by accessing the readyState

of a WS client. If the readyState of a client is not “connecting” or “open”, we delete that

connection from the Set of connectedStationsClients.

After getting a filtered list of only unconnected stations, we proceed to open a new WS

connection for each of them. It is important to note that upon creating any new connection,

we also add that client to a Set saved inside the StationsService. By using the Set API

provided by JavaScript, we ensure that each connection is unique. The Set connected-

StationsClients will also be used later to retrieve certain clients for performing operations

upon request from the user.

Finally, we define an interval where we fetch all the stations and connect/reconnect them

to central system service every five minutes. This is done inside the main.ts file, which is

the initial setup file of NestJS. Upon setting up the server, we will set an interval where we

called connectAllStationsToCentralSystem from StationsService as shown in figure 50.

However, as setInterval only calls the method after the interval period, we call the same

method right before that to ensure connections are established as soon as the server is

booted up.

await stationsService.connectAllStationsToCentralSystem();

setInterval(() => {

stationsService.connectAllStationsToCentralSystem();

}, 30000); Figure 50. Connect stations to Central System Service every five minutes

Page 49: Developing virtual OCPP stations with Node.js & React in

45

Now the stations are connected and ready to go, we will go through how we can send

OCPP message as a charge point in the next subchapter.

3.2.5 OCPP operations for stations

To handle OCPP messages, a new NestJS module is created to handle it. By utilising dif-

ferent modules for different purposes, we can separate the responsibilities for each of

them. The separation helps the code base maintain a clean structure and improves main-

tainability. The main class of this module is called ByChargePointOperationMessageGen-

erator which is included in the exports array of our module. By exporting the provider,

other modules can use them by importing the MessageModule. As seen in chapter 3.2.4,

the ByChargePointOperationMessageGenerator is imported and used by the StationWeb-

Socket client.

As each OCPP message is different, each of them requires a class to build their message

based on the station and request payload. To enforce unity between each builder, we cre-

ate an interface with build and getOperationName methods. Figure 51 shows the Interface

as well as the request builder for BootNotification that implements such interface.

export interface ByChargePointRequestBuilderInterface {

build(station?: Station, payload?: any): ByChargePointRequestTypes;

getOperationName(): string;

}

export class BootNotificationRequestBuilder implements ByChargePointRequest-

BuilderInterface {

build(station: Station, payload: any) {

const request = new BootNotifcationRequest();

request.chargePointVendor = payload?.vendor ?? station.vendor;

request.chargePointModel = payload?.model ?? station.model;

return request;

}

getOperationName = () => 'BootNotification';

} Figure 51. ByChargePointRequestBuilderInterface and example of its usage

To explain further, each OCPP message request builder must have its build & getOpera-

tion methods. The parameters in those messages are the same but the result can be dif-

ferent. In the example above, the BootNotificationRequestBuilder build the message

based on the BootNotificationRequest defined in a model folder. This is where TypeScript

Page 50: Developing virtual OCPP stations with Node.js & React in

46

comes in handy as it allows the definition of each message to be predefined. They are ex-

tremely useful when working with protocol-typed messages like this. The builder then

modifies the request based on the station’s information or payload information and return

it.

With a way to build the message, the next step is developing an API where the UI can re-

quest the server to perform the certain charge point operations. The endpoint is defined

as seen in figure 52 below.

@Post('/:id/operations/:operation')

@HttpCode(200)

@UsePipes(new ValidationPipe({ skipMissingProperties: true }))

createStationOperation(

@Param('id', ParseIntPipe) id: number,

@Param('operation') operationName: string,

@Body() stationOperationDto: StationOperationDto,

) {

return this.stationsService.sendStationOperationRequest(id, operation-

Name, stationOperationDto);

} Figure 52. Endpoint definition in StationController class

As the endpoint is defined in the StationController class, it will have this format “/sta-

tions/:id/operations/:operation”. The first parameter is the id which is the station’s ID that

can be used to find a station. The second is the operation which is one of the supported

OCPP operations by the server. The validation pipe is used so that request payload is val-

idated before passing through the service’s sendStationOperationRequest method. Since

query parameter is always parsed as a string, NestJS provides a ParseIntPipe where the

parameter is automatically converted to an integer. Like creating or updating a station, a

DTO is defined as StationOperationDto where it contains possible request payload param-

eters for an operation. Next, figure 53 shows how the StationService asks a WS client to

perform an OCPP request to the CS.

async sendStationOperationRequest(id: number, operationName: string, sta-

tionOperationDto: StationOperationDto) {

const station = await this.getStationById(id);

const wsClient = [...this.connectedStationsClients].find(st => st.sta-

tionIdentity === station.identity);

if (!wsClient || wsClient.readyState !== WebSocketReadyStates.OPEN) {

throw new BadRequestException(`Station WS client not found or not con-

nected! ${wsClient?.readyState}`);

Page 51: Developing virtual OCPP stations with Node.js & React in

47

}

const { request, response } = await this.stationWebSocketService.prepar-

eAndSendMessageToCentralSystem(

wsClient,

station,

operationName,

stationOperationDto,

);

return { request, response };

} Figure 53. Sending an OCPP request to Central System

First, a station is retrieved from the database based on the id received from the controller.

In case a station is not found, the built-in exception handler from NestJS will return a 404

API response to the client to inform of the result. If a station is found, the next step is to

look for a connected WS client in the StationService. The finding of the station was men-

tioned briefly in the opening new connection part in chapter 3.2.4. This step is done by uti-

lising JavaScript Array.find method to look for the client that has the same identity as the

station. If the client is not found or the connection has already been closed, a BadRe-

questException is thrown and NestJS handles the exception and returns a 400 API re-

sponse. If the client is found, the method prepareAndSendMessageToCentralSystem from

the StationWebSocketService is called to perform further actions of preparing OCPP mes-

sage and sending it to the CS. The implementation can be seen in Figure 54 below.

public async prepareAndSendMessageToCentralSystem(

wsClient: StationWebSocketClient,

station: Station,

operationName: string,

payload: StationOperationDto,

) {

const message = this.byChargePointOperationMessageGenerator.createMes-

sage(

operationName,

station,

wsClient.getMessageIdForCall(),

payload,

);

if (!message) {

throw new BadRequestException(`Cannot form message for operation ${op-

erationName}`);

}

Page 52: Developing virtual OCPP stations with Node.js & React in

48

this.sendMessageToCS(wsClient, message, operationName);

wsClient.expectingCallResult = true;

const response = await this.waitForMessage(wsClient);

wsClient.callResultMessageFromCS = null;

wsClient.expectingCallResult = false;

return { request: message, response };

} Figure 54. Preparing message and sending the message to CS

The message is generated by calling the helper method from ByChargePointOperation-

MessageGenerator from the MessageModule. The method will return a message string or

an empty string depending on the input. Should the output be empty, another BadRe-

questException is also thrown to inform the user of invalid request. However, if the mes-

sage is created successfully, it is sent to the CS by the method sendMessageToCS.

Besides sending the message, the same method also sets the value of callMessageOper-

ationFromStation so that it can be later referred to upon receiving a response from the

CS. Since the user would want to know the response and the sending message part is

asynchronous, we set up a waitForMessage method to continuously check for the result

from the CS. Upon receiving the message, certain attributes related to message sending

(callResultMessageFromCS, expectingCallResult) are reset, and response is included in

the API response back to the user. Figure 55 shows the implementation of the wait-

ForMessage method.

public waitForMessage = (wsClient: StationWebSocketClient): Prom-

ise<string | null> => {

return new Promise<string | null>(resolve => {

const maxNumberOfAttemps = 50;

const intervalTime = 200;

let currentAttemp = 0;

const interval = setInterval(() => {

if (currentAttemp > maxNumberOfAttemps - 1) {

clearInterval(interval);

this.logger.log('Server does not respond');

return resolve(null);

} else if (wsClient.callResultMessageFromCS) {

clearInterval(interval);

return resolve(wsClient.callResultMessageFromCS);

}

Page 53: Developing virtual OCPP stations with Node.js & React in

49

this.logger.debug('Message not yet received, checking for more');

currentAttemp++;

}, intervalTime);

});

}; Figure 55. Implementation of waitForMessasge

The method returns a promise which the caller can wait for it to be resolved. Inside the

promise, we set the maxNumberOfAttemps as well as the intervalTime. The logic is that

every 200 milliseconds, we check if the message is received in the wsClient.callResult-

MessageFromCS. The call result is added to the client upon receiving the message. Then

it is handled in the event listener for onMessage. The checking action happens 50 times

until a response is received. The maximum total time for waiting is 10 seconds. If a re-

sponse is not received after the 50th time, a null response is resolved together with log-

ging a debug message. Figure 56 shows the forming of the OCPP message in the By-

ChargePointOperationMessageGenerator provider.

constructor(private readonly byChargePointRequestBuilderFactory: ByCharge-

PointRequestBuilderFactory) {}

public createMessage(operationName: string, station: Sta-

tion, uniqueId: number, payload?: any): string {

const builder = this.byChargePointRequestBuilderFactory.getBuilderFro-

mOperationName(operationName);

if (builder === null) {

return '';

}

const chargePointRequest = builder.build(station, payload ?? {});

const message = JSON.stringify([

ChargePointMessageTypes.Call,

uniqueId.toString(),

builder.getOperationName(),

chargePointRequest,

]);

return message;

} Figure 56. ByChargePointOperationMessageGenerator class

The constructor indicates that the class relies on a request builder factory. The factory

class has a method getBuilderFromOperationName that returns a specific request builder

Page 54: Developing virtual OCPP stations with Node.js & React in

50

based on the operation. For example, if the operationName is BootNotification, the Boot-

NotificationRequestBuilder (as seen from Figure 51) is returned to build the message. Af-

ter the successful building of chargePointRequest, we bundle the message in the OCPP

defined format as shown above and return the output. Figure 57 shows all the folder con-

tent of the message module with all its request builders and unit tests.

Figure 57. Message module’s folder content

Next step is handling messages coming from the station. Details are explained further in

the following subchapter.

3.2.6 Handle incoming messages from Central System

The WS client now can send the message to the client, but it also needs to handle appro-

priately the response coming back from the server. Figure 58 shows how a message is

parsed and processed depending on its type.

Page 55: Developing virtual OCPP stations with Node.js & React in

51

public onMessage = (wsClient: StationWebSocketClient, station: Sta-

tion, data: string) => {

let parsedMessage: any;

parsedMessage = JSON.parse(data);

const messageType = parsedMessage[0] as ChargePointMessageTypes;

switch (messageType) {

case ChargePointMessageTypes.Call: {

this.processCallMsgFromCS(wsClient, station, data);

break;

}

case ChargePointMessageTypes.CallResult: {

this.processCallResultMsgFromCS(wsClient, station, data);

break;

}

default:

this.logger.log('data does not have correct messageTypeId', data);

}

}; Figure 58. Processing message coming from the CS

Each message is first parsed and categorised into its proper type according to the rRPC

framework of OCPP. If the messageType is CallResult, we know that it is a response from

a message sent previously by the WS client to the CS. The message is processed more

thoroughly in the method processCallResultMsgFromCS. Figure 59 shows a clipped im-

plementation of message processing for a StartTransaction response.

public processCallResultMsgFromCS(wsClient: StationWebSocketClient, sta-

tion: Station, response: string) {

try {

const parsedMessage = JSON.parse(response);

const [, reqId, payload] = parsedMessage as [number, string, object];

if (reqId.toString() !== wsClient.lastMessageId.toString()) return;

this.logger.log(`Received response for reqId ${wsClient.lastMes-

sageId}: ${response}`);

switch (wsClient.callMessageOperationFromStation.toLowerCase()) {

case 'starttransaction': {

const { transactionId, idTagInfo: { status } } = payload as Start-

TransactionResponse;

if (status === IdTagInfoStatusEnum.Accepted && transac-

tionId > 0) {

const dto: CreateOrUpdateStationDto = {

chargeInProgress: true,

currentTransactionId: transactionId,

};

this.stationRepository.updateStation(station, dto);

Page 56: Developing virtual OCPP stations with Node.js & React in

52

this.createMeterValueInterval(wsClient, station);

}

break;

}

// other messages processing here

}

wsClient.callResultMessageFromCS = wsClient.expectingCallResult ? re-

sponse : null;

} catch (error) {

this.logger.error(`Error processing response`, error.stack ?? '', er-

ror.message ?? '');

} finally {

wsClient.callMessageOperationFromStation = '';

}

}

} Figure 59. Implementation of processCallResultMsgFromCS

First, the response is parsed. Then the unique request Id (that was sent from the client to

the CS previously) is checked and validated to make sure that it is the expected response.

Later a switch statement is used to perform a variety of activities depending on the current

operation. Here we show only the actions performed for a StartTransaction message.

Upon receiving an “Accepted” response for a StartTransaction message, the station is up-

dated with the info received from the CS by utilising the repository introduced in chapter

3.2.3. Following that, a meter value interval is created to ensure that the client sends regu-

lar updates on the energy used by the virtual station every minute. Outside the switch

statement, the callResultMessageFromCS is updated with the response from the station

so that the waitForMessage introduced in Figure 55 can process further. Last, the value of

callMessageOperationFromStation is reset.

Next, we will have a look at how we process some of the request messages coming from

the CS. OCPP defines certain messages that a CS can send to a station. An example

would be a RemoteStartTransaction which request a station to start a transaction. The im-

plementation is shown in Figure 60 below.

public async processCallMsgFromCS(

wsClient: StationWebSocketClient,

station: Station,

request: string,

): Promise<void> {

this.logger.log('Processing request from CS', request);

try {

const parsedMessage = JSON.parse(request);

Page 57: Developing virtual OCPP stations with Node.js & React in

53

const [, uniqueId, action, payload] = parsedMessage as [num-

ber, string, string, object];

switch (action.toLowerCase()) {

case 'remotestarttransaction': {

const { idTag } = payload as RemoteStartTransactionRequest;

const responseMessage = this.buildCallResultToCSMessage(

station,

{ status: RemoteStartStopStatusEnum.Accepted },

uniqueId,

action,

);

this.sendMessageToCS(wsClient, responseMessage, '');

this.prepareAndSendMessageToCentralSystem(wsClient, sta-

tion, 'StartTransaction', { idTag });

break;

}

// other cases

}

} catch (error) {

this.logger.error(`Error processing request from CS`, er-

ror.stack ?? '', error.message ?? '');

}

} Figure 60. Processing CallMessage from CS

Like the previous handling of message, we parse the data & extract the uniqueId, action

as well as the request payload. The uniqueId must be later used to send the response

back to the CS. Based on the action, we formulate what needs to be done. In this exam-

ple, for a RemoteStartTransaction message, we send back an “Accepted” to the CS. After

sending the response, we initiate a charge flow by preparing and send a new StartTrans-

cation message with the idTag provided by the CS.

With both the core functionalities for receiving and sending messages ready, that con-

cludes the development of the Node.js server. Additionally, the user can initiate a custom

message to be sent to the CS. There are more complex messages that we at Virta

planned for this product, but they are not included in this thesis due to its complexity.

Applications, however, are not definitely ready until they are deployed. We will have a look

at how we can deploy both the UI & the server in the next chapter.

Page 58: Developing virtual OCPP stations with Node.js & React in

54

3.3 Deployment to GCP

Now that the development of the UI and the server is finished. We need to deploy them to

GCP using Kubernetes. The reason that we want to use Kubernetes is that every re-

source created for the deployment is version controlled. By having them version con-

trolled, we ensure that it is clear to other developers who work on the project at a later

stage.

3.3.1 Setting up project on internal Bitbucket

First, we combine the projects into one repository at Virta. We do this by utilising the sub-

module functionality of git. First, we created a new repository inside Virta’s organisation

called virtual-ocpp-j. Afterwards, by using submodule feature, we ensure that our open-

source software can stay in GitHub and we only need to update the submodules’ commit

hashes whenever an update is required internally.

The following commands add the submodules to the repository:

- git submodule add [email protected]:qhieu45/virtual-ocpp-j-server-ui.git - git submodule add [email protected]:qhieu45/virtual-ocpp-j-server.git

After that, we created a docker-compose file that combines both projects together so that

a developer can start a project with one command: docker-compose up without installing

any necessary dependencies. The docker-compose.yml file can be found in appendix 5.

By using our internal repository, we can make use of the Bitbucket Pipeline tool provided

by Atlassian. It is a CI tool that is used for automation in running test & deployment.

3.3.2 Set up Dockerfile for the UI repository

Since the Dockerfile for the UI repository was not set up earlier, we now write the Dock-

erfile so that it can be used for deployment. Figure 61 shows the file.

FROM node:14.15.0-alpine3.10 as base

WORKDIR /app

COPY package.json .

RUN npm install

# Later stages are for prod only

FROM base as build

ENV REACT_APP_SERVER_URL="/api"

COPY public public/

COPY tsconfig.json tsconfig.json

COPY src src/

Page 59: Developing virtual OCPP stations with Node.js & React in

55

RUN npm run build

FROM nginx

EXPOSE 3000

COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf

COPY --from=build /app/build /usr/share/nginx/html Figure 61. Dockerfile for virtual-ocpp-j-server-ui

The first step (base) is copying the file and installing the dependencies. For development,

this is the final step as there is nothing to be done further. For production, however, there

are two more steps which is the build stage and the serve stage. The build stage takes the

result from the base step and build the application with the npm run build command. The

command creates an optimised and minimised version of the application. The final (serve)

step is done by:

- copying on the build folder to the last container with Nginx image. Copying only necessary files helps to reduce the size significantly of the final container for de-ployment

- copying the ready-made Nginx configuration to the container - exposing port 3000 to outside the container so that it can be reached later

With both repositories having its Dockerfile, it is time to start with the deployment. The

next subchapter will explain the architecture of the deployment.

3.3.3 Architecture of all components inside Kubernetes

Figure 62 shows the architecture of all components inside a Kubernetes cluster.

Page 60: Developing virtual OCPP stations with Node.js & React in

56

Figure 62. Diagram of all components within Kubernetes cluster

A Kubernetes cluster usually contains more than one node. However, in this case, we only

run one node as we do not need all the extra resources. Each of the application is de-

signed to be stateless and can be deployed as easy as possible. The whole architecture

contains:

- Kubernetes cluster: the required cluster that controls the state of the applications based on the provided configuration file.

- Node: the virtual instance (or Compute Engine in GCP term) that contains the pods

- Pods that contain React UI, Node.js server & MySQL server - Persistent Volume that ensures data is secured even when the instance goes

down - NGINX Ingress Controller is the gateway for traffic management from the Internet

to the Kubernetes cluster.

Page 61: Developing virtual OCPP stations with Node.js & React in

57

3.3.4 Creating Kubernetes cluster & service account

A Kubernetes cluster is needed to deploy the containers there. To create the cluster, we

can go to GCP console → Kubernetes Engine → Create cluster. We are not going through

all the steps here as they include some trivial small steps. The result afterwards is a Ku-

bernetes with one cluster and one node inside. That node is the virtual machine (or com-

pute engine in GCP) instance that runs all the applications.

In the next step, we need to create a service account for the CI pipeline. The account will

be used by only the pipeline and no one else. The account is created by going to GCP

console → IAM & Account → Service Accounts. After creating the service account, GCP

gives out a json file that contains the token and secret that we can use to authenticate in

the CI pipeline later. After the account is ready, we need to grant enough permissions to it

so that the pipeline can perform all the actions later. Figure 63 shows the roles attached to

the account after creation.

Figure 63. Service Account bitbucket-k8s-deploy created and roles added

The role Kubernetes Engine Admin allows the account to manage the cluster. The other

two roles are necessary for the account to push and pull container image from Google

Container Registry (GCR).

3.3.5 Creating configurations files for deployment

As discussed in subchapter 3.3.3 and figure 62, the following components are needed:

- Kubernetes cluster & node - NGINX Ingress Controller - MySQL database with Persistent Volume - Node.js server - React UI application served by NGINX

Page 62: Developing virtual OCPP stations with Node.js & React in

58

Figure 64 shows how each infrastructure component is separated inside the infra folder.

By separating them in each folder, developers can have a good idea on what components

are essential for the whole application.

Figure 64. Infra folder

In addition to the components mentioned above, we can also see a migrations folder that

contains a migration job. The job is used to run migration scripts every time the server is

deployed. More information regarding migration can be found in chapter 3.2.2.

Beside the ingress service, each component folder contains a ClusterIP service configura-

tion. Having a ClusterIP service for each component allows the components to have an

interface to communicate with each other inside the Kubernetes cluster. Having the ser-

vices defined is crucial for the setup of the whole deployment. Now we will go through

some deployment files to understand how they work together. Figure 65 shows the de-

ployment configuration for the MySQL database.

apiVersion: apps/v1

kind: Deployment

metadata:

name: mysql-ocpp-j-deployment

spec:

replicas: 1

selector:

matchLabels:

component: mysql

strategy:

Page 63: Developing virtual OCPP stations with Node.js & React in

59

type: Recreate

template:

metadata:

labels:

component: mysql

spec:

containers:

- name: mysql

image: mysql:8.0.22

args: ["--default-authentication-plugin=mysql_native_password"]

env:

- name: MYSQL_ROOT_PASSWORD

valueFrom:

secretKeyRef:

name: virtual-ocpp-j-mysql-secrets

key: ROOT_PASSWORD

- name: MYSQL_DATABASE

value: #hidden

- name: MYSQL_USER

value: #hidden

- name: MYSQL_PASSWORD

valueFrom:

secretKeyRef:

name: virtual-ocpp-j-mysql-secrets

key: USER_PASSWORD

ports:

- containerPort: 3306

name: mysql

volumeMounts:

- name: mysql-persistent-storage

mountPath: /var/lib/mysql

volumes:

- name: mysql-persistent-storage

persistentVolumeClaim:

claimName: mysql-persistent-volume-claim

Figure 65. Implementation of mysql-ocpp-j-deployment.yaml

The apiVersion specifies the version of the Kubernetes API that is used to communicate

with the Kubernetes server. The kind specifies the Kubernetes Object which is Deploy-

ment in this case. Kubernetes regularly check and make sure that the requirements speci-

fied in the Deployment are satisfied. For example, here we have the number of replicas as

“1” so Kubernetes will ensure that there will always be one set of containers later (as de-

fined right below) running all the time.

Next, we look at the spec where we define the container and volumes. The containers can

include one or more definition (in this case, only one container with image: mysql:8.0.22).

Page 64: Developing virtual OCPP stations with Node.js & React in

60

The environment variables are also defined here. We can see that the passwords are

fetched from Kubernetes secrets. They must be created before the containers can be suc-

cessfully deployed. To create the secrets, we run this command inside the cloud shell in-

side GCP:

kubectl create secret generic virtual-ocpp-j-mysql-secrets --from-literal

ROOT_PASSWORD=rootpw --from-literal USER_PASSWORD=userpw

Kubernetes will retrieve the secret values when it creates the pod. The last thing to look at

is the volumeMount as it tells MySQL to use the volume created from the file mysql-per-

sistent-volume-claim.yaml as its volume. By using this persistent volume, the data stays

secure even when the pod experiences issue and dies. Now let us look at how the server

deployment is configured in Figure 66.

apiVersion: apps/v1

kind: Deployment

metadata:

name: virtual-ocpp-j-server-deployment

spec:

replicas: 1

selector:

matchLabels:

component: virtual-ocpp-j-server-deployment

template:

metadata:

labels:

component: virtual-ocpp-j-server-deployment

spec:

containers:

- name: virtual-ocpp-j-server

image: eu.gcr.io/virtual-ocpp-j/virtual-ocpp-j-server:bb-{BIT-

BUCKET_BUILD_NUMBER}

ports:

- containerPort: 8080

env:

# other ENVs. hidden

- name: DB_HOST

value: mysql-cluster-ip-service

- name: DB_PASSWORD

valueFrom:

secretKeyRef:

name: virtual-ocpp-j-mysql-secrets

key: USER_PASSWORD

Figure 66. Implementation of virtual-ocpp-j-server-deployment.yaml

Page 65: Developing virtual OCPP stations with Node.js & React in

61

Many configurations are comparable with the previous MySQL deployment. Notable differ-

ences are in the image URL and the DB_HOST environment variable. About the image

URL, the URL is typical for pushing the image to Google Container Registry. Every time a

deployment happens, a Docker image of the application is built and pushed to GCR. Soon

after, when the new deployment configuration is applied, Kubernetes will pull that image to

run. The {BITBUCKET_BUILD_NUMBER} is a unique build number provided by bitbucket

pipeline and utilised here to ensure that the image tag is unique. Moving on to the

DB_HOST variable, the value here is the MySQL ClusterIP service that was mentioned

briefly above. This is one way that Kubernetes pods communicate with each other. Figure

67 below displays the UI’s deployment configuration.

apiVersion: apps/v1

kind: Deployment

metadata:

name: virtual-ocpp-j-server-ui-deployment

spec:

replicas: 1

selector:

matchLabels:

component: virtual-ocpp-j-server-ui

template:

metadata:

labels:

component: virtual-ocpp-j-server-ui

spec:

containers:

- name: virtual-ocpp-j-server-ui

image: eu.gcr.io/virtual-ocpp-j/virtual-ocpp-j-server-ui:bb-{BIT-

BUCKET_BUILD_NUMBER}

ports:

- containerPort: 3000

env:

- name: REACT_APP_SERVER_URL

value: "/api"

Figure 67. Implementation of virtual-ocpp-j-server-ui

Everything here is almost identical to the server’s deployment. It is good to note that the

REACT_APP_SERVER_URL env variable is “/api”. The simple URL means that if the UI

has a domain of http://virta-virtual-ocpp-j.com, the server’s URL is http://virta-virtual-ocpp-

j.com/api. Let us see how this is possible from Figure 68 which shows the ClusterIP ser-

vice for the UI and from Figure 69 which shows the NGINX Ingress Controller.

Page 66: Developing virtual OCPP stations with Node.js & React in

62

apiVersion: v1

kind: Service

metadata:

name: virtual-ocpp-j-server-ui-cluster-ip-service

spec:

type: ClusterIP

selector:

component: virtual-ocpp-j-server-ui

ports:

- port: 3000

targetPort: 3000

Figure 68. Implementation of UI’s ClusterIP service

In a ClusterIP configuration, the ports configuration must be configured correctly. port is

used to configure the port that the service wants to expose itself. targetPort is the port that

the traffic is routed to. For convenience, it is good to have the same port number for both

configurations.

apiVersion: networking.k8s.io/v1beta1

kind: Ingress

metadata:

name: ingress-service

annotations:

kubernetes.io/ingress.class: nginx

nginx.ingress.kubernetes.io/use-regex: "true"

nginx.ingress.kubernetes.io/rewrite-target: /$1

spec:

rules:

- http:

paths:

- path: /?(.*)

backend:

serviceName: virtual-ocpp-j-server-ui-cluster-ip-service

servicePort: 3000

- path: /api/?(.*)

backend:

serviceName: virtual-ocpp-j-server-cluster-ip-service

servicePort: 8080

Figure 69. Implementation of Ingress service

Ingress service is used to define the routing rules for the load balancer to know which traf-

fic should go where. Here, we define two paths for different services. The basic path “/” is

used for the UI, and the “/api” path is used for the server. The servicePort defined here

Page 67: Developing virtual OCPP stations with Node.js & React in

63

should be the same as the targetPort defined previously in the ClusterIP service. When-

ever the user goes to the main domain, the traffic is routed to the UI ClusterIP service,

which will serve the React application. The React application, as mentioned earlier, has a

server URL of “/api”. As a result, all the API calls of that React application will be routed to

server as defined here by the routing rules.

One manual thing that we must do is installing the NGINX Ingress Controller to the Kuber-

netes cluster. The installation is done by using Helm (the package manager for Kuber-

netes). We do that by going to the Cloud Shell inside GCP and run the following com-

mands based on the instructions:

helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx

helm install my-release ingress-nginx/ingress-nginx

The configurations as mentioned earlier are, however, just Kubernetes configuration. One

missing piece is still necessary for the whole deployment to be automated: CI pipeline. In

the next subchapter, we will go through the BitBucket pipeline that is used to ensure that

the deployment is automated.

3.3.6 Setting up BitBucket pipeline

In the pipeline we configured, there are two steps to be executed:

1. Automatic step to run coverage test for the virtual-ocpp-j-server 2. Manual step to deploy all components to Kubernetes

Let us look at the first step from a snippet of the BitBucket pipeline in Figure 70 below.

image: atlassian/default-image:2

options:

docker: true

pipelines:

default:

- step:

name: Run test for server

image: node:14.15

script:

- git submodule init

- git config submodule.virtual-ocpp-j-

server.url https://github.com/qhieu45/virtual-ocpp-j-server.git

- git config submodule.virtual-ocpp-j-server-

ui.url https://github.com/qhieu45/virtual-ocpp-j-server-ui.git

- git submodule update --init --recursive

- cd virtual-ocpp-j-server

- npm install

Page 68: Developing virtual OCPP stations with Node.js & React in

64

- npm run test:cov Figure 70. First step to run npm test for the server repository

The code above is extracted from the file bitbucket-pipelines.yml. BitBucket will execute

the pipeline from this file if the repository has been configured to run it. Docker is set to

true so that we can later build, tag, and push the image to GCR.

In the current step for running tests for server, we reconfigure the submodule to use

HTTPS instead of ssh so that the pipeline can clone the repository to its build environ-

ment. After cloning and initiate the submodules, the pipeline installs the dependencies and

run the test coverage. Figure 71 shows how the build looked when the test step was run

successfully.

Figure 71. Example of successful test build

From the figure above, it can also be seen that the next step is to build and push. As the

name suggested, it means that the Docker images are built, pushed, and deployed in this

step. Figure 72 shows the configuration for this step.

- step:

name: Build and Push

image: google/cloud-sdk

script:

# 1st part, authenticate to GCloud

- export CLOUDSDK_CORE_DISABLE_PROMPTS=1

- echo $GCLOUD_VIRTUAL_OCPPJ_K8S_API_KEY | base64 --decode --ignore-

garbage > ./gcloud-api-key.json

- apt-get install kubectl

- gcloud auth activate-service-account --key-file gcloud-api-key.json

Page 69: Developing virtual OCPP stations with Node.js & React in

65

- gcloud config set project virtual-ocpp-j

- gcloud config set compute/zone europe-north1-a

- gcloud container clusters get-credentials virtual-ocpp-j

# 2nd part, build Docker image and push to GCR

- git submodule init

- git config submodule.virtual-ocpp-j-

server.url https://github.com/qhieu45/virtual-ocpp-j-server.git

- git config submodule.virtual-ocpp-j-server-

ui.url https://github.com/qhieu45/virtual-ocpp-j-server-ui.git

- git submodule update --init --recursive

- gcloud auth configure-docker

- docker build -t virtual-ocpp-j-server:bb-$BITBUCKET_BUILD_NUMBER -

t eu.gcr.io/virtual-ocpp-j/virtual-ocpp-j-server:bb-$BITBUCKET_BUILD_NUM-

BER ./virtual-ocpp-j-server

- docker push eu.gcr.io/virtual-ocpp-j/virtual-ocpp-j-server:bb-$BIT-

BUCKET_BUILD_NUMBER

- docker build -t virtual-ocpp-j-server-ui:bb-$BITBUCKET_BUILD_NUM-

BER -t eu.gcr.io/virtual-ocpp-j/virtual-ocpp-j-server-ui:bb-$BIT-

BUCKET_BUILD_NUMBER ./virtual-ocpp-j-server-ui

- docker push eu.gcr.io/virtual-ocpp-j/virtual-ocpp-j-server-ui:bb-

$BITBUCKET_BUILD_NUMBER

# 3rd part, apply Kubernetes configuration

- kubectl apply -f infra/ingress

- kubectl apply -f infra/database

- sed -i "s/{BITBUCKET_BUILD_NUMBER}/$BITBUCKET_BUILD_NUMBER/g" in-

fra/virtual-ocpp-j-server/virtual-ocpp-j-server-deployment.yaml

- sed -i "s/{BITBUCKET_BUILD_NUMBER}/$BITBUCKET_BUILD_NUMBER/g" in-

fra/virtual-ocpp-j-server-ui/virtual-ocpp-j-server-ui-deployment.yaml

- sed -i "s/{BITBUCKET_BUILD_NUMBER}/$BITBUCKET_BUILD_NUMBER/g" in-

fra/migrations/migration-job.yaml

- kubectl apply -f infra/virtual-ocpp-j-server

- kubectl apply -f infra/virtual-ocpp-j-server-ui

- kubectl apply -f infra/migrations

- kubectl wait --for=condition=complete job/migration-$BIT-

BUCKET_BUILD_NUMBER

- kubectl delete -f infra/migrations

trigger: manual Figure 72. Build configuration for deployment

The first step is authenticating with gcloud CLI in the build environment. This is done by

decoding the secret stored in the BitBucket build environment (added earlier by the me,

not shown in here for security purpose). After being authenticated, the project, compute

zone & clusters is set.

The second step is building the image, tagging, and pushing the image to GCR. In this

step, we also need to initiate the submodules again as it is a different build environment.

Page 70: Developing virtual OCPP stations with Node.js & React in

66

Then we run the build script for that builds and tags each image with a suffix bb-$BIT-

BUCKET_BUILD_NUMBER. The $BITBUCKET_BUILD_NUMBER is a dynamic number

provided by BitBucket in each pipeline. We can see from figure 71 which shows that the

build number was 34. By using a dynamic number for the tag, we ensure that each pipe-

line has its own built images. That in turns makes rollback easy if it is needed. We then

push newly built images to GCR for them to be used by Kubernetes later.

The third step is applying the Kubernetes configuration. We apply the configuration in

each of the directories inside infra directory. Additionally, we can see a vital script that

uses “sed” to modify the configuration file use the latest image tag dynamically. The last

step is checking that the migration is run correctly and the pod for migration is deleted.

Kubernetes will make sure that conditions always match the configuration files.

The trigger is set to manual so that deployment is only run when someone presses the

button “Run” in the build pipeline (figure 71). We do not want a new deployment every

time something new is merged to the master branch. Figure 73 shows a successful de-

ployment after we clicked “Run” in the same build pipeline.

Figure 73. Successful Deployment

That is the end of the deployment process as well as the product chapter. In the next

chapter, I discuss the problems I encountered, the pros & cons of the product as well as

what I have learned during the process.

Page 71: Developing virtual OCPP stations with Node.js & React in

67

4 Discussion

As mentioned earlier, in this chapter I go through:

- the encountered problems during the process of writing the thesis - the pros and cons of the products in the future - the lessons that I have learned during the writing of this thesis.

4.1 Problems encountered

The biggest issue that I encountered was the scope of the project. The project’s scope

turns out to be bigger than expected. I resolved this by redefining the essentials function-

alities to be included and draft plans for future improvement.

The second big issue that I encountered is that I was not so familiar with React, so it took

a bit longer than expected to finish the UI development.

The last big issue is that I have not done a full deployment pipeline before. Therefore, I

had to study from scratch Docker & Kubernetes deployment as well as CI pipeline inside

Bitbucket itself. It took more time than I expected due to the complexity of the technology.

4.2 Pros and cons of the product

This subchapter describes what I think about the pros and cons of the developed product.

4.2.1 Pros

In my opinion, the product was written cleanly with more than 90% coverage unit test for

the critical product: the server. Additionally, everything was written in TypeScript so that

the readability and maintainability are high.

The deployment was automated completely with Kubernetes and BitBucket pipeline. The

automation allows us to keep deployment transparent by having everything recorded in

git. Scaling can be done easily with GCP and its Google Kubernetes Engine. Security is

one of the strong points of this deployment as no one can access the database even if

they know the username and password. A person needs to have access to the Kuber-

netes cluster itself to be able to access it.

Page 72: Developing virtual OCPP stations with Node.js & React in

68

On the practical level, the product has already been put into use in our test environment.

QA pipeline has already been added for the new virtual station. As a result, our central

system service has a regular scheduled end-to-end test which will bring monitoring ad-

vantages in the future.

4.2.2 Cons

The product does not yet support all OCPP messages as that would require a lot more

work. We are discussing internally on what other messages to prioritise our resources on.

Some messages are rarely used by the station, and it may not be worth it to have it in this

product.

Another con is the lack of test in the UI. There is nothing wrong with the application now.

However, should we decide to add more features in the future, maintaining might take

more time due to lack of tests.

Additionally, scaling by increasing the replicas of back-end does not work well due to the

nature of the application of opening WebSocket connections for all stations. We have not

thought about how to scale this properly by increasing replica number. However, due to

the nature of the application, we do not expect to have to scale the applications any time

soon. For now, the scaling relies solely on increasing CPU & memory capacities.

4.3 Lessons learned

I have learned a great deal of new and exciting technologies during the development of

this product. At first, I thought that I am already somewhat familiar with the related tech-

nologies. However, a lot of things require in-depth knowledge to be done right for a pro-

duction-ready product.

Designing the architecture for a Node.js server is something new for me. I have learned

how to effectively organise the directory structure as well as the design pattern of Type-

Script. Furthermore, I have improved my skills in writing unit tests with Jest.

I am also glad that I had this opportunity to learn about Docker & Kubernetes deployment.

I have used Docker before, but I have broadened my knowledge on the topic to a greater

extend. Kubernetes is something that has on my mind for a while, and this product was a

Page 73: Developing virtual OCPP stations with Node.js & React in

69

good motivation boost and practice ground. There are few users so the tolerance for mis-

take is higher than our actively used applications at the company.

On a personal level, I have also learned to be self-disciplined to finish the project on-time.

The time constraint was extremely exact so there was little room for mistakes. I do have to

compensate for some setbacks with more personal input time on the project. However, on

the project-management perspective, I have learned how to divide the tasks into smaller

and manageable chunks that I can finish on-time.

Page 74: Developing virtual OCPP stations with Node.js & React in

70

References

Boduch, A. 2019. React Material-UI Cookbook. Packt Publishing. Birmingham.

Borrelli, P. 2019. A complete guide to the Node.js event loop. URL:

https://blog.logrocket.com/a-complete-guide-to-the-node-js-event-loop. Accessed: 16 Sep-

tember 2020.

Cherny, B. 2019. Programming TypeScript. O’Reilly Media, Inc. Sebastopol.

Chopra, V. 2015. WebSocket Essentials – Building Apps with HTML5 WebSockets. Packt

Publishing. Birmingham.

Crossbar.io Technologies GmbH. 2020. Message Routing in WAMP. URL: https://wamp-

proto.org/routing.html. Accessed: 10 September 2020.

Facebook Inc. 2020a. Components and Props. URL: https://reactjs.org/docs/components-

and-props.html. Accessed: 21 September 2020.

Facebook Inc. 2020b. Context. URL: https://reactjs.org/docs/context.html. Accessed: 09

November 2020.

Facebook Inc. 2020c. Create a New React App. URL: https://reactjs.org/docs/create-a-

new-react-app.html. Accessed: 10 November 2020.

Facebook Inc. 2020d. Hooks Intro. URL: https://reactjs.org/docs/hooks-intro.html. Ac-

cessed: 09 November 2020.

Facebook Inc. 2020e. Introducing JSX. URL: https://reactjs.org/docs/introducing-jsx.html.

Accessed: 21 September 2020.

Facebook Inc. 2020f. Tutorial. URL: https://reactjs.org/tutorial/tutorial.html. Accessed: 21

September 2020.

Fong, J. 2018. Are Containers Replacing Virtual Machines? URL:

https://www.docker.com/blog/containers-replacing-virtual-machines. Accessed: 21 No-

vember 2020.

Page 75: Developing virtual OCPP stations with Node.js & React in

71

Freeman, A. 2019. Essential TypeScript: From Beginner to Pro. Apress. Berkeley.

Fulton III, S. 2019. What Google Cloud Platform is and why you’d use it. URL:

https://www.zdnet.com/article/what-google-cloud-platform-is-and-why-youd-use-it. Ac-

cessed: 21 November 2020.

Haverbeke, M. 2011. Eloquent JavaScript. No Starch Press. San Francisco.

Krief, M. 2019. Learning DevOps. Packt Publishing. Birmingham.

Lombardi, A. 2015. WebSocket. O’Reilly Media, Inc. Sebastopol.

Meck, B., Young, A. & Cantelon, M. 2017. Node.js in Action. 2nd ed. Manning Publica-

tions Co. Shelter Island.

Microsoft 2020. What is TypeScript? URL: https://www.typescriptlang.org. Accessed: 08

September 2020.

Mozilla 2020a. An overview of HTTP. URL: https://developer.mozilla.org/en-

US/docs/Web/HTTP/Overview. Accessed: 07 September 2020.

Mozilla 2020b. JavaScript. URL: https://developer.mozilla.org/en-US/docs/Web/JavaS-

cript. Accessed: 08 September 2020.

Mozilla 2020c. The WebSocket API (WebSockets). URL: https://developer.mozilla.org/en-

US/docs/Web/API/WebSockets_API. Accessed: 07 September 2020.

Mysliwiec, K. 2020. Introduction. URL: https://docs.nestjs.com. Accessed: 16 September

2020.

npm, Inc . 2020. About npm. URL: https://docs.npmjs.com/about-npm. Accessed: 16 Sep-

tember 2020.

OCA 2020. Overview: OCPP versions. URL: https://www.openchargealliance.org/up-

loads/files/OCA-Overview-OCPP-versions.pdf. Accessed: 06 September 2020.

OpenJS Foundation. About Node.js. URL: https://nodejs.org/en/about/. Accessed: 11 Sep-

tember 2020.

Page 76: Developing virtual OCPP stations with Node.js & React in

72

Patel, P. 2018. What exactly is Node.js? URL: https://www.freecodecamp.org/news/what-

exactly-is-node-js-ae36e97449f5. Accessed: 11 September 2020.

The Kubernetes Authors 2020a. URL: https://kubernetes.io. Accessed: 21 November

2020.

The Kubernetes Authors 2020b. Kubernetes Components. URL: https://kuber-

netes.io/docs/concepts/overview/components. Accessed: 21 November 2020.

The Kubernetes Authors 2020c. Services, Load Balancing, and Networking. URL:

https://kubernetes.io/docs/concepts/services-networking. Accessed: 21 November 2020.

The Kubernetes Authors 2020d. Understanding Kubernetes Objects. URL: https://kuber-

netes.io/docs/concepts/overview/working-with-objects/kubernetes-objects. Accessed: 21

November 2020.

Virta Ltd. 2020. What is OCPP. URL: https://support.virta.global/support/solutions/arti-

cles/44001800358-what-is-ocpp. Accessed: 06 September 2020.

Wang, V., Moskovits, P., Pye, T. & Salim, F. 2013. The Definitive Guide to HTML5 Web-

Socket. Apress.

Wieruch, R. 2020. The road to React. Leanpub.

Page 77: Developing virtual OCPP stations with Node.js & React in

73

Tables of figures

Figure 1. EV charging ecosystem (Virta Ltd 2020) ............................................................. 4

Figure 2. No error is shown in JavaScript (adapted from Cherny 2019, Chapter 2) ............ 8

Figure 3. Error is shown in TypeScript (adapted from Cherny 2019, Chapter 2)................. 8

Figure 4. A graphical explanation of the event loop (Borrelli 2019) .................................... 9

Figure 5. Example of module structure in NestJS ............................................................ 11

Figure 6. Controller example in NestJS ............................................................................ 11

Figure 7. Example of if clause with JSX (Facebook Inc 2020e) ........................................ 12

Figure 8. Example of useState (Facebook Inc 2020d) ..................................................... 13

Figure 9. Example of useEffect ........................................................................................ 14

Figure 10. Updating component’s count with useReducer (Facebook Inc 2020d) ............ 15

Figure 11. Consuming the context value (Facebook Inc 2020b) ....................................... 16

Figure 12. Example of Dockerfile ..................................................................................... 17

Figure 13. Components of Kubernetes (The Kubernetes Authors 2020b) ........................ 18

Figure 14. Example of Deployment object (The Kubernetes Authors 2020d) ................... 19

Figure 15. Example of Service ......................................................................................... 20

Figure 16. Link between Ingress and Service (The Kubernetes Authors 2020c) .............. 20

Figure 17. Mock-up of the application’s home page ......................................................... 23

Figure 18. Implementations of SideContainer .................................................................. 24

Figure 19. Using useStyles for styling .............................................................................. 24

Figure 20. Script for creating seed data ........................................................................... 25

Figure 21. Using useEffect for fetching stations ............................................................... 26

Figure 22. Rendering list of stations with Array.map ........................................................ 26

Figure 23. Home page of the application ......................................................................... 27

Figure 24. Example of an API request action within context provider component............. 28

Figure 25. Return value of StationContextProvider .......................................................... 28

Figure 26. Wrapping Home component inside StationContextProvider ............................ 29

Figure 27. Accessing context value with useContext hook ............................................... 29

Figure 28. Station information acquired from StationContext ........................................... 30

Figure 29. Middleware to add delay of 1000ms to our response ...................................... 30

Figure 30. Initiate state for OperationContext ................................................................... 31

Figure 31. Return value of OperationContextProvider ...................................................... 31

Figure 32. Rendering operation buttons ........................................................................... 32

Figure 33. Dynamic rendering of DialogContent with switch statement ............................ 32

Figure 34. Dynamic text change event to be bound to input element ............................... 33

Figure 35. Dockerfile for the application ........................................................................... 34

Figure 36. typeOrmConfig of the application .................................................................... 35

Page 78: Developing virtual OCPP stations with Node.js & React in

74

Figure 37. Migration command and success logs ............................................................ 36

Figure 38. Station entity definition .................................................................................... 37

Figure 39. Adding POST endpoint ................................................................................... 37

Figure 40. Method createStation inside stationsService ................................................... 37

Figure 41. createStation method inside stationRepository ............................................... 38

Figure 42. Testing POST /stations with Insomnia............................................................. 38

Figure 43. Test coverage of station module ..................................................................... 39

Figure 44. StationWebSocketClient class ........................................................................ 40

Figure 45. Creating new WS connection .......................................................................... 41

Figure 46. Implementation of onConnectionOpen ............................................................ 42

Figure 47. Implementation of onConnectionClosed .......................................................... 42

Figure 48. Example of unit test for onConnectionClosed .................................................. 43

Figure 49. Methods to establish connections for station ................................................... 44

Figure 50. Connect stations to Central System Service every five minutes ...................... 44

Figure 51. ByChargePointRequestBuilderInterface and example of its usage .................. 45

Figure 52. Endpoint definition in StationController class .................................................. 46

Figure 53. Sending an OCPP request to Central System ................................................. 47

Figure 54. Preparing message and sending the message to CS ...................................... 48

Figure 55. Implementation of waitForMessasge ............................................................... 49

Figure 56. ByChargePointOperationMessageGenerator class ......................................... 49

Figure 57. Message module’s folder content .................................................................... 50

Figure 58. Processing message coming from the CS ...................................................... 51

Figure 59. Implementation of processCallResultMsgFromCS .......................................... 52

Figure 60. Processing CallMessage from CS ................................................................... 53

Figure 61. Dockerfile for virtual-ocpp-j-server-ui .............................................................. 55

Figure 62. Diagram of all components within Kubernetes cluster ..................................... 56

Figure 63. Service Account bitbucket-k8s-deploy created and roles added...................... 57

Figure 64. Infra folder ...................................................................................................... 58

Figure 65. Implementation of mysql-ocpp-j-deployment.yaml .......................................... 59

Figure 66. Implementation of virtual-ocpp-j-server-deployment.yaml ............................... 60

Figure 67. Implementation of virtual-ocpp-j-server-ui ....................................................... 61

Figure 68. Implementation of UI’s ClusterIP service ......................................................... 62

Figure 69. Implementation of Ingress service ................................................................... 62

Figure 70. First step to run npm test for the server repository .......................................... 64

Figure 71. Example of successful test build ..................................................................... 64

Figure 72. Build configuration for deployment .................................................................. 65

Figure 73. Successful Deployment ..................................... Error! Bookmark not defined.

Page 79: Developing virtual OCPP stations with Node.js & React in

75

Appendices

Appendix 1. Complete code of StationContext

import React, { createContext, useReducer } from 'react';

import { Station } from '../model/Station';

import {

StationContextState,

StationContextType,

StationReducerAction,

Actions,

} from './StationContextTypes';

const initialState: StationContextState = {

stations: [],

selectedStation: null,

error: '',

};

const StationContext = createContext<StationContextType>({

state: initialState,

selectStation: () => {},

getStations: () => Promise.resolve([]),

});

const reducer = (state: StationContextState, action: StationRe-

ducerAction) => {

switch (action.type) {

case Actions.SELECT_STATION:

return { ...state, selectedStation: action.payload.station, er-

ror: '' };

case Actions.GET_STATIONS:

return { ...state, stations: action.payload.stations, error: '' };

case Actions.REQUEST_ERROR:

return { ...state, error: action.payload.error };

default:

return state;

}

};

const StationContextProvider: React.FunctionComponent = ({ children }) => {

const [state, dispatch] = useReducer(reducer, initialState);

const dispatchRequestError = (message: string) => {

dispatch({

type: Actions.REQUEST_ERROR,

payload: {

error: message,

},

Page 80: Developing virtual OCPP stations with Node.js & React in

76

});

};

const selectStation = async (id: number) => {

console.log('fetching station info');

try {

const data = await fetch(

`${process.env.REACT_APP_SERVER_URL}/stations/${id}`

);

if (!data.ok) {

return dispatchRequestError(

`Error fetching station (id: ${id}) info (${data.statusText})`

);

}

const station = await data.json();

dispatch({

type: Actions.SELECT_STATION,

payload: { station },

});

} catch (error) {

dispatchRequestError(

`Error fetching station (id: ${id}) info (${error.message})`

);

}

};

const getStations = async (): Promise<Station[]> => {

try {

const data = await fetch(`${process.env.REACT_APP_SERVER_URL}/sta-

tions`);

if (!data.ok) {

dispatchRequestError(

`Error fetching all stations (${data.statusText})`

);

return [];

}

const stations = await data.json();

dispatch({

type: Actions.GET_STATIONS,

payload: { stations },

});

return stations;

} catch (error) {

Page 81: Developing virtual OCPP stations with Node.js & React in

77

dispatchRequestError(`Error fetching all stations (${error.mes-

sage})`);

return [];

}

};

return (

<StationContext.Provider value={{ state, selectStation, getStations }}>

{children}

</StationContext.Provider>

);

};

export { StationContextProvider, StationContext };

Page 82: Developing virtual OCPP stations with Node.js & React in

78

Appendix 2. The user interface flow for request a StartTransaction to the server

Dialog pops up after clicking on StartTransaction

Dialog is closed after filling in idTag and clicking SEND button. Result of API request is

shown at the bottom

Page 83: Developing virtual OCPP stations with Node.js & React in

79

Page 84: Developing virtual OCPP stations with Node.js & React in

80

Appendix 3. Complete set-up of server with docker-compose.yml for virtual-ocpp-j-

server

version: "3.8"

services:

ocppj-virtual-server:

build:

context: ./

dockerfile: Dockerfile

image: ocppj-virtual-server

container_name: ocppj-virtual-server

restart: unless-stopped

env_file: .env

environment:

DB_HOST: db

DB_PORT: $DB_PORT

DB_USER: $DB_USER

DB_DATABASE: $DB_DATABASE

DB_PASSWORD: $DB_PASSWORD

APP_PORT: $APP_PORT

ports:

- 8080:8080

- 9229:9229

volumes:

- ./src:/app/src

- node_modules:/app/node_modules

networks:

- ocppj-virtual-network

command: './wait-for.sh db:3306 -- npm run start:debug-migrate'

db:

image: mysql:8.0.22

container_name: db

restart: unless-stopped

env_file: .env

command: --default-authentication-plugin=mysql_native_password --charac-

ter-set-server=utf8 --collation-server=utf8_general_ci

ports:

- 3308:3306

volumes:

- db_data:/var/lib/mysql

environment:

MYSQL_ROOT_PASSWORD: $DB_ROOT_PASSWORD

MYSQL_DATABASE: $DB_DATABASE

MYSQL_USER: $DB_USER

MYSQL_PASSWORD: $DB_PASSWORD

networks:

- ocppj-virtual-network

Page 85: Developing virtual OCPP stations with Node.js & React in

81

networks:

ocppj-virtual-network:

driver: bridge

volumes:

db_data:

node_modules:

Page 86: Developing virtual OCPP stations with Node.js & React in

82

Appendix 4. StationTable migration file

import { MigrationInterface, QueryRunner, Table } from 'typeorm';

export class StationTable1602856827676 implements MigrationInterface {

public async up(queryRunner: QueryRunner): Promise<void> {

await queryRunner.createTable(

new Table({

name: 'station',

columns: [

{

name: 'id',

type: 'int',

isPrimary: true,

isGenerated: true,

generationStrategy: 'increment',

},

{

name: 'identity',

type: 'varchar',

isNullable: false,

},

{

name: 'vendor',

type: 'varchar(20)',

isNullable: false,

default: `'${process.env.DEFAULT_VENDOR}'`,

},

{

name: 'model',

type: 'varchar(20)',

isNullable: false,

default: `'${process.env.DEFAULT_MODEL}'`,

},

{

name: 'centralSystemUrl',

type: 'varchar',

isNullable: false,

},

{

name: 'meterValue',

type: 'bigint',

unsigned: true,

isNullable: false,

default: 0,

},

{

name: 'chargeInProgress',

type: 'boolean',

isNullable: false,

Page 87: Developing virtual OCPP stations with Node.js & React in

83

default: false,

},

{

name: 'currentTransactionId',

type: 'int',

isNullable: true,

},

{

name: 'currentChargingPower',

type: 'int',

unsigned: true,

isNullable: false,

default: 11000,

},

{

name: 'createdAt',

type: 'timestamp',

isNullable: false,

default: 'CURRENT_TIMESTAMP',

},

{

name: 'updatedAt',

type: 'timestamp',

isNullable: false,

default: 'CURRENT_TIMESTAMP',

onUpdate: 'CURRENT_TIMESTAMP',

},

],

}),

);

}

public async down(queryRunner: QueryRunner): Promise<void> {

queryRunner.query('DROP TABLE station;');

}

}

Page 88: Developing virtual OCPP stations with Node.js & React in

84

Appendix 5. docker-compose.yml file for both projects in Virta’s internal repository

version: "3.8"

services:

ocppj-virtual-server:

build:

context: ./virtual-ocpp-j-server

dockerfile: Dockerfile

target: base

image: ocppj-virtual-server

container_name: ocppj-virtual-server

restart: unless-stopped

env_file: .env

environment:

DB_HOST: db

DB_PORT: $DB_PORT

DB_USER: $DB_USER

DB_DATABASE: $DB_DATABASE

DB_PASSWORD: $DB_PASSWORD

APP_PORT: $APP_PORT

ports:

- 8080:8080

- 9229:9229

volumes:

- ./virtual-ocpp-j-server:/app

- node_modules:/app/node_modules

networks:

- ocppj-virtual-network

command: "./wait-for.sh db:3306 -- npm run start:debug-migrate"

db:

image: mysql:8.0.22

container_name: db

restart: unless-stopped

env_file: .env

command: --default-authentication-plugin=mysql_native_password --charac-

ter-set-server=utf8 --collation-server=utf8_general_ci

ports:

- 3308:3306

volumes:

- db_data:/var/lib/mysql

environment:

MYSQL_ROOT_PASSWORD: $DB_ROOT_PASSWORD

MYSQL_DATABASE: $DB_DATABASE

MYSQL_USER: $DB_USER

MYSQL_PASSWORD: $DB_PASSWORD

networks:

- ocppj-virtual-network

virtual-ocpp-j-ui:

Page 89: Developing virtual OCPP stations with Node.js & React in

85

container_name: virtual-ocpp-j-ui

build:

context: ./virtual-ocpp-j-server-ui

dockerfile: Dockerfile

target: base

env_file: .env

depends_on:

- ocppj-virtual-server

environment:

REACT_APP_SERVER_URL: $REACT_APP_SERVER_URL

ports:

- "3000:3000"

volumes:

- /app/node_modules

- ./virtual-ocpp-j-server-ui:/app

command: "npm run start"

stdin_open: true

networks:

ocppj-virtual-network:

driver: bridge

volumes:

db_data:

node_modules: