├── device-service ├── http │ ├── .swagger-codegen │ │ └── VERSION │ ├── go │ │ ├── README.md │ │ ├── logger.go │ │ ├── api_delete_charging_station.go │ │ ├── api_get_charging_station.go │ │ ├── api_list_charging_stations.go │ │ ├── api_create_charging_station.go │ │ ├── api_update_charging_station.go │ │ └── routers.go │ └── api │ │ ├── swagger.yaml │ │ └── index.html ├── README.md ├── ocpphandlers │ ├── HeartbeatRequestHandler.go │ ├── StatusNotificationHandler.go │ └── BootNotificationHandler.go ├── publishing │ └── MessagePublisher.go ├── main.go ├── db │ └── chargingstations.go └── subscribing │ └── MessageSubscriber.go ├── frontend-service ├── src │ ├── react-app-env.d.ts │ ├── chargetokens │ │ ├── constants │ │ │ ├── TokenType.ts │ │ │ └── TokenStatus.ts │ │ ├── util-components │ │ │ └── Item.tsx │ │ └── ChargeTokenScreen.tsx │ ├── dashboard │ │ ├── typedefs │ │ │ ├── Menu.d.ts │ │ │ └── MenuItem.d.ts │ │ ├── assets │ │ │ └── logo512.png │ │ ├── util-components │ │ │ ├── Copyright.tsx │ │ │ ├── UserPanel.tsx │ │ │ ├── UserInfo.tsx │ │ │ ├── Header.tsx │ │ │ └── Navigator.tsx │ │ ├── constants │ │ │ └── dashboardMenu.tsx │ │ └── DashboardScreen.tsx │ ├── stations │ │ ├── typedefs │ │ │ ├── ChargingStationModem.d.ts │ │ │ ├── ChargingStationLocation.d.ts │ │ │ └── ChargingStation.d.ts │ │ ├── StationsScreen.tsx │ │ └── util-components │ │ │ └── Item.tsx │ ├── app │ │ ├── apis │ │ │ └── firebase │ │ │ │ ├── MTFirestore.ts │ │ │ │ ├── AppPermissionException.ts │ │ │ │ ├── dbUtils.ts │ │ │ │ ├── Firebase.tsx │ │ │ │ └── dbFunctions.ts │ │ ├── App.test.tsx │ │ ├── contexts │ │ │ ├── Firebase.tsx │ │ │ ├── Route.tsx │ │ │ ├── SelectedMenuItem.tsx │ │ │ └── FirebaseData.tsx │ │ ├── routing │ │ │ ├── ScrollToTop.tsx │ │ │ ├── GeneralRouteTreeMT.tsx │ │ │ └── appRoutes.ts │ │ ├── theme │ │ │ ├── dimensionHelpers.ts │ │ │ └── AppTheme.tsx │ │ └── App.tsx │ ├── transactions │ │ ├── typedefs │ │ │ └── Transaction.d.ts │ │ ├── TransactionsScreen.tsx │ │ └── util-components │ │ │ └── Item.tsx │ ├── index.css │ ├── index.tsx │ ├── logo.svg │ └── login │ │ └── Login.tsx ├── .dockerignore ├── public │ ├── robots.txt │ ├── logo192.png │ ├── manifest.json │ ├── 404.html │ └── index.html ├── .npmrc ├── README.md ├── Dockerfile ├── tsconfig.json └── package.json ├── user-service ├── README.md ├── publishing │ └── MessagePublisher.go ├── main.go ├── ocpphandlers │ └── AuthorizeHandler.go ├── subscribing │ └── MessageSubscriber.go └── db │ └── IdTokens.go ├── transaction-service ├── README.md ├── main.go ├── publishing │ └── MessagePublisher.go ├── _example-messages │ ├── TransactionEventRequest_stop.json │ ├── TransactionEventRequest_start.json │ ├── TransactionEventRequest_update_1.json │ ├── TransactionEventRequest_update_2.json │ └── TransactionEventRequest_update_3.json ├── subscribing │ └── MessageSubscriber.go ├── db │ ├── Transactions.go │ └── Transaction.go └── ocpphandlers │ └── TransactionEventHandler.go ├── websocket-service ├── README.md ├── websocketserver │ ├── IndexHandler.go │ ├── ChargingStationConnection.go │ ├── server.go │ └── ChargingStationHandler.go ├── publishing │ └── MessagePublisher.go ├── main.go ├── authentication │ └── ChargingStationAuthenticator.go ├── subscribing │ └── MessageSubscriber.go └── messagemux │ └── ProcessAndPublish.go ├── .gitignore ├── Dockerfile ├── LICENSE ├── go.mod ├── docker-compose.yml └── README.md /device-service/http/.swagger-codegen/VERSION: -------------------------------------------------------------------------------- 1 | 3.0.35 -------------------------------------------------------------------------------- /frontend-service/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /frontend-service/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | Dockerfile 3 | .git 4 | .gitignore 5 | .dockerignore. 6 | .env -------------------------------------------------------------------------------- /frontend-service/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend-service/.npmrc: -------------------------------------------------------------------------------- 1 | #in .npmrc 2 | #script-shell = "C:\\windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" -------------------------------------------------------------------------------- /frontend-service/src/chargetokens/constants/TokenType.ts: -------------------------------------------------------------------------------- 1 | export enum TokenType { 2 | ISO14443 = "ISO14443", 3 | } -------------------------------------------------------------------------------- /frontend-service/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregszalay/ocpp-csms/HEAD/frontend-service/public/logo192.png -------------------------------------------------------------------------------- /frontend-service/src/dashboard/typedefs/Menu.d.ts: -------------------------------------------------------------------------------- 1 | type Menu = { 2 | headerId: string; 3 | children: MenuItem[]; 4 | }; 5 | -------------------------------------------------------------------------------- /frontend-service/src/stations/typedefs/ChargingStationModem.d.ts: -------------------------------------------------------------------------------- 1 | export type ChargingStationModem = { 2 | iccid: string; 3 | imsi: string; 4 | }; 5 | -------------------------------------------------------------------------------- /frontend-service/src/dashboard/assets/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregszalay/ocpp-csms/HEAD/frontend-service/src/dashboard/assets/logo512.png -------------------------------------------------------------------------------- /frontend-service/src/stations/typedefs/ChargingStationLocation.d.ts: -------------------------------------------------------------------------------- 1 | export type ChargingStationLocation = { 2 | lat: number; 3 | lng: number; 4 | }; 5 | -------------------------------------------------------------------------------- /frontend-service/README.md: -------------------------------------------------------------------------------- 1 | # OCPP Charging Station Management System Frontend 2 | 3 | This is currently under development. 4 | 5 | No live version available yet. -------------------------------------------------------------------------------- /frontend-service/src/dashboard/typedefs/MenuItem.d.ts: -------------------------------------------------------------------------------- 1 | type MenuItem = { 2 | id: any, 3 | label: string, 4 | icon: any, 5 | route: string, 6 | }; -------------------------------------------------------------------------------- /frontend-service/src/chargetokens/constants/TokenStatus.ts: -------------------------------------------------------------------------------- 1 | export enum TokenStatus { 2 | Accepted = "Accepted", 3 | Invalid = "Invalid", 4 | Expired = "Expired", 5 | } -------------------------------------------------------------------------------- /frontend-service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:19-alpine3.15 2 | 3 | WORKDIR /app 4 | COPY package.json . 5 | RUN apk add --no-cache git 6 | RUN npm install 7 | COPY . . 8 | EXPOSE 8085 9 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /frontend-service/src/app/apis/firebase/MTFirestore.ts: -------------------------------------------------------------------------------- 1 | export enum ExistingFirestoreCollection { 2 | idTokens = "idTokens", 3 | chargingstations = "chargingstations", 4 | transactions = "transactions", 5 | } 6 | -------------------------------------------------------------------------------- /user-service/README.md: -------------------------------------------------------------------------------- 1 | 2 | # User Management Service 3 | ### Main responsibilities of the Service 4 | 5 | - via Google Pub/sub messages: 6 | - subscribe to user-related topics (e.g. AuthorizeRequest) and publish responses (e.g. AuthorizeResponse) 7 | -------------------------------------------------------------------------------- /transaction-service/README.md: -------------------------------------------------------------------------------- 1 | 2 | # User Management Service 3 | ### Main responsibilities of the Service 4 | 5 | - via Google Pub/sub messages: 6 | - subscribe to user-related topics (e.g. AuthorizeRequest) and publish responses (e.g. AuthorizeResponse) 7 | -------------------------------------------------------------------------------- /frontend-service/src/stations/typedefs/ChargingStation.d.ts: -------------------------------------------------------------------------------- 1 | export type ChargingStation = { 2 | id: string; 3 | serialNumber: string; 4 | model: string; 5 | vendorName: string; 6 | firmwareVersion: string; 7 | modem: ChargingStationModem; 8 | location: ChargingStationLocation; 9 | lastBoot: string; 10 | }; 11 | -------------------------------------------------------------------------------- /frontend-service/src/app/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /frontend-service/src/transactions/typedefs/Transaction.d.ts: -------------------------------------------------------------------------------- 1 | import { MeterValueType } from "ocpp-messages-ts/types/TransactionEventRequest"; 2 | 3 | export type Transaction = { 4 | stationId: string; 5 | energyTransferInProgress: bool; 6 | energyTransferStarted: string; 7 | energyTransferStopped: string; 8 | meterValues: MeterValueType[]; 9 | }; 10 | -------------------------------------------------------------------------------- /frontend-service/src/app/apis/firebase/AppPermissionException.ts: -------------------------------------------------------------------------------- 1 | export default class AppPermissionException { 2 | readonly message: string; 3 | readonly name = "AppPermissionException"; 4 | 5 | constructor(message: string) { 6 | this.message = message; 7 | } 8 | 9 | toString = () => { 10 | return `${this.name}: "${this.message}"`; 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /device-service/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Device Management Service 3 | ### Main responsibilities of the Service 4 | 5 | - via REST API (see REST_API/ folder) 6 | - Authenticate charging stations (verify that the charging station is in the db) 7 | - via Google Pub/sub messages: 8 | - subscribe to device-related topics (e.g. BootNotificationRequest) and publish responses (e.g. BootNotificationResponse) 9 | -------------------------------------------------------------------------------- /websocket-service/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Websocket Service 4 | ### Main responsibilities of the Service 5 | 6 | - via Websockets 7 | - accept & validate device connections 8 | - via Google Pub/sub messages: 9 | - publish all incoming WS OCPP messages to relevant topics (e.g. HeartbeatRequest) 10 | - subscibe to outgoing WS OCPP message topics and send them to the connected devices -------------------------------------------------------------------------------- /websocket-service/websocketserver/IndexHandler.go: -------------------------------------------------------------------------------- 1 | package websocketserver 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | func Index(w http.ResponseWriter, r *http.Request) { 11 | device_endpoint := "ws://" + "" + "/ocpp/CHARGER_ID" 12 | fmt.Fprintf(w, "Please use the "+device_endpoint+" URL to connect your device.") 13 | log.Info("Incoming request to index page") 14 | } 15 | -------------------------------------------------------------------------------- /frontend-service/src/app/contexts/Firebase.tsx: -------------------------------------------------------------------------------- 1 | import React, { Context, ReactElement } from 'react'; 2 | 3 | const FirebaseContext: Context = React.createContext(null); 4 | 5 | export const withFirebaseContext = (MyComponent: ((props:any)=>JSX.Element), props:any) => ( 6 | 7 | {(firebase) => } 8 | 9 | ); 10 | 11 | 12 | export default FirebaseContext; -------------------------------------------------------------------------------- /frontend-service/src/app/routing/ScrollToTop.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | 3 | export default function ScrollToTop() { 4 | const myRef = useRef(null); 5 | 6 | useEffect(() => { 7 | const timeOutID = setTimeout(() => { 8 | if (myRef && myRef.current) { 9 | myRef.current.scrollIntoView(); 10 | } 11 | }, 0); 12 | return () => clearTimeout(timeOutID); 13 | }, [myRef]); 14 | 15 | return
; 16 | } 17 | -------------------------------------------------------------------------------- /frontend-service/src/app/contexts/Route.tsx: -------------------------------------------------------------------------------- 1 | import React, { Context, ReactElement } from 'react'; 2 | import { JsxElement } from 'typescript'; 3 | 4 | const RouteContext: Context = React.createContext(null); 5 | 6 | export const withRouteContext = (MyComponent: ((props:any)=>JSX.Element), props:any) => ( 7 | 8 | {routeMap => } 9 | 10 | ); 11 | 12 | 13 | export default RouteContext; -------------------------------------------------------------------------------- /frontend-service/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | overflow: hidden; 9 | } 10 | 11 | html{ 12 | overflow: hidden; 13 | } 14 | 15 | code { 16 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 17 | monospace; 18 | } 19 | -------------------------------------------------------------------------------- /frontend-service/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000" 24 | } 25 | -------------------------------------------------------------------------------- /frontend-service/src/app/contexts/SelectedMenuItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { Context, ReactElement } from 'react'; 2 | import { JsxElement } from 'typescript'; 3 | 4 | const SelectedMenuItemContext: Context = React.createContext(null); 5 | 6 | export const withSelectedMenuItemContext = (MyComponent: ((props:any)=>JSX.Element), props:any) => ( 7 | 8 | {({selectedMenuItem, handleMenuSelection}) => } 10 | 11 | ); 12 | 13 | 14 | export default SelectedMenuItemContext; -------------------------------------------------------------------------------- /device-service/http/go/README.md: -------------------------------------------------------------------------------- 1 | # Go API Server for swagger 2 | 3 | REST API to manage OCPP devices (e.g. Charging Stations) 4 | 5 | ## Overview 6 | This server was generated by the [swagger-codegen] 7 | (https://github.com/swagger-api/swagger-codegen) project. 8 | By using the [OpenAPI-Spec](https://github.com/OAI/OpenAPI-Specification) from a remote server, you can easily generate a server stub. 9 | - 10 | 11 | To see how to make this your own, look here: 12 | 13 | [README](https://github.com/swagger-api/swagger-codegen/blob/master/README.md) 14 | 15 | - API version: v2.0.0 16 | - Build date: 2022-10-02T11:21:41.037Z[GMT] 17 | 18 | 19 | ### Running the server 20 | To run the server, follow these simple steps: 21 | 22 | ``` 23 | go run main.go 24 | ``` 25 | 26 | -------------------------------------------------------------------------------- /frontend-service/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "es6", 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "module": "esnext", 18 | "moduleResolution": "node", 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | "noEmit": true, 22 | "jsx": "react-jsx", 23 | "noImplicitAny": true, 24 | "noImplicitThis": true, 25 | "strictNullChecks": true 26 | }, 27 | "include": [ 28 | "src" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /device-service/http/go/logger.go: -------------------------------------------------------------------------------- 1 | /* 2 | * OCPP Device Service 3 | * 4 | * REST API to manage OCPP devices (e.g. Charging Stations) 5 | * 6 | * API version: v2.0.0 7 | * Contact: gr.szalay@gmail.com 8 | * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git) 9 | */ 10 | package swagger 11 | 12 | import ( 13 | "log" 14 | "net/http" 15 | "time" 16 | ) 17 | 18 | func Logger(inner http.Handler, name string) http.Handler { 19 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 20 | start := time.Now() 21 | 22 | inner.ServeHTTP(w, r) 23 | 24 | log.Printf( 25 | "%s %s %s %s", 26 | r.Method, 27 | r.RequestURI, 28 | name, 29 | time.Since(start), 30 | ) 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | #credentials 9 | */tls-certs/* 10 | */credentials/* 11 | cert-permissions.sh 12 | PRIVATE.json 13 | 14 | # backend services 15 | *.test 16 | *.out 17 | go.work 18 | 19 | # frontend service 20 | frontend-service/node_modules 21 | frontend-service/.pnp 22 | frontend-service/.pnp.js 23 | */coverage 24 | frontend-service/build 25 | frontend-service/.DS_Store 26 | frontend-service/.env 27 | frontend-service/.env.local 28 | frontend-service/.env.development.local 29 | frontend-service/.env.test.local 30 | frontend-service/.env.production.local 31 | npm-debug.log* 32 | yarn-debug.log* 33 | yarn-error.log* 34 | 35 | # VS code 36 | .idea/ 37 | *.VSCodeCounter/ 38 | 39 | # misc 40 | *.firebase 41 | *.cache 42 | *.cache 43 | *.firebase/hosting.YnVpbGQ.cache 44 | -------------------------------------------------------------------------------- /frontend-service/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./app/App"; 4 | 5 | import appRoutes from "./app/routing/appRoutes"; 6 | import Firebase from "./app/apis/firebase/Firebase"; 7 | import FirebaseContext from "./app/contexts/Firebase"; 8 | import RouteContext from "./app/contexts/Route"; 9 | import "./index.css"; 10 | 11 | console.log("index file ran"); 12 | 13 | export const startTime = Date.now(); 14 | 15 | const firebase = new Firebase(); 16 | 17 | ReactDOM.render( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | , 26 | document.getElementById("root") 27 | ); 28 | -------------------------------------------------------------------------------- /frontend-service/src/dashboard/util-components/Copyright.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { createTheme, ThemeProvider } from "@mui/material/styles"; 3 | import useMediaQuery from "@mui/material/useMediaQuery"; 4 | import CssBaseline from "@mui/material/CssBaseline"; 5 | import Box from "@mui/material/Box"; 6 | import Typography from "@mui/material/Typography"; 7 | import Link from "@mui/material/Link"; 8 | import { Copyright as cpIcon } from "@mui/icons-material"; 9 | 10 | function Copyright() { 11 | return ( 12 | 13 | {"Copyright © "} 14 | 15 | Műszertechnika Holding Zrt. 16 | {" "} 17 | {new Date().getFullYear()}. 18 | 19 | ); 20 | } 21 | 22 | export default Copyright; -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.18-alpine as builder 2 | 3 | RUN apk add --no-cache git 4 | 5 | WORKDIR /app 6 | 7 | COPY go.mod go.sum ./ 8 | RUN go mod download 9 | 10 | COPY . . 11 | 12 | RUN mkdir build 13 | RUN go build -o build/device-service ./device-service 14 | RUN go build -o build/websocket-service ./websocket-service 15 | RUN go build -o build/user-service ./user-service 16 | RUN go build -o build/transaction-service ./transaction-service 17 | 18 | 19 | FROM alpine as runtime 20 | 21 | RUN apk add --no-cache bash 22 | 23 | SHELL ["/bin/bash", "-c"] 24 | 25 | # Copy executable binary file from the 'builder' image to this 'runtime' image 26 | COPY --from=builder /app/build/device-service /app/ 27 | COPY --from=builder /app/build/websocket-service /app/ 28 | COPY --from=builder /app/build/user-service /app/ 29 | COPY --from=builder /app/build/transaction-service /app/ -------------------------------------------------------------------------------- /frontend-service/src/app/apis/firebase/dbUtils.ts: -------------------------------------------------------------------------------- 1 | import { User } from "firebase/auth"; 2 | import { collection, Firestore, onSnapshot, query } from "firebase/firestore"; 3 | 4 | export function setUpSnapshotListener( 5 | setterCallback: (resultItems: any) => void, 6 | collectionName: string, 7 | query: any, 8 | db: Firestore | null, 9 | userInfo: User | null 10 | ) { 11 | if (db && userInfo) { 12 | const myQuery = query; 13 | // update station list if there is a change in Firestore 14 | const unsubscribe = onSnapshot( 15 | myQuery, 16 | (querySnapshot: any) => { 17 | const resultItems: any = []; 18 | querySnapshot.forEach((doc: any) => { 19 | resultItems.push(doc.data()); 20 | }); 21 | setterCallback(resultItems); 22 | }, 23 | (error) => console.log("{error} " + JSON.stringify(error)) 24 | ); 25 | return () => { 26 | unsubscribe(); 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /device-service/ocpphandlers/HeartbeatRequestHandler.go: -------------------------------------------------------------------------------- 1 | package ocpphandlers 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/gregszalay/ocpp-csms-common-types/QueuedMessage" 8 | "github.com/gregszalay/ocpp-csms/device-service/publishing" 9 | "github.com/gregszalay/ocpp-messages-go/types/HeartbeatResponse" 10 | ) 11 | 12 | func HeartbeatRequestHandler(request_json []byte, messageId string, deviceId string) { 13 | 14 | //TODO: implement unmarshallign of Heartbeatrequest, if needed 15 | 16 | //time, err := time.Parse( time.RFC3339, "2012-11-01T22:08:41+00:00") 17 | resp := HeartbeatResponse.HeartbeatResponseJson{ 18 | CurrentTime: time.Now().Format(time.RFC3339), 19 | } 20 | 21 | qm := QueuedMessage.QueuedMessage{ 22 | MessageId: messageId, 23 | DeviceId: deviceId, 24 | Payload: resp, 25 | } 26 | 27 | if err := publishing.Publish("HeartbeatResponse", qm); err != nil { 28 | fmt.Println("Error!") 29 | fmt.Println(err) 30 | panic(err) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /transaction-service/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/gregszalay/ocpp-csms/transaction-service/subscribing" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | func main() { 13 | 14 | if LOG_LEVEL := os.Getenv("LOG_LEVEL"); LOG_LEVEL != "" { 15 | setLogLevel(LOG_LEVEL) 16 | } else { 17 | setLogLevel("Info") 18 | } 19 | 20 | fmt.Println("Creating pubsub subscriptions...") 21 | subscribing.Subscribe() 22 | for { 23 | time.Sleep(time.Millisecond * 10) 24 | } 25 | 26 | } 27 | 28 | func setLogLevel(levelName string) { 29 | switch levelName { 30 | case "Panic": 31 | log.SetLevel(log.PanicLevel) 32 | case "Fatal": 33 | log.SetLevel(log.FatalLevel) 34 | case "Error": 35 | log.SetLevel(log.ErrorLevel) 36 | case "Warn": 37 | log.SetLevel(log.WarnLevel) 38 | case "Info": 39 | log.SetLevel(log.InfoLevel) 40 | case "Debug": 41 | log.SetLevel(log.DebugLevel) 42 | case "Trace": 43 | log.SetLevel(log.TraceLevel) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /frontend-service/src/app/theme/dimensionHelpers.ts: -------------------------------------------------------------------------------- 1 | export const px = (value: number): string => { 2 | return value + "px"; 3 | }; 4 | export const vw = (value: number): string => { 5 | return value + "vw"; 6 | }; 7 | export const vh = (value: number): string => { 8 | return value + "vh"; 9 | }; 10 | export const deg = (value: number): string => { 11 | return value + "deg"; 12 | }; 13 | 14 | export const toHHMMSS = function (millis: string) { 15 | var sec_num = Math.floor(parseInt(millis, 10) / 1000); // don't forget the second param 16 | var hours: number | string = Math.floor(sec_num / 3600); 17 | var minutes: number | string = Math.floor((sec_num - hours * 3600) / 60); 18 | var seconds: number | string = sec_num - hours * 3600 - minutes * 60; 19 | 20 | if (hours < 10) { 21 | hours = "0" + hours; 22 | } 23 | if (minutes < 10) { 24 | minutes = "0" + minutes; 25 | } 26 | if (seconds < 10) { 27 | seconds = "0" + seconds; 28 | } 29 | return hours + ":" + minutes + ":" + seconds; 30 | }; 31 | -------------------------------------------------------------------------------- /websocket-service/publishing/MessagePublisher.go: -------------------------------------------------------------------------------- 1 | package publishing 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | 7 | "github.com/ThreeDotsLabs/watermill" 8 | "github.com/ThreeDotsLabs/watermill-googlecloud/pkg/googlecloud" 9 | "github.com/ThreeDotsLabs/watermill/message" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | var PROJECT_ID string = os.Getenv("GCP_PROJECT_ID") 14 | 15 | var gcp_pub *googlecloud.Publisher = nil 16 | 17 | func Publish(topic string, qm interface{}) error { 18 | logger := watermill.NewStdLogger(true, true) 19 | 20 | if gcp_pub == nil { 21 | publisher, err := googlecloud.NewPublisher(googlecloud.PublisherConfig{ 22 | ProjectID: PROJECT_ID, 23 | }, logger) 24 | if err != nil { 25 | panic(err) 26 | } 27 | gcp_pub = publisher 28 | } 29 | 30 | qm_json, err := json.Marshal(qm) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | log.Debug("Publishing QueuedMessage: ", string(qm_json)) 36 | 37 | msg := message.NewMessage(watermill.NewUUID(), qm_json) 38 | if err := gcp_pub.Publish(topic, msg); err != nil { 39 | return err 40 | } 41 | return nil 42 | 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Gergely (Greg) Szalay 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /frontend-service/src/app/contexts/FirebaseData.tsx: -------------------------------------------------------------------------------- 1 | import React, { Context, ReactElement } from "react"; 2 | 3 | const FirebaseDataContext: Context = React.createContext(null); 4 | 5 | export const withFirebaseDataContext = ( 6 | MyComponent: (props: any) => JSX.Element, 7 | props: any 8 | ) => ( 9 | 10 | {({ 11 | firebase, 12 | stations, 13 | chargetokens, 14 | transactions, 15 | userInfo, 16 | handleNewToken, 17 | handleModifiedToken, 18 | handleDeletedToken, 19 | isSmUp, 20 | setRefresh, 21 | }) => ( 22 | 35 | )} 36 | 37 | ); 38 | 39 | export default FirebaseDataContext; 40 | -------------------------------------------------------------------------------- /user-service/publishing/MessagePublisher.go: -------------------------------------------------------------------------------- 1 | package publishing 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | 7 | "github.com/ThreeDotsLabs/watermill" 8 | "github.com/ThreeDotsLabs/watermill-googlecloud/pkg/googlecloud" 9 | "github.com/ThreeDotsLabs/watermill/message" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | var PROJECT_ID string = os.Getenv("GCP_PROJECT_ID") 14 | 15 | var gcp_pub *googlecloud.Publisher = nil 16 | 17 | func Publish(topic string, qm interface{}) error { 18 | logger := watermill.NewStdLogger(true, true) 19 | 20 | if gcp_pub == nil { 21 | publisher, err := googlecloud.NewPublisher(googlecloud.PublisherConfig{ 22 | ProjectID: PROJECT_ID, 23 | }, logger) 24 | if err != nil { 25 | log.Fatal("Failed to create gcp pulisher client") 26 | } 27 | gcp_pub = publisher 28 | } 29 | 30 | qm_json, err := json.Marshal(qm) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | log.Debug("QueuedMessage to be published to topic ", topic, " : ", string(qm_json)) 36 | 37 | msg := message.NewMessage(watermill.NewUUID(), qm_json) 38 | if err := gcp_pub.Publish(topic, msg); err != nil { 39 | return err 40 | } 41 | return nil 42 | 43 | } 44 | -------------------------------------------------------------------------------- /transaction-service/publishing/MessagePublisher.go: -------------------------------------------------------------------------------- 1 | package publishing 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | 7 | "github.com/ThreeDotsLabs/watermill" 8 | "github.com/ThreeDotsLabs/watermill-googlecloud/pkg/googlecloud" 9 | "github.com/ThreeDotsLabs/watermill/message" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | var PROJECT_ID string = os.Getenv("GCP_PROJECT_ID") 14 | 15 | var gcp_pub *googlecloud.Publisher = nil 16 | 17 | func Publish(topic string, qm interface{}) error { 18 | logger := watermill.NewStdLogger(true, true) 19 | 20 | if gcp_pub == nil { 21 | publisher, err := googlecloud.NewPublisher(googlecloud.PublisherConfig{ 22 | ProjectID: PROJECT_ID, 23 | }, logger) 24 | if err != nil { 25 | log.Fatal("Failed to create gcp pulisher client") 26 | } 27 | gcp_pub = publisher 28 | } 29 | 30 | qm_json, err := json.Marshal(qm) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | log.Debug("QueuedMessage to be published to topic ", topic, " : ", string(qm_json)) 36 | 37 | msg := message.NewMessage(watermill.NewUUID(), qm_json) 38 | if err := gcp_pub.Publish(topic, msg); err != nil { 39 | return err 40 | } 41 | return nil 42 | 43 | } 44 | -------------------------------------------------------------------------------- /frontend-service/src/chargetokens/util-components/Item.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import Card from "@mui/material/Card"; 4 | import CardActions from "@mui/material/CardActions"; 5 | import CardContent from "@mui/material/CardContent"; 6 | import Button from "@mui/material/Button"; 7 | import Typography from "@mui/material/Typography"; 8 | import { IdTokenType } from "ocpp-messages-ts/types/AuthorizeRequest"; 9 | import { Stack } from "@mui/material"; 10 | 11 | interface Props { 12 | token: IdTokenType; 13 | } 14 | 15 | /***************************************************************************/ 16 | 17 | export default function ChargeTokenItem(props: Props) { 18 | return ( 19 | 20 | 21 | 22 | 23 | {" "} 24 | idToken: 25 | {props.token.idToken} 26 | 27 | 28 | type: 29 | {props.token.type} 30 | 31 | 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /device-service/http/go/api_delete_charging_station.go: -------------------------------------------------------------------------------- 1 | /* 2 | * OCPP Device Service 3 | * 4 | * REST API to manage OCPP devices (e.g. Charging Stations) 5 | * 6 | * API version: v2.0.0 7 | * Contact: gr.szalay@gmail.com 8 | * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git) 9 | */ 10 | package swagger 11 | 12 | import ( 13 | "fmt" 14 | "net/http" 15 | 16 | "github.com/gorilla/mux" 17 | "github.com/gregszalay/ocpp-csms/device-service/db" 18 | log "github.com/sirupsen/logrus" 19 | ) 20 | 21 | func DeleteChargingStation(w http.ResponseWriter, r *http.Request) { 22 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 23 | 24 | // get id from path parameter 25 | vars := mux.Vars(r) 26 | id, ok := vars["id"] 27 | if !ok { 28 | log.Error("failed to retrieve charging station id from URL path parameters") 29 | } 30 | 31 | log.Info("Attempting to delete charging station with id ", id) 32 | 33 | if err := db.DeleteChargingStation(id); err != nil { 34 | log.Error("failed to delete charging station ", id) 35 | w.WriteHeader(http.StatusInternalServerError) 36 | w.Write([]byte(fmt.Sprintf("Failed to delete charging station %s", id))) 37 | return 38 | } 39 | w.WriteHeader(http.StatusOK) 40 | } 41 | -------------------------------------------------------------------------------- /device-service/publishing/MessagePublisher.go: -------------------------------------------------------------------------------- 1 | package publishing 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "time" 7 | 8 | "github.com/ThreeDotsLabs/watermill" 9 | "github.com/ThreeDotsLabs/watermill-googlecloud/pkg/googlecloud" 10 | "github.com/ThreeDotsLabs/watermill/message" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | var PROJECT_ID string = os.Getenv("GCP_PROJECT_ID") 15 | 16 | var gcp_pub *googlecloud.Publisher = nil 17 | 18 | func Publish(topic string, qm interface{}) error { 19 | logger := watermill.NewStdLogger(true, true) 20 | 21 | if gcp_pub == nil { 22 | publisher, err := googlecloud.NewPublisher(googlecloud.PublisherConfig{ 23 | ProjectID: PROJECT_ID, 24 | ConnectTimeout: time.Second * 60, 25 | PublishTimeout: time.Second * 60, 26 | }, logger) 27 | if err != nil { 28 | log.Fatal("Failed to create gcp pulisher client") 29 | } 30 | gcp_pub = publisher 31 | } 32 | 33 | qm_json, err := json.Marshal(qm) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | log.Debug("QueuedMessage to be published to topic ", topic, " : ", string(qm_json)) 39 | 40 | msg := message.NewMessage(watermill.NewUUID(), qm_json) 41 | if err := gcp_pub.Publish(topic, msg); err != nil { 42 | return err 43 | } 44 | return nil 45 | 46 | } 47 | -------------------------------------------------------------------------------- /device-service/ocpphandlers/StatusNotificationHandler.go: -------------------------------------------------------------------------------- 1 | package ocpphandlers 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gregszalay/ocpp-csms-common-types/QueuedMessage" 7 | "github.com/gregszalay/ocpp-csms/device-service/publishing" 8 | "github.com/gregszalay/ocpp-messages-go/types/StatusNotificationRequest" 9 | "github.com/gregszalay/ocpp-messages-go/types/StatusNotificationResponse" 10 | "github.com/sanity-io/litter" 11 | ) 12 | 13 | func StatusNotificationHandler(request_json []byte, messageId string, deviceId string) { 14 | 15 | var req StatusNotificationRequest.StatusNotificationRequestJson 16 | payload_unmarshal_err := req.UnmarshalJSON(request_json) 17 | if payload_unmarshal_err != nil { 18 | fmt.Printf("Failed to unmarshal StatusNotificationRequest message payload. Error: %s", payload_unmarshal_err) 19 | } else { 20 | fmt.Println("Payload as an OBJECT:") 21 | litter.Dump(req) 22 | } 23 | 24 | resp := StatusNotificationResponse.StatusNotificationResponseJson{} 25 | 26 | qm := QueuedMessage.QueuedMessage{ 27 | MessageId: messageId, 28 | DeviceId: deviceId, 29 | Payload: resp, 30 | } 31 | 32 | if err := publishing.Publish("StatusNotificationResponse", qm); err != nil { 33 | fmt.Println("Error!") 34 | fmt.Println(err) 35 | panic(err) 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /websocket-service/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "sync" 6 | 7 | "github.com/gregszalay/ocpp-csms/websocket-service/subscribing" 8 | "github.com/gregszalay/ocpp-csms/websocket-service/websocketserver" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | func main() { 13 | 14 | if LOG_LEVEL := os.Getenv("LOG_LEVEL"); LOG_LEVEL != "" { 15 | setLogLevel(LOG_LEVEL) 16 | } else { 17 | setLogLevel("Info") 18 | } 19 | 20 | var waitgroup sync.WaitGroup 21 | 22 | waitgroup.Add(1) 23 | go func() { 24 | log.Info("Creating pubsub subscriptions...") 25 | subscribing.Subscribe() 26 | waitgroup.Done() 27 | }() 28 | 29 | waitgroup.Add(1) 30 | go func() { 31 | log.Info("Starting Websocket server...") 32 | websocketserver.Start() 33 | waitgroup.Done() 34 | }() 35 | 36 | waitgroup.Wait() 37 | 38 | } 39 | 40 | func setLogLevel(levelName string) { 41 | switch levelName { 42 | case "Panic": 43 | log.SetLevel(log.PanicLevel) 44 | case "Fatal": 45 | log.SetLevel(log.FatalLevel) 46 | case "Error": 47 | log.SetLevel(log.ErrorLevel) 48 | case "Warn": 49 | log.SetLevel(log.WarnLevel) 50 | case "Info": 51 | log.SetLevel(log.InfoLevel) 52 | case "Debug": 53 | log.SetLevel(log.DebugLevel) 54 | case "Trace": 55 | log.SetLevel(log.TraceLevel) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /device-service/http/go/api_get_charging_station.go: -------------------------------------------------------------------------------- 1 | /* 2 | * OCPP Device Service 3 | * 4 | * REST API to manage OCPP devices (e.g. Charging Stations) 5 | * 6 | * API version: v2.0.0 7 | * Contact: gr.szalay@gmail.com 8 | * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git) 9 | */ 10 | package swagger 11 | 12 | import ( 13 | "encoding/json" 14 | "net/http" 15 | 16 | "github.com/gorilla/mux" 17 | "github.com/gregszalay/ocpp-csms/device-service/db" 18 | log "github.com/sirupsen/logrus" 19 | ) 20 | 21 | func GetChargingStation(w http.ResponseWriter, r *http.Request) { 22 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 23 | 24 | // get id from path parameter 25 | vars := mux.Vars(r) 26 | id, ok := vars["id"] 27 | if !ok { 28 | log.Error("failed to retrieve charging station id from URL path parameters") 29 | } 30 | 31 | log.Info("Attempting to get charging station with id ", id) 32 | 33 | device, err := db.GetChargingStation(id) 34 | if err != nil { 35 | log.Error("failed to get charging station ", id) 36 | w.WriteHeader(http.StatusInternalServerError) 37 | w.Write([]byte("Failed to get charging station.")) 38 | return 39 | } 40 | jData, err := json.Marshal(device) 41 | if err != nil { 42 | log.Error(err) 43 | } 44 | w.WriteHeader(http.StatusOK) 45 | w.Write(jData) 46 | 47 | } 48 | -------------------------------------------------------------------------------- /device-service/http/go/api_list_charging_stations.go: -------------------------------------------------------------------------------- 1 | /* 2 | * OCPP Device Service 3 | * 4 | * REST API to manage OCPP devices (e.g. Charging Stations) 5 | * 6 | * API version: v2.0.0 7 | * Contact: gr.szalay@gmail.com 8 | * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git) 9 | */ 10 | package swagger 11 | 12 | import ( 13 | "encoding/json" 14 | "net/http" 15 | 16 | "github.com/gregszalay/ocpp-csms/device-service/db" 17 | log "github.com/sirupsen/logrus" 18 | ) 19 | 20 | func GetAllChargingStations(w http.ResponseWriter, r *http.Request) { 21 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 22 | 23 | list, err := db.ListChargingStations() 24 | if err != nil || list == nil { 25 | log.Error("failed to get a list of charging stations from db") 26 | w.WriteHeader(http.StatusInternalServerError) 27 | w.Write([]byte("Failed to get a list of charging stations")) 28 | return 29 | } 30 | 31 | jData, err := json.Marshal(list) 32 | if err != nil { 33 | log.Error("could not marshal charging station list json: ", err) 34 | w.WriteHeader(http.StatusInternalServerError) 35 | w.Write([]byte("Failed to get a list of charging stations")) 36 | return 37 | } 38 | 39 | w.Write(jData) 40 | w.WriteHeader(http.StatusOK) 41 | //json.NewEncoder(w).Encode(devices.ListChargingStations()) 42 | 43 | } 44 | -------------------------------------------------------------------------------- /device-service/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "sync" 8 | 9 | sw "github.com/gregszalay/ocpp-csms/device-service/http/go" 10 | "github.com/gregszalay/ocpp-csms/device-service/subscribing" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | func main() { 15 | 16 | if LOG_LEVEL := os.Getenv("LOG_LEVEL"); LOG_LEVEL != "" { 17 | setLogLevel(LOG_LEVEL) 18 | } else { 19 | setLogLevel("Info") 20 | } 21 | 22 | var waitgroup sync.WaitGroup 23 | 24 | waitgroup.Add(1) 25 | go func() { 26 | fmt.Println("Creating http server...") 27 | router := sw.NewRouter() 28 | log.Fatal(http.ListenAndServe(":5000", router)) 29 | waitgroup.Done() 30 | }() 31 | 32 | waitgroup.Add(1) 33 | go func() { 34 | fmt.Println("Creating pubsub subscriptions...") 35 | subscribing.Subscribe() 36 | waitgroup.Done() 37 | }() 38 | 39 | waitgroup.Wait() 40 | 41 | } 42 | 43 | func setLogLevel(levelName string) { 44 | switch levelName { 45 | case "Panic": 46 | log.SetLevel(log.PanicLevel) 47 | case "Fatal": 48 | log.SetLevel(log.FatalLevel) 49 | case "Error": 50 | log.SetLevel(log.ErrorLevel) 51 | case "Warn": 52 | log.SetLevel(log.WarnLevel) 53 | case "Info": 54 | log.SetLevel(log.InfoLevel) 55 | case "Debug": 56 | log.SetLevel(log.DebugLevel) 57 | case "Trace": 58 | log.SetLevel(log.TraceLevel) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /user-service/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/gregszalay/ocpp-csms/user-service/subscribing" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | func main() { 13 | 14 | if LOG_LEVEL := os.Getenv("LOG_LEVEL"); LOG_LEVEL != "" { 15 | setLogLevel(LOG_LEVEL) 16 | } else { 17 | setLogLevel("Info") 18 | } 19 | 20 | // FOR TESTING PURPOSES ONLY: 21 | 22 | // newIdTokenInfo := AuthorizeRequest.IdTokenType{ 23 | // IdToken: "AA00001", 24 | // Type: AuthorizeRequest.IdTokenEnumType_1_ISO14443, 25 | // } 26 | // db.CreateIdToken("TK-1", newIdTokenInfo) 27 | 28 | // newIdTokenInfo2 := AuthorizeRequest.IdTokenType{ 29 | // IdToken: "AA00002", 30 | // Type: AuthorizeRequest.IdTokenEnumType_1_ISO14443, 31 | // } 32 | // db.CreateIdToken("TK-2", newIdTokenInfo2) 33 | 34 | fmt.Println("Creating pubsub subscriptions...") 35 | subscribing.Subscribe() 36 | for { 37 | time.Sleep(time.Millisecond * 10) 38 | } 39 | 40 | } 41 | 42 | func setLogLevel(levelName string) { 43 | switch levelName { 44 | case "Panic": 45 | log.SetLevel(log.PanicLevel) 46 | case "Fatal": 47 | log.SetLevel(log.FatalLevel) 48 | case "Error": 49 | log.SetLevel(log.ErrorLevel) 50 | case "Warn": 51 | log.SetLevel(log.WarnLevel) 52 | case "Info": 53 | log.SetLevel(log.InfoLevel) 54 | case "Debug": 55 | log.SetLevel(log.DebugLevel) 56 | case "Trace": 57 | log.SetLevel(log.TraceLevel) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /frontend-service/src/app/routing/GeneralRouteTreeMT.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BrowserRouter, Routes, Route, Outlet } from "react-router-dom"; 3 | import ScrollToTop from "./ScrollToTop"; 4 | 5 | interface Props { 6 | routeMap: Object; 7 | } 8 | 9 | function buildRouteTree(routes: Object) { 10 | return Object.entries(routes).map( 11 | ([route, { component, subRoutes }]: any) => { 12 | if ( 13 | Object.keys(subRoutes).length === 0 && 14 | Object.getPrototypeOf(subRoutes) === Object.prototype 15 | ) { 16 | return ; 17 | } else { 18 | return ( 19 | 27 | 28 | 29 | 30 | ) 31 | } 32 | > 33 | { 34 | //Recursive call 35 | buildRouteTree(subRoutes) 36 | } 37 | 38 | ); 39 | } 40 | } 41 | ); 42 | } 43 | 44 | function GeneralRouteTreeMT(props: Props) { 45 | return ( 46 | 47 | {buildRouteTree(props.routeMap)} 48 | 49 | ); 50 | } 51 | 52 | export default GeneralRouteTreeMT; 53 | -------------------------------------------------------------------------------- /transaction-service/_example-messages/TransactionEventRequest_stop.json: -------------------------------------------------------------------------------- 1 | [ 2 | 2, 3 | "71b09dda-76a4-426e-a45f-2675a0dfcd08", 4 | "TransactionEvent", 5 | { 6 | "eventType": "Ended", 7 | "meterValue": [ 8 | { 9 | "sampledValue": [ 10 | { 11 | "measurand": "Energy.Active.Net", 12 | "unitOfMeasure": { 13 | "multiplier": 0, 14 | "unit": "Wh" 15 | }, 16 | "value": 0.8 17 | }, 18 | { 19 | "measurand": "Power.Active.Import", 20 | "unitOfMeasure": { 21 | "multiplier": 3, 22 | "unit": "W" 23 | }, 24 | "value": 0.0 25 | } 26 | ], 27 | "timestamp": "2022-10-01T22:42:41+00:00" 28 | }, 29 | { 30 | "sampledValue": [ 31 | { 32 | "measurand": "Energy.Active.Net", 33 | "unitOfMeasure": { 34 | "multiplier": 3, 35 | "unit": "Wh" 36 | }, 37 | "value": 0.8 38 | }, 39 | { 40 | "measurand": "Power.Active.Import", 41 | "unitOfMeasure": { 42 | "multiplier": 3, 43 | "unit": "W" 44 | }, 45 | "value": 0.0 46 | } 47 | ], 48 | "timestamp": "2022-10-01T22:47:41+00:00" 49 | } 50 | ], 51 | "seqNo": 5, 52 | "timestamp": "2022-10-01T22:50:41+00:00", 53 | "transactionInfo": { 54 | "transactionId": "TX001" 55 | }, 56 | "triggerReason": "ChargingStateChanged" 57 | } 58 | ] 59 | -------------------------------------------------------------------------------- /transaction-service/_example-messages/TransactionEventRequest_start.json: -------------------------------------------------------------------------------- 1 | [ 2 | 2, 3 | "71b09dda-76a4-426e-a45f-2675a0dfcd08", 4 | "TransactionEvent", 5 | { 6 | "eventType": "Started", 7 | "meterValue": [ 8 | { 9 | "sampledValue": [ 10 | { 11 | "measurand": "Energy.Active.Net", 12 | "unitOfMeasure": { 13 | "multiplier": 0, 14 | "unit": "Wh" 15 | }, 16 | "value": 0.0 17 | }, 18 | { 19 | "measurand": "Power.Active.Import", 20 | "unitOfMeasure": { 21 | "multiplier": 3, 22 | "unit": "W" 23 | }, 24 | "value": 22.0 25 | } 26 | ], 27 | "timestamp": "2022-10-01T22:02:41+00:00" 28 | }, 29 | { 30 | "sampledValue": [ 31 | { 32 | "measurand": "Energy.Active.Net", 33 | "unitOfMeasure": { 34 | "multiplier": 3, 35 | "unit": "Wh" 36 | }, 37 | "value": 0.1 38 | }, 39 | { 40 | "measurand": "Power.Active.Import", 41 | "unitOfMeasure": { 42 | "multiplier": 3, 43 | "unit": "W" 44 | }, 45 | "value": 22.0 46 | } 47 | ], 48 | "timestamp": "2022-10-01T22:07:41+00:00" 49 | } 50 | ], 51 | "seqNo": 1, 52 | "timestamp": "2022-10-01T22:10:41+00:00", 53 | "transactionInfo": { 54 | "transactionId": "TX001" 55 | }, 56 | "triggerReason": "ChargingStateChanged" 57 | } 58 | ] 59 | -------------------------------------------------------------------------------- /transaction-service/_example-messages/TransactionEventRequest_update_1.json: -------------------------------------------------------------------------------- 1 | [ 2 | 2, 3 | "71b09dda-76a4-426e-a45f-2675a0dfcd08", 4 | "TransactionEvent", 5 | { 6 | "eventType": "Updated", 7 | "meterValue": [ 8 | { 9 | "sampledValue": [ 10 | { 11 | "measurand": "Energy.Active.Net", 12 | "unitOfMeasure": { 13 | "multiplier": 0, 14 | "unit": "Wh" 15 | }, 16 | "value": 0.2 17 | }, 18 | { 19 | "measurand": "Power.Active.Import", 20 | "unitOfMeasure": { 21 | "multiplier": 3, 22 | "unit": "W" 23 | }, 24 | "value": 22.0 25 | } 26 | ], 27 | "timestamp": "2022-10-01T22:12:41+00:00" 28 | }, 29 | { 30 | "sampledValue": [ 31 | { 32 | "measurand": "Energy.Active.Net", 33 | "unitOfMeasure": { 34 | "multiplier": 3, 35 | "unit": "Wh" 36 | }, 37 | "value": 0.3 38 | }, 39 | { 40 | "measurand": "Power.Active.Import", 41 | "unitOfMeasure": { 42 | "multiplier": 3, 43 | "unit": "W" 44 | }, 45 | "value": 22.0 46 | } 47 | ], 48 | "timestamp": "2022-10-01T22:17:41+00:00" 49 | } 50 | ], 51 | "seqNo": 2, 52 | "timestamp": "2022-10-01T22:20:41+00:00", 53 | "transactionInfo": { 54 | "transactionId": "TX001" 55 | }, 56 | "triggerReason": "ChargingStateChanged" 57 | } 58 | ] 59 | -------------------------------------------------------------------------------- /transaction-service/_example-messages/TransactionEventRequest_update_2.json: -------------------------------------------------------------------------------- 1 | [ 2 | 2, 3 | "71b09dda-76a4-426e-a45f-2675a0dfcd08", 4 | "TransactionEvent", 5 | { 6 | "eventType": "Updated", 7 | "meterValue": [ 8 | { 9 | "sampledValue": [ 10 | { 11 | "measurand": "Energy.Active.Net", 12 | "unitOfMeasure": { 13 | "multiplier": 0, 14 | "unit": "Wh" 15 | }, 16 | "value": 0.4 17 | }, 18 | { 19 | "measurand": "Power.Active.Import", 20 | "unitOfMeasure": { 21 | "multiplier": 3, 22 | "unit": "W" 23 | }, 24 | "value": 22.0 25 | } 26 | ], 27 | "timestamp": "2022-10-01T22:22:41+00:00" 28 | }, 29 | { 30 | "sampledValue": [ 31 | { 32 | "measurand": "Energy.Active.Net", 33 | "unitOfMeasure": { 34 | "multiplier": 3, 35 | "unit": "Wh" 36 | }, 37 | "value": 0.5 38 | }, 39 | { 40 | "measurand": "Power.Active.Import", 41 | "unitOfMeasure": { 42 | "multiplier": 3, 43 | "unit": "W" 44 | }, 45 | "value": 22.0 46 | } 47 | ], 48 | "timestamp": "2022-10-01T22:27:41+00:00" 49 | } 50 | ], 51 | "seqNo": 3, 52 | "timestamp": "2022-10-01T22:30:41+00:00", 53 | "transactionInfo": { 54 | "transactionId": "TX001" 55 | }, 56 | "triggerReason": "ChargingStateChanged" 57 | } 58 | ] 59 | -------------------------------------------------------------------------------- /transaction-service/_example-messages/TransactionEventRequest_update_3.json: -------------------------------------------------------------------------------- 1 | [ 2 | 2, 3 | "71b09dda-76a4-426e-a45f-2675a0dfcd08", 4 | "TransactionEvent", 5 | { 6 | "eventType": "Updated", 7 | "meterValue": [ 8 | { 9 | "sampledValue": [ 10 | { 11 | "measurand": "Energy.Active.Net", 12 | "unitOfMeasure": { 13 | "multiplier": 0, 14 | "unit": "Wh" 15 | }, 16 | "value": 0.6 17 | }, 18 | { 19 | "measurand": "Power.Active.Import", 20 | "unitOfMeasure": { 21 | "multiplier": 3, 22 | "unit": "W" 23 | }, 24 | "value": 22.0 25 | } 26 | ], 27 | "timestamp": "2022-10-01T22:32:41+00:00" 28 | }, 29 | { 30 | "sampledValue": [ 31 | { 32 | "measurand": "Energy.Active.Net", 33 | "unitOfMeasure": { 34 | "multiplier": 3, 35 | "unit": "Wh" 36 | }, 37 | "value": 0.7 38 | }, 39 | { 40 | "measurand": "Power.Active.Import", 41 | "unitOfMeasure": { 42 | "multiplier": 3, 43 | "unit": "W" 44 | }, 45 | "value": 22.0 46 | } 47 | ], 48 | "timestamp": "2022-10-01T22:37:41+00:00" 49 | } 50 | ], 51 | "seqNo": 4, 52 | "timestamp": "2022-10-01T22:40:41+00:00", 53 | "transactionInfo": { 54 | "transactionId": "TX001" 55 | }, 56 | "triggerReason": "ChargingStateChanged" 57 | } 58 | ] 59 | -------------------------------------------------------------------------------- /device-service/http/go/api_create_charging_station.go: -------------------------------------------------------------------------------- 1 | /* 2 | * OCPP Device Service 3 | * 4 | * REST API to manage OCPP devices (e.g. Charging Stations) 5 | * 6 | * API version: v2.0.0 7 | * Contact: gr.szalay@gmail.com 8 | * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git) 9 | */ 10 | package swagger 11 | 12 | import ( 13 | "encoding/json" 14 | "fmt" 15 | "io/ioutil" 16 | "net/http" 17 | 18 | "github.com/gregszalay/ocpp-csms-common-types/devices" 19 | "github.com/gregszalay/ocpp-csms/device-service/db" 20 | log "github.com/sirupsen/logrus" 21 | ) 22 | 23 | func CreateChargingStation(w http.ResponseWriter, r *http.Request) { 24 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 25 | 26 | body, err := ioutil.ReadAll(r.Body) 27 | if err != nil { 28 | log.Error("failed to read http request body: ", err) 29 | return 30 | } 31 | 32 | var newChargingStation devices.ChargingStation 33 | if err := json.Unmarshal(body, &newChargingStation); err != nil { 34 | log.Error("failed to unmarshal json in http request body to ChargingStation object: ", err) 35 | return 36 | } 37 | 38 | log.Info("Attempting to create new charging station: ", newChargingStation) 39 | 40 | if err := db.CreateChargingStation(newChargingStation.Id, newChargingStation); err != nil { 41 | w.WriteHeader(http.StatusInternalServerError) 42 | log.Error("failed to delete charging station") 43 | w.Write([]byte("Failed to create charging station.")) 44 | return 45 | } 46 | 47 | w.WriteHeader(http.StatusOK) 48 | w.Write([]byte(fmt.Sprintf("Successfully created charging station with id %s", newChargingStation.Id))) 49 | } 50 | -------------------------------------------------------------------------------- /frontend-service/src/dashboard/constants/dashboardMenu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import PeopleIcon from "@mui/icons-material/People"; 4 | import SettingsIcon from "@mui/icons-material/Settings"; 5 | import EvStationTwoToneIcon from "@mui/icons-material/EvStationTwoTone"; 6 | import ElectricCarIcon from "@mui/icons-material/ElectricCar"; 7 | import TapAndPlayIcon from "@mui/icons-material/TapAndPlay"; 8 | import DashboardIcon from "@mui/icons-material/Dashboard"; 9 | import PieChartIcon from "@mui/icons-material/PieChart"; 10 | import MapIcon from "@mui/icons-material/Map"; 11 | import BackupTableIcon from "@mui/icons-material/BackupTable"; 12 | import CreditScoreIcon from "@mui/icons-material/CreditScore"; 13 | import GroupIcon from "@mui/icons-material/Group"; 14 | import PersonIcon from "@mui/icons-material/Person"; 15 | 16 | const dashboardMenu: Menu[] = [ 17 | { 18 | headerId: "", 19 | children: [ 20 | { 21 | id: "", 22 | label: "List of charging stations", 23 | icon: , 24 | route: "/dashboard/stations/list", 25 | }, 26 | ], 27 | }, 28 | { 29 | headerId: "", 30 | children: [ 31 | { 32 | id: "", 33 | label: "List of charging stations", 34 | icon: , 35 | route: "/dashboard/transactions", 36 | }, 37 | ], 38 | }, 39 | { 40 | headerId: "", 41 | children: [ 42 | { 43 | id: "", 44 | label: "List of RFIDs", 45 | icon: , 46 | route: "/dashboard/chargetokens/list", 47 | }, 48 | ], 49 | }, 50 | ]; 51 | 52 | export default dashboardMenu; 53 | -------------------------------------------------------------------------------- /websocket-service/websocketserver/ChargingStationConnection.go: -------------------------------------------------------------------------------- 1 | package websocketserver 2 | 3 | import ( 4 | "github.com/gorilla/websocket" 5 | "github.com/gregszalay/ocpp-csms-common-types/QueuedMessage" 6 | "github.com/gregszalay/ocpp-csms/websocket-service/messagemux" 7 | "github.com/gregszalay/ocpp-messages-go/wrappers" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | type ChargingStationConnection struct { 12 | stationId string 13 | wsConn *websocket.Conn 14 | } 15 | 16 | var AllMessagesToDeviceMap map[string]chan *QueuedMessage.QueuedMessage = map[string]chan *QueuedMessage.QueuedMessage{} 17 | 18 | func (conn *ChargingStationConnection) processIncomingMessages() { 19 | for { 20 | _, receivedMessage, err := conn.wsConn.ReadMessage() 21 | if err != nil { 22 | log.Error(err) 23 | return 24 | } 25 | log.Debug("received new message from charging station with id ", conn.stationId, " message: ", string(receivedMessage)) 26 | log.Debug("Putting message into channel for processing") 27 | messagemux.ProcessAndPublish(conn.stationId, receivedMessage) 28 | } 29 | } 30 | 31 | func (conn *ChargingStationConnection) writeMessagesToDevice() { 32 | stationChannel := AllMessagesToDeviceMap[conn.stationId] 33 | for qm := range stationChannel { 34 | log.Debug("Writing message to charging station ", conn.stationId, " via ws connection. message: ", qm) 35 | callresult := wrappers.CALLRESULT{ 36 | MessageTypeId: wrappers.CALLRESULT_TYPE, 37 | MessageId: qm.MessageId, 38 | Payload: qm.Payload, 39 | } 40 | if err := conn.wsConn.WriteMessage(1, callresult.Marshal()); err != nil { 41 | log.Error("failed to write message to charging station ", conn.stationId) 42 | log.Error(err) 43 | return 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /frontend-service/src/app/theme/AppTheme.tsx: -------------------------------------------------------------------------------- 1 | import { createTheme, ThemeProvider } from "@mui/material/styles"; 2 | import createPalette from "@mui/material/styles/createPalette"; 3 | import type {} from "@mui/x-data-grid/themeAugmentation"; 4 | 5 | const theme = createTheme({ 6 | palette: { 7 | primary: { 8 | main: "#32681d", 9 | light: "#619648", 10 | dark: "#003d00", 11 | contrastText: "#edf2f2", 12 | }, 13 | secondary: { 14 | main: "#c0ca33", 15 | light: "#ffffcf", 16 | dark: "#a69b97", 17 | contrastText: "#060614", 18 | }, 19 | info: { 20 | main: "#2394ec", 21 | light: "#4ea7ec", 22 | dark: "#1b69a6", 23 | }, 24 | success: { 25 | main: "#4cad50", 26 | light: "#7ac97e", 27 | dark: "#367539", 28 | }, 29 | divider: "rgba(4,4,4,0.12)", 30 | error: { 31 | main: "#f54336", 32 | light: "#f96a60", 33 | dark: "#a22b23", 34 | }, 35 | background: { 36 | default: "#ffff56", 37 | }, 38 | }, 39 | typography: { 40 | fontFamily: [ 41 | "Archivo", 42 | "sans-serif", 43 | /*'Exo', 44 | 'sans-serif',*/ 45 | ].join(","), 46 | h5: { 47 | fontWeight: 300, 48 | fontSize: 26, 49 | letterSpacing: 0.5, 50 | }, 51 | h6: { 52 | fontWeight: 200, 53 | fontSize: 20, 54 | letterSpacing: 0.4, 55 | }, 56 | }, 57 | shape: { 58 | borderRadius: 8, 59 | }, 60 | mixins: { 61 | toolbar: { 62 | minHeight: 28, 63 | }, 64 | }, 65 | components: { 66 | // Use `MuiDataGrid` on both DataGrid and DataGridPro 67 | MuiDataGrid: { 68 | styleOverrides: { 69 | root: { 70 | backgroundColor: "#fafafa", 71 | }, 72 | }, 73 | }, 74 | }, 75 | }); 76 | 77 | export default theme; 78 | -------------------------------------------------------------------------------- /websocket-service/authentication/ChargingStationAuthenticator.go: -------------------------------------------------------------------------------- 1 | package authentication 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "os" 10 | 11 | "github.com/gregszalay/ocpp-csms-common-types/devices" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | func AuthenticateChargingStation(chargerId string, r *http.Request) error { 16 | //Currently, authentication means checking that the charging station 17 | //is already registered in the database 18 | station, err := GetChargingStationInfo(chargerId) 19 | if err != nil { 20 | log.Error(err) 21 | return errors.New(fmt.Sprintf("error: authentication failed for charging station %s", chargerId)) 22 | } 23 | 24 | log.Info("Charging station info: ", station) 25 | 26 | return nil 27 | } 28 | 29 | func GetChargingStationInfo(stationId string) (devices.ChargingStation, error) { 30 | device_service_hostname := "device-service" //with docker desktop you can use: "host.docker.internal" 31 | if d := os.Getenv("DEVICE_SERVICE_HOST"); d != "" { 32 | device_service_hostname = d 33 | } 34 | GET_device_port := "5000" 35 | if d := os.Getenv("DEVICE_SERVICE_PORT"); d != "" { 36 | GET_device_port = d 37 | } 38 | GET_device_url := "/chargingstations/station" 39 | if d := os.Getenv("DEVICE_SERVICE_GET_STATION_URL"); d != "" { 40 | GET_device_url = d 41 | } 42 | resp, err := http.Get(fmt.Sprintf("http://%s:%s%s/%s", device_service_hostname, GET_device_port, GET_device_url, stationId)) 43 | if err != nil { 44 | return devices.ChargingStation{}, err 45 | } 46 | body, err := ioutil.ReadAll(resp.Body) 47 | if err != nil { 48 | return devices.ChargingStation{}, err 49 | } 50 | var newCharger devices.ChargingStation 51 | error := json.Unmarshal(body, &newCharger) 52 | if error != nil { 53 | return devices.ChargingStation{}, err 54 | } 55 | return newCharger, nil 56 | } 57 | -------------------------------------------------------------------------------- /device-service/http/go/api_update_charging_station.go: -------------------------------------------------------------------------------- 1 | /* 2 | * OCPP Device Service 3 | * 4 | * REST API to manage OCPP devices (e.g. Charging Stations) 5 | * 6 | * API version: v2.0.0 7 | * Contact: gr.szalay@gmail.com 8 | * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git) 9 | */ 10 | package swagger 11 | 12 | import ( 13 | "encoding/json" 14 | "fmt" 15 | "io/ioutil" 16 | "net/http" 17 | 18 | "github.com/gorilla/mux" 19 | "github.com/gregszalay/ocpp-csms-common-types/devices" 20 | "github.com/gregszalay/ocpp-csms/device-service/db" 21 | log "github.com/sirupsen/logrus" 22 | ) 23 | 24 | func UpdateChargingStation(w http.ResponseWriter, r *http.Request) { 25 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 26 | 27 | // get id from path parameter 28 | vars := mux.Vars(r) 29 | id, ok := vars["id"] 30 | if !ok { 31 | log.Error("failed to retrieve charging station id from URL path parameters") 32 | } 33 | 34 | log.Info("Attempting to update charging station with id ", id) 35 | 36 | body, err := ioutil.ReadAll(r.Body) 37 | if err != nil { 38 | log.Error("failed to read http request body: ", err) 39 | return 40 | } 41 | 42 | var updatedChargingStation devices.ChargingStation 43 | error := json.Unmarshal(body, &updatedChargingStation) 44 | if error != nil { 45 | log.Error("could not unmarshal updated charging station json: %s\n", error) 46 | return 47 | } 48 | log.Info("Charging station updated") 49 | 50 | if err := db.UpdateChargingStation(id, updatedChargingStation); err != nil { 51 | log.Error("Failed to update charging station ", id) 52 | w.WriteHeader(http.StatusInternalServerError) 53 | w.Write([]byte(fmt.Sprintf("Failed to update charging station %s", id))) 54 | return 55 | } 56 | w.WriteHeader(http.StatusOK) 57 | w.Write([]byte(fmt.Sprintf("Successfully updated charging station %s", id))) 58 | } 59 | -------------------------------------------------------------------------------- /frontend-service/src/stations/StationsScreen.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Stack from "@mui/material/Stack"; 3 | import Typography from "@mui/material/Typography"; 4 | 5 | import Firebase from "../app/apis/firebase/Firebase"; 6 | import { IdTokenType } from "ocpp-messages-ts/types/AuthorizeRequest"; 7 | import { Transaction } from "../transactions/typedefs/Transaction"; 8 | import StationItem from "./util-components/Item"; 9 | import { CircularProgress, Container, List } from "@mui/material"; 10 | import { ChargingStation } from "./typedefs/ChargingStation"; 11 | 12 | /***************************************************************************/ 13 | 14 | interface Props { 15 | firebase: Firebase; 16 | stations: ChargingStation[]; 17 | transactions: Transaction[]; 18 | chargetokens: IdTokenType[]; 19 | } 20 | 21 | /***************************************************************************/ 22 | 23 | export default function StationsScreen(props: Props) { 24 | return ( 25 | 26 | 27 | 36 | Charging Stations 37 | 38 | {props.stations && props.stations.length > 0 ? ( 39 | props.stations.map((station: ChargingStation) => ( 40 | 41 | )) 42 | ) : ( 43 | 49 | 50 | 51 | )} 52 | 53 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /frontend-service/src/stations/util-components/Item.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Card from "@mui/material/Card"; 3 | import CardActions from "@mui/material/CardActions"; 4 | import CardContent from "@mui/material/CardContent"; 5 | import Button from "@mui/material/Button"; 6 | import Typography from "@mui/material/Typography"; 7 | import Firebase from "../../app/apis/firebase/Firebase"; 8 | import { ChargingStation } from "../typedefs/ChargingStation"; 9 | import { Stack } from "@mui/material"; 10 | /***************************************************************************/ 11 | 12 | interface Props { 13 | station: ChargingStation; 14 | } 15 | 16 | /***************************************************************************/ 17 | 18 | export default function StationItem(props: Props) { 19 | return ( 20 | 21 | 22 | 23 | 24 | {props.station.id} 25 | 26 | 27 | {" "} 28 | Model: 29 | {props.station.model} 30 | 31 | 32 | Model: 33 | {props.station.serialNumber} 34 | 35 | 36 | Model: 37 | {props.station.vendorName} 38 | 39 | 40 | firmwareVersion: 41 | {props.station.firmwareVersion} 42 | 43 | 44 | lastBoot: 45 | {props.station.lastBoot} 46 | 47 | 48 | 49 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /device-service/ocpphandlers/BootNotificationHandler.go: -------------------------------------------------------------------------------- 1 | package ocpphandlers 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/gregszalay/ocpp-csms-common-types/QueuedMessage" 8 | "github.com/gregszalay/ocpp-csms/device-service/db" 9 | "github.com/gregszalay/ocpp-csms/device-service/publishing" 10 | "github.com/gregszalay/ocpp-messages-go/types/BootNotificationRequest" 11 | "github.com/gregszalay/ocpp-messages-go/types/BootNotificationResponse" 12 | "github.com/sanity-io/litter" 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | func BootNotificationHandler(request_json []byte, messageId string, deviceId string) { 17 | 18 | var req BootNotificationRequest.BootNotificationRequestJson 19 | payload_unmarshal_err := req.UnmarshalJSON(request_json) 20 | if payload_unmarshal_err != nil { 21 | fmt.Printf("Failed to unmarshal BootNotificationRequest message payload. Error: %s", payload_unmarshal_err) 22 | } else { 23 | fmt.Println("Payload as an OBJECT:") 24 | litter.Dump(req) 25 | } 26 | 27 | station, err_get := db.GetChargingStation(deviceId) 28 | if err_get != nil { 29 | log.Error("Failed to get charging station from db. error: ", err_get) 30 | } 31 | 32 | station.LastBoot = time.Now().Format(time.RFC3339) 33 | 34 | err_update := db.UpdateChargingStation(deviceId, station) 35 | if err_update != nil { 36 | log.Error("Failed to update charging station. error: ", err_update) 37 | } 38 | 39 | resp := BootNotificationResponse.BootNotificationResponseJson{ 40 | CurrentTime: "", 41 | Interval: 60, 42 | Status: BootNotificationResponse.RegistrationStatusEnumType_1_Accepted, 43 | } 44 | 45 | qm := QueuedMessage.QueuedMessage{ 46 | MessageId: messageId, 47 | DeviceId: deviceId, 48 | Payload: resp, 49 | } 50 | 51 | if err := publishing.Publish("BootNotificationResponse", qm); err != nil { 52 | fmt.Println("Error!") 53 | fmt.Println(err) 54 | panic(err) 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /frontend-service/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Page Not Found 7 | 8 | 23 | 24 | 25 |
26 |

