diff --git a/.github/workflows/containerize_fleet_analysis_backend.yaml b/.github/workflows/containerize_fleet_analysis_backend.yaml
new file mode 100644
index 0000000..9c4ffa2
--- /dev/null
+++ b/.github/workflows/containerize_fleet_analysis_backend.yaml
@@ -0,0 +1,61 @@
+# /********************************************************************************
+# * Copyright (c) 2026 Contributors to the Eclipse Foundation
+# *
+# * See the NOTICE file(s) distributed with this work for additional
+# * information regarding copyright ownership.
+# *
+# * This program and the accompanying materials are made available under the
+# * terms of the Apache License 2.0 which is available at
+# * https://www.apache.org/licenses/LICENSE-2.0
+# *
+# * SPDX-License-Identifier: Apache-2.0
+# ********************************************************************************/
+name: Containerize Fleet Analysis Backend and Push to Container Registry
+
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - "components/backend-fleet-analysis-java/**"
+ - ".github/workflows/containerize_fleet_analysis_backend.yaml"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ packages: write
+
+jobs:
+ build-and-push:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Log in to GHCR
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Extract image metadata
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ghcr.io/${{ github.repository }}/fleet-analysis-backend
+
+ - name: Build and push
+ uses: docker/build-push-action@v5
+ with:
+ context: components/backend-fleet-analysis-java
+ platforms: linux/amd64,linux/arm64
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
diff --git a/README.md b/README.md
index ec114c4..57dcd3a 100644
--- a/README.md
+++ b/README.md
@@ -56,6 +56,8 @@ docker compose -f ./fms-blueprint-compose.yaml -f ./fms-blueprint-compose-zenoh.
This will pull (or build if necessary) the container images and create and start all components.
+The stack includes the Jakarta EE fleet analysis backend and exposes it on `http://127.0.0.1:8082/fleet-analysis/api`.
+
Once all services have been started, the current vehicle status can be viewed on a [Grafana dashboard](http://127.0.0.1:3000),
using *admin*/*admin* as username and password for logging in.
@@ -114,6 +116,8 @@ setup manually.
Additional information can be found in the components' corresponding subfolders.
+The fleet analysis backend component lives in `components/backend-fleet-analysis-java`.
+
# Contributing
We are looking forward to your ideas and PRs. Each PRs triggers a GitHub action which checks the formating, performs
diff --git a/components/backend-fleet-analysis-java/.dockerignore b/components/backend-fleet-analysis-java/.dockerignore
new file mode 100644
index 0000000..612c5bc
--- /dev/null
+++ b/components/backend-fleet-analysis-java/.dockerignore
@@ -0,0 +1,3 @@
+target
+.idea
+*.iml
diff --git a/components/backend-fleet-analysis-java/Dockerfile b/components/backend-fleet-analysis-java/Dockerfile
new file mode 100644
index 0000000..2cafa10
--- /dev/null
+++ b/components/backend-fleet-analysis-java/Dockerfile
@@ -0,0 +1,34 @@
+# /********************************************************************************
+# * Copyright (c) 2026 Contributors to the Eclipse Foundation
+# *
+# * See the NOTICE file(s) distributed with this work for additional
+# * information regarding copyright ownership.
+# *
+# * This program and the accompanying materials are made available under the
+# * terms of the Apache License 2.0 which is available at
+# * https://www.apache.org/licenses/LICENSE-2.0
+# *
+# * SPDX-License-Identifier: Apache-2.0
+# ********************************************************************************/
+
+FROM maven:3.9.6-eclipse-temurin-21 AS build
+
+WORKDIR /workspace
+COPY pom.xml .
+COPY src ./src
+RUN mvn -q -DskipTests package
+
+FROM eclipse-temurin:21-jre
+
+ARG PAYARA_MICRO_VERSION=6.2024.2
+WORKDIR /opt/payara
+ADD https://repo1.maven.org/maven2/fish/payara/extras/payara-micro/${PAYARA_MICRO_VERSION}/payara-micro-${PAYARA_MICRO_VERSION}.jar /opt/payara/payara-micro.jar
+
+COPY --from=build /workspace/target/*.war /opt/payara/fleet-analysis-backend.war
+
+ENV TMPDIR=/tmp
+
+RUN mkdir -p /tmp && chmod 1777 /tmp
+
+EXPOSE 8080
+ENTRYPOINT ["java","-Djava.io.tmpdir=/tmp","-jar","/opt/payara/payara-micro.jar","--deploy","/opt/payara/fleet-analysis-backend.war","--contextRoot","/fleet-analysis"]
diff --git a/components/backend-fleet-analysis-java/README.md b/components/backend-fleet-analysis-java/README.md
new file mode 100644
index 0000000..8c2cdd6
--- /dev/null
+++ b/components/backend-fleet-analysis-java/README.md
@@ -0,0 +1,157 @@
+# Fleet Analysis Backend (Jakarta EE 11)
+
+This service provides a small Jakarta EE 11 backend to analyze Fleet Management telemetry and
+return summary statistics for dashboards or alerts.
+
+## Responsibilities
+
+- Accepts fleet telemetry snapshots as JSON.
+- Returns computed summary metrics (fleet size, average speed, battery SOC range, braking count).
+- Writes header/snapshot telemetry to InfluxDB when requested.
+- Reads InfluxDB telemetry and writes fleet statistics periodically.
+
+## Build
+
+```bash
+mvn package
+```
+
+## Run (example with Payara Micro)
+
+1. Download [Payara Micro 6](https://www.payara.fish/downloads/payara-platform-community-edition/).
+2. Deploy the WAR:
+
+```bash
+java -jar payara-micro.jar --deploy target/fleet-analysis-backend.war --contextRoot /fleet-analysis
+```
+
+The API will be available at `http://localhost:8080/fleet-analysis/api`.
+
+## Build Docker image (without Docker Compose)
+
+From the repository root:
+
+```bash
+docker build -t fleet-analysis-backend:local components/backend-fleet-analysis-java
+```
+
+Run it manually (example):
+
+```bash
+docker run --rm -p 8082:8080 --name fleet-analysis-backend \
+ -e INFLUXDB_TOKEN_FILE=/tmp/fms-demo.token \
+ -e INFLUXDB_STATS_INTERVAL_SECONDS=30 \
+ -v /path/to/fms-demo.token:/tmp/fms-demo.token:ro \
+ fleet-analysis-backend:local
+```
+
+## Accessing output
+
+Variant 1: Docker container logs
+
+```bash
+docker logs -f fleet-analysis-backend
+```
+
+Variant 2: API
+
+```bash
+curl -s http://localhost:8082/fleet-analysis/api/analysis/stats | jq
+```
+
+## API
+
+### `POST /api/analysis/summary`
+
+Request body (example):
+
+```json
+[
+ {
+ "vehicleId": "bike-001",
+ "speedKph": 42.3,
+ "batterySoc": 0.78,
+ "brakeActive": false,
+ "updatedAt": "2024-06-10T10:15:30Z"
+ },
+ {
+ "vehicleId": "bike-002",
+ "speedKph": 12.4,
+ "batterySoc": 0.52,
+ "brakeActive": true,
+ "updatedAt": "2024-06-10T10:15:32Z"
+ }
+]
+```
+
+Response:
+
+```json
+{
+ "vehicleCount": 2,
+ "averageSpeedKph": 27.35,
+ "minBatterySoc": 0.52,
+ "maxBatterySoc": 0.78,
+ "brakingVehicles": 1
+}
+```
+
+### `POST /api/telemetry/ingest`
+
+Writes header and/or snapshot measurements into InfluxDB. Configure the client using:
+
+- `INFLUXDB_URI` (default: `http://influxdb:8086`)
+- `INFLUXDB_ORG` (default: `sdv`)
+- `INFLUXDB_BUCKET` (default: `demo`)
+- `INFLUXDB_TOKEN` or `INFLUXDB_TOKEN_FILE`
+
+Request body (example):
+
+```json
+{
+ "vin": "truck-001",
+ "trigger": "periodic",
+ "createdDateTime": 1737940602000,
+ "header": {
+ "hrTotalVehicleDistance": 12345.6,
+ "grossCombinationVehicleWeight": 18100.2,
+ "totalEngineHours": 82.5,
+ "engineTotalFuelUsed": 221.9,
+ "driver1Id": "driver-01",
+ "driver1IdCardIssuer": "fleet"
+ },
+ "snapshot": {
+ "latitude": 37.7749,
+ "longitude": -122.4194,
+ "speed": 54.2,
+ "positionDateTime": 1737940602,
+ "wheelBasedSpeed": 53.7,
+ "fuelLevel1": 0.42,
+ "parkingBrakeSwitch": false
+ }
+}
+```
+
+### Fleet stats (InfluxDB)
+
+The service periodically computes fleet statistics from the `header`/`snapshot` measurements and
+writes them into a `fleet_stats` measurement.
+
+Default schedule: every 30 seconds (configure with `INFLUXDB_STATS_INTERVAL_SECONDS` as an env var
+or Java system property).
+Initial delay before the first stats run defaults to 10 seconds (configure with
+`INFLUXDB_STATS_INITIAL_DELAY_SECONDS`).
+
+**Measurement: `fleet_stats`**
+
+Fields:
+
+- `vehicleCount` (unique `vin` across all time)
+- `headerCount` (total number of header points)
+- `snapshotCount` (total number of snapshot points)
+- `totalCount` (header + snapshot points)
+- `generatedAt` (ms since Unix epoch)
+
+### `GET /api/analysis/stats`
+
+Returns the latest stats snapshot (and triggers a refresh if none exist yet).
diff --git a/components/backend-fleet-analysis-java/pom.xml b/components/backend-fleet-analysis-java/pom.xml
new file mode 100644
index 0000000..5300cfa
--- /dev/null
+++ b/components/backend-fleet-analysis-java/pom.xml
@@ -0,0 +1,29 @@
+
+ 4.0.0
+
+ org.eclipse.sdv
+ fleet-analysis-backend
+ 1.0.0-SNAPSHOT
+ war
+
+
+ 21
+ 21
+ UTF-8
+
+
+
+
+ jakarta.platform
+ jakarta.jakartaee-web-api
+ 10.0.0
+ provided
+
+
+ com.influxdb
+ influxdb-client-java
+ 7.0.0
+
+
+
diff --git a/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/AnalysisApplication.java b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/AnalysisApplication.java
new file mode 100644
index 0000000..cf44516
--- /dev/null
+++ b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/AnalysisApplication.java
@@ -0,0 +1,21 @@
+// ********************************************************************************
+// * Copyright (c) 2026 Contributors to the Eclipse Foundation
+// *
+// * See the NOTICE file(s) distributed with this work for additional
+// * information regarding copyright ownership.
+// *
+// * This program and the accompanying materials are made available under the
+// * terms of the Apache License 2.0 which is available at
+// * https://www.apache.org/licenses/LICENSE-2.0
+// *
+// * SPDX-License-Identifier: Apache-2.0
+// ********************************************************************************
+
+package org.eclipse.sdv.fleet.analysis;
+
+import jakarta.ws.rs.ApplicationPath;
+import jakarta.ws.rs.core.Application;
+
+@ApplicationPath("/api")
+public class AnalysisApplication extends Application {
+}
diff --git a/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/FleetAnalysisResource.java b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/FleetAnalysisResource.java
new file mode 100644
index 0000000..638a2d0
--- /dev/null
+++ b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/FleetAnalysisResource.java
@@ -0,0 +1,53 @@
+// ********************************************************************************
+// * Copyright (c) 2026 Contributors to the Eclipse Foundation
+// *
+// * See the NOTICE file(s) distributed with this work for additional
+// * information regarding copyright ownership.
+// *
+// * This program and the accompanying materials are made available under the
+// * terms of the Apache License 2.0 which is available at
+// * https://www.apache.org/licenses/LICENSE-2.0
+// *
+// * SPDX-License-Identifier: Apache-2.0
+// ********************************************************************************
+
+package org.eclipse.sdv.fleet.analysis;
+
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+
+import java.util.List;
+
+@Path("/analysis")
+@Consumes(MediaType.APPLICATION_JSON)
+@Produces(MediaType.APPLICATION_JSON)
+public class FleetAnalysisResource {
+
+ @POST
+ @Path("/summary")
+ public FleetAnalysisSummary summarize(List telemetry) {
+ FleetAnalysisSummary summary = new FleetAnalysisSummary();
+ if (telemetry == null || telemetry.isEmpty()) {
+ summary.setVehicleCount(0);
+ summary.setAverageSpeedKph(0.0);
+ summary.setMinBatterySoc(0.0);
+ summary.setMaxBatterySoc(0.0);
+ summary.setBrakingVehicles(0);
+ return summary;
+ }
+
+ summary.setVehicleCount(telemetry.size());
+ summary.setAverageSpeedKph(
+ telemetry.stream().mapToDouble(FleetTelemetry::getSpeedKph).average().orElse(0.0));
+ summary.setMinBatterySoc(
+ telemetry.stream().mapToDouble(FleetTelemetry::getBatterySoc).min().orElse(0.0));
+ summary.setMaxBatterySoc(
+ telemetry.stream().mapToDouble(FleetTelemetry::getBatterySoc).max().orElse(0.0));
+ summary.setBrakingVehicles(telemetry.stream().filter(FleetTelemetry::isBrakeActive).count());
+
+ return summary;
+ }
+}
diff --git a/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/FleetAnalysisSummary.java b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/FleetAnalysisSummary.java
new file mode 100644
index 0000000..2665010
--- /dev/null
+++ b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/FleetAnalysisSummary.java
@@ -0,0 +1,62 @@
+// ********************************************************************************
+// * Copyright (c) 2026 Contributors to the Eclipse Foundation
+// *
+// * See the NOTICE file(s) distributed with this work for additional
+// * information regarding copyright ownership.
+// *
+// * This program and the accompanying materials are made available under the
+// * terms of the Apache License 2.0 which is available at
+// * https://www.apache.org/licenses/LICENSE-2.0
+// *
+// * SPDX-License-Identifier: Apache-2.0
+// ********************************************************************************
+
+package org.eclipse.sdv.fleet.analysis;
+
+public class FleetAnalysisSummary {
+ private int vehicleCount;
+ private double averageSpeedKph;
+ private double minBatterySoc;
+ private double maxBatterySoc;
+ private long brakingVehicles;
+
+ public int getVehicleCount() {
+ return vehicleCount;
+ }
+
+ public void setVehicleCount(int vehicleCount) {
+ this.vehicleCount = vehicleCount;
+ }
+
+ public double getAverageSpeedKph() {
+ return averageSpeedKph;
+ }
+
+ public void setAverageSpeedKph(double averageSpeedKph) {
+ this.averageSpeedKph = averageSpeedKph;
+ }
+
+ public double getMinBatterySoc() {
+ return minBatterySoc;
+ }
+
+ public void setMinBatterySoc(double minBatterySoc) {
+ this.minBatterySoc = minBatterySoc;
+ }
+
+ public double getMaxBatterySoc() {
+ return maxBatterySoc;
+ }
+
+ public void setMaxBatterySoc(double maxBatterySoc) {
+ this.maxBatterySoc = maxBatterySoc;
+ }
+
+ public long getBrakingVehicles() {
+ return brakingVehicles;
+ }
+
+ public void setBrakingVehicles(long brakingVehicles) {
+ this.brakingVehicles = brakingVehicles;
+ }
+}
diff --git a/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/FleetStatsResource.java b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/FleetStatsResource.java
new file mode 100644
index 0000000..49801a5
--- /dev/null
+++ b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/FleetStatsResource.java
@@ -0,0 +1,35 @@
+// ********************************************************************************
+// * Copyright (c) 2026 Contributors to the Eclipse Foundation
+// *
+// * See the NOTICE file(s) distributed with this work for additional
+// * information regarding copyright ownership.
+// *
+// * This program and the accompanying materials are made available under the
+// * terms of the Apache License 2.0 which is available at
+// * https://www.apache.org/licenses/LICENSE-2.0
+// *
+// * SPDX-License-Identifier: Apache-2.0
+// ********************************************************************************
+
+package org.eclipse.sdv.fleet.analysis;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+
+@Path("/analysis")
+@Produces(MediaType.APPLICATION_JSON)
+public class FleetStatsResource {
+
+ @Inject
+ private InfluxStatsService statsService;
+
+ @GET
+ @Path("/stats")
+ public FleetStatsSummary getStats() {
+ FleetStatsSummary stats = statsService.getLatestStats(true);
+ return stats == null ? new FleetStatsSummary() : stats;
+ }
+}
diff --git a/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/FleetStatsSummary.java b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/FleetStatsSummary.java
new file mode 100644
index 0000000..c620b6f
--- /dev/null
+++ b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/FleetStatsSummary.java
@@ -0,0 +1,62 @@
+// ********************************************************************************
+// * Copyright (c) 2026 Contributors to the Eclipse Foundation
+// *
+// * See the NOTICE file(s) distributed with this work for additional
+// * information regarding copyright ownership.
+// *
+// * This program and the accompanying materials are made available under the
+// * terms of the Apache License 2.0 which is available at
+// * https://www.apache.org/licenses/LICENSE-2.0
+// *
+// * SPDX-License-Identifier: Apache-2.0
+// ********************************************************************************
+
+package org.eclipse.sdv.fleet.analysis;
+
+public class FleetStatsSummary {
+ private long vehicleCount;
+ private long headerCount;
+ private long snapshotCount;
+ private long totalCount;
+ private long generatedAt;
+
+ public long getVehicleCount() {
+ return vehicleCount;
+ }
+
+ public void setVehicleCount(long vehicleCount) {
+ this.vehicleCount = vehicleCount;
+ }
+
+ public long getHeaderCount() {
+ return headerCount;
+ }
+
+ public void setHeaderCount(long headerCount) {
+ this.headerCount = headerCount;
+ }
+
+ public long getSnapshotCount() {
+ return snapshotCount;
+ }
+
+ public void setSnapshotCount(long snapshotCount) {
+ this.snapshotCount = snapshotCount;
+ }
+
+ public long getTotalCount() {
+ return totalCount;
+ }
+
+ public void setTotalCount(long totalCount) {
+ this.totalCount = totalCount;
+ }
+
+ public long getGeneratedAt() {
+ return generatedAt;
+ }
+
+ public void setGeneratedAt(long generatedAt) {
+ this.generatedAt = generatedAt;
+ }
+}
diff --git a/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/FleetTelemetry.java b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/FleetTelemetry.java
new file mode 100644
index 0000000..3e31101
--- /dev/null
+++ b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/FleetTelemetry.java
@@ -0,0 +1,64 @@
+// ********************************************************************************
+// * Copyright (c) 2026 Contributors to the Eclipse Foundation
+// *
+// * See the NOTICE file(s) distributed with this work for additional
+// * information regarding copyright ownership.
+// *
+// * This program and the accompanying materials are made available under the
+// * terms of the Apache License 2.0 which is available at
+// * https://www.apache.org/licenses/LICENSE-2.0
+// *
+// * SPDX-License-Identifier: Apache-2.0
+// ********************************************************************************
+
+package org.eclipse.sdv.fleet.analysis;
+
+import java.time.Instant;
+
+public class FleetTelemetry {
+ private String vehicleId;
+ private double speedKph;
+ private double batterySoc;
+ private boolean brakeActive;
+ private Instant updatedAt;
+
+ public String getVehicleId() {
+ return vehicleId;
+ }
+
+ public void setVehicleId(String vehicleId) {
+ this.vehicleId = vehicleId;
+ }
+
+ public double getSpeedKph() {
+ return speedKph;
+ }
+
+ public void setSpeedKph(double speedKph) {
+ this.speedKph = speedKph;
+ }
+
+ public double getBatterySoc() {
+ return batterySoc;
+ }
+
+ public void setBatterySoc(double batterySoc) {
+ this.batterySoc = batterySoc;
+ }
+
+ public boolean isBrakeActive() {
+ return brakeActive;
+ }
+
+ public void setBrakeActive(boolean brakeActive) {
+ this.brakeActive = brakeActive;
+ }
+
+ public Instant getUpdatedAt() {
+ return updatedAt;
+ }
+
+ public void setUpdatedAt(Instant updatedAt) {
+ this.updatedAt = updatedAt;
+ }
+}
diff --git a/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxDbConfig.java b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxDbConfig.java
new file mode 100644
index 0000000..fc6e444
--- /dev/null
+++ b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxDbConfig.java
@@ -0,0 +1,95 @@
+// ********************************************************************************
+// * Copyright (c) 2026 Contributors to the Eclipse Foundation
+// *
+// * See the NOTICE file(s) distributed with this work for additional
+// * information regarding copyright ownership.
+// *
+// * This program and the accompanying materials are made available under the
+// * terms of the Apache License 2.0 which is available at
+// * https://www.apache.org/licenses/LICENSE-2.0
+// *
+// * SPDX-License-Identifier: Apache-2.0
+// ********************************************************************************
+
+package org.eclipse.sdv.fleet.analysis;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public class InfluxDbConfig {
+ private static final String DEFAULT_URI = "http://influxdb:8086";
+ private static final String DEFAULT_ORG = "sdv";
+ private static final String DEFAULT_BUCKET = "demo";
+
+ private final String uri;
+ private final String org;
+ private final String bucket;
+ private final String token;
+
+ private InfluxDbConfig(String uri, String org, String bucket, String token) {
+ this.uri = uri;
+ this.org = org;
+ this.bucket = bucket;
+ this.token = token;
+ }
+
+ public static InfluxDbConfig fromEnv() {
+ String uri = envOrDefault("INFLUXDB_URI", DEFAULT_URI);
+ String org = envOrDefault("INFLUXDB_ORG", DEFAULT_ORG);
+ String bucket = envOrDefault("INFLUXDB_BUCKET", DEFAULT_BUCKET);
+ String token = env("INFLUXDB_TOKEN");
+
+ if (isBlank(token)) {
+ String tokenFile = env("INFLUXDB_TOKEN_FILE");
+ if (!isBlank(tokenFile)) {
+ token = readTokenFile(tokenFile);
+ }
+ }
+
+ if (isBlank(token)) {
+ throw new IllegalStateException(
+ "Missing InfluxDB token. Set INFLUXDB_TOKEN or INFLUXDB_TOKEN_FILE.");
+ }
+
+ return new InfluxDbConfig(uri, org, bucket, token);
+ }
+
+ public String getUri() {
+ return uri;
+ }
+
+ public String getOrg() {
+ return org;
+ }
+
+ public String getBucket() {
+ return bucket;
+ }
+
+ public String getToken() {
+ return token;
+ }
+
+ private static String env(String key) {
+ return System.getenv(key);
+ }
+
+ private static String envOrDefault(String key, String fallback) {
+ String value = env(key);
+ return isBlank(value) ? fallback : value;
+ }
+
+ private static boolean isBlank(String value) {
+ return value == null || value.trim().isEmpty();
+ }
+
+ private static String readTokenFile(String tokenFile) {
+ try {
+ return Files.readString(Path.of(tokenFile), StandardCharsets.UTF_8).trim();
+ } catch (IOException ex) {
+ throw new IllegalStateException("Failed to read InfluxDB token file: " + tokenFile, ex);
+ }
+ }
+}
diff --git a/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxDbWriter.java b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxDbWriter.java
new file mode 100644
index 0000000..cc39a27
--- /dev/null
+++ b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxDbWriter.java
@@ -0,0 +1,136 @@
+// ********************************************************************************
+// * Copyright (c) 2026 Contributors to the Eclipse Foundation
+// *
+// * See the NOTICE file(s) distributed with this work for additional
+// * information regarding copyright ownership.
+// *
+// * This program and the accompanying materials are made available under the
+// * terms of the Apache License 2.0 which is available at
+// * https://www.apache.org/licenses/LICENSE-2.0
+// *
+// * SPDX-License-Identifier: Apache-2.0
+// ********************************************************************************
+
+package org.eclipse.sdv.fleet.analysis;
+
+import com.influxdb.client.InfluxDBClient;
+import com.influxdb.client.InfluxDBClientFactory;
+import com.influxdb.client.WriteApi;
+import com.influxdb.client.domain.WritePrecision;
+import com.influxdb.client.write.Point;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+import jakarta.enterprise.context.ApplicationScoped;
+
+@ApplicationScoped
+public class InfluxDbWriter {
+ private InfluxDBClient client;
+ private WriteApi writeApi;
+
+ @PostConstruct
+ void init() {
+ InfluxDbConfig config = InfluxDbConfig.fromEnv();
+ client =
+ InfluxDBClientFactory.create(
+ config.getUri(), config.getToken().toCharArray(), config.getOrg(), config.getBucket());
+ writeApi = client.getWriteApi();
+ }
+
+ @PreDestroy
+ void shutdown() {
+ if (writeApi != null) {
+ writeApi.close();
+ }
+ if (client != null) {
+ client.close();
+ }
+ }
+
+ public void writeHeader(
+ String vin, String trigger, long createdDateTime, InfluxTelemetryPayload.Header header) {
+ Point point =
+ Point.measurement("header")
+ .addTag("vin", vin)
+ .addTag("trigger", trigger)
+ .addField("createdDateTime", createdDateTime)
+ .time(createdDateTime, WritePrecision.MS);
+
+ if (header != null) {
+ addFieldIfPresent(point, "hrTotalVehicleDistance", header.getHrTotalVehicleDistance());
+ addFieldIfPresent(point, "grossCombinationVehicleWeight", header.getGrossCombinationVehicleWeight());
+ addFieldIfPresent(point, "totalEngineHours", header.getTotalEngineHours());
+ addFieldIfPresent(point, "totalElectricMotorHours", header.getTotalElectricMotorHours());
+ addFieldIfPresent(point, "engineTotalFuelUsed", header.getEngineTotalFuelUsed());
+ addFieldIfPresent(point, "driver1Id", header.getDriver1Id());
+ addFieldIfPresent(point, "driver1IdCardIssuer", header.getDriver1IdCardIssuer());
+ }
+
+ writeApi.writePoint(point);
+ }
+
+ public void writeSnapshot(
+ String vin, String trigger, long createdDateTime, InfluxTelemetryPayload.Snapshot snapshot) {
+ Point point =
+ Point.measurement("snapshot")
+ .addTag("vin", vin)
+ .addTag("trigger", trigger)
+ .addField("createdDateTime", createdDateTime)
+ .time(createdDateTime, WritePrecision.MS);
+
+ if (snapshot != null) {
+ addFieldIfPresent(point, "latitude", snapshot.getLatitude());
+ addFieldIfPresent(point, "longitude", snapshot.getLongitude());
+ addFieldIfPresent(point, "heading", snapshot.getHeading());
+ addFieldIfPresent(point, "altitude", snapshot.getAltitude());
+ addFieldIfPresent(point, "speed", snapshot.getSpeed());
+ addFieldIfPresent(point, "positionDateTime", snapshot.getPositionDateTime());
+ addFieldIfPresent(point, "wheelBasedSpeed", snapshot.getWheelBasedSpeed());
+ addFieldIfPresent(point, "tachographSpeed", snapshot.getTachographSpeed());
+ addFieldIfPresent(point, "engineSpeed", snapshot.getEngineSpeed());
+ addFieldIfPresent(point, "fuelType", snapshot.getFuelType());
+ addFieldIfPresent(point, "catalystFuelLevel", snapshot.getCatalystFuelLevel());
+ addFieldIfPresent(point, "fuelLevel1", snapshot.getFuelLevel1());
+ addFieldIfPresent(point, "fuelLevel2", snapshot.getFuelLevel2());
+ addFieldIfPresent(point, "driver1WorkingState", snapshot.getDriver1WorkingState());
+ addFieldIfPresent(point, "driver2WorkingState", snapshot.getDriver2WorkingState());
+ addFieldIfPresent(point, "ambientAirTemperature", snapshot.getAmbientAirTemperature());
+ addFieldIfPresent(point, "parkingBrakeSwitch", snapshot.getParkingBrakeSwitch());
+ addFieldIfPresent(point, "estimatedDistanceToEmptyFuel", snapshot.getEstimatedDistanceToEmptyFuel());
+ addFieldIfPresent(point, "estimatedDistanceToEmptyTotal", snapshot.getEstimatedDistanceToEmptyTotal());
+ addFieldIfPresent(point, "driver2Id", snapshot.getDriver2Id());
+ addFieldIfPresent(point, "driver2IdCardIssuer", snapshot.getDriver2IdCardIssuer());
+ }
+
+ writeApi.writePoint(point);
+ }
+
+ private static void addFieldIfPresent(Point point, String name, Double value) {
+ if (value != null) {
+ point.addField(name, value);
+ }
+ }
+
+ private static void addFieldIfPresent(Point point, String name, Long value) {
+ if (value != null) {
+ point.addField(name, value);
+ }
+ }
+
+ private static void addFieldIfPresent(Point point, String name, Integer value) {
+ if (value != null) {
+ point.addField(name, value);
+ }
+ }
+
+ private static void addFieldIfPresent(Point point, String name, Boolean value) {
+ if (value != null) {
+ point.addField(name, value);
+ }
+ }
+
+ private static void addFieldIfPresent(Point point, String name, String value) {
+ if (value != null && !value.isBlank()) {
+ point.addField(name, value);
+ }
+ }
+}
diff --git a/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxStatsConfig.java b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxStatsConfig.java
new file mode 100644
index 0000000..86812f3
--- /dev/null
+++ b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxStatsConfig.java
@@ -0,0 +1,72 @@
+// ********************************************************************************
+// * Copyright (c) 2026 Contributors to the Eclipse Foundation
+// *
+// * See the NOTICE file(s) distributed with this work for additional
+// * information regarding copyright ownership.
+// *
+// * This program and the accompanying materials are made available under the
+// * terms of the Apache License 2.0 which is available at
+// * https://www.apache.org/licenses/LICENSE-2.0
+// *
+// * SPDX-License-Identifier: Apache-2.0
+// ********************************************************************************
+
+package org.eclipse.sdv.fleet.analysis;
+
+public class InfluxStatsConfig {
+ private static final long DEFAULT_INTERVAL_SECONDS = 30;
+ private static final long DEFAULT_INITIAL_DELAY_SECONDS = 10;
+ private final long intervalSeconds;
+ private final long initialDelaySeconds;
+
+ private InfluxStatsConfig(long intervalSeconds, long initialDelaySeconds) {
+ this.intervalSeconds = intervalSeconds;
+ this.initialDelaySeconds = initialDelaySeconds;
+ }
+
+ public static InfluxStatsConfig fromEnv() {
+ String raw =
+ System.getProperty(
+ "INFLUXDB_STATS_INTERVAL_SECONDS",
+ System.getenv("INFLUXDB_STATS_INTERVAL_SECONDS"));
+ long interval = parseInterval(raw);
+ String rawDelay =
+ System.getProperty(
+ "INFLUXDB_STATS_INITIAL_DELAY_SECONDS",
+ System.getenv("INFLUXDB_STATS_INITIAL_DELAY_SECONDS"));
+ long initialDelay = parseInitialDelay(rawDelay);
+ return new InfluxStatsConfig(interval, initialDelay);
+ }
+
+ public long getIntervalSeconds() {
+ return intervalSeconds;
+ }
+
+ public long getInitialDelaySeconds() {
+ return initialDelaySeconds;
+ }
+
+ private static long parseInterval(String raw) {
+ if (raw == null || raw.isBlank()) {
+ return DEFAULT_INTERVAL_SECONDS;
+ }
+ try {
+ long value = Long.parseLong(raw.trim());
+ return value > 0 ? value : DEFAULT_INTERVAL_SECONDS;
+ } catch (NumberFormatException ex) {
+ return DEFAULT_INTERVAL_SECONDS;
+ }
+ }
+
+ private static long parseInitialDelay(String raw) {
+ if (raw == null || raw.isBlank()) {
+ return DEFAULT_INITIAL_DELAY_SECONDS;
+ }
+ try {
+ long value = Long.parseLong(raw.trim());
+ return value >= 0 ? value : DEFAULT_INITIAL_DELAY_SECONDS;
+ } catch (NumberFormatException ex) {
+ return DEFAULT_INITIAL_DELAY_SECONDS;
+ }
+ }
+}
diff --git a/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxStatsService.java b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxStatsService.java
new file mode 100644
index 0000000..f75f8af
--- /dev/null
+++ b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxStatsService.java
@@ -0,0 +1,242 @@
+// ********************************************************************************
+// * Copyright (c) 2026 Contributors to the Eclipse Foundation
+// *
+// * See the NOTICE file(s) distributed with this work for additional
+// * information regarding copyright ownership.
+// *
+// * This program and the accompanying materials are made available under the
+// * terms of the Apache License 2.0 which is available at
+// * https://www.apache.org/licenses/LICENSE-2.0
+// *
+// * SPDX-License-Identifier: Apache-2.0
+// ********************************************************************************
+
+package org.eclipse.sdv.fleet.analysis;
+
+import com.influxdb.client.InfluxDBClient;
+import com.influxdb.client.InfluxDBClientFactory;
+import com.influxdb.client.QueryApi;
+import com.influxdb.client.WriteApi;
+import com.influxdb.client.domain.WritePrecision;
+import com.influxdb.client.write.Point;
+import com.influxdb.query.FluxRecord;
+import com.influxdb.query.FluxTable;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+import jakarta.enterprise.context.ApplicationScoped;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+@ApplicationScoped
+public class InfluxStatsService {
+ private static final Logger LOGGER = Logger.getLogger(InfluxStatsService.class.getName());
+
+ private InfluxDbConfig influxConfig;
+ private InfluxStatsConfig statsConfig;
+ private InfluxDBClient client;
+ private WriteApi writeApi;
+ private ScheduledExecutorService scheduler;
+
+ @PostConstruct
+ void init() {
+ influxConfig = InfluxDbConfig.fromEnv();
+ statsConfig = InfluxStatsConfig.fromEnv();
+ client =
+ InfluxDBClientFactory.create(
+ influxConfig.getUri(),
+ influxConfig.getToken().toCharArray(),
+ influxConfig.getOrg(),
+ influxConfig.getBucket());
+ writeApi = client.getWriteApi();
+
+ scheduler =
+ Executors.newSingleThreadScheduledExecutor(
+ runnable -> {
+ Thread thread = new Thread(runnable, "fleet-stats-scheduler");
+ thread.setDaemon(true);
+ return thread;
+ });
+ scheduler.scheduleAtFixedRate(
+ this::refreshAndPersistStats,
+ statsConfig.getInitialDelaySeconds(),
+ statsConfig.getIntervalSeconds(),
+ TimeUnit.SECONDS);
+ }
+
+ @PreDestroy
+ void shutdown() {
+ if (scheduler != null) {
+ scheduler.shutdownNow();
+ }
+ if (writeApi != null) {
+ writeApi.close();
+ }
+ if (client != null) {
+ client.close();
+ }
+ }
+
+ public FleetStatsSummary getLatestStats(boolean computeIfMissing) {
+ FleetStatsSummary latest = queryLatestStats();
+ if (latest == null && computeIfMissing) {
+ latest = computeStats();
+ writeStats(latest);
+ }
+ return latest;
+ }
+
+ private void refreshAndPersistStats() {
+ try {
+ FleetStatsSummary stats = computeStats();
+ writeStats(stats);
+ } catch (Exception ex) {
+ LOGGER.log(Level.WARNING, "Failed to refresh fleet stats.", ex);
+ }
+ }
+
+ private FleetStatsSummary computeStats() {
+ long vehicleCount = queryVehicleCount();
+ long headerCount = queryMeasurementCount("header");
+ long snapshotCount = queryMeasurementCount("snapshot");
+
+ FleetStatsSummary summary = new FleetStatsSummary();
+ summary.setVehicleCount(vehicleCount);
+ summary.setHeaderCount(headerCount);
+ summary.setSnapshotCount(snapshotCount);
+ summary.setTotalCount(headerCount + snapshotCount);
+ summary.setGeneratedAt(System.currentTimeMillis());
+ return summary;
+ }
+
+ private void writeStats(FleetStatsSummary summary) {
+ Point point =
+ Point.measurement("fleet_stats")
+ .addField("vehicleCount", summary.getVehicleCount())
+ .addField("headerCount", summary.getHeaderCount())
+ .addField("snapshotCount", summary.getSnapshotCount())
+ .addField("totalCount", summary.getTotalCount())
+ .addField("generatedAt", summary.getGeneratedAt())
+ .time(summary.getGeneratedAt(), WritePrecision.MS);
+ writeApi.writePoint(point);
+ }
+
+ private FleetStatsSummary queryLatestStats() {
+ String flux =
+ "from(bucket: \""
+ + influxConfig.getBucket()
+ + "\")"
+ + " |> range(start: 0)"
+ + " |> filter(fn: (r) => r._measurement == \"fleet_stats\")"
+ + " |> last()";
+
+ List tables = client.getQueryApi().query(flux);
+ if (tables == null || tables.isEmpty()) {
+ return null;
+ }
+
+ FleetStatsSummary summary = new FleetStatsSummary();
+ boolean hasData = false;
+ for (FluxTable table : tables) {
+ for (FluxRecord record : table.getRecords()) {
+ Object field = record.getField();
+ Object value = record.getValue();
+ if (field == null || value == null) {
+ continue;
+ }
+ String name = field.toString();
+ long numeric = toLong(value);
+ switch (name) {
+ case "vehicleCount" -> summary.setVehicleCount(numeric);
+ case "headerCount" -> summary.setHeaderCount(numeric);
+ case "snapshotCount" -> summary.setSnapshotCount(numeric);
+ case "totalCount" -> summary.setTotalCount(numeric);
+ case "generatedAt" -> summary.setGeneratedAt(numeric);
+ default -> {
+ }
+ }
+ hasData = true;
+ }
+ }
+
+ return hasData ? summary : null;
+ }
+
+ private long queryVehicleCount() {
+ String flux =
+ "import \"influxdata/influxdb/schema\"\n"
+ + "schema.tagValues(bucket: \""
+ + influxConfig.getBucket()
+ + "\", tag: \"vin\")"
+ + " |> group(columns: [])"
+ + " |> count(column: \"_value\")";
+ return querySingleLong(flux);
+ }
+
+ private long queryMeasurementCount(String measurement) {
+ String flux =
+ "from(bucket: \""
+ + influxConfig.getBucket()
+ + "\")"
+ + " |> range(start: 0)"
+ + " |> filter(fn: (r) => r._measurement == \""
+ + measurement
+ + "\")"
+ + " |> keep(columns: [\"_time\", \"vin\"])"
+ + " |> map(fn: (r) => ({_value: string(v: r._time) + \":\" + r.vin}))"
+ + " |> distinct(column: \"_value\")"
+ + " |> group(columns: [])"
+ + " |> count(column: \"_value\")";
+ return querySingleLong(flux);
+ }
+
+ private long querySingleLong(String flux) {
+ QueryApi queryApi = client.getQueryApi();
+ List tables = queryApi.query(flux);
+ if (tables == null || tables.isEmpty()) {
+ return 0L;
+ }
+ for (FluxTable table : tables) {
+ for (FluxRecord record : table.getRecords()) {
+ Object value = record.getValue();
+ if (value == null) {
+ value = record.getValueByKey("count");
+ }
+ if (value != null) {
+ return toLong(value);
+ }
+ }
+ }
+ return 0L;
+ }
+
+ private long querySumLong(String flux) {
+ QueryApi queryApi = client.getQueryApi();
+ List tables = queryApi.query(flux);
+ if (tables == null || tables.isEmpty()) {
+ return 0L;
+ }
+ long sum = 0L;
+ for (FluxTable table : tables) {
+ for (FluxRecord record : table.getRecords()) {
+ Object value = record.getValue();
+ if (value != null) {
+ sum += toLong(value);
+ }
+ }
+ }
+ return sum;
+ }
+
+ private long toLong(Object value) {
+ if (value instanceof Number number) {
+ return number.longValue();
+ }
+ return Long.parseLong(Objects.toString(value, "0"));
+ }
+}
diff --git a/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxTelemetryPayload.java b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxTelemetryPayload.java
new file mode 100644
index 0000000..e574125
--- /dev/null
+++ b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxTelemetryPayload.java
@@ -0,0 +1,320 @@
+// ********************************************************************************
+// * Copyright (c) 2026 Contributors to the Eclipse Foundation
+// *
+// * See the NOTICE file(s) distributed with this work for additional
+// * information regarding copyright ownership.
+// *
+// * This program and the accompanying materials are made available under the
+// * terms of the Apache License 2.0 which is available at
+// * https://www.apache.org/licenses/LICENSE-2.0
+// *
+// * SPDX-License-Identifier: Apache-2.0
+// ********************************************************************************
+
+package org.eclipse.sdv.fleet.analysis;
+
+public class InfluxTelemetryPayload {
+ private String vin;
+ private String trigger;
+ private Long createdDateTime;
+ private Header header;
+ private Snapshot snapshot;
+
+ public String getVin() {
+ return vin;
+ }
+
+ public void setVin(String vin) {
+ this.vin = vin;
+ }
+
+ public String getTrigger() {
+ return trigger;
+ }
+
+ public void setTrigger(String trigger) {
+ this.trigger = trigger;
+ }
+
+ public Long getCreatedDateTime() {
+ return createdDateTime;
+ }
+
+ public void setCreatedDateTime(Long createdDateTime) {
+ this.createdDateTime = createdDateTime;
+ }
+
+ public Header getHeader() {
+ return header;
+ }
+
+ public void setHeader(Header header) {
+ this.header = header;
+ }
+
+ public Snapshot getSnapshot() {
+ return snapshot;
+ }
+
+ public void setSnapshot(Snapshot snapshot) {
+ this.snapshot = snapshot;
+ }
+
+ public static class Header {
+ private Double hrTotalVehicleDistance;
+ private Double grossCombinationVehicleWeight;
+ private Double totalEngineHours;
+ private Double totalElectricMotorHours;
+ private Double engineTotalFuelUsed;
+ private String driver1Id;
+ private String driver1IdCardIssuer;
+
+ public Double getHrTotalVehicleDistance() {
+ return hrTotalVehicleDistance;
+ }
+
+ public void setHrTotalVehicleDistance(Double hrTotalVehicleDistance) {
+ this.hrTotalVehicleDistance = hrTotalVehicleDistance;
+ }
+
+ public Double getGrossCombinationVehicleWeight() {
+ return grossCombinationVehicleWeight;
+ }
+
+ public void setGrossCombinationVehicleWeight(Double grossCombinationVehicleWeight) {
+ this.grossCombinationVehicleWeight = grossCombinationVehicleWeight;
+ }
+
+ public Double getTotalEngineHours() {
+ return totalEngineHours;
+ }
+
+ public void setTotalEngineHours(Double totalEngineHours) {
+ this.totalEngineHours = totalEngineHours;
+ }
+
+ public Double getTotalElectricMotorHours() {
+ return totalElectricMotorHours;
+ }
+
+ public void setTotalElectricMotorHours(Double totalElectricMotorHours) {
+ this.totalElectricMotorHours = totalElectricMotorHours;
+ }
+
+ public Double getEngineTotalFuelUsed() {
+ return engineTotalFuelUsed;
+ }
+
+ public void setEngineTotalFuelUsed(Double engineTotalFuelUsed) {
+ this.engineTotalFuelUsed = engineTotalFuelUsed;
+ }
+
+ public String getDriver1Id() {
+ return driver1Id;
+ }
+
+ public void setDriver1Id(String driver1Id) {
+ this.driver1Id = driver1Id;
+ }
+
+ public String getDriver1IdCardIssuer() {
+ return driver1IdCardIssuer;
+ }
+
+ public void setDriver1IdCardIssuer(String driver1IdCardIssuer) {
+ this.driver1IdCardIssuer = driver1IdCardIssuer;
+ }
+ }
+
+ public static class Snapshot {
+ private Double latitude;
+ private Double longitude;
+ private Double heading;
+ private Double altitude;
+ private Double speed;
+ private Long positionDateTime;
+ private Double wheelBasedSpeed;
+ private Double tachographSpeed;
+ private Double engineSpeed;
+ private String fuelType;
+ private Double catalystFuelLevel;
+ private Double fuelLevel1;
+ private Double fuelLevel2;
+ private String driver1WorkingState;
+ private String driver2WorkingState;
+ private Double ambientAirTemperature;
+ private Boolean parkingBrakeSwitch;
+ private Double estimatedDistanceToEmptyFuel;
+ private Double estimatedDistanceToEmptyTotal;
+ private String driver2Id;
+ private String driver2IdCardIssuer;
+
+ public Double getLatitude() {
+ return latitude;
+ }
+
+ public void setLatitude(Double latitude) {
+ this.latitude = latitude;
+ }
+
+ public Double getLongitude() {
+ return longitude;
+ }
+
+ public void setLongitude(Double longitude) {
+ this.longitude = longitude;
+ }
+
+ public Double getHeading() {
+ return heading;
+ }
+
+ public void setHeading(Double heading) {
+ this.heading = heading;
+ }
+
+ public Double getAltitude() {
+ return altitude;
+ }
+
+ public void setAltitude(Double altitude) {
+ this.altitude = altitude;
+ }
+
+ public Double getSpeed() {
+ return speed;
+ }
+
+ public void setSpeed(Double speed) {
+ this.speed = speed;
+ }
+
+ public Long getPositionDateTime() {
+ return positionDateTime;
+ }
+
+ public void setPositionDateTime(Long positionDateTime) {
+ this.positionDateTime = positionDateTime;
+ }
+
+ public Double getWheelBasedSpeed() {
+ return wheelBasedSpeed;
+ }
+
+ public void setWheelBasedSpeed(Double wheelBasedSpeed) {
+ this.wheelBasedSpeed = wheelBasedSpeed;
+ }
+
+ public Double getTachographSpeed() {
+ return tachographSpeed;
+ }
+
+ public void setTachographSpeed(Double tachographSpeed) {
+ this.tachographSpeed = tachographSpeed;
+ }
+
+ public Double getEngineSpeed() {
+ return engineSpeed;
+ }
+
+ public void setEngineSpeed(Double engineSpeed) {
+ this.engineSpeed = engineSpeed;
+ }
+
+ public String getFuelType() {
+ return fuelType;
+ }
+
+ public void setFuelType(String fuelType) {
+ this.fuelType = fuelType;
+ }
+
+ public Double getCatalystFuelLevel() {
+ return catalystFuelLevel;
+ }
+
+ public void setCatalystFuelLevel(Double catalystFuelLevel) {
+ this.catalystFuelLevel = catalystFuelLevel;
+ }
+
+ public Double getFuelLevel1() {
+ return fuelLevel1;
+ }
+
+ public void setFuelLevel1(Double fuelLevel1) {
+ this.fuelLevel1 = fuelLevel1;
+ }
+
+ public Double getFuelLevel2() {
+ return fuelLevel2;
+ }
+
+ public void setFuelLevel2(Double fuelLevel2) {
+ this.fuelLevel2 = fuelLevel2;
+ }
+
+ public String getDriver1WorkingState() {
+ return driver1WorkingState;
+ }
+
+ public void setDriver1WorkingState(String driver1WorkingState) {
+ this.driver1WorkingState = driver1WorkingState;
+ }
+
+ public String getDriver2WorkingState() {
+ return driver2WorkingState;
+ }
+
+ public void setDriver2WorkingState(String driver2WorkingState) {
+ this.driver2WorkingState = driver2WorkingState;
+ }
+
+ public Double getAmbientAirTemperature() {
+ return ambientAirTemperature;
+ }
+
+ public void setAmbientAirTemperature(Double ambientAirTemperature) {
+ this.ambientAirTemperature = ambientAirTemperature;
+ }
+
+ public Boolean getParkingBrakeSwitch() {
+ return parkingBrakeSwitch;
+ }
+
+ public void setParkingBrakeSwitch(Boolean parkingBrakeSwitch) {
+ this.parkingBrakeSwitch = parkingBrakeSwitch;
+ }
+
+ public Double getEstimatedDistanceToEmptyFuel() {
+ return estimatedDistanceToEmptyFuel;
+ }
+
+ public void setEstimatedDistanceToEmptyFuel(Double estimatedDistanceToEmptyFuel) {
+ this.estimatedDistanceToEmptyFuel = estimatedDistanceToEmptyFuel;
+ }
+
+ public Double getEstimatedDistanceToEmptyTotal() {
+ return estimatedDistanceToEmptyTotal;
+ }
+
+ public void setEstimatedDistanceToEmptyTotal(Double estimatedDistanceToEmptyTotal) {
+ this.estimatedDistanceToEmptyTotal = estimatedDistanceToEmptyTotal;
+ }
+
+ public String getDriver2Id() {
+ return driver2Id;
+ }
+
+ public void setDriver2Id(String driver2Id) {
+ this.driver2Id = driver2Id;
+ }
+
+ public String getDriver2IdCardIssuer() {
+ return driver2IdCardIssuer;
+ }
+
+ public void setDriver2IdCardIssuer(String driver2IdCardIssuer) {
+ this.driver2IdCardIssuer = driver2IdCardIssuer;
+ }
+ }
+}
diff --git a/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxTelemetryResource.java b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxTelemetryResource.java
new file mode 100644
index 0000000..242187d
--- /dev/null
+++ b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxTelemetryResource.java
@@ -0,0 +1,78 @@
+// ********************************************************************************
+// * Copyright (c) 2026 Contributors to the Eclipse Foundation
+// *
+// * See the NOTICE file(s) distributed with this work for additional
+// * information regarding copyright ownership.
+// *
+// * This program and the accompanying materials are made available under the
+// * terms of the Apache License 2.0 which is available at
+// * https://www.apache.org/licenses/LICENSE-2.0
+// *
+// * SPDX-License-Identifier: Apache-2.0
+// ********************************************************************************
+
+package org.eclipse.sdv.fleet.analysis;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.BadRequestException;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+
+@Path("/telemetry")
+@Consumes(MediaType.APPLICATION_JSON)
+@Produces(MediaType.APPLICATION_JSON)
+public class InfluxTelemetryResource {
+
+ @Inject
+ private InfluxDbWriter influxDbWriter;
+
+ @POST
+ @Path("/ingest")
+ public InfluxWriteResult ingest(InfluxTelemetryPayload payload) {
+ if (payload == null) {
+ throw new BadRequestException("Payload is required.");
+ }
+
+ if (payload.getVin() == null || payload.getVin().isBlank()) {
+ throw new BadRequestException("vin is required.");
+ }
+
+ if (payload.getTrigger() == null || payload.getTrigger().isBlank()) {
+ throw new BadRequestException("trigger is required.");
+ }
+
+ if (payload.getCreatedDateTime() == null) {
+ throw new BadRequestException("createdDateTime is required.");
+ }
+
+ boolean wroteHeader = false;
+ boolean wroteSnapshot = false;
+
+ if (payload.getHeader() != null) {
+ influxDbWriter.writeHeader(
+ payload.getVin(),
+ payload.getTrigger(),
+ payload.getCreatedDateTime(),
+ payload.getHeader());
+ wroteHeader = true;
+ }
+
+ if (payload.getSnapshot() != null) {
+ influxDbWriter.writeSnapshot(
+ payload.getVin(),
+ payload.getTrigger(),
+ payload.getCreatedDateTime(),
+ payload.getSnapshot());
+ wroteSnapshot = true;
+ }
+
+ if (!wroteHeader && !wroteSnapshot) {
+ throw new BadRequestException("Provide header and/or snapshot data to write.");
+ }
+
+ return new InfluxWriteResult(wroteHeader, wroteSnapshot);
+ }
+}
diff --git a/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxWriteResult.java b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxWriteResult.java
new file mode 100644
index 0000000..91ed27b
--- /dev/null
+++ b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxWriteResult.java
@@ -0,0 +1,43 @@
+// ********************************************************************************
+// * Copyright (c) 2026 Contributors to the Eclipse Foundation
+// *
+// * See the NOTICE file(s) distributed with this work for additional
+// * information regarding copyright ownership.
+// *
+// * This program and the accompanying materials are made available under the
+// * terms of the Apache License 2.0 which is available at
+// * https://www.apache.org/licenses/LICENSE-2.0
+// *
+// * SPDX-License-Identifier: Apache-2.0
+// ********************************************************************************
+
+package org.eclipse.sdv.fleet.analysis;
+
+public class InfluxWriteResult {
+ private boolean headerWritten;
+ private boolean snapshotWritten;
+
+ public InfluxWriteResult() {
+ }
+
+ public InfluxWriteResult(boolean headerWritten, boolean snapshotWritten) {
+ this.headerWritten = headerWritten;
+ this.snapshotWritten = snapshotWritten;
+ }
+
+ public boolean isHeaderWritten() {
+ return headerWritten;
+ }
+
+ public void setHeaderWritten(boolean headerWritten) {
+ this.headerWritten = headerWritten;
+ }
+
+ public boolean isSnapshotWritten() {
+ return snapshotWritten;
+ }
+
+ public void setSnapshotWritten(boolean snapshotWritten) {
+ this.snapshotWritten = snapshotWritten;
+ }
+}
diff --git a/docs/docusaurus.md b/docs/docusaurus.md
index 6bd11de..c63dc44 100644
--- a/docs/docusaurus.md
+++ b/docs/docusaurus.md
@@ -22,7 +22,7 @@ plugins:[ [
// the base directory to output to.
outDir: "docs/fleet-management",
// the file names to download
- documents: ["introduction.md"],
+ documents: ["introduction.md", "fleet-analysis-backend.md"],
},
], [
"docusaurus-plugin-remote-content",
diff --git a/docs/fleet-analysis-backend.md b/docs/fleet-analysis-backend.md
new file mode 100644
index 0000000..25f1b98
--- /dev/null
+++ b/docs/fleet-analysis-backend.md
@@ -0,0 +1,55 @@
+---
+sidebar_position: 2
+title: Fleet Analysis Backend
+---
+
+## Fleet Analysis Backend
+
+The Fleet Management Blueprint includes a Jakarta EE backend service that provides fleet-level analytics on top of telemetry stored in InfluxDB.
+
+## Overview
+
+The backend runs in the same Docker Compose stack as the Fleet Management services and exposes REST APIs for:
+
+- Computing summary statistics from telemetry snapshots
+- Ingesting telemetry payloads into InfluxDB
+- Reading periodically refreshed fleet statistics
+
+## Endpoints
+
+### POST /api/analysis/summary
+
+Accepts a JSON array of vehicle snapshots and returns aggregate values such as fleet size, average speed and battery range.
+
+### POST /api/telemetry/ingest
+
+Writes header and snapshot telemetry to InfluxDB.
+
+### GET /api/analysis/stats
+
+Returns the latest precomputed fleet statistics snapshot.
+
+## Configuration
+
+The component supports these environment variables:
+
+- INFLUXDB_URI (default: `http://influxdb:8086`)
+- INFLUXDB_ORG (default: sdv)
+- INFLUXDB_BUCKET (default: demo)
+- INFLUXDB_TOKEN or INFLUXDB_TOKEN_FILE
+- INFLUXDB_STATS_INTERVAL_SECONDS (default: 30)
+- INFLUXDB_STATS_INITIAL_DELAY_SECONDS (default: 10)
+
+## Build and Run
+
+Build and run from the Fleet Management repository root:
+
+```bash
+docker compose -f ./fms-blueprint-compose.yaml -f ./fms-blueprint-compose-zenoh.yaml up --detach
+```
+
+The service is exposed at:
+
+- `http://127.0.0.1:8082/fleet-analysis/api`
+
+The source code is located in components/backend-fleet-analysis-java.
diff --git a/docs/introduction.md b/docs/introduction.md
index b4dab22..ff92b6a 100644
--- a/docs/introduction.md
+++ b/docs/introduction.md
@@ -8,8 +8,8 @@ title: Introduction
| | |
|------------------|---|
|Short Summary | This repository contains the **Fleet Management Blueprint** which is a close to *real-life* showcase for truck fleet management where trucks run an SDV software stack so that logistics fleet operators can manage apps, data and services for a diverse set of vehicles. |
-|What is in the showcase | In-vehicle data collection using VSS Signal Specification, data transfer to cloud back end, rFMS server|
+|What is in the showcase | In-vehicle data collection using VSS Signal Specification, data transfer to cloud back end, rFMS server, fleet analysis REST API |
|SDV Projects Involved| Eclipse Leda, Eclipse Kuksa |
-|Other interesting Technologies|InfluxDB, Eclipse Hono, Eclipse Kanto, Eclipse Paho |
+|Other interesting Technologies|InfluxDB, Eclipse Hono, Eclipse Kanto, Eclipse Paho, Jakarta EE |
| Architecture Overview | |
| Distro | TBD |
diff --git a/fms-blueprint-compose-zenoh.yaml b/fms-blueprint-compose-zenoh.yaml
index 76e86fe..e3c0681 100644
--- a/fms-blueprint-compose-zenoh.yaml
+++ b/fms-blueprint-compose-zenoh.yaml
@@ -51,7 +51,7 @@ services:
fleet-analysis-backend:
build:
- context: "../../devices/backend-fleet-analysis-java"
+ context: "./components/backend-fleet-analysis-java"
dockerfile: "Dockerfile"
container_name: "fleet-analysis-backend"
networks: