Add option to serve a static frontend

This commit is contained in:
Oliver Traber 2025-07-27 18:42:31 +00:00
parent b3804f3d58
commit f6117d04ee
Signed by: Bluemedia
GPG key ID: C0674B105057136C
9 changed files with 64 additions and 33 deletions

View file

@ -1,6 +1,6 @@
# Simple OCCP Central System
# LibreCharge
This is a simple implementation of a basic [OCPP](https://openchargealliance.org/protocols/open-charge-point-protocol/) 2.0.1 compliant central system (backend) for EV charging stations.
LibreCharge is a simple implementation of a basic [OCPP](https://openchargealliance.org/protocols/open-charge-point-protocol/) 2.0.1 compliant central system (backend) for EV charging stations.
## Features
@ -12,5 +12,7 @@ This is a simple implementation of a basic [OCPP](https://openchargealliance.org
- ✅ RESTful API
## Tested charging stations
This project has been successfully tested with the following charging stations:
- Alfen Eve Single S-line

3
backend/.gitignore vendored
View file

@ -1,3 +1,4 @@
**/__pycache__
simple-ocpp-cs.db
.env
.env
static

View file

@ -1,6 +1,8 @@
import os
from dotenv import load_dotenv
from fastapi import APIRouter, FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from starlette.middleware.authentication import AuthenticationMiddleware
load_dotenv()
@ -17,25 +19,46 @@ from app.routers import (
)
from app.security.websocket_auth_backend import BasicAuthBackend
def create_ocpp_app():
app_ocpp = FastAPI(
responses={404: {"description": "Not found"}},
)
app_ocpp.include_router(ocpp_v1.router)
app_ocpp.add_middleware(AuthenticationMiddleware, backend=BasicAuthBackend())
return app_ocpp
def create_app():
app = FastAPI(
title="simple-ocpp-cs",
version="0.2.0",
summary="Simple implementation of a basic OCPP 2.0.1 compliant central system (backend) for EV charging stations",
responses={404: {"description": "Not found"}},
# Common app config
title="LibreCharge"
version="0.2.0"
summary="Simple implementation of a basic OCPP 2.0.1 compliant central system (backend) for EV charging stations"
responses={404: {"description": "Not found"}}
# Root FastAPI app
root_app = FastAPI(
title=title,
version=version,
summary=summary,
responses=responses,
docs_url=None,
redoc_url=None,
)
# FastAPI app for OCPP handler
ocpp_app = FastAPI(
title=title,
version=version,
summary=summary,
responses=responses,
docs_url=None,
redoc_url=None,
)
ocpp_app.include_router(ocpp_v1.router)
ocpp_app.add_middleware(AuthenticationMiddleware, backend=BasicAuthBackend())
root_app.mount(path="/v1/ocpp", app=ocpp_app)
# FastAPI app for REST API
api_app = FastAPI(
title=title,
version=version,
summary=summary,
responses=responses,
)
api_v1_router = APIRouter(
prefix="/api/v1"
prefix="/v1"
)
api_v1_router.include_router(auth_v1.router)
api_v1_router.include_router(chargepoint_v1.router)
@ -45,15 +68,20 @@ def create_app():
api_v1_router.include_router(meter_value_v1.router)
api_v1_router.include_router(transaction_v1.router)
app.include_router(api_v1_router)
app.mount(path="/v1/ocpp", app=create_ocpp_app())
api_app.include_router(api_v1_router)
root_app.mount(path="/api", app=api_app)
# Serve static files if existent
if os.path.isdir('static'):
static_files = StaticFiles(directory="static", html=True)
root_app.mount(path="/", app=static_files)
origins = [
"http://localhost",
"http://localhost:5173",
]
app.add_middleware(
root_app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
@ -61,6 +89,6 @@ def create_app():
allow_headers=["*"],
)
return app
return root_app
app = create_app()

View file

@ -55,7 +55,7 @@ function createTokenRefreshInterceptor() {
// Retry failed, clean up and reject the promise
clearLoginState();
axios.defaults.headers.common['Authorization'] = "";
goto('/login?reauth')
goto('#/login?reauth')
return Promise.reject(retryError);
})
.finally(createTokenRefreshInterceptor); // Re-attach interceptor for future requests
@ -101,7 +101,7 @@ export const logout = function() {
.then(() => {
clearLoginState();
axios.defaults.headers.common['Authorization'] = "";
goto('/login?logout')
goto('#/login?logout')
});
}

View file

@ -59,7 +59,7 @@
</div>
</td>
<td>
<a href="/chargepoint/{transaction.chargepoint.id}" class="btn btn-sm btn-primary">
<a href="#/chargepoint/{transaction.chargepoint.id}" class="btn btn-sm btn-primary">
<i class="bi bi-plug-fill text-lg"></i>
{transaction.chargepoint.identity}
</a>
@ -71,7 +71,7 @@
>{((transaction.meter_end - transaction.meter_start) * transaction.price).toFixed(2)}</td
>
<th>
<a href="/transaction/{transaction.id}" class="btn btn-sm btn-primary">
<a href="#/transaction/{transaction.id}" class="btn btn-sm btn-primary">
{$i18n.t('common:transactionTable.detailButton')}
<i class="bi bi-arrow-right"></i>
</a>

View file

@ -7,7 +7,7 @@
let { children } = $props()
if (!$persistentSettings.loggedIn) {
goto('/login')
goto('#/login')
}
let drawerOpen = $state(false)
@ -44,7 +44,7 @@
onclick={() => {
drawerOpen = !drawerOpen
}}
href="/idtoken"
href="#/idtoken"
>
<i class="bi bi-credit-card-fill text-xl"></i>
<span>{$i18n.t('common:navbar.link.idtoken')}</span>
@ -55,7 +55,7 @@
onclick={() => {
drawerOpen = !drawerOpen
}}
href="/transaction"
href="#/transaction"
>
<i class="bi bi-battery-charging text-xl"></i>
<span>{$i18n.t('common:navbar.link.transaction')}</span>
@ -66,7 +66,7 @@
onclick={() => {
drawerOpen = !drawerOpen
}}
href="/chargepoint"
href="#/chargepoint"
>
<i class="bi bi-plug-fill text-xl"></i>
<span>{$i18n.t('common:navbar.link.chargepoint')}</span>
@ -87,7 +87,7 @@
tabindex="-1"
class="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow"
>
<li><a href="/profile">{$i18n.t('common:navbar.link.profile')}</a></li>
<li><a href="#/profile">{$i18n.t('common:navbar.link.profile')}</a></li>
<li><button onclick={logout}>{$i18n.t('common:navbar.link.logout')}</button></li>
</ul>
</div>

View file

@ -81,7 +81,7 @@
</td>
<td>{chargepoint.price}</td>
<th>
<a href="/chargepoint/{chargepoint.id}" class="btn btn-sm btn-primary">
<a href="#/chargepoint/{chargepoint.id}" class="btn btn-sm btn-primary">
{$i18n.t('common:transactionTable.detailButton')}
<i class="bi bi-arrow-right"></i>
</a>

View file

@ -1 +0,0 @@
export const ssr = false

View file

@ -11,6 +11,7 @@ const config = {
adapter: adapter({
fallback: 'index.html',
}),
router: { type: 'hash' },
},
}