404

27 |

Page Not Found

28 |

The specified file was not found on this website. Please check the URL for mistakes and try again.

29 |

Why am I seeing this?

30 |

This page was generated by the Firebase Command-Line Interface. To modify it, edit the 404.html file in your project's configured public directory.

31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /frontend-service/src/chargetokens/ChargeTokenScreen.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import Stack from "@mui/material/Stack"; 4 | import Typography from "@mui/material/Typography"; 5 | import { Button, CircularProgress, Container, Divider } from "@mui/material"; 6 | 7 | import ChargeToken from "./util-components/Item"; 8 | import Firebase from "../app/apis/firebase/Firebase"; 9 | import { IdTokenType } from "ocpp-messages-ts/types/AuthorizeRequest"; 10 | import { Transaction } from "../transactions/typedefs/Transaction"; 11 | import { ChargingStation } from "../stations/typedefs/ChargingStation"; 12 | 13 | /***************************************************************************/ 14 | 15 | interface Props { 16 | firebase: Firebase; 17 | stations: ChargingStation[]; 18 | transactions: Transaction[]; 19 | chargetokens: IdTokenType[]; 20 | } 21 | 22 | /***************************************************************************/ 23 | 24 | export default function ChargeTokenScreen(props: Props) { 25 | return ( 26 | 27 | 28 | 37 | RFID Tokens 38 | 39 | {props.chargetokens && props.chargetokens.length > 0 ? ( 40 | props.chargetokens.map((token: IdTokenType) => ( 41 | 42 | )) 43 | ) : ( 44 | 50 | 51 | 52 | )} 53 | 54 | 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /frontend-service/src/transactions/TransactionsScreen.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Firebase from "../app/apis/firebase/Firebase"; 3 | 4 | import { CircularProgress, Container, List, Typography } from "@mui/material"; 5 | import Box from "@mui/material/Box"; 6 | import Stack from "@mui/material/Stack"; 7 | 8 | import TransactionItem from "./util-components/Item"; 9 | import { IdTokenType } from "ocpp-messages-ts/types/AuthorizeRequest"; 10 | import { Transaction } from "../transactions/typedefs/Transaction"; 11 | import { ChargingStation } from "../stations/typedefs/ChargingStation"; 12 | 13 | /***************************************************************************/ 14 | 15 | interface Props { 16 | firebase: Firebase; 17 | stations: ChargingStation[]; 18 | transactions: Transaction[]; 19 | chargetokens: IdTokenType[]; 20 | } 21 | 22 | /***************************************************************************/ 23 | 24 | export default function TransactionsScreen(props: Props) { 25 | return ( 26 | 27 | 28 | 37 | Transactions 38 | 39 | {props.transactions && props.transactions.length > 0 ? ( 40 | props.transactions.map((transaction: any) => ( 41 | 42 | )) 43 | ) : ( 44 | 50 | 51 | 52 | )} 53 | 54 | 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /user-service/ocpphandlers/AuthorizeHandler.go: -------------------------------------------------------------------------------- 1 | package ocpphandlers 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gregszalay/ocpp-csms-common-types/QueuedMessage" 7 | "github.com/gregszalay/ocpp-csms/user-service/db" 8 | "github.com/gregszalay/ocpp-csms/user-service/publishing" 9 | "github.com/gregszalay/ocpp-messages-go/types/AuthorizeRequest" 10 | "github.com/gregszalay/ocpp-messages-go/types/AuthorizeResponse" 11 | "github.com/sanity-io/litter" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | func AuthorizeHandler(request_json []byte, messageId string, deviceId string) { 16 | 17 | var req AuthorizeRequest.AuthorizeRequestJson 18 | payload_unmarshal_err := req.UnmarshalJSON(request_json) 19 | if payload_unmarshal_err != nil { 20 | fmt.Printf("Failed to unmarshal AuthorizeRequest message payload. Error: %s", payload_unmarshal_err) 21 | } else { 22 | fmt.Println("Payload as an OBJECT:") 23 | litter.Dump(req) 24 | } 25 | 26 | all_tokens, err := db.ListIdTokens() 27 | if err != nil { 28 | log.Error("failed to get idtokens from db", err) 29 | } 30 | 31 | var status AuthorizeResponse.AuthorizationStatusEnumType_1 = AuthorizeResponse.AuthorizationStatusEnumType_1_Invalid 32 | for _, id_token := range *all_tokens { 33 | if id_token.IdToken == req.IdToken.IdToken { 34 | status = AuthorizeResponse.AuthorizationStatusEnumType_1_Accepted 35 | } 36 | } 37 | 38 | if status != AuthorizeResponse.AuthorizationStatusEnumType_1_Accepted{ 39 | log.Warning("Authorization failed") 40 | } 41 | 42 | resp := AuthorizeResponse.AuthorizeResponseJson{ 43 | IdTokenInfo: AuthorizeResponse.IdTokenInfoType{ 44 | Status: status, 45 | EvseId: []int{0}, 46 | }, 47 | } 48 | qm := QueuedMessage.QueuedMessage{ 49 | MessageId: messageId, 50 | DeviceId: deviceId, 51 | Payload: resp, 52 | } 53 | 54 | if err := publishing.Publish("AuthorizeResponse", qm); err != nil { 55 | log.Error("failed to publish AuthorizeResponse") 56 | log.Error(err) 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /device-service/http/go/routers.go: -------------------------------------------------------------------------------- 1 | /* 2 | * OCPP Device Service 3 | * 4 | * REST API to manage OCPP devices (e.g. Charging Stations) 5 | * 6 | * API version: v2.0.0 7 | * Contact: gr.szalay@gmail.com 8 | * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git) 9 | */ 10 | package swagger 11 | 12 | import ( 13 | "fmt" 14 | "net/http" 15 | "strings" 16 | 17 | "github.com/gorilla/mux" 18 | ) 19 | 20 | type Route struct { 21 | Name string 22 | Method string 23 | Pattern string 24 | HandlerFunc http.HandlerFunc 25 | } 26 | 27 | type Routes []Route 28 | 29 | func NewRouter() *mux.Router { 30 | router := mux.NewRouter().StrictSlash(true) 31 | for _, route := range routes { 32 | var handler http.Handler 33 | handler = route.HandlerFunc 34 | handler = Logger(handler, route.Name) 35 | 36 | router. 37 | Methods(route.Method). 38 | Path(route.Pattern). 39 | Name(route.Name). 40 | Handler(handler) 41 | } 42 | 43 | return router 44 | } 45 | 46 | func Index(w http.ResponseWriter, r *http.Request) { 47 | //http.FileServer(http.Dir("../api/index.html")) 48 | fmt.Fprint(w, "See API docs") 49 | } 50 | 51 | var routes = Routes{ 52 | Route{ 53 | "Index", 54 | "GET", 55 | "/", 56 | Index, 57 | }, 58 | 59 | Route{ 60 | "CreateChargingStation", 61 | strings.ToUpper("Post"), 62 | "/chargingstations/create", 63 | CreateChargingStation, 64 | }, 65 | 66 | Route{ 67 | "DeleteChargingStation", 68 | strings.ToUpper("Post"), 69 | "/chargingstations/delete/{id}", 70 | DeleteChargingStation, 71 | }, 72 | 73 | Route{ 74 | "GetChargingStation", 75 | strings.ToUpper("Get"), 76 | "/chargingstations/station/{id}", 77 | GetChargingStation, 78 | }, 79 | 80 | Route{ 81 | "GetAllChargingStations", 82 | strings.ToUpper("Get"), 83 | "/chargingstations/list", 84 | GetAllChargingStations, 85 | }, 86 | 87 | Route{ 88 | "UpdateChargingStation", 89 | strings.ToUpper("Post"), 90 | "/chargingstations/update/{id}", 91 | UpdateChargingStation, 92 | }, 93 | } 94 | -------------------------------------------------------------------------------- /frontend-service/src/app/routing/appRoutes.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import DashboardScreen from "../../dashboard/DashboardScreen"; 3 | import Login from "../../login/Login"; 4 | import { BrowserRouter, Routes, Route, Outlet } from "react-router-dom"; 5 | import ChargeTokenScreen from "../../chargetokens/ChargeTokenScreen"; 6 | import { JsxElement } from "typescript"; 7 | import StationsScreen from "../../stations/StationsScreen"; 8 | import dashboardMenu from "../../dashboard/constants/dashboardMenu"; 9 | import appTheme from "../theme/AppTheme"; 10 | import { withSelectedMenuItemContext } from "../contexts/SelectedMenuItem"; 11 | import TransactionsScreen from "../../transactions/TransactionsScreen"; 12 | import { withRouteContext } from "../contexts/Route"; 13 | import { withFirebaseDataContext } from "../contexts/FirebaseData"; 14 | 15 | const appRoutes = { 16 | "/": { 17 | label: "Login page", 18 | component: withFirebaseDataContext(Login, {}), 19 | subRoutes: {}, 20 | }, 21 | login: { 22 | label: "Login page", 23 | component: withFirebaseDataContext(Login, {}), 24 | subRoutes: {}, 25 | }, 26 | dashboard: { 27 | label: "Dashboard", 28 | component: withSelectedMenuItemContext(DashboardScreen, { 29 | menu: dashboardMenu, 30 | theme: appTheme, 31 | }), 32 | subRoutes: { 33 | stations: { 34 | label: "Charging station list", 35 | component: React.Fragment, 36 | subRoutes: { 37 | list: { 38 | label: "Charging station list", 39 | component: withFirebaseDataContext(StationsScreen, {}), 40 | subRoutes: {}, 41 | }, 42 | }, 43 | }, 44 | chargetokens: { 45 | label: "RFIDs", 46 | component: React.Fragment, 47 | subRoutes: { 48 | list: { 49 | label: "Token list", 50 | component: withFirebaseDataContext(ChargeTokenScreen, {}), 51 | subRoutes: {}, 52 | }, 53 | }, 54 | }, 55 | transactions: { 56 | label: "Transactions", 57 | component: withFirebaseDataContext(TransactionsScreen, {}), 58 | subRoutes: {}, 59 | }, 60 | }, 61 | }, 62 | }; 63 | 64 | export default appRoutes; 65 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gregszalay/ocpp-csms 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/ThreeDotsLabs/watermill v1.2.0-rc.11 7 | github.com/ThreeDotsLabs/watermill-googlecloud v1.0.13 8 | github.com/gorilla/mux v1.8.0 9 | github.com/gorilla/websocket v1.5.0 10 | github.com/gregszalay/firestore-go v0.0.0-20221002200711-639573dfc2d8 11 | github.com/gregszalay/ocpp-csms-common-types v0.0.0-20221014160809-cc4d653e9116 12 | github.com/gregszalay/ocpp-messages-go v0.0.0-20220923195318-07563a96dc30 13 | github.com/sanity-io/litter v1.5.5 14 | github.com/sirupsen/logrus v1.9.0 15 | ) 16 | 17 | require ( 18 | cloud.google.com/go v0.104.0 // indirect 19 | cloud.google.com/go/compute v1.10.0 // indirect 20 | cloud.google.com/go/firestore v1.6.1 // indirect 21 | cloud.google.com/go/iam v0.5.0 // indirect 22 | cloud.google.com/go/pubsub v1.25.1 // indirect 23 | cloud.google.com/go/storage v1.27.0 // indirect 24 | firebase.google.com/go v3.13.0+incompatible // indirect 25 | github.com/cenkalti/backoff/v3 v3.2.2 // indirect 26 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 27 | github.com/golang/protobuf v1.5.2 // indirect 28 | github.com/google/go-cmp v0.5.9 // indirect 29 | github.com/google/uuid v1.3.0 // indirect 30 | github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect 31 | github.com/googleapis/gax-go/v2 v2.5.1 // indirect 32 | github.com/hashicorp/errwrap v1.1.0 // indirect 33 | github.com/hashicorp/go-multierror v1.1.1 // indirect 34 | github.com/lithammer/shortuuid/v3 v3.0.7 // indirect 35 | github.com/oklog/ulid v1.3.1 // indirect 36 | github.com/pkg/errors v0.9.1 // indirect 37 | go.opencensus.io v0.23.0 // indirect 38 | golang.org/x/net v0.0.0-20221002022538-bcab6841153b // indirect 39 | golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 // indirect 40 | golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0 // indirect 41 | golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect 42 | golang.org/x/text v0.3.7 // indirect 43 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect 44 | google.golang.org/api v0.98.0 // indirect 45 | google.golang.org/appengine v1.6.7 // indirect 46 | google.golang.org/genproto v0.0.0-20220930163606-c98284e70a91 // indirect 47 | google.golang.org/grpc v1.49.0 // indirect 48 | google.golang.org/protobuf v1.28.1 // indirect 49 | ) 50 | -------------------------------------------------------------------------------- /frontend-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "name": "csms-frontend", 4 | "version": "0.1.0", 5 | "private": true, 6 | "dependencies": { 7 | "@date-io/date-fns": "^2.11.0", 8 | "@date-io/dayjs": "^2.11.0", 9 | "@date-io/luxon": "^2.11.1", 10 | "@date-io/moment": "^2.11.0", 11 | "@emotion/react": "^11.6.0", 12 | "@emotion/styled": "^11.6.0", 13 | "@firebase/testing": "^0.20.11", 14 | "@mui/icons-material": "^5.4.1", 15 | "@mui/lab": "^5.0.0-alpha.57", 16 | "@mui/material": "^5.4.1", 17 | "@mui/styles": "^5.4.1", 18 | "@mui/x-data-grid": "^5.4.1", 19 | "@testing-library/jest-dom": "^5.11.4", 20 | "@testing-library/react": "^11.1.0", 21 | "@testing-library/user-event": "^12.1.10", 22 | "@types/d3-array": "^3.0.2", 23 | "@types/jest": "^26.0.15", 24 | "@types/node": "^12.0.0", 25 | "@types/react": "^17.0.0", 26 | "@types/react-dom": "^17.0.0", 27 | "@types/react-router-dom": "^5.3.2", 28 | "add": "^2.0.6", 29 | "cross-env": "^7.0.3", 30 | "date-fns": "^2.26.0", 31 | "dotenv": "^16.0.0", 32 | "firebase": "^9.6.0", 33 | "logrocket": "^2.1.3", 34 | "mtcharger-messages": "https://github.com/szekelyisz/ocpp-2.0.1-message-types", 35 | "ocpp-messages-ts": "^1.0.0", 36 | "react": "^17.0.2", 37 | "react-dom": "^17.0.2", 38 | "react-router-dom": "^6.0.2", 39 | "react-scripts": "4.0.3", 40 | "react-smoothie": "^0.13.1", 41 | "react-spring": "^9.4.2", 42 | "typescript": "^4.1.2", 43 | "web-vitals": "^1.0.1", 44 | "yarn": "^1.22.17" 45 | }, 46 | "scripts": { 47 | "start": "PORT=8085 react-scripts start", 48 | "build": "CI=false react-scripts build", 49 | "test": "react-scripts test", 50 | "eject": "react-scripts eject" 51 | }, 52 | "eslintConfig": { 53 | "extends": [ 54 | "react-app", 55 | "react-app/jest" 56 | ] 57 | }, 58 | "browserslist": { 59 | "production": [ 60 | ">0.2%", 61 | "not dead", 62 | "not op_mini all" 63 | ], 64 | "development": [ 65 | "last 1 chrome version", 66 | "last 1 firefox version", 67 | "last 1 safari version" 68 | ] 69 | }, 70 | "devDependencies": { 71 | "@types/d3-time-format": "^4.0.0", 72 | "@types/google-map-react": "^2.1.3", 73 | "eslint-plugin-react-hooks": "^4.3.0" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /frontend-service/src/dashboard/util-components/UserPanel.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Button, CircularProgress, Stack } from "@mui/material"; 3 | import { useNavigate } from "react-router-dom"; 4 | import UserInfo from "./UserInfo"; 5 | import { User } from "firebase/auth"; 6 | import Firebase from "../../app/apis/firebase/Firebase"; 7 | import theme from "../../app/theme/AppTheme"; 8 | 9 | import { IdTokenType } from "ocpp-messages-ts/types/AuthorizeRequest"; 10 | import { Transaction } from "../../transactions/typedefs/Transaction"; 11 | import { ChargingStation } from "../../stations/typedefs/ChargingStation"; 12 | 13 | /***************************************************************************/ 14 | 15 | interface Props { 16 | firebase: Firebase; 17 | stations: ChargingStation[]; 18 | transactions: Transaction[]; 19 | chargetokens: IdTokenType[]; 20 | userInfo: User | null; 21 | selectedMenuItem_passedDown: MenuItem; 22 | closeHandler: () => {}; 23 | } 24 | 25 | /***************************************************************************/ 26 | 27 | export default function UserPanel(props: Props) { 28 | const navigate = useNavigate(); 29 | 30 | return ( 31 | 42 | {props.userInfo ? ( 43 | 49 | 50 | 51 | 61 | 62 | ) : ( 63 | 69 | 70 | 71 | )} 72 | 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /websocket-service/websocketserver/server.go: -------------------------------------------------------------------------------- 1 | package websocketserver 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | "strings" 10 | 11 | "github.com/gorilla/mux" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | type Route struct { 16 | Name string 17 | Method string 18 | Pattern string 19 | HandlerFunc http.HandlerFunc 20 | } 21 | 22 | type Routes []Route 23 | 24 | var OCPP_CONNECTION_URL string = "/ocpp/{id}" 25 | var OCPP_PORT string = ":3000" 26 | 27 | func Start() { 28 | router := newRouter() 29 | SERVER_TLS_CERT_FILE := os.Getenv("SERVER_TLS_CERT_FILE") 30 | SERVER_TLS_KEY_FILE := os.Getenv("SERVER_TLS_KEY_FILE") 31 | log.Info("SERVER_TLS_CERT_FILE path:") 32 | log.Info(SERVER_TLS_CERT_FILE) 33 | log.Info("SERVER_TLS_KEY_FILE path:") 34 | log.Info(SERVER_TLS_KEY_FILE) 35 | 36 | CLIENT_TLS_CERT_FILE := os.Getenv("CLIENT_TLS_CERT_FILE") 37 | log.Info("CLIENT_TLS_CERT_FILE path:") 38 | log.Info(CLIENT_TLS_CERT_FILE) 39 | 40 | if SERVER_TLS_CERT_FILE != "" && SERVER_TLS_KEY_FILE != "" { 41 | 42 | caCert, _ := ioutil.ReadFile(CLIENT_TLS_CERT_FILE) 43 | caCertPool := x509.NewCertPool() 44 | caCertPool.AppendCertsFromPEM(caCert) 45 | 46 | tlsConfig := &tls.Config{ 47 | ClientCAs: caCertPool, 48 | ClientAuth: tls.RequireAndVerifyClientCert, 49 | } 50 | tlsConfig.BuildNameToCertificate() 51 | 52 | server := &http.Server{ 53 | Handler: router, 54 | Addr: OCPP_PORT, 55 | TLSConfig: tlsConfig, 56 | } 57 | 58 | log.Info("TLS certificate found, serving secure TLS") 59 | log.Fatal(server.ListenAndServeTLS(SERVER_TLS_CERT_FILE, SERVER_TLS_KEY_FILE)) 60 | } else { 61 | log.Warn("TLS certificate not found, serving unsecure ws") 62 | log.Fatal(http.ListenAndServe(OCPP_PORT, router)) 63 | } 64 | 65 | } 66 | 67 | func newRouter() *mux.Router { 68 | router := mux.NewRouter().StrictSlash(true) 69 | for _, route := range routes { 70 | var handler = route.HandlerFunc 71 | router. 72 | Methods(route.Method). 73 | Path(route.Pattern). 74 | Name(route.Name). 75 | Handler(handler) 76 | } 77 | return router 78 | } 79 | 80 | var routes = Routes{ 81 | Route{ 82 | "Index", 83 | "GET", 84 | "/", 85 | Index, 86 | }, 87 | 88 | Route{ 89 | "ocpp", 90 | strings.ToUpper("Get"), 91 | OCPP_CONNECTION_URL, 92 | ChargingStationHandler, 93 | }, 94 | } 95 | -------------------------------------------------------------------------------- /frontend-service/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 19 | 20 | 29 | 33 | 34 | 35 | 36 | MT Station Manager 37 | 38 | 39 | 40 |
41 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /websocket-service/subscribing/MessageSubscriber.go: -------------------------------------------------------------------------------- 1 | package subscribing 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/ThreeDotsLabs/watermill" 8 | "github.com/ThreeDotsLabs/watermill-googlecloud/pkg/googlecloud" 9 | "github.com/ThreeDotsLabs/watermill/message" 10 | "github.com/gregszalay/ocpp-csms-common-types/QueuedMessage" 11 | "github.com/gregszalay/ocpp-csms/websocket-service/websocketserver" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | var PROJECT_ID string = os.Getenv("GCP_PROJECT_ID") 16 | var SERVICE_APP_NAME string = os.Getenv("SERVICE_APP_NAME") 17 | 18 | var out_topics []string = []string{ 19 | "BootNotificationResponse", 20 | "AuthorizeResponse", 21 | "TransactionEventResponse", 22 | "HeartbeatResponse", 23 | "StatusNotificationResponse", 24 | } 25 | 26 | func Subscribe() { 27 | 28 | logger := watermill.NewStdLogger(true, true) 29 | subscriber, err := googlecloud.NewSubscriber( 30 | googlecloud.SubscriberConfig{ 31 | // custom function to generate Subscription Name, 32 | // there are also predefined TopicSubscriptionName and TopicSubscriptionNameWithSuffix available. 33 | GenerateSubscriptionName: func(topic string) string { 34 | return SERVICE_APP_NAME + "_" + topic 35 | }, 36 | ProjectID: PROJECT_ID, 37 | }, 38 | logger, 39 | ) 40 | if err != nil { 41 | panic(err) 42 | } 43 | 44 | for _, topic := range out_topics { 45 | // Subscribe will create the subscription. Only messages that are sent after the subscription is created may be received. 46 | messages, err := subscriber.Subscribe(context.Background(), topic) 47 | if err != nil { 48 | panic(err) 49 | } 50 | go process(topic, messages) 51 | } 52 | } 53 | 54 | func process(topic string, messages <-chan *message.Message) { 55 | for msg := range messages { 56 | 57 | log.Info("received message: %s, topic: %s, payload: %s", msg.UUID, topic, string(msg.Payload)) 58 | 59 | var qm QueuedMessage.QueuedMessage 60 | err := qm.UnmarshalJSON(msg.Payload) 61 | if err != nil { 62 | log.Error("failed to unmarshal QueuedMessage message. Error: %s", err) 63 | } 64 | 65 | if websocketserver.AllMessagesToDeviceMap[qm.DeviceId] == nil { 66 | msg.Ack() 67 | continue 68 | } 69 | log.Debug("Putting msg into MessagesToDevice") 70 | websocketserver.AllMessagesToDeviceMap[qm.DeviceId] <- &qm 71 | 72 | // we need to Acknowledge that we received and processed the message, 73 | // otherwise, it will be resent over and over again. 74 | msg.Ack() 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /frontend-service/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend-service/src/dashboard/util-components/UserInfo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Box, CircularProgress, Stack, Typography } from "@mui/material"; 3 | import { User } from "firebase/auth"; 4 | import { Firestore } from "firebase/firestore"; 5 | 6 | interface Props { 7 | userInfo: User; 8 | } 9 | 10 | export default function UserInfo(props: Props) { 11 | { 12 | props.userInfo.getIdTokenResult().then((result) => { 13 | console.log("getIdTokenResult claims: " + JSON.stringify(result.claims)); 14 | }); 15 | } 16 | 17 | return ( 18 | 19 | 20 | 21 | {props.userInfo.displayName ? ( 22 | 23 | {props.userInfo.displayName} 24 | 25 | ) : null} 26 | 27 | {props.userInfo.email ? ( 28 | 29 | 30 | Email address: 31 | 32 | 33 | {props.userInfo.email} 34 | 35 | 36 | ) : null} 37 | 38 | {props.userInfo.phoneNumber ? ( 39 | 40 | {props.userInfo.phoneNumber} 41 | 42 | ) : null} 43 | 44 | {props.userInfo.photoURL ? ( 45 | 46 | {props.userInfo.photoURL} 47 | 48 | ) : null} 49 | 50 | {props.userInfo.providerId ? ( 51 | 52 | 53 | Authentication provider: 54 | 55 | 56 | {props.userInfo.providerId} 57 | 58 | 59 | ) : null} 60 | 61 | {props.userInfo.uid ? ( 62 | 63 | UID: 64 | 65 | {props.userInfo.uid} 66 | 67 | 68 | ) : null} 69 | 70 | 71 | 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /user-service/subscribing/MessageSubscriber.go: -------------------------------------------------------------------------------- 1 | package subscribing 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/ThreeDotsLabs/watermill" 10 | "github.com/ThreeDotsLabs/watermill-googlecloud/pkg/googlecloud" 11 | "github.com/ThreeDotsLabs/watermill/message" 12 | "github.com/gregszalay/ocpp-csms-common-types/QueuedMessage" 13 | "github.com/gregszalay/ocpp-csms/user-service/ocpphandlers" 14 | log "github.com/sirupsen/logrus" 15 | ) 16 | 17 | var PROJECT_ID string = os.Getenv("GCP_PROJECT_ID") 18 | var SERVICE_APP_NAME string = os.Getenv("SERVICE_APP_NAME") 19 | 20 | var call_topics map[string]func([]byte, string, string) = map[string]func([]byte, string, string){ 21 | "AuthorizeRequest": ocpphandlers.AuthorizeHandler, 22 | } 23 | 24 | var subs []<-chan *message.Message = []<-chan *message.Message{} 25 | 26 | func Subscribe() { 27 | 28 | logger := watermill.NewStdLogger(true, true) 29 | subscriber, err := googlecloud.NewSubscriber( 30 | googlecloud.SubscriberConfig{ 31 | // custom function to generate Subscription Name, 32 | // there are also predefined TopicSubscriptionName and TopicSubscriptionNameWithSuffix available. 33 | GenerateSubscriptionName: func(topic string) string { 34 | return SERVICE_APP_NAME + "_" + topic 35 | }, 36 | ProjectID: PROJECT_ID, 37 | }, 38 | logger, 39 | ) 40 | if err != nil { 41 | log.Fatal("failed to create gcp subscriber", err) 42 | } 43 | 44 | for topic, handler := range call_topics { 45 | // Subscribe will create the subscription. Only messages that are sent after the subscription is created may be received. 46 | messages, err := subscriber.Subscribe(context.Background(), topic) 47 | if err != nil { 48 | log.Fatal("failed to subscribe to topic ", topic, "error: ", err) 49 | } 50 | subs = append(subs, messages) 51 | 52 | go process(topic, messages, handler) 53 | 54 | } 55 | } 56 | 57 | func process(topic string, messages <-chan *message.Message, callback func([]byte, string, string)) { 58 | for msg := range messages { 59 | log.Info("subscriber received message: %s, topic: %s, payload: %s", msg.UUID, topic, string(msg.Payload)) 60 | 61 | var qm QueuedMessage.QueuedMessage 62 | err := qm.UnmarshalJSON(msg.Payload) 63 | if err != nil { 64 | fmt.Printf("Failed to unmarshal QueuedMessage message. Error: %s", err) 65 | } 66 | 67 | log.Debug("received QueuedMessage object: ", qm) 68 | 69 | result, err := json.Marshal(qm.Payload) 70 | if err != nil { 71 | fmt.Printf("Could not re-marshal OCPP payload: %s\n", err) 72 | } 73 | 74 | callback(result, qm.MessageId, qm.DeviceId) 75 | 76 | msg.Ack() 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /transaction-service/subscribing/MessageSubscriber.go: -------------------------------------------------------------------------------- 1 | package subscribing 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/ThreeDotsLabs/watermill" 10 | "github.com/ThreeDotsLabs/watermill-googlecloud/pkg/googlecloud" 11 | "github.com/ThreeDotsLabs/watermill/message" 12 | "github.com/gregszalay/ocpp-csms-common-types/QueuedMessage" 13 | "github.com/gregszalay/ocpp-csms/transaction-service/ocpphandlers" 14 | log "github.com/sirupsen/logrus" 15 | ) 16 | 17 | var PROJECT_ID string = os.Getenv("GCP_PROJECT_ID") 18 | var SERVICE_APP_NAME string = os.Getenv("SERVICE_APP_NAME") 19 | 20 | var call_topics map[string]func([]byte, string, string) = map[string]func([]byte, string, string){ 21 | "TransactionEventRequest": ocpphandlers.TransactionEventHandler, 22 | } 23 | 24 | var subs []<-chan *message.Message = []<-chan *message.Message{} 25 | 26 | func Subscribe() { 27 | 28 | logger := watermill.NewStdLogger(true, true) 29 | subscriber, err := googlecloud.NewSubscriber( 30 | googlecloud.SubscriberConfig{ 31 | // custom function to generate Subscription Name, 32 | // there are also predefined TopicSubscriptionName and TopicSubscriptionNameWithSuffix available. 33 | GenerateSubscriptionName: func(topic string) string { 34 | return SERVICE_APP_NAME + "_" + topic 35 | }, 36 | ProjectID: PROJECT_ID, 37 | }, 38 | logger, 39 | ) 40 | if err != nil { 41 | log.Fatal("failed to create gcp subscriber", err) 42 | } 43 | 44 | for topic, handler := range call_topics { 45 | // Subscribe will create the subscription. Only messages that are sent after the subscription is created may be received. 46 | messages, err := subscriber.Subscribe(context.Background(), topic) 47 | if err != nil { 48 | log.Fatal("failed to subscribe to topic ", topic, "error: ", err) 49 | } 50 | subs = append(subs, messages) 51 | 52 | go process(topic, messages, handler) 53 | 54 | } 55 | } 56 | 57 | func process(topic string, messages <-chan *message.Message, callback func([]byte, string, string)) { 58 | for msg := range messages { 59 | log.Info("subscriber received message: %s, topic: %s, payload: %s", msg.UUID, topic, string(msg.Payload)) 60 | 61 | var qm QueuedMessage.QueuedMessage 62 | err := qm.UnmarshalJSON(msg.Payload) 63 | if err != nil { 64 | fmt.Printf("Failed to unmarshal QueuedMessage message. Error: %s", err) 65 | } 66 | 67 | log.Debug("received QueuedMessage object: ", qm) 68 | 69 | result, err := json.Marshal(qm.Payload) 70 | if err != nil { 71 | fmt.Printf("Could not re-marshal OCPP payload: %s\n", err) 72 | } 73 | 74 | callback(result, qm.MessageId, qm.DeviceId) 75 | 76 | msg.Ack() 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /user-service/db/IdTokens.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/gregszalay/firestore-go/firego" 7 | "github.com/gregszalay/ocpp-messages-go/types/AuthorizeRequest" 8 | 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | const COLLECTION string = "idTokens" 13 | 14 | func GetIdToken(id string) (AuthorizeRequest.IdTokenType, error) { 15 | result, err := firego.Get(COLLECTION, id) 16 | 17 | jsonStr, err_marshal := json.Marshal(result) 18 | if err_marshal != nil { 19 | log.Error("failed to marshal IdTokenList", err_marshal) 20 | } 21 | 22 | var idTokenList AuthorizeRequest.IdTokenType 23 | if err_unmarshal := json.Unmarshal(jsonStr, &idTokenList); err != nil { 24 | log.Error("failed to unmarshal IdTokenList json", err_unmarshal) 25 | 26 | } 27 | return idTokenList, err 28 | } 29 | 30 | func ListIdTokens() (*[]AuthorizeRequest.IdTokenType, error) { 31 | list, err := firego.ListAll(COLLECTION) 32 | idTokenList := []AuthorizeRequest.IdTokenType{} 33 | for index, value := range *list { 34 | jsonStr, err := json.Marshal(value) 35 | if err != nil { 36 | log.Error("failed to marshal IdTokenList list element ", index, " error: ", err) 37 | } 38 | var idToken AuthorizeRequest.IdTokenType 39 | if err := json.Unmarshal(jsonStr, &idToken); err != nil { 40 | log.Error("failed to unmarshal IdTokenList list element ", index, " error: ", err) 41 | } 42 | idTokenList = append(idTokenList, idToken) 43 | } 44 | log.Debug("List of IdTokens: ", idTokenList) 45 | return &idTokenList, err 46 | } 47 | 48 | func CreateIdToken(id string, newIdToken AuthorizeRequest.IdTokenType) error { 49 | marshalled, marshal_err := json.Marshal(newIdToken) 50 | if marshal_err != nil { 51 | log.Error("CreateTransaction marshal error: ", marshal_err) 52 | } 53 | var unmarshalled map[string]interface{} 54 | unmarshal_err := json.Unmarshal(marshalled, &unmarshalled) 55 | if unmarshal_err != nil { 56 | log.Error("CreateTransaction unmarshal error: ", unmarshal_err) 57 | } 58 | return firego.Create(COLLECTION, id, unmarshalled) 59 | } 60 | 61 | func UpdateIdToken(id string, newIdToken AuthorizeRequest.IdTokenType) error { 62 | marshalled, marshal_err := json.Marshal(newIdToken) 63 | if marshal_err != nil { 64 | log.Error("CreateTransaction marshal error: ", marshal_err) 65 | } 66 | var unmarshalled map[string]interface{} 67 | unmarshal_err := json.Unmarshal(marshalled, &unmarshalled) 68 | if unmarshal_err != nil { 69 | log.Error("CreateTransaction unmarshal error: ", unmarshal_err) 70 | } 71 | return firego.Update(COLLECTION, id, unmarshalled) 72 | } 73 | 74 | func DeleteIdToken(id string) error { 75 | return firego.Delete(COLLECTION, id) 76 | } 77 | -------------------------------------------------------------------------------- /device-service/db/chargingstations.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/gregszalay/firestore-go/firego" 7 | "github.com/gregszalay/ocpp-csms-common-types/devices" 8 | 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | var collection string = "chargingstations" 13 | 14 | func GetChargingStation(id string) (devices.ChargingStation, error) { 15 | result, err := firego.Get(collection, id) 16 | 17 | jsonStr, err_marshal := json.Marshal(result) 18 | if err_marshal != nil { 19 | log.Error("failed to marshal charging station", err_marshal) 20 | } 21 | 22 | var charger devices.ChargingStation 23 | if err_unmarshal := json.Unmarshal(jsonStr, &charger); err != nil { 24 | log.Error("failed to unmarshal charging station json", err_unmarshal) 25 | 26 | } 27 | return charger, err 28 | } 29 | 30 | func ListChargingStations() (*[]devices.ChargingStation, error) { 31 | list, err := firego.ListAll(collection) 32 | chargerList := []devices.ChargingStation{} 33 | for index, value := range *list { 34 | jsonStr, err := json.Marshal(value) 35 | if err != nil { 36 | log.Error("failed to marshal charging station list element ", index, " error: ", err) 37 | } 38 | var charger devices.ChargingStation 39 | if err := json.Unmarshal(jsonStr, &charger); err != nil { 40 | log.Error("failed to unmarshal charging station list element ", index, " error: ", err) 41 | } 42 | chargerList = append(chargerList, charger) 43 | } 44 | log.Debug("List of charging stations: ", chargerList) 45 | return &chargerList, err 46 | } 47 | 48 | func CreateChargingStation(id string, newCharger devices.ChargingStation) error { 49 | marshalled, marshal_err := json.Marshal(newCharger) 50 | if marshal_err != nil { 51 | log.Error("CreateTransaction marshal error: ", marshal_err) 52 | } 53 | var unmarshalled map[string]interface{} 54 | unmarshal_err := json.Unmarshal(marshalled, &unmarshalled) 55 | if unmarshal_err != nil { 56 | log.Error("CreateTransaction unmarshal error: ", unmarshal_err) 57 | } 58 | return firego.Create(collection, id, unmarshalled) 59 | } 60 | 61 | func UpdateChargingStation(id string, newCharger devices.ChargingStation) error { 62 | marshalled, marshal_err := json.Marshal(newCharger) 63 | if marshal_err != nil { 64 | log.Error("CreateTransaction marshal error: ", marshal_err) 65 | } 66 | var unmarshalled map[string]interface{} 67 | unmarshal_err := json.Unmarshal(marshalled, &unmarshalled) 68 | if unmarshal_err != nil { 69 | log.Error("CreateTransaction unmarshal error: ", unmarshal_err) 70 | } 71 | return firego.Update(collection, id, unmarshalled) 72 | } 73 | 74 | func DeleteChargingStation(id string) error { 75 | return firego.Delete(collection, id) 76 | } 77 | -------------------------------------------------------------------------------- /transaction-service/db/Transactions.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/gregszalay/firestore-go/firego" 7 | 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | const COLLECTION string = "transactions" 12 | 13 | func GetTransaction(id string) (*Transaction, error) { 14 | result, db_err := firego.Get(COLLECTION, id) 15 | if db_err != nil { 16 | log.Error("failed to get transaction from db", db_err) 17 | return nil, db_err 18 | } 19 | 20 | jsonStr, err_marshal := json.Marshal(result) 21 | if err_marshal != nil { 22 | log.Error("failed to marshal transaction", err_marshal) 23 | return nil, err_marshal 24 | } 25 | 26 | var transaction *Transaction = &Transaction{} 27 | if err_unmarshal := transaction.UnmarshalJSON(jsonStr); err_unmarshal != nil { 28 | log.Error("failed to unmarshal transaction json", err_unmarshal) 29 | return nil, err_unmarshal 30 | } 31 | 32 | return transaction, nil 33 | } 34 | 35 | func ListTransactions() (*[]Transaction, error) { 36 | list, err := firego.ListAll(COLLECTION) 37 | transactionList := []Transaction{} 38 | for index, value := range *list { 39 | jsonStr, err := json.Marshal(value) 40 | if err != nil { 41 | log.Error("failed to marshal transactionList list element ", index, " error: ", err) 42 | } 43 | var tx Transaction 44 | if err := json.Unmarshal(jsonStr, &tx); err != nil { 45 | log.Error("failed to unmarshal transactionList list element ", index, " error: ", err) 46 | } 47 | transactionList = append(transactionList, tx) 48 | } 49 | log.Debug("List of transactions: ", transactionList) 50 | return &transactionList, err 51 | } 52 | 53 | func CreateTransaction(id string, newTransaction Transaction) error { 54 | marshalled, marshal_err := json.Marshal(newTransaction) 55 | if marshal_err != nil { 56 | log.Error("CreateTransaction marshal error: ", marshal_err) 57 | } 58 | var unmarshalled map[string]interface{} 59 | unmarshal_err := json.Unmarshal(marshalled, &unmarshalled) 60 | if unmarshal_err != nil { 61 | log.Error("CreateTransaction unmarshal error: ", unmarshal_err) 62 | } 63 | return firego.Create(COLLECTION, id, unmarshalled) 64 | } 65 | 66 | func UpdateTransaction(id string, newTransaction Transaction) error { 67 | marshalled, marshal_err := json.Marshal(newTransaction) 68 | if marshal_err != nil { 69 | log.Error("CreateTransaction marshal error: ", marshal_err) 70 | } 71 | var unmarshalled map[string]interface{} 72 | unmarshal_err := json.Unmarshal(marshalled, &unmarshalled) 73 | if unmarshal_err != nil { 74 | log.Error("CreateTransaction unmarshal error: ", unmarshal_err) 75 | } 76 | return firego.Update(COLLECTION, id, unmarshalled) 77 | } 78 | 79 | func DeleteTransaction(id string) error { 80 | return firego.Delete(COLLECTION, id) 81 | } 82 | -------------------------------------------------------------------------------- /websocket-service/websocketserver/ChargingStationHandler.go: -------------------------------------------------------------------------------- 1 | package websocketserver 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gregszalay/ocpp-csms-common-types/QueuedMessage" 7 | "github.com/gregszalay/ocpp-csms/websocket-service/authentication" 8 | 9 | "github.com/gorilla/mux" 10 | "github.com/gorilla/websocket" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | var upgrader = websocket.Upgrader{ 15 | ReadBufferSize: 1024, 16 | WriteBufferSize: 1024, 17 | EnableCompression: true, 18 | } 19 | 20 | var openConnections map[string]*ChargingStationConnection = map[string]*ChargingStationConnection{} 21 | 22 | func ChargingStationHandler(w http.ResponseWriter, r *http.Request) { 23 | 24 | // parse charging station identity 25 | id, ok := mux.Vars(r)["id"] 26 | if !ok { 27 | log.Error("cannot find id in connection url. refusing connection") 28 | return 29 | } 30 | log.Info("websocket connection initiated by charging station with id ", id) 31 | 32 | if _, ok := openConnections[id]; ok { 33 | log.Error("Client with this id is already connected. Refusing connection.") 34 | return 35 | } 36 | 37 | // authenticate connection 38 | err := authentication.AuthenticateChargingStation(id, r) 39 | if err != nil { 40 | log.Error("failed to authenticate charging station with id ", id, ". refusing connection.") 41 | w.WriteHeader(http.StatusUnauthorized) 42 | w.Write([]byte("Failed to authenticate charging station.")) 43 | return 44 | } 45 | log.Info("charger successfully authenticated") 46 | 47 | // upgrade to websocket connection 48 | //upgrader.CheckOrigin = func(r *http.Request) bool { return true } 49 | ws, err := upgrader.Upgrade(w, r, nil) 50 | if err != nil { 51 | log.Error("failed to establish websocket connection on the server, error: ", err) 52 | w.WriteHeader(http.StatusInternalServerError) 53 | w.Write([]byte("failed to establish websocket connection on the server")) 54 | return 55 | } 56 | _ = ws.SetCompressionLevel(9) 57 | log.Info("successfully established websocket connection") 58 | 59 | // Create and save ws connection object 60 | new_connection := ChargingStationConnection{ 61 | stationId: id, 62 | wsConn: ws, 63 | } 64 | openConnections[id] = &new_connection 65 | log.Info("number of open connections: ", len(openConnections)) 66 | 67 | // Create channel for messages bound for the charging station 68 | AllMessagesToDeviceMap[id] = make(chan *QueuedMessage.QueuedMessage) 69 | 70 | // Start message handlers for new connection 71 | go new_connection.writeMessagesToDevice() 72 | go new_connection.processIncomingMessages() 73 | 74 | ws.SetCloseHandler(func(code int, text string) error { 75 | delete(openConnections, id) 76 | log.Info("connection closed to charging station ", id) 77 | log.Info("number of remaining open connections: ", len(openConnections)) 78 | return nil 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | websocket-service: 5 | image: gregszalay/websocket-service:v1.0 6 | build: . 7 | command: /app/websocket-service 8 | volumes: 9 | #- ./websocket-service/credentials/PRIVATE.json:/tmp/keys/PRIVATE.json:ro 10 | - ./websocket-service/credentials:/tmp/keys:ro 11 | environment: 12 | - GOOGLE_APPLICATION_CREDENTIALS=/tmp/keys/PRIVATE.json 13 | - GCP_PROJECT_ID=chargerevolutioncloud 14 | - SERVICE_APP_NAME=device-service 15 | # LOG_LEVEL Panic, Fatal, Error, Warn, Info, Debug, Trace 16 | - LOG_LEVEL=Info 17 | - DEVICE_SERVICE_HOST=device-service 18 | - DEVICE_SERVICE_PORT=5000 19 | - DEVICE_SERVICE_GET_STATION_URL=/chargingstations/station 20 | - SERVER_TLS_CERT_FILE=/tmp/keys/fullchain.pem 21 | - SERVER_TLS_KEY_FILE=/tmp/keys/privkey.pem 22 | - CLIENT_TLS_CERT_FILE=/tmp/keys/client_cert.pem 23 | ports: 24 | - "3000:3000" 25 | device-service: 26 | image: gregszalay/device-service:v1.0 27 | build: . 28 | command: /app/device-service 29 | volumes: 30 | - ./device-service/credentials/PRIVATE.json:/tmp/keys/PRIVATE.json:ro 31 | environment: 32 | - GOOGLE_APPLICATION_CREDENTIALS=/tmp/keys/PRIVATE.json 33 | - GCP_PROJECT_ID=chargerevolutioncloud 34 | - SERVICE_APP_NAME=websocket-service 35 | # LOG_LEVEL Panic, Fatal, Error, Warn, Info, Debug, Trace 36 | - LOG_LEVEL=Info 37 | ports: 38 | - "5000:5000" 39 | user-service: 40 | image: gregszalay/user-service:v1.0 41 | build: . 42 | command: /app/user-service 43 | volumes: 44 | - ./user-service/credentials/PRIVATE.json:/tmp/keys/PRIVATE.json:ro 45 | environment: 46 | - GOOGLE_APPLICATION_CREDENTIALS=/tmp/keys/PRIVATE.json 47 | - GCP_PROJECT_ID=chargerevolutioncloud 48 | - SERVICE_APP_NAME=user-service 49 | # LOG_LEVEL Panic, Fatal, Error, Warn, Info, Debug, Trace 50 | - LOG_LEVEL=Info 51 | transaction-service: 52 | image: gregszalay/transaction-service:v1.0 53 | build: . 54 | command: /app/transaction-service 55 | volumes: 56 | - ./transaction-service/credentials/PRIVATE.json:/tmp/keys/PRIVATE.json:ro 57 | environment: 58 | - GOOGLE_APPLICATION_CREDENTIALS=/tmp/keys/PRIVATE.json 59 | - GCP_PROJECT_ID=chargerevolutioncloud 60 | - SERVICE_APP_NAME=transaction-service 61 | # LOG_LEVEL Panic, Fatal, Error, Warn, Info, Debug, Trace 62 | - LOG_LEVEL=Info 63 | # TODO: configure container for frontend-service 64 | # frontend-service: 65 | # build: 66 | # context: ./frontend-service 67 | # dockerfile: Dockerfile 68 | # volumes: 69 | # - ./frontend-service/src:/app/src 70 | #command: npm run start 71 | #ports: 72 | # - "8085:8085" 73 | 74 | # environment: #NODE_ENV: development 75 | -------------------------------------------------------------------------------- /device-service/subscribing/MessageSubscriber.go: -------------------------------------------------------------------------------- 1 | package subscribing 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "time" 9 | 10 | "github.com/ThreeDotsLabs/watermill" 11 | "github.com/ThreeDotsLabs/watermill-googlecloud/pkg/googlecloud" 12 | "github.com/ThreeDotsLabs/watermill/message" 13 | "github.com/gregszalay/ocpp-csms-common-types/QueuedMessage" 14 | "github.com/gregszalay/ocpp-csms/device-service/ocpphandlers" 15 | log "github.com/sirupsen/logrus" 16 | ) 17 | 18 | var PROJECT_ID string = os.Getenv("GCP_PROJECT_ID") 19 | var SERVICE_APP_NAME string = os.Getenv("SERVICE_APP_NAME") 20 | 21 | var call_topics map[string]func([]byte, string, string) = map[string]func([]byte, string, string){ 22 | "BootNotificationRequest": ocpphandlers.BootNotificationHandler, 23 | "HeartbeatRequest": ocpphandlers.HeartbeatRequestHandler, 24 | "StatusNotificationRequest": ocpphandlers.StatusNotificationHandler, 25 | } 26 | 27 | var subs []<-chan *message.Message = []<-chan *message.Message{} 28 | 29 | func Subscribe() { 30 | 31 | logger := watermill.NewStdLogger(true, true) 32 | subscriber, err := googlecloud.NewSubscriber( 33 | googlecloud.SubscriberConfig{ 34 | // custom function to generate Subscription Name, 35 | // there are also predefined TopicSubscriptionName and TopicSubscriptionNameWithSuffix available. 36 | GenerateSubscriptionName: func(topic string) string { 37 | return SERVICE_APP_NAME + "_" + topic 38 | }, 39 | ConnectTimeout: time.Second * 60, 40 | InitializeTimeout: time.Second * 60, 41 | ProjectID: PROJECT_ID, 42 | }, 43 | logger, 44 | ) 45 | if err != nil { 46 | log.Fatal("failed to create gcp subscriber", err) 47 | } 48 | 49 | for topic, handler := range call_topics { 50 | // Subscribe will create the subscription. Only messages that are sent after the subscription is created may be received. 51 | messages, err := subscriber.Subscribe(context.Background(), topic) 52 | if err != nil { 53 | log.Fatal("failed to subscribe to topic ", topic, "error: ", err) 54 | } 55 | subs = append(subs, messages) 56 | go process(topic, messages, handler) 57 | } 58 | } 59 | 60 | func process(topic string, messages <-chan *message.Message, callback func([]byte, string, string)) { 61 | for msg := range messages { 62 | log.Info("subscriber received message: %s, topic: %s, payload: %s", msg.UUID, topic, string(msg.Payload)) 63 | 64 | var qm QueuedMessage.QueuedMessage 65 | err := qm.UnmarshalJSON(msg.Payload) 66 | if err != nil { 67 | fmt.Printf("Failed to unmarshal QueuedMessage message. Error: %s", err) 68 | } 69 | 70 | log.Debug("received QueuedMessage object: ", qm) 71 | 72 | result, err := json.Marshal(qm.Payload) 73 | if err != nil { 74 | fmt.Printf("Could not re-marshal OCPP payload: %s\n", err) 75 | } 76 | 77 | callback(result, qm.MessageId, qm.DeviceId) 78 | 79 | msg.Ack() 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /frontend-service/src/login/Login.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import TextField from "@mui/material/TextField"; 3 | import { useNavigate } from "react-router-dom"; 4 | import { Button, Stack } from "@mui/material"; 5 | import { User, UserCredential } from "firebase/auth"; 6 | 7 | export default function Login(props: any) { 8 | let navigate = useNavigate(); 9 | const [email, setEmail] = React.useState(""); 10 | const [emailIsValid, setEmailIsValid] = React.useState(true); 11 | const [password, setPassword] = React.useState(""); 12 | const [userSubmit, setUserSubmit] = React.useState(false); 13 | 14 | function validateEmail() { 15 | const re = /^[a-zA-Z0-9]+@[a-zA-Z0-9]+.+[A-Za-z]+$/; 16 | let valid = re.test(email); 17 | setEmailIsValid(valid); 18 | } 19 | 20 | function handleSubmit(event: any) { 21 | event.preventDefault(); 22 | validateEmail(); 23 | setUserSubmit(true); 24 | if (email && emailIsValid && password) 25 | props.firebase 26 | .signInWithEmailAndPassword(email, password) 27 | .then((authUser: UserCredential) => { 28 | console.log("authUser: " + { ...authUser }); 29 | props.refresh(authUser); 30 | navigate("/dashboard/stations/list/"); 31 | }) 32 | .catch((error: any) => { 33 | console.log("{error} " + { error }); 34 | navigate("/error"); 35 | }); 36 | } 37 | 38 | return ( 39 | 40 | 47 | 58 | ) => { 64 | setEmail(event.target.value); 65 | if (userSubmit) validateEmail(); 66 | }} 67 | /> 68 | ) => 75 | setPassword(event.target.value) 76 | } 77 | /> 78 | 81 | 82 | 83 | 84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /frontend-service/src/app/apis/firebase/Firebase.tsx: -------------------------------------------------------------------------------- 1 | import { FirebaseApp, initializeApp } from "firebase/app"; 2 | import * as firebaseAuthApi from "firebase/auth"; 3 | import React from "react"; 4 | import { 5 | Auth, 6 | AuthCredential, 7 | EmailAuthCredential, 8 | EmailAuthProvider, 9 | User, 10 | UserCredential, 11 | } from "firebase/auth"; 12 | 13 | import dotenv from "dotenv"; 14 | dotenv.config(); 15 | console.log(`Your port is ${process.env.PORT}`); 16 | console.log(`Your projectId is ${process.env.REACT_APP_PROJECT_ID}`); 17 | console.log("Your projectId is" + process.env.REACT_APP_PROJECT_ID); 18 | 19 | const firebaseConfig = { 20 | apiKey: process.env.REACT_APP_API_KEY, 21 | authDomain: process.env.REACT_APP_AUTH_DOMAIN, 22 | projectId: process.env.REACT_APP_PROJECT_ID!, 23 | storageBucket: process.env.REACT_APP_STORAGE_BUCKET, 24 | messagingSenderId: process.env.REACT_APP_MESSAGING_SENDER_ID, 25 | appId: process.env.REACT_APP_APP_ID, 26 | measurementId: process.env.REACT_APP_APP_ID_MEASUREMENT_ID, 27 | }; 28 | 29 | export default class Firebase { 30 | readonly auth: Auth; 31 | readonly firebaseApi: any; 32 | readonly app: FirebaseApp; 33 | currentUserCredential: EmailAuthCredential = new EmailAuthCredential(); 34 | 35 | constructor() { 36 | console.log("firebaseConfig.apiKey " + firebaseConfig.apiKey); 37 | this.app = initializeApp(firebaseConfig); 38 | this.auth = firebaseAuthApi.getAuth(); 39 | } 40 | 41 | // *** Auth API *** 42 | createUserWithEmailAndPassword = ( 43 | email: string, 44 | password: string 45 | ): [Promise, string] => { 46 | return [ 47 | firebaseAuthApi.createUserWithEmailAndPassword( 48 | this.auth, 49 | email, 50 | password 51 | ), 52 | password, 53 | ]; 54 | }; 55 | 56 | signInWithCredential = ( 57 | auth: Auth, 58 | credential: AuthCredential 59 | ): Promise => { 60 | return firebaseAuthApi.signInWithCredential(auth, credential); 61 | }; 62 | 63 | signInWithEmailAndPassword = ( 64 | email: any, 65 | password: any, 66 | initialPassword: string 67 | ): Promise | null => { 68 | if (password !== initialPassword) { 69 | const result = firebaseAuthApi.signInWithEmailAndPassword( 70 | this.auth, 71 | email, 72 | password 73 | ); 74 | this.currentUserCredential = EmailAuthProvider.credential( 75 | email, 76 | password 77 | ); 78 | return result; 79 | } else return null; 80 | }; 81 | 82 | signOut = () => firebaseAuthApi.signOut(this.auth); 83 | 84 | resetPassword = (/*auth: Auth,*/ email: any) => 85 | firebaseAuthApi.sendPasswordResetEmail(/*auth,*/ this.auth, email); 86 | 87 | updatePassword = (password: any) => 88 | firebaseAuthApi.updatePassword(password.currentUser!, password); 89 | 90 | public get userInfo(): User | null { 91 | return this.auth.currentUser; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /transaction-service/db/Transaction.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/gregszalay/ocpp-messages-go/types/TransactionEventRequest" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | type Transaction struct { 13 | StationId string `json:"stationId" yaml:"stationId"` 14 | EnergyTransferInProgress bool `json:"energyTransferInProgress" yaml:"energyTransferInProgress"` 15 | EnergyTransferStarted string `json:"energyTransferStarted" yaml:"energyTransferStarted"` 16 | EnergyTransferStopped string `json:"energyTransferStopped" yaml:"energyTransferStopped"` 17 | MeterValues []TransactionEventRequest.MeterValueType `json:"meterValues" yaml:"meterValues"` 18 | } 19 | 20 | func (j *Transaction) UnmarshalJSON(b []byte) error { 21 | var raw map[string]interface{} 22 | err := json.Unmarshal(b, &raw) 23 | if err != nil { 24 | log.Error("could not unmarshal jso") 25 | fmt.Printf("could not unmarshal json: %s\n", err) 26 | return err 27 | } 28 | 29 | device_id, err_device_id := raw["stationId"].(string) 30 | if !err_device_id { 31 | return errors.New("field stationId is not a string") 32 | } 33 | 34 | energyTransferInProgress, err_energyTransferInProgress := raw["energyTransferInProgress"].(bool) 35 | if !err_energyTransferInProgress { 36 | return errors.New("field EnergyTransferInProgress is not a bool") 37 | } 38 | 39 | energyTransferStarted, err_energyTransferStarted := raw["energyTransferStarted"].(string) 40 | if !err_energyTransferStarted { 41 | return errors.New("field EnergyTransferStarted is not a string") 42 | } 43 | 44 | energyTransferStopped, err_energyTransferStopped := raw["energyTransferStopped"].(string) 45 | if !err_energyTransferStopped { 46 | return errors.New("field energyTransferStopped is not a string") 47 | } 48 | 49 | meterValues, err_meterValues := raw["meterValues"].([]interface{}) 50 | if !err_meterValues { 51 | return errors.New("field MeterValues is not a []interface{}") 52 | } 53 | 54 | var decoded_meterValues []TransactionEventRequest.MeterValueType 55 | for _, value := range meterValues { 56 | var meter_value_element TransactionEventRequest.MeterValueType 57 | marshalled_element, marshal_element_err := json.Marshal(value) 58 | if marshal_element_err != nil { 59 | log.Error(marshal_element_err) 60 | return errors.New("failed to re-marshal raw MeterValues element") 61 | } 62 | err_elem := meter_value_element.UnmarshalJSON(marshalled_element) 63 | if err_elem != nil { 64 | log.Error(err_elem) 65 | return errors.New("MeterValues element is not a TransactionEventRequest.MeterValueType") 66 | } 67 | decoded_meterValues = append(decoded_meterValues, meter_value_element) 68 | } 69 | 70 | *j = Transaction{ 71 | StationId: device_id, 72 | EnergyTransferInProgress: energyTransferInProgress, 73 | EnergyTransferStarted: energyTransferStarted, 74 | EnergyTransferStopped: energyTransferStopped, 75 | MeterValues: decoded_meterValues, 76 | } 77 | 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /frontend-service/src/dashboard/util-components/Header.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import AppBar from "@mui/material/AppBar"; 3 | import Avatar from "@mui/material/Avatar"; 4 | import Button from "@mui/material/Button"; 5 | import Grid from "@mui/material/Grid"; 6 | import HelpIcon from "@mui/icons-material/Help"; 7 | import IconButton from "@mui/material/IconButton"; 8 | import Link from "@mui/material/Link"; 9 | import MenuIcon from "@mui/icons-material/Menu"; 10 | import NotificationsIcon from "@mui/icons-material/Notifications"; 11 | import Toolbar from "@mui/material/Toolbar"; 12 | import Tooltip from "@mui/material/Tooltip"; 13 | import Typography from "@mui/material/Typography"; 14 | import { useTheme } from "@mui/system"; 15 | import { Drawer } from "@mui/material"; 16 | import { withFirebaseContext } from "../../app/contexts/Firebase"; 17 | import UserPanel from "./UserPanel"; 18 | import { withFirebaseDataContext } from "../../app/contexts/FirebaseData"; 19 | 20 | interface HeaderProps { 21 | onDrawerToggle: () => void; 22 | theme: any; 23 | selectedMenuItem_passedDown: MenuItem; 24 | } 25 | 26 | export default function Header(props: HeaderProps) { 27 | const theme = useTheme(props.theme); 28 | const appbarHeight = 80; 29 | 30 | const [userDrawerOpen, setuserDrawerOpen] = React.useState(false); 31 | 32 | const toggleDrawer = () => setuserDrawerOpen(!userDrawerOpen); 33 | 34 | return ( 35 | 36 | 49 | 50 | 51 | 52 | 58 | 59 | 60 | 61 | 62 | 68 | {props.selectedMenuItem_passedDown.id} 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | {withFirebaseDataContext(UserPanel, { 91 | selectedMenuItem_passedDown: props.selectedMenuItem_passedDown, 92 | closeHandler: toggleDrawer, 93 | })} 94 | 95 | 96 | 97 | ); 98 | } 99 | -------------------------------------------------------------------------------- /frontend-service/src/app/apis/firebase/dbFunctions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | addDoc, 3 | collection, 4 | deleteDoc, 5 | doc, 6 | Firestore, 7 | getDoc, 8 | getFirestore, 9 | setDoc, 10 | updateDoc, 11 | } from "firebase/firestore"; 12 | import Firebase from "./Firebase"; 13 | 14 | import AppPermissionException from "./AppPermissionException"; 15 | import { ExistingFirestoreCollection } from "./MTFirestore"; 16 | import { ChargingStation } from "../../../stations/typedefs/ChargingStation"; 17 | import { Transaction } from "../../../transactions/typedefs/Transaction"; 18 | import { IdTokenType } from "ocpp-messages-ts/types/AuthorizeRequest"; 19 | 20 | /** 21 | * CRUD functions 22 | */ 23 | 24 | export type FirestoreCRUDParamsType = { 25 | firebase: Firebase; 26 | db?: Firestore | null; 27 | record: ChargingStation | Transaction | IdTokenType; 28 | collectionName: string; 29 | }; 30 | 31 | export async function createRecordInDB( 32 | firestoreRecordParams: FirestoreCRUDParamsType, 33 | id: string 34 | ): Promise { 35 | const { firebase, db, record, collectionName } = firestoreRecordParams; 36 | try { 37 | const db = getFirestore(firebase.app); 38 | if (db) { 39 | console.log("db: ", { ...db }); 40 | console.log("new record: ", { ...record }); 41 | const response: any = await setDoc(doc(db, collectionName, id), record); 42 | console.log("response: ", { ...response }); 43 | } else { 44 | throw new AppPermissionException("DB IS NULL"); 45 | } 46 | } catch (err) { 47 | console.error( 48 | "Failed to create " + collectionName + " in DB, error: ", 49 | err 50 | ); 51 | return false; 52 | } 53 | return true; 54 | } 55 | 56 | export async function createRecordInDBWithoutId( 57 | firestoreRecordParams: FirestoreCRUDParamsType 58 | ): Promise { 59 | const { firebase, db, record, collectionName } = firestoreRecordParams; 60 | try { 61 | const db = getFirestore(firebase.app); 62 | if (db) { 63 | console.log("db: ", { ...db }); 64 | console.log("new record: ", { ...record }); 65 | const response: any = await addDoc( 66 | collection(db, collectionName), 67 | record 68 | ); 69 | const newId = (await getDoc(response)).id; 70 | const response2: any = await setDoc(doc(db, collectionName, newId), { 71 | ...record, 72 | Id: newId, 73 | }); 74 | console.log("response: ", { ...response }); 75 | } else { 76 | throw new AppPermissionException("DB IS NULL"); 77 | } 78 | } catch (err) { 79 | console.error( 80 | "Failed to create " + collectionName + " in DB, error: ", 81 | err 82 | ); 83 | return false; 84 | } 85 | return true; 86 | } 87 | 88 | export async function updateRecordInDB( 89 | firestoreRecordParams: FirestoreCRUDParamsType, 90 | id: string 91 | ): Promise { 92 | const { firebase, record, collectionName } = firestoreRecordParams; 93 | try { 94 | const db = getFirestore(firebase.app); 95 | const response: any = await setDoc(doc(db, collectionName, id), record); 96 | } catch (err) { 97 | console.error( 98 | "Failed to update " + collectionName + " in DB, error: ", 99 | err 100 | ); 101 | return false; 102 | } 103 | return true; 104 | } 105 | 106 | export async function deleteRecordInDB( 107 | firestoreRecordParams: FirestoreCRUDParamsType, 108 | id: string 109 | ): Promise { 110 | const { firebase, record, collectionName } = firestoreRecordParams; 111 | try { 112 | const db = getFirestore(firebase.app); 113 | const response: any = await deleteDoc(doc(db, collectionName, id)); 114 | } catch (err) { 115 | console.error( 116 | "Failed to delete " + collectionName + " in DB, error:" + err 117 | ); 118 | return false; 119 | } 120 | return true; 121 | } 122 | -------------------------------------------------------------------------------- /transaction-service/ocpphandlers/TransactionEventHandler.go: -------------------------------------------------------------------------------- 1 | package ocpphandlers 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gregszalay/ocpp-csms-common-types/QueuedMessage" 7 | "github.com/gregszalay/ocpp-csms/transaction-service/db" 8 | "github.com/gregszalay/ocpp-csms/transaction-service/publishing" 9 | "github.com/gregszalay/ocpp-messages-go/types/TransactionEventResponse" 10 | 11 | "github.com/gregszalay/ocpp-messages-go/types/TransactionEventRequest" 12 | "github.com/sanity-io/litter" 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | func TransactionEventHandler(request_json []byte, messageId string, device_id string) { 17 | 18 | var req TransactionEventRequest.TransactionEventRequestJson 19 | payload_unmarshal_err := req.UnmarshalJSON(request_json) 20 | if payload_unmarshal_err != nil { 21 | fmt.Printf("Failed to unmarshal TransactionEventRequest message payload. Error: %s", payload_unmarshal_err) 22 | } else { 23 | if log.GetLevel() == log.DebugLevel { 24 | log.Info("Payload as an OBJECT: ") 25 | litter.Dump(req) 26 | } 27 | } 28 | 29 | switch req.EventType { 30 | case TransactionEventRequest.TransactionEventEnumType_1_Started: 31 | db_id := req.TransactionInfo.TransactionId 32 | db.CreateTransaction(db_id, db.Transaction{ 33 | StationId: device_id, 34 | EnergyTransferInProgress: true, 35 | EnergyTransferStarted: req.Timestamp, 36 | EnergyTransferStopped: "", 37 | MeterValues: req.MeterValue, 38 | }) 39 | case TransactionEventRequest.TransactionEventEnumType_1_Updated: 40 | currentTx, err := db.GetTransaction(req.TransactionInfo.TransactionId) 41 | if err != nil { 42 | log.Error("failed to get previous transaction info from db, error: ", err) 43 | return 44 | } 45 | if currentTx == nil { 46 | log.Error("currentTx is a nil pointer ") 47 | return 48 | } 49 | originalMeterValues := currentTx.MeterValues 50 | latestMeterValues := req.MeterValue 51 | newMeterValues := append(originalMeterValues, latestMeterValues...) 52 | db_id := req.TransactionInfo.TransactionId 53 | db.UpdateTransaction(db_id, db.Transaction{ 54 | StationId: device_id, 55 | EnergyTransferInProgress: true, 56 | EnergyTransferStarted: currentTx.EnergyTransferStarted, 57 | EnergyTransferStopped: currentTx.EnergyTransferStopped, 58 | MeterValues: newMeterValues, 59 | }) 60 | case TransactionEventRequest.TransactionEventEnumType_1_Ended: 61 | currentTx, err := db.GetTransaction(req.TransactionInfo.TransactionId) 62 | if err != nil { 63 | log.Error("failed to get previous transaction info from db, error: ", err) 64 | return 65 | } 66 | if currentTx == nil { 67 | log.Error("currentTx is a nil pointer ") 68 | return 69 | } 70 | originalMeterValues := currentTx.MeterValues 71 | latestMeterValues := req.MeterValue 72 | newMeterValues := append(originalMeterValues, latestMeterValues...) 73 | db_id := req.TransactionInfo.TransactionId 74 | db.UpdateTransaction(db_id, db.Transaction{ 75 | StationId: device_id, 76 | EnergyTransferInProgress: false, 77 | EnergyTransferStarted: currentTx.EnergyTransferStarted, 78 | EnergyTransferStopped: req.Timestamp, 79 | MeterValues: newMeterValues, 80 | }) 81 | } 82 | 83 | resp := TransactionEventResponse.TransactionEventResponseJson{ 84 | UpdatedPersonalMessage: &TransactionEventResponse.MessageContentType{ 85 | Format: TransactionEventResponse.MessageFormatEnumTypeUTF8, 86 | Content: "Charging is in progress, your current bill is $5.00", 87 | }, 88 | } 89 | 90 | qm := QueuedMessage.QueuedMessage{ 91 | MessageId: messageId, 92 | DeviceId: device_id, 93 | Payload: resp, 94 | } 95 | 96 | if err := publishing.Publish("TransactionEventResponse", qm); err != nil { 97 | log.Error("failed to publish TransactionEventResponse") 98 | log.Error(err) 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /websocket-service/messagemux/ProcessAndPublish.go: -------------------------------------------------------------------------------- 1 | package messagemux 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/gregszalay/ocpp-csms-common-types/QueuedError" 7 | "github.com/gregszalay/ocpp-csms-common-types/QueuedMessage" 8 | "github.com/gregszalay/ocpp-csms/websocket-service/publishing" 9 | "github.com/gregszalay/ocpp-messages-go/wrappers" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | var calls_awaiting_response map[string]wrappers.CALL = map[string]wrappers.CALL{} 14 | 15 | func ProcessAndPublish(stationId string, message []byte) error { 16 | messageTypeId, err := parseMessageTypeId(stationId, message) 17 | if err != nil { 18 | log.Error("could not parse message type id") 19 | return err 20 | } 21 | 22 | switch messageTypeId { 23 | case wrappers.CALL_TYPE: 24 | return process_as_CALL(stationId, message) 25 | case wrappers.CALLRESULT_TYPE: 26 | return process_as_CALLRESULT(stationId, message) 27 | case wrappers.CALLERROR_TYPE: 28 | return process_as_CALLERROR(stationId, message) 29 | } 30 | return nil 31 | } 32 | 33 | // Processes the the incoming message (the receiver type) as a CALL message 34 | func process_as_CALL(stationId string, message []byte) error { 35 | var call wrappers.CALL 36 | err := call.UnmarshalJSON([]byte(message)) 37 | if err != nil { 38 | log.Error("failed to unmarshal OCPP CALL message. Error: %s", err) 39 | return err 40 | } 41 | qm := QueuedMessage.QueuedMessage{ 42 | MessageId: call.MessageId, 43 | DeviceId: stationId, 44 | Payload: call.Payload, 45 | } 46 | call_topic := call.Action + "Request" 47 | // We publish the CALLRESULT to the relevant pupbsub topic 48 | if err := publishing.Publish(call_topic, qm); err != nil { 49 | log.Error(err) 50 | } 51 | return nil 52 | } 53 | 54 | // Processes the the incoming message (the receiver type) as a CALLRESULT message 55 | // These are messages that are repsonses to CALLs we have sent out to the charging station 56 | func process_as_CALLRESULT(stationId string, message []byte) error { 57 | var callresult wrappers.CALLRESULT 58 | err := callresult.UnmarshalJSON(message) 59 | if err != nil { 60 | log.Error("failed to unmarshal OCPP CALLRESULT message. Error: ", err) 61 | } 62 | qm := QueuedMessage.QueuedMessage{ 63 | MessageId: callresult.MessageId, 64 | DeviceId: stationId, 65 | Payload: callresult.Payload, 66 | } 67 | // We retrieve the original CALL message that we previously sent out to the charging station 68 | // so that we know which topic to put the response in 69 | original_call_message := calls_awaiting_response[callresult.MessageId] 70 | callresult_topic := original_call_message.Action + "Response" 71 | // We publish the CALLRESULT to the relevant upbsub topic 72 | if err := publishing.Publish(callresult_topic, qm); err != nil { 73 | log.Error(err) 74 | } 75 | return nil 76 | } 77 | 78 | // Processes the the incoming message (the receiver type) as a CALLERROR message 79 | func process_as_CALLERROR(stationId string, message []byte) error { 80 | var callerror wrappers.CALLERROR 81 | err := callerror.UnmarshalJSON([]byte(message)) 82 | if err != nil { 83 | log.Error("failed to unmarshal OCPP CALLERROR message. Error: %s", err) 84 | return err 85 | } 86 | qm := QueuedError.QueuedError{ 87 | MessageId: callerror.MessageId, 88 | DeviceId: stationId, 89 | ErrorCode: callerror.ErrorCode, 90 | ErrorDescription: callerror.ErrorDescription, 91 | ErrorDetails: callerror.ErrorDetails, 92 | } 93 | // We retrieve the original CALL message that we previously sent out to the charging station 94 | // so that we know which topic to put the error response in 95 | original_call_message := calls_awaiting_response[callerror.MessageId] 96 | callerror_topic := original_call_message.Action + "Error" 97 | // We publish the CALLRESULT to the relevant upbsub topic 98 | if err := publishing.Publish(callerror_topic, qm); err != nil { 99 | log.Error(err) 100 | } 101 | return nil 102 | 103 | } 104 | 105 | func parseMessageTypeId(stationId string, message []byte) (int, error) { 106 | var data []interface{} 107 | err := json.Unmarshal([]byte(message), &data) 108 | if err != nil { 109 | log.Error("could not unmarshal json", err) 110 | return 0, err 111 | } 112 | messageTypeId, ok := data[0].(float64) 113 | if !ok { 114 | log.Error("data[0] is not a uint8", err) 115 | return 0, err 116 | } 117 | return int(messageTypeId), nil 118 | } 119 | -------------------------------------------------------------------------------- /frontend-service/src/transactions/util-components/Item.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { ReactElement, useState } from "react"; 3 | 4 | import Typography from "@mui/material/Typography"; 5 | import { CircularProgress, Grid, Stack } from "@mui/material"; 6 | 7 | import Card from "@mui/material/Card"; 8 | import CardActions from "@mui/material/CardActions"; 9 | import CardContent from "@mui/material/CardContent"; 10 | import Button from "@mui/material/Button"; 11 | 12 | import { Transaction } from "../typedefs/Transaction"; 13 | import { MeterValueType } from "ocpp-messages-ts/types/TransactionEventRequest"; 14 | import { Unsubscribe } from "@mui/icons-material"; 15 | import appTheme from "../../app/theme/AppTheme"; 16 | 17 | 18 | /***************************************************************************/ 19 | 20 | interface Props { 21 | transaction: Transaction; 22 | } 23 | 24 | /***************************************************************************/ 25 | 26 | export default function TransactionItem(props: Props) { 27 | let meterValues = props.transaction.meterValues; 28 | let meterValuesLen = meterValues.length; 29 | let meterValuesLast: MeterValueType = meterValues[meterValuesLen - 1]; 30 | 31 | return ( 32 | 40 | 41 | 50 | 51 | 52 | {"Charging Station ID: "} 53 | 58 | {" "} 59 | {props.transaction.stationId} 60 | 61 | 62 | 63 | {"Energy Transfer In Progress: "} 64 | 69 | {" "} 70 | {props.transaction.energyTransferInProgress ? "TRUE" : "FALSE"} 71 | 72 | 73 | 74 | 75 | {"Energy Transfer Started: "} 76 | 81 | {" "} 82 | {props.transaction.energyTransferStarted} 83 | 84 | 85 | 86 | {"Energy Transfer Stopped: "} 87 | 92 | {" "} 93 | {props.transaction.energyTransferStopped} 94 | 95 | 96 | 97 | 98 | {meterValuesLast.sampledValue[0].measurand + ": "} 99 | 100 | 105 | {" "} 106 | {meterValuesLast.sampledValue[0].value} 107 | {meterValuesLast.sampledValue[0].unitOfMeasure?.unit} 108 | 109 | 110 | 111 | 112 | {meterValuesLast.sampledValue[1].measurand + ": "} 113 | 114 | 119 | {" "} 120 | {meterValuesLast.sampledValue[1].value} 121 | {meterValuesLast.sampledValue[1].unitOfMeasure?.unit} 122 | 123 | 124 | 125 | 126 | 127 | 128 | ); 129 | } 130 | -------------------------------------------------------------------------------- /frontend-service/src/dashboard/DashboardScreen.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { createTheme, ThemeProvider } from "@mui/material/styles"; 3 | import { makeStyles } from "@mui/styles"; 4 | import useMediaQuery from "@mui/material/useMediaQuery"; 5 | import CssBaseline from "@mui/material/CssBaseline"; 6 | import Box from "@mui/material/Box"; 7 | import Navigator from "./util-components/Navigator"; 8 | import DnsRoundedIcon from "@mui/icons-material/DnsRounded"; 9 | //import theme from "./AppTheme"; 10 | //import { BrowserRouter, Switch, Route } from 'react-router-dom'; 11 | import PeopleIcon from "@mui/icons-material/People"; 12 | import PermMediaOutlinedIcon from "@mui/icons-material/PhotoSizeSelectActual"; 13 | import PublicIcon from "@mui/icons-material/Public"; 14 | import SettingsEthernetIcon from "@mui/icons-material/SettingsEthernet"; 15 | import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputComponent"; 16 | import TimerIcon from "@mui/icons-material/Timer"; 17 | import SettingsIcon from "@mui/icons-material/Settings"; 18 | import PhonelinkSetupIcon from "@mui/icons-material/PhonelinkSetup"; 19 | import EvStationTwoToneIcon from "@mui/icons-material/EvStationTwoTone"; 20 | import StationList from "../stations/StationsScreen"; 21 | import Button from "@mui/material/Button"; 22 | import { Container, Paper } from "@mui/material"; 23 | import { AnalyticsTwoTone } from "@mui/icons-material"; 24 | import { Outlet } from "react-router-dom"; 25 | import Header from "./util-components/Header"; 26 | import theme from "../app/theme/AppTheme"; 27 | 28 | const drawerWidth = 120; 29 | 30 | interface Props { 31 | selectedMenuItem_fromContext: MenuItem; 32 | handleMenuClick_fromContext: () => void; 33 | theme: any; 34 | menu: any[]; 35 | isSmUp: boolean; 36 | } 37 | 38 | function DashboardScreen(props: Props) { 39 | const [mobileOpen, setMobileOpen] = React.useState(false); 40 | const isSmUp = useMediaQuery(props.theme.breakpoints.up("lg")); 41 | //const isSmUp = useMediaQuery("(max-width:899px)"); 42 | 43 | 44 | const [categories, setCategories] = React.useState(props.menu); 45 | 46 | const handleDrawerToggle = () => { 47 | setMobileOpen(!mobileOpen); 48 | }; 49 | 50 | React.useEffect(() => { 51 | console.log("props.isSmUp: " + isSmUp); 52 | console.log("mobileOpen: " + mobileOpen); 53 | }); 54 | //const classes = useStyles(appTheme); 55 | 56 | return ( 57 | 65 | 76 | {isSmUp ? null : ( 77 | 87 | )} 88 | 99 | 100 | 101 | 112 |
117 | 122 | 123 | 124 |
125 |
126 | ); 127 | } 128 | 129 | export default DashboardScreen; 130 | -------------------------------------------------------------------------------- /frontend-service/src/dashboard/util-components/Navigator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Divider from "@mui/material/Divider"; 3 | import Drawer, { DrawerProps } from "@mui/material/Drawer"; 4 | import List from "@mui/material/List"; 5 | import Box from "@mui/material/Box"; 6 | import ListItem from "@mui/material/ListItem"; 7 | import ListItemButton from "@mui/material/ListItemButton"; 8 | import ListItemIcon from "@mui/material/ListItemIcon"; 9 | import ListItemText from "@mui/material/ListItemText"; 10 | import { AppBar, Button, Stack, Typography } from "@mui/material"; 11 | import { useLocation, useNavigate } from "react-router-dom"; 12 | import Copyright from "./Copyright"; 13 | import theme from "../../app/theme/AppTheme"; 14 | import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon"; 15 | 16 | const content = { 17 | dashboardHeader1: "OCPP", 18 | dashboardHeader2: "Web", 19 | }; 20 | 21 | const classes = { 22 | listHeaders: { 23 | py: 2, 24 | px: 3, 25 | color: "#0000000", 26 | }, 27 | myDrawer: { 28 | height: "100vh", 29 | //background: theme.palette.primary.dark, 30 | //boxShadow: "16px", 31 | }, 32 | myStack: { height: "100vh", background: theme.palette.secondary.main }, 33 | item: { 34 | py: "0px", 35 | px: 4, 36 | /*color: '#60666b',*/ 37 | "&:hover, &:focus": { 38 | bgcolor: "rgba(155, 155, 255, 0.08)", 39 | }, 40 | }, 41 | itemCategory: { 42 | boxShadow: "0 -1px 0 rgb(155,255,255,0.1) inset", 43 | py: 0, 44 | px: 0, 45 | }, 46 | divider: { mt: 2 }, 47 | }; 48 | 49 | interface Props { 50 | PaperProps?: any; 51 | variant?: any; 52 | open?: boolean; 53 | onClose?: () => void; 54 | theme?: any; 55 | categories: Menu[]; 56 | selectedMenuItem_passedDown: MenuItem; 57 | handleMenuClick_passedDown: (menuItem: MenuItem) => void; 58 | sx?: any; 59 | } 60 | 61 | export default function Navigator(props: Props) { 62 | const { ...other } = props; 63 | const myCategories: Menu[] = props.categories; 64 | const navigate = useNavigate(); 65 | const location = useLocation(); 66 | 67 | return ( 68 | 69 | 81 | 87 | 94 | {content.dashboardHeader1} 95 | 96 | 103 | {content.dashboardHeader2} 104 | 105 | 106 | 107 | 112 | 113 | 116 | {myCategories.map(({ headerId, children }) => ( 117 | 118 | 119 | {headerId} 120 | 121 | {children.map(({ id: name, icon, label, route }) => ( 122 | 123 | { 127 | navigate(location.pathname.split("/")[0] + route); 128 | props.handleMenuClick_passedDown({ 129 | id: name, 130 | label, 131 | icon, 132 | route, 133 | }); 134 | }} 135 | > 136 | {icon} 137 | {name} 138 | 139 | 140 | ))} 141 | 142 | 143 | ))} 144 | 145 | 146 | 147 | 148 | 149 | ); 150 | } 151 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OCPP CSMS (OCPP 2.0.1.) 2 | 3 | Charging station Management system for electric vehicle charging stations. 4 | 5 | Based on: OCPP-2.0.1 (Full protocol documentation: 6 | [openchargealliance.org/downloads](https://www.openchargealliance.org/downloads/)) 7 | 8 | **_ Under development _** 9 | 10 | ## Working principle 11 | 12 | E.g. incoming BootNotificationRequest: 13 | 14 | ==> REQUEST data flow: 15 | 16 | charging station -> websocket-service -> GCP Pub/Sub topic "BootNotificationRequest" -> device-service 17 | 18 | <== RESPONSE data flow: 19 | 20 | charging station <- websocket-service <- GCP Pub/Sub topic "BootNotificationResponse" <- device-service 21 | 22 | ## Functional features: 23 | 24 | - Implemented OCPP messages: 25 | 26 | | Implemented OCPP Messages | Microservice name | 27 | | ----------------------------------------------------- | ------------------- | 28 | | BootNotificationRequest, BootNotificationResponse | device-service | 29 | | HeartbeatRequest, HeartbeatResponse | device-service | 30 | | StatusNotificationRequest, StatusNotificationResponse | device-service | 31 | | TransactionEventRequest, TransactionEventResponse | transaction-service | 32 | | AuthorizeRequest, AuthorizeResponse | user-service | 33 | 34 | ## Technical features: 35 | 36 | - microservice-based architecture 37 | - GO: managing multiple websocket connections easily with goroutines 38 | - Mutual TLS authentication 39 | - Google Firestore for simple data persistence 40 | - Google Cloud Pub/Sub for async messaging between services (using [watermill.io](https://github.com/ThreeDotsLabs/watermill)) 41 | - REST API for managing charging station information (API description: [swaggerhub.com](https://app.swaggerhub.com/apis/gregszalay/ocpp_device_service/v2.0.0)) 42 | 43 | ## Quick Start 44 | 45 | 1. Create a **Google Cloud Platform project**. Follow the [Google guide](https://cloud.google.com/resource-manager/docs/creating-managing-projects). 46 | 47 | 2. Create a **service account** and create **JSON credentials** for it. Follow the [Google guide](https://developers.google.com/workspace/guides/create-credentials) (see the _"Service account credentials"_ section). When you create the service account, under "select a role" choose `Pub/Sub Admin` and `Firebase Admin` (note: for production use, you may want to restrict these to lower roles, needs to be tested). 48 | 49 | 3. Once you have the JSON credentials, place them in the following directories, under the name 'PRIVATE.json': 50 | 51 | - device-service/credentials/PRIVATE.json 52 | - websocket-service/credentials/PRIVATE.json 53 | - user-service/credentials/PRIVATE.json 54 | - transaction-service/credentials/PRIVATE.json 55 | 56 | > Note: you can use the same service account and the same JSON credentials file in all places, or you can create separate ones for extra security if you want 57 | 58 | 4. Create a Cloud Firestore database Follow the [Google guide](https://firebase.google.com/docs/firestore/quickstart) 59 | 60 | 5. In the **docker-compose.yml** file, replace `chargerevolutioncloud` name with the **project id** of your own GCP project. 61 | 62 | 6. Install docker and docker-compose if you haven't already 63 | 64 | > If you are deploying on a remote machine or VPS, **open** up port `3000` and port `5000` so you can access the app remotely from your own machine. 65 | > Important: For actual deployment you should add a reverse proxy layer or some other security measure to protect your open port. A good place to start: [How To Deploy a Go Web Application with Docker and Nginx on Ubuntu 18.04](https://www.digitalocean.com/community/tutorials/how-to-deploy-a-go-web-application-with-docker-and-nginx-on-ubuntu-18-04) 66 | 67 | 7. Build and run the app on your machine (or VPS) 68 | 69 | docker compose up --build 70 | 71 | 8. The device service should now be running on `localhost:5000`. Use Postman or some other tool to send a post request and create a charging station (API description: [swaggerhub.com](https://app.swaggerhub.com/apis/gregszalay/ocpp_device_service/v2.0.0)) For example, you can send something like this in the request payload: 72 | 73 | POST: `localhost:5000/chargingstations/create` 74 | 75 | { 76 | "id": "CS123", 77 | "serialNumber": "5", 78 | "model": "CS-5500", 79 | "vendorName": "ChargerMaker Inc.", 80 | "firmwareVersion": "1.8", 81 | "modem": { 82 | "iccid": "24", 83 | "imsi": "24" 84 | }, 85 | "location": { 86 | "lat": 41.366446, 87 | "lng": -38.1854651 88 | } 89 | } 90 | 91 | If this is successful, a collection named _"chargingstations"_ and a document named _"CS123"_ should have been created in the Firestore database of your project. 92 | 93 | 9. The websocket service should be running on `localhost:3000`. Connect to port 3000 as a websocket client (use the station id you have created in the previous step): 94 | 95 | `ws://{HOST}:3000/ocpp/{stationid}` 96 | 97 | > Tip: Postman now supports websocket connections (beta version), but only on the Windows desktop app I believe. This could be useful for testing. 98 | 99 | ### To-do 100 | 101 | - full implementation of all basic messages (reset, getconfig etc.) 102 | -------------------------------------------------------------------------------- /device-service/http/api/swagger.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: OCPP Device Service 4 | description: REST API to manage OCPP devices (e.g. Charging Stations) 5 | termsOfService: "" 6 | contact: 7 | email: gr.szalay@gmail.com 8 | version: v2.0.0 9 | servers: 10 | - url: https://virtserver.swaggerhub.com/gregszalay/ocpp_device_service/v2.0.0 11 | description: SwaggerHub API Auto Mocking 12 | - url: / 13 | tags: 14 | - name: Get Charging Station 15 | description: Get a single charging station in the database. 16 | - name: List Charging Stations 17 | description: List all charging stations in the database. 18 | - name: Create Charging Station 19 | description: Create a charging station in the database. 20 | - name: Update Charging Station 21 | description: Update a charging station in the database. 22 | - name: Delete Charging Station 23 | description: Delete a charging station in the database. 24 | paths: 25 | /chargingstations/station/{id}: 26 | get: 27 | tags: 28 | - Get Charging Station 29 | summary: Gets charging station data based on id 30 | description: Get charging station data 31 | operationId: GetChargingStation 32 | parameters: 33 | - name: id 34 | in: path 35 | description: "" 36 | required: true 37 | style: simple 38 | explode: false 39 | schema: 40 | $ref: '#/components/schemas/ChargingStationId' 41 | responses: 42 | "200": 43 | description: successful operation 44 | content: 45 | application/json: 46 | schema: 47 | $ref: '#/components/schemas/ChargingStation' 48 | "400": 49 | description: Invalid status value 50 | /chargingstations/list: 51 | get: 52 | tags: 53 | - List Charging Stations 54 | summary: Get array of all charging stations 55 | description: Get charging station data 56 | operationId: GetAllChargingStations 57 | responses: 58 | "200": 59 | description: successful operation 60 | content: 61 | application/json: 62 | schema: 63 | $ref: '#/components/schemas/ChargingStationList' 64 | "400": 65 | description: Invalid status value 66 | /chargingstations/create: 67 | post: 68 | tags: 69 | - Create Charging Station 70 | summary: Adds a new charger in db 71 | operationId: CreateChargingStation 72 | requestBody: 73 | description: Creates a new charging station in the db 74 | content: 75 | application/json: 76 | schema: 77 | $ref: '#/components/schemas/ChargingStation' 78 | required: true 79 | responses: 80 | "200": 81 | description: Successful registration of new device 82 | content: 83 | application/json: 84 | schema: 85 | $ref: '#/components/schemas/ChargingStationId' 86 | "400": 87 | description: Unsuccessful creation 88 | /chargingstations/update/{id}: 89 | post: 90 | tags: 91 | - Update Charging Station 92 | summary: Updates a charging station in the database 93 | operationId: UpdateChargingStation 94 | parameters: 95 | - name: id 96 | in: path 97 | description: "" 98 | required: true 99 | style: simple 100 | explode: false 101 | schema: 102 | $ref: '#/components/schemas/ChargingStationId' 103 | requestBody: 104 | description: Updates charging station 105 | content: 106 | application/json: 107 | schema: 108 | $ref: '#/components/schemas/ChargingStation' 109 | required: true 110 | responses: 111 | "200": 112 | description: Successful registration of new charging station 113 | content: 114 | application/json: 115 | schema: 116 | $ref: '#/components/schemas/ChargingStationId' 117 | "400": 118 | description: Unsuccessful registration of new charging station 119 | /chargingstations/delete/{id}: 120 | post: 121 | tags: 122 | - Delete Charging Station 123 | summary: Deletes a charger in db 124 | operationId: DeleteChargingStation 125 | parameters: 126 | - name: id 127 | in: path 128 | description: "" 129 | required: true 130 | style: simple 131 | explode: false 132 | schema: 133 | $ref: '#/components/schemas/ChargingStationId' 134 | responses: 135 | "200": 136 | description: Successful deletion of charging station 137 | content: 138 | application/json: 139 | schema: 140 | $ref: '#/components/schemas/ChargingStation' 141 | "400": 142 | description: Unsuccessful deletion 143 | components: 144 | schemas: 145 | ChargingStation: 146 | required: 147 | - id 148 | - model 149 | - vendorName 150 | type: object 151 | properties: 152 | id: 153 | $ref: '#/components/schemas/ChargingStationId' 154 | serialNumber: 155 | type: string 156 | description: OCPP - Optional. Vendor-specific device identifier. 157 | example: "24" 158 | model: 159 | type: string 160 | description: OCPP - Required. Defines the model of the device. 161 | example: GT-5000 162 | vendorName: 163 | type: string 164 | description: OCPP - Required. Identifies the vendor (not necessarily in 165 | a unique manner). 166 | example: ChargingStationMaker Inc. 167 | firmwareVersion: 168 | type: string 169 | description: OCPP - Optional. This contains the firmware version of the 170 | Charging Station. 171 | example: "2.8" 172 | modem: 173 | $ref: '#/components/schemas/ChargingStation_modem' 174 | location: 175 | $ref: '#/components/schemas/ChargingStation_location' 176 | lastBoot: 177 | type: string 178 | description: Date and time of last BootNotification received. As defined 179 | by date-time - RFC3339 180 | format: date-time 181 | example: 182 | serialNumber: "24" 183 | lastBoot: 2000-01-23T04:56:07.000+00:00 184 | modem: 185 | iccid: "24" 186 | imsi: "24" 187 | model: GT-5000 188 | location: 189 | lng: -71.1854651 190 | lat: 42.366446 191 | id: id 192 | vendorName: ChargingStationMaker Inc. 193 | firmwareVersion: "2.8" 194 | ChargingStationList: 195 | type: array 196 | items: 197 | $ref: '#/components/schemas/ChargingStation' 198 | x-schema-name: ChargingStationList 199 | ChargingStationId: 200 | type: string 201 | ChargingStation_modem: 202 | type: object 203 | properties: 204 | iccid: 205 | type: string 206 | description: OCPP - Optional. This contains the ICCID of the modem’s SIMcard. 207 | example: "24" 208 | imsi: 209 | type: string 210 | description: OCPP - Optional. This contains the IMSI of the modem’s SIM 211 | card. 212 | example: "24" 213 | example: 214 | iccid: "24" 215 | imsi: "24" 216 | ChargingStation_location: 217 | type: object 218 | properties: 219 | lat: 220 | type: number 221 | example: 42.366446 222 | lng: 223 | type: number 224 | example: -71.1854651 225 | example: 226 | lng: -71.1854651 227 | lat: 42.366446 228 | -------------------------------------------------------------------------------- /frontend-service/src/app/App.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeProvider } from "@mui/material/styles"; 2 | import { Box, CssBaseline, useMediaQuery } from "@mui/material"; 3 | 4 | import dashboardMenu from "../dashboard/constants/dashboardMenu"; 5 | import appTheme from "./theme/AppTheme"; 6 | 7 | import React, { useRef, useState } from "react"; 8 | import appRoutes from "./routing/appRoutes"; 9 | import SelectedMenuItemContext from "./contexts/SelectedMenuItem"; 10 | import GeneralRouteTreeMT from "./routing/GeneralRouteTreeMT"; 11 | import FirebaseDataContext from "./contexts/FirebaseData"; 12 | import { 13 | collection, 14 | doc, 15 | Firestore, 16 | getDoc, 17 | getDocs, 18 | getDocsFromServer, 19 | getFirestore, 20 | limit, 21 | onSnapshot, 22 | orderBy, 23 | query, 24 | where, 25 | } from "firebase/firestore"; 26 | import { setUpSnapshotListener } from "./apis/firebase/dbUtils"; 27 | import { User, UserCredential } from "firebase/auth"; 28 | import Firebase from "./apis/firebase/Firebase"; 29 | import { ExistingFirestoreCollection } from "./apis/firebase/MTFirestore"; 30 | import * as firebaseAuthApi from "firebase/auth"; 31 | import { 32 | createRecordInDB, 33 | createRecordInDBWithoutId, 34 | deleteRecordInDB, 35 | updateRecordInDB, 36 | } from "./apis/firebase/dbFunctions"; 37 | import { startTime } from ".."; 38 | import AppPermissionException from "./apis/firebase/AppPermissionException"; 39 | 40 | import { IdTokenType } from "ocpp-messages-ts/types/AuthorizeRequest"; 41 | import { Transaction } from "../transactions/typedefs/Transaction"; 42 | import { ChargingStation } from "../stations/typedefs/ChargingStation"; 43 | 44 | /***************************************************************************/ 45 | 46 | interface Props { 47 | firebase: Firebase; 48 | } 49 | 50 | /***************************************************************************/ 51 | 52 | function App(props: Props) { 53 | console.log("function App called"); 54 | 55 | const [selectedMenuItem, setSelectedMenuItem] = React.useState(() => { 56 | let mainIndex = 0; 57 | let childIndex = 0; 58 | dashboardMenu.forEach((mainItem, index1) => { 59 | mainItem.children.forEach((childItem, index2) => { 60 | if (window.location.href.includes(childItem.route)) { 61 | mainIndex = index1; 62 | childIndex = index2; 63 | } 64 | }); 65 | }); 66 | return dashboardMenu[mainIndex].children[childIndex]; 67 | }); 68 | const [authUser, setAuthUser] = useState(null); 69 | 70 | const [userInfo, setUserInfo] = useState(null); 71 | const [db, setDb] = useState(null); 72 | const [stations, setStations] = useState([]); 73 | const [transactions, setTransactions] = useState([]); 74 | const [chargetokens, setChargeTokens] = useState([]); 75 | 76 | const isSmUp = useMediaQuery(appTheme.breakpoints.up("md")); 77 | 78 | const handleMenuClick = (newMenuItem: MenuItem) => { 79 | setSelectedMenuItem(newMenuItem); 80 | }; 81 | //Wait for db and userInfo to arrive (it does not load on component load right away) 82 | 83 | console.log(`App.tsx first render - elapsed: ${Date.now() - startTime} ms`); 84 | 85 | if (props.firebase.userInfo) { 86 | console.log( 87 | `App.tsx userInfo arrived - elapsed: ${Date.now() - startTime} ms` 88 | ); 89 | } 90 | if (props.firebase.app) { 91 | console.log( 92 | `App.tsx firt app obj arrived - elapsed: ${Date.now() - startTime} ms` 93 | ); 94 | } 95 | 96 | React.useEffect(() => { 97 | const timeOutID = setTimeout(() => { 98 | setUserInfo(props.firebase.userInfo); 99 | setDb(getFirestore(props.firebase.app)); 100 | }, 1000); 101 | return () => clearTimeout(timeOutID); 102 | }); 103 | 104 | React.useEffect(() => { 105 | if (!db || !userInfo || !userInfo.email) { 106 | console.log( 107 | "=> useffect STATIONS -------!db || !userInfo || !userInfo.email " 108 | ); 109 | return; 110 | } 111 | let q = null; 112 | q = query(collection(db, "chargingstations")); 113 | return setUpSnapshotListener( 114 | (resultItems: ChargingStation[]) => { 115 | console.log("=> stationResults"); 116 | console.log("=> ", [...resultItems]); 117 | setStations(resultItems); 118 | }, 119 | "chargingstations", 120 | q, 121 | db, 122 | userInfo 123 | ); 124 | }, [db, userInfo]); 125 | 126 | React.useEffect(() => { 127 | if (!db || !userInfo || !(stations.length > 0)) { 128 | console.log( 129 | "=> useffect TRANSACTIONS ------- !db !currentUserRecord ||!userInfo || !currentUserPermissions.transactions || !(stations.length > 0)" 130 | ); 131 | return; 132 | } 133 | let q = null; 134 | q = query(collection(db, "transactions"), orderBy("energyTransferStarted", "desc")); 135 | return setUpSnapshotListener( 136 | (resultItems: Transaction[]) => { 137 | console.log("=> transactionResults"); 138 | console.log("=> ", [...resultItems]); 139 | setTransactions(resultItems); 140 | }, 141 | "transactions", 142 | q, 143 | db, 144 | userInfo 145 | ); 146 | }, [db, userInfo, stations]); 147 | 148 | 149 | React.useEffect(() => { 150 | if (!db || !userInfo) { 151 | console.log( 152 | "=> useffect TOKENS ------- !db !currentUserRecord ||!userInfo || !currentUserPermissions.transactions || !(stations.length > 0)" 153 | ); 154 | return; 155 | } 156 | let q = null; 157 | q = query(collection(db, "idTokens") /*orderBy("status"))*/); 158 | return setUpSnapshotListener( 159 | (resultItems: IdTokenType[]) => { 160 | console.log("=> tokenResults"); 161 | console.log("=> ", [...resultItems]); 162 | setChargeTokens(resultItems); 163 | }, 164 | "idTokens", 165 | q, 166 | db, 167 | userInfo 168 | ); 169 | }, [db, userInfo, transactions]); 170 | 171 | const handleNewToken = async (newToken: IdTokenType, id: string) => { 172 | try { 173 | await createRecordInDB( 174 | { 175 | firebase: props.firebase, 176 | //db: db, 177 | record: newToken, 178 | collectionName: /*ExistingFirestoreCollection.idTokens*/ "idTokens", 179 | }, 180 | id 181 | ); 182 | } catch (err) { 183 | console.error(err); 184 | } 185 | }; 186 | 187 | const handleModifiedToken = async ( 188 | modifiedToken: IdTokenType, 189 | id: string 190 | ) => { 191 | try { 192 | await updateRecordInDB( 193 | { 194 | firebase: props.firebase, 195 | record: modifiedToken, 196 | collectionName: ExistingFirestoreCollection.idTokens, 197 | }, 198 | id 199 | ); 200 | } catch (err) { 201 | console.error(err); 202 | } 203 | }; 204 | 205 | const handleDeletedToken = async (deletedToken: IdTokenType, id: string) => { 206 | try { 207 | await deleteRecordInDB( 208 | { 209 | firebase: props.firebase, 210 | record: deletedToken, 211 | collectionName: ExistingFirestoreCollection.idTokens, 212 | }, 213 | id 214 | ); 215 | } catch (err) { 216 | console.error(err); 217 | } 218 | }; 219 | 220 | return ( 221 | setAuthUser(newAuthUser), 233 | }} 234 | > 235 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | ); 248 | } 249 | 250 | export default App; 251 | -------------------------------------------------------------------------------- /device-service/http/api/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OCPP Device Service 5 | 179 | 180 | 181 |

OCPP Device Service

182 |
REST API to manage OCPP devices (e.g. Charging Stations)
183 |
More information: https://helloreverb.com
184 |
Contact Info: gr.szalay@gmail.com
185 |
Version: v2.0.0
186 |
BasePath:/gregszalay/ocpp_device_service/v2.0.0
187 |
All rights reserved
188 |
http://apache.org/licenses/LICENSE-2.0.html
189 |

Access

190 | 191 |

Methods

192 | [ Jump to Models ] 193 | 194 |

Table of Contents

195 |
196 |

CreateChargingStation

197 | 200 |

DeleteChargingStation

201 | 204 |

GetChargingStation

205 | 208 |

ListChargingStations

209 | 212 |

UpdateChargingStation

213 | 216 | 217 |

CreateChargingStation

218 |
219 |
220 | Up 221 |
post /chargingstations/create
222 |
Adds a new charger in db (createChargingStation)
223 |
224 | 225 | 226 |

Consumes

227 | This API call consumes the following media types via the Content-Type request header: 228 |
    229 |
  • application/json
  • 230 |
231 | 232 |

Request body

233 |
234 |
body ChargingStation (required)
235 | 236 |
Body Parameter — Creates a new charging station in the db
237 |
238 | 239 | 240 | 241 | 242 |

Return type

243 |
244 | ChargingStationId 245 | 246 |
247 | 248 | 249 | 250 |

Example data

251 |
Content-Type: application/json
252 |
""
253 | 254 |

Produces

255 | This API call produces the following media types according to the Accept request header; 256 | the media type will be conveyed by the Content-Type response header. 257 |
    258 |
  • application/json
  • 259 |
260 | 261 |

Responses

262 |

200

263 | Successful registration of new device 264 | ChargingStationId 265 |

400

266 | Unsuccessful creation 267 | 268 |
269 |
270 |

DeleteChargingStation

271 |
272 |
273 | Up 274 |
post /chargingstations/delete/{id}
275 |
Deletes a charger in db (deleteChargingStation)
276 |
277 | 278 |

Path parameters

279 |
280 |
id (required)
281 | 282 |
Path Parameter
283 | 284 | 285 | 286 | 287 | 288 | 289 |

Return type

290 |
291 | ChargingStation 292 | 293 |
294 | 295 | 296 | 297 |

Example data

298 |
Content-Type: application/json
299 |
{
300 |   "serialNumber" : "24",
301 |   "lastBoot" : "2000-01-23T04:56:07.000+00:00",
302 |   "modem" : {
303 |     "iccid" : "24",
304 |     "imsi" : "24"
305 |   },
306 |   "model" : "GT-5000",
307 |   "location" : {
308 |     "lng" : -71.1854651,
309 |     "lat" : 42.366446
310 |   },
311 |   "id" : "id",
312 |   "vendorName" : "ChargingStationMaker Inc.",
313 |   "firmwareVersion" : "2.8"
314 | }
315 | 316 |

Produces

317 | This API call produces the following media types according to the Accept request header; 318 | the media type will be conveyed by the Content-Type response header. 319 |
    320 |
  • application/json
  • 321 |
322 | 323 |

Responses

324 |

200

325 | Successful deletion of charging station 326 | ChargingStation 327 |

400

328 | Unsuccessful deletion 329 | 330 |
331 |
332 |

GetChargingStation

333 |
334 |
335 | Up 336 |
get /chargingstations/station/{id}
337 |
Gets charging station data based on id (getChargingStations)
338 |
Get charging station data
339 | 340 |

Path parameters

341 |
342 |
id (required)
343 | 344 |
Path Parameter
345 | 346 | 347 | 348 | 349 | 350 | 351 |

Return type

352 |
353 | ChargingStation 354 | 355 |
356 | 357 | 358 | 359 |

Example data

360 |
Content-Type: application/json
361 |
{
362 |   "serialNumber" : "24",
363 |   "lastBoot" : "2000-01-23T04:56:07.000+00:00",
364 |   "modem" : {
365 |     "iccid" : "24",
366 |     "imsi" : "24"
367 |   },
368 |   "model" : "GT-5000",
369 |   "location" : {
370 |     "lng" : -71.1854651,
371 |     "lat" : 42.366446
372 |   },
373 |   "id" : "id",
374 |   "vendorName" : "ChargingStationMaker Inc.",
375 |   "firmwareVersion" : "2.8"
376 | }
377 | 378 |

Produces

379 | This API call produces the following media types according to the Accept request header; 380 | the media type will be conveyed by the Content-Type response header. 381 |
    382 |
  • application/json
  • 383 |
384 | 385 |

Responses

386 |

200

387 | successful operation 388 | ChargingStation 389 |

400

390 | Invalid status value 391 | 392 |
393 |
394 |

ListChargingStations

395 |
396 |
397 | Up 398 |
get /chargingstations/list
399 |
Get array of all charging stations (getAllChargingStations)
400 |
Get charging station data
401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 |

Return type

409 |
410 | ChargingStationList 411 | 412 |
413 | 414 | 415 | 416 |

Example data

417 |
Content-Type: application/json
418 |
[ {
419 |   "serialNumber" : "24",
420 |   "lastBoot" : "2000-01-23T04:56:07.000+00:00",
421 |   "modem" : {
422 |     "iccid" : "24",
423 |     "imsi" : "24"
424 |   },
425 |   "model" : "GT-5000",
426 |   "location" : {
427 |     "lng" : -71.1854651,
428 |     "lat" : 42.366446
429 |   },
430 |   "id" : "id",
431 |   "vendorName" : "ChargingStationMaker Inc.",
432 |   "firmwareVersion" : "2.8"
433 | }, {
434 |   "serialNumber" : "24",
435 |   "lastBoot" : "2000-01-23T04:56:07.000+00:00",
436 |   "modem" : {
437 |     "iccid" : "24",
438 |     "imsi" : "24"
439 |   },
440 |   "model" : "GT-5000",
441 |   "location" : {
442 |     "lng" : -71.1854651,
443 |     "lat" : 42.366446
444 |   },
445 |   "id" : "id",
446 |   "vendorName" : "ChargingStationMaker Inc.",
447 |   "firmwareVersion" : "2.8"
448 | } ]
449 | 450 |

Produces

451 | This API call produces the following media types according to the Accept request header; 452 | the media type will be conveyed by the Content-Type response header. 453 |
    454 |
  • application/json
  • 455 |
456 | 457 |

Responses

458 |

200

459 | successful operation 460 | ChargingStationList 461 |

400

462 | Invalid status value 463 | 464 |
465 |
466 |

UpdateChargingStation

467 |
468 |
469 | Up 470 |
post /chargingstations/update/{id}
471 |
Updates a charging station in the database (updateChargingStation)
472 |
473 | 474 |

Path parameters

475 |
476 |
id (required)
477 | 478 |
Path Parameter
479 | 480 |

Consumes

481 | This API call consumes the following media types via the Content-Type request header: 482 |
    483 |
  • application/json
  • 484 |
485 | 486 |

Request body

487 |
488 |
body ChargingStation (required)
489 | 490 |
Body Parameter — Updates charging station
491 |
492 | 493 | 494 | 495 | 496 |

Return type

497 |
498 | ChargingStationId 499 | 500 |
501 | 502 | 503 | 504 |

Example data

505 |
Content-Type: application/json
506 |
""
507 | 508 |

Produces

509 | This API call produces the following media types according to the Accept request header; 510 | the media type will be conveyed by the Content-Type response header. 511 |
    512 |
  • application/json
  • 513 |
514 | 515 |

Responses

516 |

200

517 | Successful registration of new charging station 518 | ChargingStationId 519 |

400

520 | Unsuccessful registration of new charging station 521 | 522 |
523 |
524 | 525 |

Models

526 | [ Jump to Methods ] 527 | 528 |

Table of Contents

529 |
    530 |
  1. ChargingStation
  2. 531 |
  3. ChargingStationId
  4. 532 |
  5. ChargingStationList
  6. 533 |
  7. ChargingStation_location
  8. 534 |
  9. ChargingStation_modem
  10. 535 |
536 | 537 |
538 |

ChargingStation Up

539 | 540 |
541 |
id
542 |
serialNumber (optional)
String OCPP - Optional. Vendor-specific device identifier.
543 |
example: 24
544 |
model
String OCPP - Required. Defines the model of the device.
545 |
example: GT-5000
546 |
vendorName
String OCPP - Required. Identifies the vendor (not necessarily in a unique manner).
547 |
example: ChargingStationMaker Inc.
548 |
firmwareVersion (optional)
String OCPP - Optional. This contains the firmware version of the Charging Station.
549 |
example: 2.8
550 |
modem (optional)
551 |
location (optional)
552 |
lastBoot (optional)
Date Date and time of last BootNotification received. As defined by date-time - RFC3339 format: date-time
553 |
554 |
555 |
556 |

ChargingStationId Up

557 | 558 |
559 |
560 |
561 |
562 |

ChargingStationList Up

563 | 564 |
565 |
566 |
567 |
568 |

ChargingStation_location Up

569 | 570 |
571 |
lat (optional)
572 |
example: 42.366446
573 |
lng (optional)
574 |
example: -71.1854651
575 |
576 |
577 |
578 |

ChargingStation_modem Up

579 | 580 |
581 |
iccid (optional)
String OCPP - Optional. This contains the ICCID of the modem’s SIMcard.
582 |
example: 24
583 |
imsi (optional)
String OCPP - Optional. This contains the IMSI of the modem’s SIM card.
584 |
example: 24
585 |
586 |
587 | 588 | 589 | --------------------------------------------------------------------------------