Explorar el Código

Implemented basic integration, fixed some bugs

Lukas Cerny hace 1 año
padre
commit
5a143e0b8b
Se han modificado 35 ficheros con 1175 adiciones y 504 borrados
  1. 1 0
      build.gradle
  2. 18 17
      doc/db_model.svg
  3. 1 0
      docker-compose.yaml
  4. 45 0
      init.sql
  5. 1 1
      src/main/java/cz/senslog/telemetry/app/Application.java
  6. 17 7
      src/main/java/cz/senslog/telemetry/database/domain/Sensor.java
  7. 14 0
      src/main/java/cz/senslog/telemetry/database/domain/UnitTelemetry.java
  8. 2 3
      src/main/java/cz/senslog/telemetry/database/repository/CachedMapLogRepository.java
  9. 46 23
      src/main/java/cz/senslog/telemetry/database/repository/MapLogRepository.java
  10. 6 6
      src/main/java/cz/senslog/telemetry/database/repository/SensLogRepository.java
  11. 10 1
      src/main/java/cz/senslog/telemetry/protocol/domain/AVLDriverActivityPacket.java
  12. 3 0
      src/main/java/cz/senslog/telemetry/protocol/domain/AVLPacket.java
  13. 10 1
      src/main/java/cz/senslog/telemetry/protocol/domain/AVLTelemetryPacket.java
  14. 4 4
      src/main/java/cz/senslog/telemetry/protocol/domain/IOProperty.java
  15. 5 3
      src/main/java/cz/senslog/telemetry/protocol/parser/Fm4exC12.java
  16. 5 3
      src/main/java/cz/senslog/telemetry/protocol/parser/Fm4exC13.java
  17. 3 2
      src/main/java/cz/senslog/telemetry/protocol/parser/Fm4exC8.java
  18. 4 3
      src/main/java/cz/senslog/telemetry/protocol/parser/Fm4exC8Ex.java
  19. 23 24
      src/main/java/cz/senslog/telemetry/server/Fm4exSocketHandler.java
  20. 11 2
      src/main/java/cz/senslog/telemetry/server/HttpVertxServer.java
  21. 1 0
      src/main/java/cz/senslog/telemetry/server/LoopInvoker.java
  22. 9 4
      src/main/java/cz/senslog/telemetry/server/TCPVertxServer.java
  23. 14 5
      src/main/java/cz/senslog/telemetry/server/ws/ContentType.java
  24. 388 329
      src/main/java/cz/senslog/telemetry/server/ws/OpenAPIHandler.java
  25. 6 0
      src/main/java/cz/senslog/telemetry/utils/ResourcesUtils.java
  26. BIN
      src/main/resources/certificate_template.pdf
  27. 29 0
      src/main/resources/no_order.html
  28. 265 10
      src/main/resources/openAPISpec.yaml
  29. 67 0
      src/main/resources/tracking_off.html
  30. 68 0
      src/main/resources/tracking_on.html
  31. 5 2
      src/test/java/cz/senslog/telemetry/DataSet.java
  32. 18 18
      src/test/java/cz/senslog/telemetry/MockSensLogRepository.java
  33. 14 14
      src/test/java/cz/senslog/telemetry/database/repository/MapLogRepositoryTest.java
  34. 9 8
      src/test/java/cz/senslog/telemetry/server/Fm4exSocketHandlerTest.java
  35. 53 14
      src/test/java/cz/senslog/telemetry/server/ws/OpenAPIHandlerTest.java

+ 1 - 0
build.gradle

@@ -61,6 +61,7 @@ dependencies {
     implementation 'org.postgresql:postgresql:42.7.3'
     implementation 'com.ongres.scram:client:2.1'
 
+    implementation 'org.apache.pdfbox:pdfbox:3.0.3'
 
     testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2'
     testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.2'

+ 18 - 17
doc/db_model.svg

@@ -32,23 +32,23 @@
 <polyline fill="none" stroke="black" points="918.5,-182 1212.5,-182 "/>
 <text text-anchor="start" x="926.5" y="-166.8" font-family="Times,serif" font-size="14.00">id : integer</text>
 </g>
-<!-- driver -->
+<!-- entity -->
 <g id="node3" class="node">
-<title>driver</title>
+<title>entity</title>
 <polygon fill="none" stroke="black" points="0,-181.5 0,-250.5 161,-250.5 161,-181.5 0,-181.5"/>
-<text text-anchor="middle" x="80.5" y="-235.3" font-family="Times,serif" font-size="14.00">driver</text>
+<text text-anchor="middle" x="80.5" y="-235.3" font-family="Times,serif" font-size="14.00">entity</text>
 <polyline fill="none" stroke="black" points="0,-227.5 161,-227.5 "/>
 <text text-anchor="start" x="8" y="-212.3" font-family="Times,serif" font-size="14.00">name : varchar(100)</text>
 <polyline fill="none" stroke="black" points="0,-204.5 161,-204.5 "/>
-<text text-anchor="start" x="8" y="-189.3" font-family="Times,serif" font-size="14.00">driver_id : integer</text>
+<text text-anchor="start" x="8" y="-189.3" font-family="Times,serif" font-size="14.00">entity_id : integer</text>
 </g>
-<!-- driver_to_action -->
+<!-- event -->
 <g id="node4" class="node">
-<title>driver_to_action</title>
+<title>event</title>
 <polygon fill="none" stroke="black" points="93.5,-340 93.5,-469 387.5,-469 387.5,-340 93.5,-340"/>
-<text text-anchor="middle" x="240.5" y="-453.8" font-family="Times,serif" font-size="14.00">driver_to_action</text>
+<text text-anchor="middle" x="240.5" y="-453.8" font-family="Times,serif" font-size="14.00">event</text>
 <polyline fill="none" stroke="black" points="93.5,-446 387.5,-446 "/>
-<text text-anchor="start" x="101.5" y="-430.8" font-family="Times,serif" font-size="14.00">driver_id : integer</text>
+<text text-anchor="start" x="101.5" y="-430.8" font-family="Times,serif" font-size="14.00">entity_id : integer</text>
 <text text-anchor="start" x="101.5" y="-415.8" font-family="Times,serif" font-size="14.00"> action_id : integer</text>
 <text text-anchor="start" x="101.5" y="-400.8" font-family="Times,serif" font-size="14.00"> unit_id : bigint</text>
 <text text-anchor="start" x="101.5" y="-385.8" font-family="Times,serif" font-size="14.00"> from_time : timestamp with time zone</text>
@@ -56,19 +56,19 @@
 <polyline fill="none" stroke="black" points="93.5,-363 387.5,-363 "/>
 <text text-anchor="start" x="101.5" y="-347.8" font-family="Times,serif" font-size="14.00">id : integer</text>
 </g>
-<!-- driver_to_action&#45;&gt;action -->
+<!-- event&#45;&gt;action -->
 <g id="edge1" class="edge">
-<title>driver_to_action&#45;&gt;action</title>
+<title>event&#45;&gt;action</title>
 <path fill="none" stroke="#595959" d="M259.5,-339.89C259.5,-339.89 259.5,-260.61 259.5,-260.61"/>
 <polygon fill="#595959" stroke="#595959" points="263,-260.61 259.5,-250.61 256,-260.61 263,-260.61"/>
 <text text-anchor="middle" x="291.5" y="-302.8" font-family="Times,serif" font-size="14.00">action_id</text>
 </g>
-<!-- driver_to_action&#45;&gt;driver -->
+<!-- event&#45;&gt;entity -->
 <g id="edge2" class="edge">
-<title>driver_to_action&#45;&gt;driver</title>
+<title>event&#45;&gt;entity</title>
 <path fill="none" stroke="#595959" d="M127.25,-339.89C127.25,-339.89 127.25,-260.61 127.25,-260.61"/>
 <polygon fill="#595959" stroke="#595959" points="130.75,-260.61 127.25,-250.61 123.75,-260.61 130.75,-260.61"/>
-<text text-anchor="middle" x="112.5" y="-302.8" font-family="Times,serif" font-size="14.00">driver_id</text>
+<text text-anchor="middle" x="112.5" y="-302.8" font-family="Times,serif" font-size="14.00">entity_id</text>
 </g>
 <!-- unit -->
 <g id="node9" class="node">
@@ -84,9 +84,9 @@
 <polyline fill="none" stroke="black" points="609.5,-174.5 805.5,-174.5 "/>
 <text text-anchor="start" x="617.5" y="-159.3" font-family="Times,serif" font-size="14.00">unit_id : bigint</text>
 </g>
-<!-- driver_to_action&#45;&gt;unit -->
+<!-- event&#45;&gt;unit -->
 <g id="edge3" class="edge">
-<title>driver_to_action&#45;&gt;unit</title>
+<title>event&#45;&gt;unit</title>
 <path fill="none" stroke="#595959" d="M382,-339.79C382,-317.48 382,-298 382,-298 382,-298 658.5,-298 658.5,-298 658.5,-298 658.5,-290.61 658.5,-290.61"/>
 <polygon fill="#595959" stroke="#595959" points="662,-290.61 658.5,-280.61 655,-290.61 662,-290.61"/>
 <text text-anchor="middle" x="409" y="-302.8" font-family="Times,serif" font-size="14.00">unit_id</text>
@@ -134,8 +134,9 @@
 <text text-anchor="start" x="384.5" y="-242.3" font-family="Times,serif" font-size="14.00">name : varchar(100)</text>
 <text text-anchor="start" x="384.5" y="-227.3" font-family="Times,serif" font-size="14.00"> type : text</text>
 <text text-anchor="start" x="384.5" y="-212.3" font-family="Times,serif" font-size="14.00"> io_id : integer</text>
-<text text-anchor="start" x="384.5" y="-197.3" font-family="Times,serif" font-size="14.00"> phenomenon_id : integer</text>
-<text text-anchor="start" x="384.5" y="-182.3" font-family="Times,serif" font-size="14.00"> description : text</text>
+<text text-anchor="start" x="384.5" y="-197.3" font-family="Times,serif" font-size="14.00"> io_multiplier : double</text>
+<text text-anchor="start" x="384.5" y="-182.3" font-family="Times,serif" font-size="14.00"> phenomenon_id : integer</text>
+<text text-anchor="start" x="384.5" y="-167.3" font-family="Times,serif" font-size="14.00"> description : text</text>
 <polyline fill="none" stroke="black" points="376.5,-174.5 574.5,-174.5 "/>
 <text text-anchor="start" x="384.5" y="-159.3" font-family="Times,serif" font-size="14.00">sensor_id : bigint</text>
 </g>

+ 1 - 0
docker-compose.yaml

@@ -22,6 +22,7 @@ services:
       - TZ="Europe/Prague"
     ports:
       - "8085:8085"
+#      - "9999:9999"
       - "5005:5005"
     depends_on:
       - telemetry-db

+ 45 - 0
init.sql

@@ -153,6 +153,7 @@ CREATE TABLE maplog.sensor (
     type TEXT,
     description TEXT,
     io_id INTEGER NOT NULL,
+    io_multiplier DOUBLE PRECISION NOT NULL DEFAULT 1.0,
     phenomenon_id INTEGER NOT NULL
 );
 ALTER TABLE maplog.sensor OWNER TO senslog;
@@ -326,6 +327,50 @@ ALTER TABLE ONLY maplog.event ADD CONSTRAINT event_actionid_fk FOREIGN KEY (acti
 
 ALTER TABLE ONLY maplog.alert ADD CONSTRAINT alert_unitid_fk FOREIGN KEY (unit_id) REFERENCES maplog.unit(unit_id) ON UPDATE CASCADE ON DELETE CASCADE;
 
+ALTER TABLE ONLY maplog.unit_static_to_mobile ADD CONSTRAINT static_mobile_st_fk FOREIGN KEY (static_unit_id) REFERENCES maplog.unit(unit_id) ON UPDATE CASCADE ON DELETE CASCADE;
+
+ALTER TABLE ONLY maplog.unit_static_to_mobile ADD CONSTRAINT static_mobile_mb_fk FOREIGN KEY (mobile_unit_id) REFERENCES maplog.unit(unit_id) ON UPDATE CASCADE ON DELETE CASCADE;
 
 REVOKE USAGE ON SCHEMA public FROM PUBLIC;
 GRANT ALL ON SCHEMA public TO PUBLIC;
+
+
+CREATE SCHEMA tracking;
+ALTER SCHEMA tracking OWNER TO senslog;
+
+CREATE TYPE order_status AS ENUM ('READY', 'MONITORING', 'DELIVERED');
+
+CREATE TYPE delivery_type AS ENUM ('SERVICE_FOFR', 'VEHICLE_CITROEN', 'VEHICLE_PEUGEOT');
+
+CREATE TABLE tracking.record (
+    id BIGINT NOT NULL PRIMARY KEY,
+    unit_id BIGINT NOT NULL,
+    order_id BIGINT NOT NULL,
+    tracking_id BIGINT NOT NULL,
+    status order_status NOT NULL DEFAULT 'READY',
+    delivery_type delivery_type NOT NULL,
+    time_tracking_start TIMESTAMP WITH TIME ZONE DEFAULT null,
+    time_tracking_stop TIMESTAMP WITH TIME ZONE DEFAULT null,
+    time_received TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
+);
+ALTER TABLE tracking.record OWNER TO senslog;
+
+CREATE UNIQUE INDEX tracking_record_uniq_open ON tracking.record(unit_id) WHERE (status = 'READY' OR status = 'MONITORING');
+
+CREATE SEQUENCE tracking.record_id_seq START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1;
+
+ALTER TABLE tracking.record_id_seq OWNER TO senslog;
+
+ALTER SEQUENCE tracking.record_id_seq OWNED BY tracking.record.id;
+
+ALTER TABLE ONLY tracking.record ALTER COLUMN id SET DEFAULT nextval('tracking.record_id_seq'::regclass);
+
+CREATE TABLE tracking.order_to_event (
+    record_id   BIGINT NOT NULL,
+    event_id    BIGINT NOT NULL,
+    PRIMARY KEY (record_id, event_id)
+);
+ALTER TABLE tracking.order_to_event OWNER TO senslog;
+
+ALTER TABLE ONLY tracking.order_to_event ADD CONSTRAINT order_event_rec_fk FOREIGN KEY (record_id) REFERENCES tracking.record(id) ON UPDATE CASCADE ON DELETE CASCADE;
+ALTER TABLE ONLY tracking.order_to_event ADD CONSTRAINT order_event_ev_fk FOREIGN KEY (event_id) REFERENCES maplog.event(id) ON UPDATE CASCADE ON DELETE CASCADE;

+ 1 - 1
src/main/java/cz/senslog/telemetry/app/Application.java

@@ -91,7 +91,7 @@ public final class Application {
 
         Vertx.vertx().deployVerticle(VertxDeployer.deploy(
                 new HttpVertxServer(MapLogRepository.create(dbPool)),
-                new TCPVertxServer(CachedMapLogRepository.create(dbPool)),
+                new TCPVertxServer(MapLogRepository.create(dbPool)),
                 new AnalyticModule(), new AlertModule()
         ), options, res -> {
             if(res.succeeded()) {

+ 17 - 7
src/main/java/cz/senslog/telemetry/database/domain/Sensor.java

@@ -6,30 +6,36 @@ public class Sensor {
     private final String name;
     private final String type;
     private final int ioID;
+    private final double ioMultiplier;
     private final Phenomenon phenomenon;
     private final String description;
 
-    public static Sensor of(long sensorId, String name, String type, int ioID, Phenomenon phenomenon, String description) {
-        return new Sensor(sensorId, name, type, ioID, phenomenon, description);
+    public static Sensor of(long sensorId, String name, String type, int ioID, double ioMultiplier, Phenomenon phenomenon, String description) {
+        return new Sensor(sensorId, name, type, ioID, ioMultiplier, phenomenon, description);
+    }
+
+    public static Sensor of(long sensorId, String name, String type, int ioID, double ioMultiplier) {
+        return of(sensorId, name, type, ioID, ioMultiplier, null, null);
     }
 
     public static Sensor of(long sensorId, String name, String type) {
-        return of(sensorId, name, type, -1, null, null);
+        return of(sensorId, name, type, -1, 1.0, null, null);
     }
 
     public static Sensor of(long sensorId, String name) {
-        return of(sensorId, name, null, -1, null, null);
+        return of(sensorId, name, null, -1, 1.0, null, null);
     }
 
-    public static Sensor of(long sensorId, String name, int ioId) {
-        return of(sensorId, name, null, ioId, null, null);
+    public static Sensor of(long sensorId, String name, int ioId, double ioMultiplier) {
+        return of(sensorId, name, null, ioId, ioMultiplier, null, null);
     }
 
-    private Sensor(long sensorId, String name, String type, int ioID, Phenomenon phenomenon, String description) {
+    private Sensor(long sensorId, String name, String type, int ioID, double ioMultiplier, Phenomenon phenomenon, String description) {
         this.sensorId = sensorId;
         this.name = name;
         this.type = type;
         this.ioID = ioID;
+        this.ioMultiplier = ioMultiplier;
         this.phenomenon = phenomenon;
         this.description = description;
     }
@@ -50,6 +56,10 @@ public class Sensor {
         return ioID;
     }
 
+    public double getIoMultiplier() {
+        return ioMultiplier;
+    }
+
     public Phenomenon getPhenomenon() {
         return phenomenon;
     }

+ 14 - 0
src/main/java/cz/senslog/telemetry/database/domain/UnitTelemetry.java

@@ -6,6 +6,7 @@ import java.time.OffsetDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 import java.util.stream.Stream;
 
 public class UnitTelemetry {
@@ -70,6 +71,19 @@ public class UnitTelemetry {
                 '}';
     }
 
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        UnitTelemetry telemetry = (UnitTelemetry) o;
+        return unitId == telemetry.unitId && Objects.equals(timestamp, telemetry.timestamp);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(unitId, timestamp);
+    }
+
     public static class Converter {
 
         public static Stream<SensLogObservation> toSensLogObservationAsStream(UnitTelemetry unitTelemetry) {

+ 2 - 3
src/main/java/cz/senslog/telemetry/database/repository/CachedMapLogRepository.java

@@ -3,7 +3,6 @@ package cz.senslog.telemetry.database.repository;
 import cz.senslog.telemetry.database.domain.Sensor;
 import cz.senslog.telemetry.database.domain.Unit;
 import io.vertx.core.Future;
-import io.vertx.pgclient.PgPool;
 import io.vertx.sqlclient.Pool;
 
 import java.util.Map;
@@ -30,10 +29,10 @@ public class CachedMapLogRepository extends MapLogRepository {
                 .andThen(u -> CACHE_IMEI_TO_UNIT.put(imei, u.result()));
     }
 
-    private final Map<Long, Map<Long, Sensor>> CACHE_UNIT_TO_ioSENSORS = new ConcurrentHashMap<>();
+    private final Map<Long, Map<Integer, Sensor>> CACHE_UNIT_TO_ioSENSORS = new ConcurrentHashMap<>();
 
     @Override
-    public Future<Map<Long, Sensor>> findSensorsByUnitIdGroupById(long unitId) {
+    public Future<Map<Integer, Sensor>> findSensorsByUnitIdGroupById(long unitId) {
         if (CACHE_UNIT_TO_ioSENSORS.containsKey(unitId)) {
             return Future.succeededFuture(CACHE_UNIT_TO_ioSENSORS.get(unitId));
         }

+ 46 - 23
src/main/java/cz/senslog/telemetry/database/repository/MapLogRepository.java

@@ -318,6 +318,7 @@ public class MapLogRepository implements SensLogRepository {
             row.getString("name"),
             row.getString("type"),
             row.getInteger("io_id"),
+            row.getDouble("io_multiplier"),
             Phenomenon.of(
                     row.getInteger("phenomenon_id"),
                     row.getString("phenomenon_name")
@@ -327,7 +328,7 @@ public class MapLogRepository implements SensLogRepository {
 
     @Override
     public Future<Sensor> findSensorById(long sensorId) {
-        return client.preparedQuery("SELECT s.sensor_id, s.name, s.type, s.io_id, s.description, p.id AS phenomenon_id, p.name AS phenomenon_name " +
+        return client.preparedQuery("SELECT s.sensor_id, s.name, s.type, s.io_id, s.io_multiplier, s.description, p.id AS phenomenon_id, p.name AS phenomenon_name " +
                         "FROM maplog.sensor AS s " +
                         "JOIN maplog.phenomenon p on p.id = s.phenomenon_id " +
                         "WHERE s.sensor_id = $1")
@@ -340,7 +341,7 @@ public class MapLogRepository implements SensLogRepository {
 
     @Override
     public Future<Sensor> findSensorByIdentityAndId(String userIdentity, long sensorId) {
-        return client.preparedQuery("SELECT s.sensor_id, s.name, s.type, s.io_id, s.description, p.id AS phenomenon_id, p.name AS phenomenon_name, p.uom, p.uom_link " +
+        return client.preparedQuery("SELECT s.sensor_id, s.name, s.type, s.io_id, s.io_multiplier, s.description, p.id AS phenomenon_id, p.name AS phenomenon_name, p.uom, p.uom_link " +
                         "FROM maplog.sensor AS s " +
                         "JOIN maplog.phenomenon p on p.id = s.phenomenon_id " +
                         "JOIN maplog.unit_to_sensor uts on s.sensor_id = uts.sensor_id " +
@@ -361,13 +362,14 @@ public class MapLogRepository implements SensLogRepository {
             row.getString("name"),
             row.getString("type"),
             row.getInteger("io_id"),
+            row.getDouble("io_multiplier"),
             Phenomenon.of(row.getLong("phenomenon_id"), null),
             row.getString("description")
     );
 
     @Override
     public Future<Sensor> findSensorByIOAndUnitId(int ioID, long unitId) {
-        return client.preparedQuery("SELECT s.sensor_id, s.name, s.type, s.io_id, s.phenomenon_id, s.description " +
+        return client.preparedQuery("SELECT s.sensor_id, s.name, s.type, s.io_id, s.io_multiplier, s.phenomenon_id, s.description " +
                         "FROM maplog.sensor AS s " +
                         "JOIN maplog.unit_to_sensor uts ON s.sensor_id = uts.sensor_id " +
                         "WHERE s.io_id = $1 AND uts.unit_id = $2")
@@ -380,7 +382,7 @@ public class MapLogRepository implements SensLogRepository {
 
     @Override
     public Future<List<Sensor>> allSensors() {
-        return client.query("SELECT sensor_id, name, type, io_id, phenomenon_id, description FROM maplog.sensor ORDER BY sensor_id")
+        return client.query("SELECT sensor_id, name, type, io_id, io_multiplier, phenomenon_id, description FROM maplog.sensor ORDER BY sensor_id")
                 .execute()
                 .map(rs -> StreamSupport.stream(rs.spliterator(), false)
                         .map((row) -> Sensor.of(
@@ -388,6 +390,7 @@ public class MapLogRepository implements SensLogRepository {
                                 row.getString("name"),
                                 row.getString("type"),
                                 row.getInteger("io_id"),
+                                row.getDouble("io_multiplier"),
                                 Phenomenon.of(row.getLong("phenomenon_id"), null),
                                 row.getString("description")
                         )).collect(toList())
@@ -396,7 +399,7 @@ public class MapLogRepository implements SensLogRepository {
 
     @Override
     public Future<List<Sensor>> allSensorsByIdentity(String userIdentity) {
-        return client.preparedQuery("SELECT s.sensor_id, s.name, s.type, s.io_id, s.phenomenon_id, s.description FROM maplog.sensor AS s " +
+        return client.preparedQuery("SELECT s.sensor_id, s.name, s.type, s.io_id, s.io_multiplier, s.phenomenon_id, s.description FROM maplog.sensor AS s " +
                         "JOIN maplog.unit_to_sensor uts on s.sensor_id = uts.sensor_id " +
                         "JOIN maplog.unit_to_campaign utc on uts.unit_id = utc.unit_id " +
                         "JOIN maplog.event e on e.unit_id = utc.unit_id " +
@@ -410,6 +413,7 @@ public class MapLogRepository implements SensLogRepository {
                                 row.getString("name"),
                                 row.getString("type"),
                                 row.getInteger("io_id"),
+                                row.getDouble("io_multiplier"),
                                 Phenomenon.of(row.getLong("phenomenon_id"), null),
                                 row.getString("description")
                         )).collect(toList())
@@ -418,7 +422,7 @@ public class MapLogRepository implements SensLogRepository {
 
     @Override
     public Future<List<Sensor>> findSensorsByUnitId(long unitId) {
-        return client.preparedQuery("SELECT s.sensor_id, s.name, s.type " +
+        return client.preparedQuery("SELECT s.sensor_id, s.name, s.type, s.io_id, s.io_multiplier " +
                         "FROM maplog.unit_to_sensor AS uts " +
                         "JOIN maplog.sensor s on s.sensor_id = uts.sensor_id " +
                         "WHERE uts.unit_id = $1 ORDER BY s.sensor_id")
@@ -427,7 +431,9 @@ public class MapLogRepository implements SensLogRepository {
                         .map((row) -> Sensor.of(
                                 row.getLong("sensor_id"),
                                 row.getString("name"),
-                                row.getString("type")
+                                row.getString("type"),
+                                row.getInteger("io_id"),
+                                row.getDouble("io_multiplier")
                         )).collect(toList())
                 );
     }
@@ -524,7 +530,7 @@ public class MapLogRepository implements SensLogRepository {
 
     @Override
     public Future<Sensor> findSensorByCampaignIdAndUnitId(long campaignId, long unitId, long sensorId) {
-        return client.preparedQuery("SELECT s.sensor_id, s.name, s.type, s.io_id, s.description, p.id AS phenomenon_id, p.name AS phenomenon_name " +
+        return client.preparedQuery("SELECT s.sensor_id, s.name, s.type, s.io_id, s.io_multiplier, s.description, p.id AS phenomenon_id, p.name AS phenomenon_name " +
                         "FROM maplog.sensor AS s " +
                         "JOIN maplog.phenomenon AS p ON p.id = s.phenomenon_id " +
                         "JOIN maplog.unit_to_sensor AS uts ON s.sensor_id = uts.sensor_id " +
@@ -896,10 +902,28 @@ public class MapLogRepository implements SensLogRepository {
     }
 
     @Override
-    public Future<List<Event>> findEventsByEntityIdAndUnitIdAndActionId(int entityId, long unitId, int actionId) {
+    public Future<List<Event>> findEventsByEntityIdAndUnitIdAndActionId(int entityId, long unitId, int actionId, OffsetDateTime from, OffsetDateTime to) {
+        String sqlWhereClause;
+        Tuple tupleParams;
+
+        // TODO implement 'from' and 'to' -> verify the correctness
+        if (from != null && to != null) {
+            sqlWhereClause = "entity_id = $1 AND unit_id = $2 AND action_id = $3 AND from_time <= $4 AND to_time <= $5"; // TO_TIME can be null too
+            tupleParams = Tuple.of(entityId, unitId, actionId, from, to);
+        } else if (from != null) {
+            sqlWhereClause = "entity_id = $1 AND unit_id = $2 AND action_id = $3 AND (($4 <= event.from_time) OR (from_time <= $4 AND (to_time IS NULL OR to_time >= $4)))";
+            tupleParams = Tuple.of(entityId, unitId, actionId, from);
+        } else if (to != null) {
+            sqlWhereClause = "entity_id = $1 AND unit_id = $2 AND action_id = $3 AND ((to_time IS NULL AND from_time <= $4 OR (to_time IS NOT NULL AND to_time <= $4)))";
+            tupleParams = Tuple.of(entityId, unitId, actionId, to);
+        } else {
+            sqlWhereClause = "entity_id = $1 AND unit_id = $2 AND action_id = $3";
+            tupleParams = Tuple.of(entityId, unitId, actionId);
+        }
+
         return client.preparedQuery("SELECT id, entity_id, action_id, unit_id, from_time, to_time FROM maplog.event " +
-                        "WHERE entity_id = $1 AND unit_id = $2 AND action_id = $3 ORDER BY id")
-                .execute(Tuple.of(entityId, unitId, actionId))
+                        "WHERE "+sqlWhereClause+" ORDER BY id")
+                .execute(tupleParams)
                 .map(rs -> StreamSupport.stream(rs.spliterator(), false)
                         .map(row -> Event.of(
                                 row.getLong("id"),
@@ -913,7 +937,7 @@ public class MapLogRepository implements SensLogRepository {
     }
 
     @Override
-    public Future<List<Event>> findEventsByIdentityAndEntityIdAndUnitIdAndActionId(String userIdentity, int entityId, long unitId, int actionId) {
+    public Future<List<Event>> findEventsByIdentityAndEntityIdAndUnitIdAndActionId(String userIdentity, int entityId, long unitId, int actionId, OffsetDateTime from, OffsetDateTime to) {
         return client.preparedQuery("SELECT e.id, e.entity_id, e.action_id, e.unit_id, e.from_time, e.to_time FROM maplog.event As e " +
                         "JOIN maplog.user_to_campaign_config uc ON uc.entity_id = e.entity_id " +
                         "JOIN maplog.unit_to_campaign utc on utc.campaign_id = uc.campaign_id and utc.unit_id = e.unit_id " +
@@ -1224,9 +1248,9 @@ public class MapLogRepository implements SensLogRepository {
     }
 
     @Override
-    public Future<Map<Long, Sensor>> findSensorsByUnitIdGroupById(long unitId) {
+    public Future<Map<Integer, Sensor>> findSensorsByUnitIdGroupById(long unitId) {
         return findSensorsByUnitId(unitId).map(sensors -> sensors.stream()
-                .collect(Collectors.toMap(Sensor::getSensorId, Function.identity())));
+                .collect(Collectors.toMap(Sensor::getIoID, Function.identity())));
     }
 
     @Override
@@ -1643,7 +1667,7 @@ public class MapLogRepository implements SensLogRepository {
 
     @Override
     public Future<List<UnitTelemetry>> findObservationsByIdentityAndCampaignIdAndUnitId(
-            String userIdentity, long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, int offset, int limit, List<Filter> filters
+            String userIdentity, long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, int offset, int limit, SortType sort, List<Filter> filters
     ) {
         String whereTimestampClause;
         Tuple tupleParams;
@@ -1701,7 +1725,7 @@ public class MapLogRepository implements SensLogRepository {
                         "JOIN maplog.user_to_campaign_config AS uc ON uc.campaign_id = utc.campaign_id and uc.entity_id = e.entity_id " +
                         "JOIN maplog.system_user AS su ON su.id = uc.user_id " +
                         "WHERE utc.campaign_id = $1 AND utc.unit_id = $2 AND su.identity = $5 AND " + whereTimestampClause + whereFiltersClause + " " +
-                        "ORDER BY tel.time_stamp OFFSET $3 LIMIT $4";
+                        "ORDER BY tel.time_stamp "+sort.name()+" OFFSET $3 LIMIT $4";
 
         return client.preparedQuery("SELECT s.sensor_id, s.name FROM maplog.sensor s " +
                         "JOIN maplog.unit_to_sensor uts ON uts.sensor_id = s.sensor_id " +
@@ -1732,19 +1756,20 @@ public class MapLogRepository implements SensLogRepository {
     public Future<List<UnitTelemetry>> findObservationsByEventId(
             long eventId, OffsetDateTime from, OffsetDateTime to, int offset, int limit, List<Filter> filters
     ) {
-        String whereTimestampClause; // TODO check timestamps
+        String whereTimestampClause;
         Tuple tupleParams;
         if (from != null && to != null) {
-            whereTimestampClause = "tel.time_stamp >= (CASE WHEN $4 < e.from_time THEN $4 ELSE e.from_time END) AND tel.time_stamp <= (CASE WHEN e.to_time IS NULL OR $5 < e.to_time THEN $5 ELSE e.to_time END)";
+            whereTimestampClause = "tel.time_stamp >= (CASE WHEN $4 < e.from_time THEN e.from_time ELSE $4 END) " +
+                    "AND tel.time_stamp < (CASE WHEN e.to_time IS NULL THEN $5 ELSE (CASE WHEN $5 > e.to_time THEN e.to_time ELSE $5 END) END)";
             tupleParams = Tuple.of(eventId, offset, limit, from, to);
         } else if (from != null) {
-            whereTimestampClause = "tel.time_stamp >= (CASE WHEN $4 < e.from_time THEN $4 ELSE e.from_time END) AND tel.time_stamp <= (CASE WHEN e.to_time IS NULL THEN now() ELSE e.to_time END)";
+            whereTimestampClause = "tel.time_stamp >= (CASE WHEN $4 < e.from_time THEN e.from_time ELSE $4 END) AND tel.time_stamp < (CASE WHEN e.to_time IS NULL THEN now() ELSE e.to_time END)";
             tupleParams = Tuple.of(eventId, offset, limit, from);
         } else if (to != null) {
-            whereTimestampClause = "tel.time_stamp >= e.from_time AND tel.time_stamp <= (CASE WHEN e.to_time IS NULL OR $4 < e.to_time THEN $4 ELSE e.to_time END)";
+            whereTimestampClause = "tel.time_stamp >= e.from_time AND tel.time_stamp < (CASE WHEN e.to_time IS NULL THEN $4 ELSE (CASE WHEN $4 > e.to_time THEN e.to_time ELSE $4 END) END)";
             tupleParams = Tuple.of(eventId, offset, limit, to);
         } else {
-            whereTimestampClause = "tel.time_stamp >= e.from_time AND tel.time_stamp <= (CASE WHEN e.to_time IS NULL THEN now() ELSE e.to_time END)";
+            whereTimestampClause = "tel.time_stamp >= e.from_time AND tel.time_stamp < (CASE WHEN e.to_time IS NULL THEN now() ELSE e.to_time END)";
             tupleParams = Tuple.of(eventId, offset, limit);
         }
 
@@ -3010,6 +3035,4 @@ public class MapLogRepository implements SensLogRepository {
                         .collect(toList())
                 );
     }
-
-
 }

+ 6 - 6
src/main/java/cz/senslog/telemetry/database/repository/SensLogRepository.java

@@ -52,7 +52,7 @@ public interface SensLogRepository {
     Future<Sensor> findSensorById(long sensorId);
     Future<Sensor> findSensorByIdentityAndId(String userIdentity, long sensorId);
     Future<Sensor> findSensorByIOAndUnitId(int ioID, long unitId);
-    Future<Map<Long, Sensor>> findSensorsByUnitIdGroupById(long unitId);
+    Future<Map<Integer, Sensor>> findSensorsByUnitIdGroupById(long unitId);
     Future<Map<Long, List<Sensor>>> findSensorsByCampaignIdGroupByUnitId(long campaignId);
     Future<List<Sensor>> findSensorsByUnitId(long unitId);
     Future<List<Sensor>> findSensorsByIdentityAndUnitId(String userIdentity, long unitId);
@@ -94,8 +94,8 @@ public interface SensLogRepository {
     Future<Action> findActionByIdentityAndIdAndEntityIdAndUnitId(String userIdentity, int actionId, int entityId, long unitId);
 
 
-    Future<List<Event>> findEventsByEntityIdAndUnitIdAndActionId(int entityId, long unitId, int actionId);
-    Future<List<Event>> findEventsByIdentityAndEntityIdAndUnitIdAndActionId(String userIdentity, int entityId, long unitId, int actionId);
+    Future<List<Event>> findEventsByEntityIdAndUnitIdAndActionId(int entityId, long unitId, int actionId, OffsetDateTime from, OffsetDateTime to);
+    Future<List<Event>> findEventsByIdentityAndEntityIdAndUnitIdAndActionId(String userIdentity, int entityId, long unitId, int actionId, OffsetDateTime from, OffsetDateTime to);
     Future<Event> findEventById(long eventId);
     Future<Event> findEventByIdentityAndId(String userIdentity, long eventId);
 
@@ -136,9 +136,9 @@ public interface SensLogRepository {
                     return new PagingRetrieve<>(hasNext, data.size(), data);
                 });
     }
-    Future<List<UnitTelemetry>> findObservationsByIdentityAndCampaignIdAndUnitId(String userIdentity, long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, int offset, int limit, List<Filter> filters);
-    default Future<PagingRetrieve<List<UnitTelemetry>>> findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging(String userIdentity, long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, int offset, int limit, List<Filter> filters) {
-        return findObservationsByIdentityAndCampaignIdAndUnitId(userIdentity, campaignId, unitId, from, to, offset, limit+1, filters)
+    Future<List<UnitTelemetry>> findObservationsByIdentityAndCampaignIdAndUnitId(String userIdentity, long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, int offset, int limit, SortType sort, List<Filter> filters);
+    default Future<PagingRetrieve<List<UnitTelemetry>>> findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging(String userIdentity, long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, int offset, int limit, SortType sort, List<Filter> filters) {
+        return findObservationsByIdentityAndCampaignIdAndUnitId(userIdentity, campaignId, unitId, from, to, offset, limit+1, sort, filters)
                 .map(data -> {
                     boolean hasNext = data.size() > limit;
                     if (hasNext) {

+ 10 - 1
src/main/java/cz/senslog/telemetry/protocol/domain/AVLDriverActivityPacket.java

@@ -1,16 +1,20 @@
 package cz.senslog.telemetry.protocol.domain;
 
+import cz.senslog.telemetry.protocol.Fm4exCodecType;
+
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.List;
 
 public class AVLDriverActivityPacket implements AVLPacket {
 
+    private final Fm4exCodecType codecType;
     private Instant timestamp;
     private final List<IOProperty> ioProperties;
 
-    public AVLDriverActivityPacket() {
+    public AVLDriverActivityPacket(Fm4exCodecType codecType) {
         this.ioProperties = new ArrayList<>();
+        this.codecType = codecType;
     }
 
     public Instant getTimestamp() {
@@ -26,6 +30,11 @@ public class AVLDriverActivityPacket implements AVLPacket {
     }
 
     @Override
+    public Fm4exCodecType codecType() {
+        return codecType;
+    }
+
+    @Override
     public AVLPacketType type() {
         return AVLPacketType.DRIVER_ACTIVITY;
     }

+ 3 - 0
src/main/java/cz/senslog/telemetry/protocol/domain/AVLPacket.java

@@ -1,6 +1,9 @@
 package cz.senslog.telemetry.protocol.domain;
 
+import cz.senslog.telemetry.protocol.Fm4exCodecType;
+
 public interface AVLPacket {
 
+    Fm4exCodecType codecType();
     AVLPacketType type();
 }

+ 10 - 1
src/main/java/cz/senslog/telemetry/protocol/domain/AVLTelemetryPacket.java

@@ -1,10 +1,14 @@
 package cz.senslog.telemetry.protocol.domain;
 
+import cz.senslog.telemetry.protocol.Fm4exCodecType;
+
 public class AVLTelemetryPacket implements AVLPacket {
 
+    private final Fm4exCodecType codecType;
     private final AVLTelemetryObservation[] observations;
 
-    public AVLTelemetryPacket(AVLTelemetryObservation[] observations) {
+    public AVLTelemetryPacket(Fm4exCodecType codecType, AVLTelemetryObservation[] observations) {
+        this.codecType = codecType;
         this.observations = observations;
     }
 
@@ -13,6 +17,11 @@ public class AVLTelemetryPacket implements AVLPacket {
     }
 
     @Override
+    public Fm4exCodecType codecType() {
+        return codecType;
+    }
+
+    @Override
     public AVLPacketType type() {
         return AVLPacketType.TELEMETRY_OBSERVATION;
     }

+ 4 - 4
src/main/java/cz/senslog/telemetry/protocol/domain/IOProperty.java

@@ -3,13 +3,13 @@ package cz.senslog.telemetry.protocol.domain;
 public class IOProperty {
 
     private final int id;
-    private final long value;
+    private final double value;
 
-    public static IOProperty create(int id, long value) {
+    public static IOProperty create(int id, double value) {
         return new IOProperty(id, value);
     }
 
-    private IOProperty(int id, long value) {
+    private IOProperty(int id, double value) {
         this.id = id;
         this.value = value;
     }
@@ -18,7 +18,7 @@ public class IOProperty {
         return id;
     }
 
-    public long getValue() {
+    public double getValue() {
         return value;
     }
 

+ 5 - 3
src/main/java/cz/senslog/telemetry/protocol/parser/Fm4exC12.java

@@ -7,6 +7,8 @@ import io.vertx.core.buffer.Buffer;
 
 import java.util.Optional;
 
+import static cz.senslog.telemetry.protocol.Fm4exCodecType.CODEC_12;
+
 public class Fm4exC12 implements Fm4exParser {
 
     @Override
@@ -15,9 +17,9 @@ public class Fm4exC12 implements Fm4exParser {
 
         short codec = buffer.getUnsignedByte(byteIndex++);
         Optional<Fm4exCodecType> codecType = Fm4exCodecType.getType(codec);
-        if (codecType.isEmpty() || codecType.get() != Fm4exCodecType.CODEC_12) {
+        if (codecType.isEmpty() || codecType.get() != CODEC_12) {
             throw new IllegalArgumentException(String.format(
-                    "Unsupported codec type. Expected <%d> was <%d>", Fm4exCodecType.CODEC_12.codecId(), codec)
+                    "Unsupported codec type. Expected <%d> was <%d>", CODEC_12.codecId(), codec)
             );
         }
 
@@ -33,7 +35,7 @@ public class Fm4exC12 implements Fm4exParser {
         byte nmOf1BIOs = buffer.getByte(byteIndex);
         byteIndex+=1;
 
-        AVLDriverActivityPacket packet = new AVLDriverActivityPacket();
+        AVLDriverActivityPacket packet = new AVLDriverActivityPacket(CODEC_12);
         for (int count = 0; count < nmOf1BIOs; count++, byteIndex+=1) {
             int io1BId = Short.toUnsignedInt(buffer.getUnsignedByte(byteIndex++));
             short io1BVal = buffer.getUnsignedByte(byteIndex);

+ 5 - 3
src/main/java/cz/senslog/telemetry/protocol/parser/Fm4exC13.java

@@ -8,6 +8,8 @@ import io.vertx.core.buffer.Buffer;
 import java.time.Instant;
 import java.util.Optional;
 
+import static cz.senslog.telemetry.protocol.Fm4exCodecType.CODEC_13;
+
 public class Fm4exC13 implements Fm4exParser {
 
     @Override
@@ -16,12 +18,12 @@ public class Fm4exC13 implements Fm4exParser {
 
         short codec = buffer.getUnsignedByte(byteIndex++);
         Optional<Fm4exCodecType> codecType = Fm4exCodecType.getType(codec);
-        if (codecType.isEmpty() || codecType.get() != Fm4exCodecType.CODEC_13) {
+        if (codecType.isEmpty() || codecType.get() != CODEC_13) {
             throw new IllegalArgumentException(String.format(
-                    "Unsupported codec type. Expected <%d> was <%d>", Fm4exCodecType.CODEC_13.codecId(), codec)
+                    "Unsupported codec type. Expected <%d> was <%d>", CODEC_13.codecId(), codec)
             );
         }
-        AVLDriverActivityPacket packet = new AVLDriverActivityPacket();
+        AVLDriverActivityPacket packet = new AVLDriverActivityPacket(CODEC_13);
 
         short cmdQuality1 = buffer.getUnsignedByte(byteIndex);
         byteIndex += 1;

+ 3 - 2
src/main/java/cz/senslog/telemetry/protocol/parser/Fm4exC8.java

@@ -10,6 +10,7 @@ import java.time.Instant;
 import java.util.Optional;
 
 import static cz.senslog.telemetry.protocol.Fm4ex.coordToDecimalDegrees;
+import static cz.senslog.telemetry.protocol.Fm4exCodecType.CODEC_8;
 
 public class Fm4exC8 implements Fm4exParser {
 
@@ -19,7 +20,7 @@ public class Fm4exC8 implements Fm4exParser {
 
         short codec = buffer.getUnsignedByte(byteIndex++);
         Optional<Fm4exCodecType> codecType = Fm4exCodecType.getType(codec);
-        if (codecType.isEmpty() || codecType.get() != Fm4exCodecType.CODEC_8) {
+        if (codecType.isEmpty() || codecType.get() != CODEC_8) {
             return null;
         }
 
@@ -107,6 +108,6 @@ public class Fm4exC8 implements Fm4exParser {
             records[recordIndex] = tel;
         }
 
-        return new AVLTelemetryPacket(records);
+        return new AVLTelemetryPacket(CODEC_8, records);
     }
 }

+ 4 - 3
src/main/java/cz/senslog/telemetry/protocol/parser/Fm4exC8Ex.java

@@ -10,6 +10,7 @@ import java.time.Instant;
 import java.util.Optional;
 
 import static cz.senslog.telemetry.protocol.Fm4ex.coordToDecimalDegrees;
+import static cz.senslog.telemetry.protocol.Fm4exCodecType.CODEC_8Ex;
 
 public class Fm4exC8Ex implements Fm4exParser {
 
@@ -19,9 +20,9 @@ public class Fm4exC8Ex implements Fm4exParser {
 
         short codec = buffer.getUnsignedByte(byteIndex++);
         Optional<Fm4exCodecType> codecType = Fm4exCodecType.getType(codec);
-        if (codecType.isEmpty() || codecType.get() != Fm4exCodecType.CODEC_8Ex) {
+        if (codecType.isEmpty() || codecType.get() != CODEC_8Ex) {
             throw new IllegalArgumentException(String.format(
-                    "Unsupported codec type. Expected <%d> was <%d>", Fm4exCodecType.CODEC_8Ex.codecId(), codec)
+                    "Unsupported codec type. Expected <%d> was <%d>", CODEC_8Ex.codecId(), codec)
             );
         }
 
@@ -125,6 +126,6 @@ public class Fm4exC8Ex implements Fm4exParser {
             );
         }
 
-        return new AVLTelemetryPacket(records);
+        return new AVLTelemetryPacket(CODEC_8Ex, records);
     }
 }

+ 23 - 24
src/main/java/cz/senslog/telemetry/server/Fm4exSocketHandler.java

@@ -2,10 +2,8 @@ package cz.senslog.telemetry.server;
 
 import cz.senslog.telemetry.database.domain.*;
 import cz.senslog.telemetry.database.repository.SensLogRepository;
-import cz.senslog.telemetry.module.EventBusModulePaths;
 import cz.senslog.telemetry.protocol.domain.*;
 import cz.senslog.telemetry.protocol.Fm4ex;
-import cz.senslog.telemetry.utils.Tuple;
 import io.vertx.core.Future;
 import io.vertx.core.Vertx;
 import io.vertx.core.buffer.Buffer;
@@ -14,10 +12,6 @@ import io.vertx.core.json.JsonObject;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.time.Instant;
 import java.time.OffsetDateTime;
 import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
@@ -61,7 +55,8 @@ public class Fm4exSocketHandler {
 
         private static final Logger logger = LogManager.getLogger(SocketContext.class);
 
-        private static final Buffer SUCCESS = Buffer.buffer(new byte[]{0x01}), ERROR = Buffer.buffer(new byte[]{0x00});
+        private static final Buffer ERROR = Buffer.buffer().appendInt(0x00);
+        private static final Function<Integer, Buffer> MAKE_SUCCESS = count -> count > 0 ? Buffer.buffer().appendInt(count.byteValue()) : ERROR;
 
         private final Vertx vertx;
 
@@ -92,11 +87,11 @@ public class Fm4exSocketHandler {
             return repo.findUnitByIMEI(imei).map(u -> {
                 if (u != null) {
                     contextUnit = u;
-                    logger.info("[{}] Enabling communication for IMEI '{}' as the unit '{}'.", socketId, u.getImei(), u.getUnitId());
-                    return SUCCESS;
+                    logger.info("[{}] Enabling IMEI '{}' as the unit '{}'.", socketId, u.getImei(), u.getUnitId());
+                    return MAKE_SUCCESS.apply(0x01);
                 } else {
                     contextUnit = null;
-                    logger.warn("[{}] Rejecting communication for the IMEI '{}' due to: Unit does not exist.", socketId, imei);
+                    logger.warn("[{}] Rejecting the IMEI '{}' due to: Unit does not exist.", socketId, imei);
                     return ERROR;
                 }
             });
@@ -117,6 +112,7 @@ public class Fm4exSocketHandler {
                 return Future.failedFuture("No AVL Data.");
             }
 
+            logger.info("[{}] AVL Packet parsed by <{}> codec.", socketId, avlPacket.codecType().name());
             switch (avlPacket.type()) {
                 case TELEMETRY_OBSERVATION -> {
                     return handleTelemetryObservation((AVLTelemetryPacket) avlPacket);
@@ -138,8 +134,8 @@ public class Fm4exSocketHandler {
             Long actionId = null;
             for (IOProperty io : avlPacket.getIoProperties()) {
                 switch (io.getId()) {
-                    case DRIVER_IO_ID -> driverId = io.getValue();
-                    case ACTION_IO_ID -> actionId = io.getValue();
+                    case DRIVER_IO_ID -> driverId = ((Double) io.getValue()).longValue();
+                    case ACTION_IO_ID -> actionId = ((Double) io.getValue()).longValue();
                 }
             }
 
@@ -149,8 +145,8 @@ public class Fm4exSocketHandler {
 
             OffsetDateTime timestamp = avlPacket.getTimestamp().atZone(UTC).toOffsetDateTime();
             return repo.updateEvent(new EntityAction(contextUnit.getUnitId(), driverId, actionId, timestamp))
-                    .map(res -> res > 0 ? SUCCESS : ERROR)
-                    .onSuccess(res -> logger.info("[{}] AVL Driver Activity Packet was saved successfully.", socketId));
+                    .map(MAKE_SUCCESS)
+                    .onSuccess(res -> logger.info("[{}] AVL Driver Activity Packet<{}> saved.", socketId, res));
         }
 
         private Future<Buffer> handleTelemetryObservation(AVLTelemetryPacket avlPacket) {
@@ -167,29 +163,32 @@ public class Fm4exSocketHandler {
                     Location location = Location.of(avl.getLongitude(), avl.getLatitude(), avl.getAltitude(), avl.getAngle());
                     OffsetDateTime timestamp = avl.getTimestamp().atZone(UTC).toOffsetDateTime();
                     JsonObject observedValues = new JsonObject();
+                    List<Integer> unknownIOs = new ArrayList<>();
                     for (IOProperty io : avl.getIoProperties()) {
-                        Sensor sensor = ioToSensor.get((long)io.getId());
+                        Sensor sensor = ioToSensor.get(io.getId());
                         if (sensor != null) {
-                            observedValues.put(Long.toString(sensor.getSensorId()), io.getValue());
+                            observedValues.put(Long.toString(sensor.getSensorId()), io.getValue() * sensor.getIoMultiplier());
                          } else {
-                            logger.error("Sensor for the IO Property <{}> does not exist.", io.getId());
+                            unknownIOs.add(io.getId());
                         }
                     }
+                    if (!unknownIOs.isEmpty()) {
+                        logger.error("[{}] No sensors for IOs<{}>.", socketId, Arrays.toString(unknownIOs.toArray()));
+                    }
                     telemetries.add(UnitTelemetry.of(unitId, timestamp, location, avl.getSpeed(), observedValues));
                 }
                 return telemetries;
-            }).compose(data -> repo.saveAllTelemetry(data).map(count -> Tuple.of(count, data)))
-                    .flatMap(dataTuple -> {
+            }).flatMap(data -> repo.saveAllTelemetry(data).map(count -> data))
+                    .compose(data -> {
                         JsonArray sensLogObsArr = new JsonArray(UnitTelemetry.Converter
-                                .toSensLogObservationAsStream(dataTuple.item2())
+                                .toSensLogObservationAsStream(data)
                                 .map(SensLogObservation::toJsonObject).toList()
                         );
                         vertx.eventBus().request(SENSLOG_OBSERVATIONS, sensLogObsArr)
                                 .onSuccess(v -> logger.info(v.body())).onFailure(logger::error);
 
-                        logger.info("[{}] AVL Records <{}> was saved successfully.", socketId, dataTuple.item1());
-                        return Future.succeededFuture(SUCCESS);
-                    });
+                        return Future.succeededFuture(MAKE_SUCCESS.apply(data.size()));
+                    }).onSuccess(resBuffer -> logger.info("[{}] AVL<{}> saved.", socketId, resBuffer.getByte(0)));
         }
     }
-}
+}

+ 11 - 2
src/main/java/cz/senslog/telemetry/server/HttpVertxServer.java

@@ -8,10 +8,8 @@ import cz.senslog.telemetry.server.ws.OpenAPIHandler;
 import cz.senslog.telemetry.utils.ResourcesUtils;
 import io.vertx.core.*;
 import io.vertx.core.http.HttpMethod;
-import io.vertx.core.json.JsonArray;
 import io.vertx.core.json.JsonObject;
 import io.vertx.ext.auth.KeyStoreOptions;
-import io.vertx.ext.auth.User;
 import io.vertx.ext.auth.jwt.JWTAuth;
 import io.vertx.ext.auth.jwt.JWTAuthOptions;
 import io.vertx.ext.web.Router;
@@ -137,8 +135,19 @@ public final class HttpVertxServer extends AbstractVerticle {
                     openAPIRouterBuilder.operation("campaignIdUnitIdAlertsGET").handler(apiHandler::campaignIdUnitIdAlertsGET);
                     openAPIRouterBuilder.operation("entityIdActionIdUnitIdAlertsGET").handler(apiHandler::entityIdActionIdUnitIdAlertsGET);
 
+
+                    // TODO delete in future
+                    openAPIRouterBuilder.operation("testObservationsPOST").handler(apiHandler::testObservationsPOST);
+
                     openAPIRouterBuilder.operation("legacyInsertObservationsGET").handler(apiHandler::legacyInsertObservationsGET);
 
+                    // TODO only temporary for integration
+                    openAPIRouterBuilder.operation("integrationCertificateGET").handler(apiHandler::integrationCertificateGET);
+                    openAPIRouterBuilder.operation("integrationTrackingAllGET").handler(apiHandler::integrationTrackingAllGET);
+                    openAPIRouterBuilder.operation("integrationTrackingStatusGET").handler(apiHandler::integrationTrackingStatusGET);
+                    openAPIRouterBuilder.operation("integrationOrderCreateGET").handler(apiHandler::integrationOrderCreateGET);
+                    openAPIRouterBuilder.operation("integrationTrackingChangeGET").handler(apiHandler::integrationTrackingChangeGET);
+
                     Router mainRouter = openAPIRouterBuilder.createRouter();
 
                     mainRouter.route().failureHandler(ExceptionHandler.createAsJSON());

+ 1 - 0
src/main/java/cz/senslog/telemetry/server/LoopInvoker.java

@@ -2,6 +2,7 @@ package cz.senslog.telemetry.server;
 
 import cz.senslog.telemetry.utils.Tuple;
 import io.vertx.core.Future;
+import io.vertx.core.buffer.Buffer;
 
 import java.util.ArrayList;
 import java.util.List;

+ 9 - 4
src/main/java/cz/senslog/telemetry/server/TCPVertxServer.java

@@ -2,7 +2,9 @@ package cz.senslog.telemetry.server;
 
 import cz.senslog.telemetry.database.repository.SensLogRepository;
 import io.vertx.core.AbstractVerticle;
+import io.vertx.core.Future;
 import io.vertx.core.Promise;
+import io.vertx.core.buffer.Buffer;
 import io.vertx.core.json.JsonObject;
 import io.vertx.core.net.NetServer;
 import io.vertx.core.net.NetServerOptions;
@@ -27,16 +29,19 @@ public final class TCPVertxServer extends AbstractVerticle {
         Fm4exSocketHandler socHandler = Fm4exSocketHandler.create(vertx, repo);
         server.connectHandler(socket -> socket.handler(
                         buffer -> socHandler.process(socket.writeHandlerID(), buffer)
-                                .onSuccess(socket::write)
+                                .onSuccess(res -> {
+                                    logger.info("[{}] Success response: {}", socket.writeHandlerID(), res.getByte(0));
+                                    socket.write(res);
+                                })
                                 .onFailure(th -> {
-                                    logger.error(String.format("[%s] %s", socket.writeHandlerID(), th.getMessage()));
+                                    logger.error("[{}] {}", socket.writeHandlerID(), th.getMessage());
                                     socHandler.destroySocket(socket.writeHandlerID())
                                             .onComplete(res -> socket.close());
                                 }))
                 .exceptionHandler(logger::error)
                 .closeHandler(v -> socHandler.destroySocket(socket.writeHandlerID())
-                        .onSuccess(id -> logger.info(String.format("[%s] The socket has been closed", id)))
-                        .onFailure(th -> logger.error(String.format("[%s] %s", socket.writeHandlerID(), th.getMessage()))))
+                        .onSuccess(id -> logger.info("[{}] The socket has been closed", id))
+                        .onFailure(th -> logger.error("[{}] {}", socket.writeHandlerID(), th.getMessage())))
         );
 
         JsonObject serverConfig = config().getJsonObject("server");

+ 14 - 5
src/main/java/cz/senslog/telemetry/server/ws/ContentType.java

@@ -1,9 +1,13 @@
 package cz.senslog.telemetry.server.ws;
 
+import java.util.Optional;
+
 public enum ContentType {
-    JSON    ("application/json"),
-    GEOJSON ("application/geojson"),
-    JSON_SCHEMA ("application/json+schema")
+    JSON        ("application/json"),
+    GEOJSON     ("application/geojson"),
+    JSON_SCHEMA ("application/json+schema"),
+    HTML        ("text/html"),
+    TEXT        ("text/plain"),
     ;
 
     private final String contentType;
@@ -17,12 +21,17 @@ public enum ContentType {
     }
 
     public static ContentType ofType(String contentType) {
+        return ofTypeSave(contentType)
+                .orElseThrow(() -> new IllegalArgumentException(String.format("No enum constant %s for the type '%s'.", ContentType.class.getName(), contentType)));
+    }
+
+    public static Optional<ContentType> ofTypeSave(String contentType) {
         for (ContentType value : values()) {
             if (value.contentType.equalsIgnoreCase(contentType)) {
-                return value;
+                return Optional.of(value);
             }
         }
-        throw new IllegalArgumentException(String.format("No enum constant %s for the type '%s'.", ContentType.class.getName(), contentType));
+        return Optional.empty();
     }
 
     public String contentType() {

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 388 - 329
src/main/java/cz/senslog/telemetry/server/ws/OpenAPIHandler.java


+ 6 - 0
src/main/java/cz/senslog/telemetry/utils/ResourcesUtils.java

@@ -1,5 +1,6 @@
 package cz.senslog.telemetry.utils;
 
+import java.io.InputStream;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 
@@ -8,4 +9,9 @@ public final class ResourcesUtils {
     public static Path getPath(String resourceFile) {
         return Paths.get(resourceFile);
     }
+
+    public static InputStream getInputStream(String resourceFile) {
+        ClassLoader classloader = Thread.currentThread().getContextClassLoader();
+        return classloader.getResourceAsStream(resourceFile);
+    }
 }

BIN
src/main/resources/certificate_template.pdf


+ 29 - 0
src/main/resources/no_order.html

@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Theros tracking</title>
+    <style>
+        body {
+            margin: 0;
+            height: 100vh;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            background-color: #f5f5f5;
+            font-family: Arial, sans-serif;
+        }
+        .button-container {
+            text-align: center;
+        }
+    </style>
+</head>
+<body>
+    <div class="button-container">
+        <h3>Unit ID</h3>
+        <h3 id="unit_id">$__unit_id</h3>
+        <h4>No order for the unit</h4>
+    </div>
+</body>
+</html>

+ 265 - 10
src/main/resources/openAPISpec.yaml

@@ -154,11 +154,12 @@ paths:
       parameters:
         - $ref: '#/components/parameters/campaignIdParam'
       requestBody:
-        required: true
+#        required: true
         content:
           application/json:
             schema:
               type: array
+              description: JSON Array of Observations. The attribute 'timestamp' accepts only ISO8601 or the pattern 'yyyy-MM-dd[+][ ]HH:mm:ss'. The attribute can be substituted by 'epoch'.
               items:
                 $ref: '#/components/schemas/CampaignDataObservation'
           application/geojson:
@@ -170,7 +171,43 @@ paths:
           content:
             application/json:
               schema:
-                $ref: '#/components/schemas/PostResponse'
+                type: array
+                items:
+                  $ref: '#/components/schemas/CampaignDataObservation'
+        default:
+          description: unexpected error
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+
+  /test/observations:
+    post:
+      operationId: testObservationsPOST
+      tags:
+        - Test
+      requestBody:
+        #        required: true
+        content:
+          application/json:
+            schema:
+              type: array
+              description: JSON Array of Observations. The attribute 'timestamp' accepts only ISO8601 or the pattern 'yyyy-MM-dd[+][ ]HH:mm:ss'. The attribute can be substituted by 'epoch'.
+              minLength: 1
+              items:
+                $ref: '#/components/schemas/CampaignDataObservation'
+          application/geojson:
+            schema:
+              $ref: '#/components/schemas/GeoFeatureCollectionUnit'
+      responses:
+        200:
+          description: JSON
+          content:
+            application/json:
+              schema:
+                type: array
+                items:
+                  $ref: '#/components/schemas/CampaignDataObservation'
         default:
           description: unexpected error
           content:
@@ -183,7 +220,7 @@ paths:
       operationId: campaignIdUnitsObservationsLocationsGET
       summary: Publish info about all data of units merged together within the campaign
       tags:
-        - Observation
+        - Locations
       security:
         - bearerAuth: [read:personal]
         - bearerAuth: [read:infrastructure]
@@ -217,7 +254,7 @@ paths:
       operationId: campaignIdUnitsObservationsLocationsPUT
       summary: Adjusting locations of all observations within 'fromTime' to 'toTime' interval.
       tags:
-        - Observation
+        - Locations
       security:
         - bearerAuth: [ read:infrastructure ]
       parameters:
@@ -290,6 +327,7 @@ paths:
         - $ref: '#/components/parameters/zoneParam'
         - $ref: '#/components/parameters/offsetParam'
         - $ref: '#/components/parameters/limitParam'
+        - $ref: '#/components/parameters/sortParam'
         - $ref: '#/components/parameters/formatParam'
         - $ref: '#/components/parameters/filterParam'
         - $ref: '#/components/parameters/navigationLinksParam'
@@ -315,7 +353,7 @@ paths:
       operationId: campaignIdUnitIdLocationsGET
       summary: Publish locations of the unit within the campaign
       tags:
-        - Observation
+        - Locations
       security:
         - bearerAuth: [read:personal]
         - bearerAuth: [read:infrastructure]
@@ -1037,6 +1075,8 @@ paths:
         - $ref: '#/components/parameters/entityIdParam'
         - $ref: '#/components/parameters/unitIdParam'
         - $ref: '#/components/parameters/actionIdParam'
+        - $ref: '#/components/parameters/fromParam'
+        - $ref: '#/components/parameters/toParam'
         - $ref: '#/components/parameters/zoneParam'
         - $ref: '#/components/parameters/navigationLinksParam'
       responses:
@@ -1162,6 +1202,8 @@ paths:
       parameters:
         - $ref: '#/components/parameters/eventIdParam'
         - $ref: '#/components/parameters/zoneParam'
+        - $ref: '#/components/parameters/fromParam'
+        - $ref: '#/components/parameters/toParam'
         - $ref: '#/components/parameters/offsetParam'
         - $ref: '#/components/parameters/limitParam'
         - $ref: '#/components/parameters/formatParam'
@@ -1189,7 +1231,7 @@ paths:
       operationId: eventIdLocationsGET
       summary: Publish locations created by the entity while performing specific action on the unit at the time/event
       tags:
-        - Observation
+        - Locations
       security:
         - bearerAuth: [read:personal]
         - bearerAuth: [read:infrastructure]
@@ -1530,6 +1572,212 @@ paths:
               schema:
                 type: boolean
 
+  /integration/order/create:
+    get:
+      operationId: integrationOrderCreateGET
+      tags:
+        - Integration
+      parameters:
+        - in: query
+          description: Order ID is the unique identifier of the order.
+          name: order_id
+          required: true
+          schema:
+            type: integer
+            format: int64
+            minimum: 1
+        - in: query
+          description: Unit ID is the unique identifier of the box.
+          name: unit_id
+          required: true
+          schema:
+            type: integer
+            format: int64
+            minimum: 1
+        - in: query
+          description: Tracking ID is a related number to the delivery_type parameter. If the delivery_type is chosen as SERVICE_XYZ (e.g., SERVICE_FOFR) than the tracking ID is the number for tracking provided by the parcel service. Otherwise, it is the identifier (i.e., unit id) of the transporting vehicle (e.g., Peugeot).
+          name: tracking_id
+          required: true
+          schema:
+            type: integer
+            format: int64
+            minimum: 1
+        - in: query
+          name: delivery_type
+          required: true
+          schema:
+            type: string
+            enum:
+              - SERVICE_FOFR
+              - VEHICLE_CITROEN
+              - VEHICLE_PEUGEOT
+
+      responses:
+        200:
+          description: Text information about new order.
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  message:
+                    type: string
+        default:
+          description: unexpected error
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+
+  /integration/tracking/all:
+    get:
+      operationId: integrationTrackingAllGET
+      tags:
+        - Integration
+      responses:
+        200:
+          description: JSON Array of all active tracking packages.
+          content:
+            application/json:
+              schema:
+                type: array
+                items:
+                  type: object
+                  properties:
+                    parcelId:
+                      type: integer
+                      format: int64
+                      minimum: 1
+                    unitId:
+                      type: integer
+                      format: int64
+                      minimum: 1
+                    orderId:
+                      type: integer
+                      format: int64
+                      minimum: 1
+        default:
+          description: unexpected error
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+
+  /integration/tracking/change:
+    get:
+      operationId: integrationTrackingChangeGET
+      tags:
+        - Integration
+      parameters:
+        - in: query
+          name: unit_id
+          required: true
+          schema:
+            type: integer
+            format: int64
+            minimum: 1
+        - in: query
+          name: action
+          required: true
+          schema:
+            type: string
+            enum:
+              - START
+              - STOP
+      responses:
+        200:
+          description: JSON Object including message of the change.
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  message:
+                    type: string
+            plain/text:
+              schema:
+                type: string
+        default:
+          description: unexpected error
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+
+  /integration/tracking/status:
+    get:
+      operationId: integrationTrackingStatusGET
+      tags:
+        - Integration
+      parameters:
+        - in: query
+          name: unit_id
+          required: true
+          schema:
+            type: integer
+            format: int64
+            minimum: 1
+      responses:
+        200:
+          description: Text information about tracking
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  unitId:
+                    type: integer
+                    format: int64
+                    minimum: 1
+                  orderId:
+                    type: integer
+                    format: int64
+                    minimum: 1
+                  isDelivering:
+                    type: boolean
+                  deliveryType:
+                    type: string
+                    enum:
+                      - SERVICE_FOFR
+                      - CAR_CITROEN
+                      - CAR_PEUGEOT
+            text/html:
+              schema:
+                type: string
+        default:
+          description: unexpected error
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+
+  /integration/certificate:
+    get:
+      operationId: integrationCertificateGET
+      tags:
+        - Integration
+      parameters:
+        - in: query
+          description: Order ID encoded by Base64
+          name: h
+          required: true
+          schema:
+            type: string
+      responses:
+        200:
+          description: A PDF file
+          content:
+            application/pdf:
+              schema:
+                type: string
+                format: binary
+        default:
+          description: unexpected error
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+
 components:
   securitySchemes:
     bearerAuth:
@@ -1978,6 +2226,7 @@ components:
       required:
         - sensorId
         - ioId
+        - ioMultiplier
         - name
         - phenomenon
         - type
@@ -2025,6 +2274,8 @@ components:
           type: integer
           format: int32
           minimum: 1
+        ioMultiplier:
+          type: number
         name:
           type: string
         phenomenon:
@@ -2041,6 +2292,7 @@ components:
         Observations@NavigationLink: "<domain>/campaigns/1/units/25/sensors/105/observations"
         sensorId: 105
         ioId: 98
+        ioMultiplier: 0.1
         name: "Sensor 105"
         description: "Description of the sensor 105"
         type: "type of sensor"
@@ -2300,9 +2552,6 @@ components:
       type: object
       required:
         - unitId
-        - timestamp
-        - speed
-        - location
         - observedValues
       properties:
         unitId:
@@ -2310,7 +2559,9 @@ components:
           format: int64
         timestamp:
           type: string
-          format: date-time
+        epoch:
+          type: integer
+          format: int64
         speed:
           type: integer
           format: int64
@@ -3015,6 +3266,7 @@ components:
       required:
         - sensorId
         - ioId
+        - ioMultiplier
         - name
         - phenomenon
       x-NavigationLinks:
@@ -3047,6 +3299,8 @@ components:
           type: integer
           format: int32
           minimum: 1
+        ioMultiplier:
+          type: number
         name:
           type: string
         phenomenon:
@@ -3061,6 +3315,7 @@ components:
         Units@NavigationLink: "<domain>/sensors/105/units"
         sensorId: 105
         ioId: 98
+        ioMultiplier: 0.1
         name: "Sensor 105"
         description: "Description of the sensor 105"
         type: "type of sensor"

+ 67 - 0
src/main/resources/tracking_off.html

@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Theros tracking</title>
+    <style>
+        body {
+            margin: 0;
+            height: 100vh;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            background-color: #f5f5f5;
+            font-family: Arial, sans-serif;
+        }
+        .button-container {
+            text-align: center;
+        }
+        button {
+            margin: 10px;
+            padding: 10px 20px;
+            font-size: 16px;
+            border: none;
+            border-radius: 5px;
+            cursor: pointer;
+            color: white;
+        }
+        button:hover {
+            background-color: #0056b3;
+        }
+    </style>
+</head>
+<body>
+    <div class="button-container">
+        <h3>Unit ID</h3>
+        <h3 id="unit_id">$__unit_id</h3>
+        <button style="background-color: green" onclick="makeAction('START')">Start tracking</button>
+    </div>
+
+    <script>
+        function makeAction(action) {
+            const domain = `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : ''}`;
+            const api = domain + "/integration/tracking/change";
+            const url = api + "?action="+action+"&unit_id=" + document.getElementById("unit_id").textContent;
+            console.log(url);
+            const xhr = new XMLHttpRequest();
+            xhr.open('GET', url, true);
+
+            xhr.onreadystatechange = function () {
+                const jsonRes = JSON.parse(xhr.responseText);
+
+                if (xhr.readyState === XMLHttpRequest.DONE) {
+                    if (xhr.status === 200) {
+                        location.reload(true);
+                    } else {
+                        alert(jsonRes["message"]);
+                        console.error('Request failed with status:', xhr.status);
+                    }
+                }
+             };
+
+            xhr.send();
+          }
+    </script>
+</body>
+</html>

+ 68 - 0
src/main/resources/tracking_on.html

@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Theros tracking</title>
+    <style>
+        body {
+            margin: 0;
+            height: 100vh;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            background-color: #f5f5f5;
+            font-family: Arial, sans-serif;
+        }
+        .button-container {
+            text-align: center;
+        }
+        button {
+            margin: 10px;
+            padding: 10px 20px;
+            font-size: 16px;
+            border: none;
+            border-radius: 5px;
+            cursor: pointer;
+            color: white;
+        }
+        button:hover {
+            background-color: #0056b3;
+        }
+    </style>
+</head>
+<body>
+    <div class="button-container">
+        <h3>Unit ID</h3>
+        <h3 id="unit_id">$__unit_id</h3>
+        <h4>Tracking started at $___timestamp</h4>
+        <button style="background-color: red" onclick="makeAction('STOP')">Stop tracking</button>
+    </div>
+
+    <script>
+        function makeAction(action) {
+            const domain = `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : ''}`;
+            const api = domain + "/integration/tracking/change";
+            const url = api + "?action="+action+"&unit_id=" + document.getElementById("unit_id").textContent;
+            console.log(url);
+            const xhr = new XMLHttpRequest();
+            xhr.open('GET', url, true);
+
+            xhr.onreadystatechange = function () {
+                const jsonRes = JSON.parse(xhr.responseText);
+
+                if (xhr.readyState === XMLHttpRequest.DONE) {
+                    if (xhr.status === 200) {
+                        location.reload(true);
+                    } else {
+                        alert(jsonRes["message"]);
+                        console.error('Request failed with status:', xhr.status);
+                    }
+                }
+             };
+
+            xhr.send();
+          }
+    </script>
+</body>
+</html>

+ 5 - 2
src/test/java/cz/senslog/telemetry/DataSet.java

@@ -1,6 +1,7 @@
 package cz.senslog.telemetry;
 
 import cz.senslog.telemetry.database.domain.UnitTelemetry;
+import cz.senslog.telemetry.protocol.Fm4exCodecType;
 import cz.senslog.telemetry.protocol.domain.AVLDriverActivityPacket;
 import cz.senslog.telemetry.protocol.domain.AVLPacket;
 import cz.senslog.telemetry.protocol.domain.IOProperty;
@@ -10,6 +11,8 @@ import io.vertx.core.buffer.Buffer;
 import java.time.Instant;
 import java.util.List;
 
+import static cz.senslog.telemetry.protocol.Fm4exCodecType.CODEC_12;
+import static cz.senslog.telemetry.protocol.Fm4exCodecType.CODEC_13;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
 public class DataSet {
@@ -253,7 +256,7 @@ public class DataSet {
     public static BinaryDataSet<AVLPacket> AVLData_codec12_1() {
         return new BinaryDataSet<>(
                 readResourceFile("binary/Fm4exC12_1.bin"),
-                new AVLDriverActivityPacket() {{
+                new AVLDriverActivityPacket(CODEC_12) {{
                     getIoProperties().add(IOProperty.create(2, 0));
                     getIoProperties().add(IOProperty.create(148, 0));
                     getIoProperties().add(IOProperty.create(149, 0));
@@ -272,7 +275,7 @@ public class DataSet {
     public static BinaryDataSet<AVLPacket> AVLData_codec13_1() {
         return new BinaryDataSet<>(
                 readResourceFile("binary/Fm4exC13_1.bin"),
-                new AVLDriverActivityPacket() {{
+                new AVLDriverActivityPacket(CODEC_13) {{
                             setTimestamp(Instant.parse("2023-12-11T13:27:25Z"));
                             getIoProperties().add(IOProperty.create(2, 0));
                             getIoProperties().add(IOProperty.create(148, 1));

+ 18 - 18
src/test/java/cz/senslog/telemetry/MockSensLogRepository.java

@@ -177,9 +177,9 @@ public class MockSensLogRepository implements SensLogRepository {
     @Override
     public Future<List<Sensor>> allSensors() {
         return Future.succeededFuture(List.of(
-                Sensor.of(105, "mock(name)", "M", 98, Phenomenon.of(15, "mock(phenomenon)"), "mock(description)"),
-                Sensor.of(106, "mock(name)", "M", 99, Phenomenon.of(16, "mock(phenomenon)"), "mock(description)"),
-                Sensor.of(107, "mock(name)", "M", 100, Phenomenon.of(17, "mock(phenomenon)"), "mock(description)"))
+                Sensor.of(105, "mock(name)", "M", 98, 1.0, Phenomenon.of(15, "mock(phenomenon)"), "mock(description)"),
+                Sensor.of(106, "mock(name)", "M", 99, 1.0, Phenomenon.of(16, "mock(phenomenon)"), "mock(description)"),
+                Sensor.of(107, "mock(name)", "M", 100, 1.0, Phenomenon.of(17, "mock(phenomenon)"), "mock(description)"))
         );
     }
 
@@ -191,7 +191,7 @@ public class MockSensLogRepository implements SensLogRepository {
     @Override
     public Future<Sensor> findSensorById(long sensorId) {
         return Future.succeededFuture(
-                Sensor.of(105, "mock(name)", "M", 98, Phenomenon.of(15, "mock(phenomenon)"), "mock(description)")
+                Sensor.of(105, "mock(name)", "M", 98, 1.0, Phenomenon.of(15, "mock(phenomenon)"), "mock(description)")
         );
     }
 
@@ -202,15 +202,15 @@ public class MockSensLogRepository implements SensLogRepository {
 
     @Override
     public Future<Sensor> findSensorByIOAndUnitId(int ioID, long unitId) {
-        return Future.succeededFuture(Sensor.of(105, "mock(name)", "M", 98, Phenomenon.of(15, "mock(phenomenon)"), "mock(description)"));
+        return Future.succeededFuture(Sensor.of(105, "mock(name)", "M", 98, 1.0, Phenomenon.of(15, "mock(phenomenon)"), "mock(description)"));
     }
 
     @Override
-    public Future<Map<Long, Sensor>> findSensorsByUnitIdGroupById(long unitId) {
+    public Future<Map<Integer, Sensor>> findSensorsByUnitIdGroupById(long unitId) {
         return Future.succeededFuture(Map.of(
-                105L, Sensor.of(105, "mock(name)", "X"),
-                106L, Sensor.of(106, "mock(name)", "X"),
-                107L, Sensor.of(107, "mock(name)", "X")
+                105, Sensor.of(105, "mock(name)", "X"),
+                106, Sensor.of(106, "mock(name)", "X"),
+                107, Sensor.of(107, "mock(name)", "X")
         ));
     }
 
@@ -245,9 +245,9 @@ public class MockSensLogRepository implements SensLogRepository {
     @Override
     public Future<List<Sensor>> findSensorsByPhenomenonId(long phenomenonId) {
         return Future.succeededFuture(List.of(
-                Sensor.of(105, "mock(name)", "M", 98, Phenomenon.of(15, "mock(phenomenon)"), "mock(description)"),
-                Sensor.of(106, "mock(name)", "M", 99, Phenomenon.of(17, "mock(phenomenon)"), "mock(description)"),
-                Sensor.of(107, "mock(name)", "M", 100, Phenomenon.of(18, "mock(phenomenon)"), "mock(description)")
+                Sensor.of(105, "mock(name)", "M", 98, 1.0, Phenomenon.of(15, "mock(phenomenon)"), "mock(description)"),
+                Sensor.of(106, "mock(name)", "M", 99, 1.0, Phenomenon.of(17, "mock(phenomenon)"), "mock(description)"),
+                Sensor.of(107, "mock(name)", "M", 100, 1.0, Phenomenon.of(18, "mock(phenomenon)"), "mock(description)")
         ));
     }
 
@@ -273,7 +273,7 @@ public class MockSensLogRepository implements SensLogRepository {
     @Override
     public Future<Sensor> findSensorByCampaignIdAndUnitId(long campaignId, long unitId, long sensorId) {
         return Future.succeededFuture(
-                Sensor.of(105, "mock(name)", "M", 98, Phenomenon.of(15, "mock(phenomenon)"), "mock(description)")
+                Sensor.of(105, "mock(name)", "M", 98, 1.0, Phenomenon.of(15, "mock(phenomenon)"), "mock(description)")
         );
     }
 
@@ -431,7 +431,7 @@ public class MockSensLogRepository implements SensLogRepository {
     }
 
     @Override
-    public Future<List<Event>> findEventsByEntityIdAndUnitIdAndActionId(int entityId, long unitId, int actionId) {
+    public Future<List<Event>> findEventsByEntityIdAndUnitIdAndActionId(int entityId, long unitId, int actionId, OffsetDateTime from, OffsetDateTime to) {
         OffsetDateTime baseTimestamp = OffsetDateTime.ofInstant(BASE_INSTANT_TIMESTAMP, ZoneOffset.UTC);
         return Future.succeededFuture(List.of(
                 Event.of(1, 1, 1,1000, baseTimestamp, baseTimestamp.plusHours(8)),
@@ -441,8 +441,8 @@ public class MockSensLogRepository implements SensLogRepository {
     }
 
     @Override
-    public Future<List<Event>> findEventsByIdentityAndEntityIdAndUnitIdAndActionId(String userIdentity, int entityId, long unitId, int actionId) {
-        return findEventsByEntityIdAndUnitIdAndActionId(entityId, unitId, actionId);
+    public Future<List<Event>> findEventsByIdentityAndEntityIdAndUnitIdAndActionId(String userIdentity, int entityId, long unitId, int actionId, OffsetDateTime from, OffsetDateTime to) {
+        return findEventsByEntityIdAndUnitIdAndActionId(entityId, unitId, actionId, from, to);
     }
 
     @Override
@@ -514,12 +514,12 @@ public class MockSensLogRepository implements SensLogRepository {
     }
 
     @Override
-    public Future<List<UnitTelemetry>> findObservationsByIdentityAndCampaignIdAndUnitId(String userIdentity, long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, int offset, int limit, List<Filter> filters) {
+    public Future<List<UnitTelemetry>> findObservationsByIdentityAndCampaignIdAndUnitId(String userIdentity, long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, int offset, int limit, SortType sort, List<Filter> filters) {
         return findObservationsByCampaignIdAndUnitId(campaignId, unitId, from, to, offset, limit, SortType.ASC, filters);
     }
 
     @Override
-    public Future<PagingRetrieve<List<UnitTelemetry>>> findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging(String userIdentity, long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, int offset, int limit, List<Filter> filters) {
+    public Future<PagingRetrieve<List<UnitTelemetry>>> findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging(String userIdentity, long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, int offset, int limit, SortType sort, List<Filter> filters) {
         return findObservationsByCampaignIdAndUnitIdWithPaging(campaignId, unitId, from, to, offset, limit, SortType.ASC, filters);
     }
 

+ 14 - 14
src/test/java/cz/senslog/telemetry/database/repository/MapLogRepositoryTest.java

@@ -1089,7 +1089,7 @@ class MapLogRepositoryTest {
     @Test
     @DisplayName("DB Test: findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging#1")
     void findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging_NM1(Vertx vertx, VertxTestContext testContext) {
-        repo.findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging(USER_IDENTITY, 2, 2000, null, null, 0, 4, emptyList()).onComplete(testContext.succeeding(data -> testContext.verify(() -> {
+        repo.findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging(USER_IDENTITY, 2, 2000, null, null, 0, 4, SortType.ASC, emptyList()).onComplete(testContext.succeeding(data -> testContext.verify(() -> {
             assertThat(data.size()).isEqualTo(4);
             assertThat(data.hasNext()).isTrue();
 
@@ -1128,7 +1128,7 @@ class MapLogRepositoryTest {
 
         OffsetDateTime from = OffsetDateTime.of(2023, 12, 20, 0, 0, 0, 0, UTC);
 
-        repo.findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging(USER_IDENTITY,2, 2000, from, null, 0, 5, emptyList()).onComplete(testContext.succeeding(data -> testContext.verify(() -> {
+        repo.findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging(USER_IDENTITY,2, 2000, from, null, 0, 5, SortType.ASC, emptyList()).onComplete(testContext.succeeding(data -> testContext.verify(() -> {
             assertThat(data.size()).isEqualTo(5);
             assertThat(data.hasNext()).isTrue();
 
@@ -1151,7 +1151,7 @@ class MapLogRepositoryTest {
         OffsetDateTime from = OffsetDateTime.of(2023, 12, 20, 0, 0, 0, 0, UTC);
         OffsetDateTime to = OffsetDateTime.of(2023, 12, 25, 0, 0, 0, 0, UTC);
 
-        repo.findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging(USER_IDENTITY,2, 2000, from, to, 0, 10, emptyList()).onComplete(testContext.succeeding(data -> testContext.verify(() -> {
+        repo.findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging(USER_IDENTITY,2, 2000, from, to, 0, 10, SortType.ASC, emptyList()).onComplete(testContext.succeeding(data -> testContext.verify(() -> {
             assertThat(data.size()).isEqualTo(3);
             assertThat(data.hasNext()).isFalse();
 
@@ -1174,7 +1174,7 @@ class MapLogRepositoryTest {
         OffsetDateTime from = OffsetDateTime.of(2023, 12, 25, 0, 0, 0, 0, UTC);
         OffsetDateTime to = OffsetDateTime.of(2024, 1, 1, 0, 0, 0, 0, UTC);
 
-        repo.findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging(USER_IDENTITY,2, 2000, from, to, 0, 10, emptyList()).onComplete(testContext.succeeding(data -> testContext.verify(() -> {
+        repo.findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging(USER_IDENTITY,2, 2000, from, to, 0, 10, SortType.ASC, emptyList()).onComplete(testContext.succeeding(data -> testContext.verify(() -> {
             assertThat(data.size()).isEqualTo(2);
             assertThat(data.hasNext()).isFalse();
 
@@ -1196,7 +1196,7 @@ class MapLogRepositoryTest {
 
         OffsetDateTime to = OffsetDateTime.of(2023, 12, 25, 0, 0, 0, 0, UTC);
 
-        repo.findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging(USER_IDENTITY,2, 2000, null, to, 0, 10, emptyList()).onComplete(testContext.succeeding(data -> testContext.verify(() -> {
+        repo.findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging(USER_IDENTITY,2, 2000, null, to, 0, 10, SortType.ASC, emptyList()).onComplete(testContext.succeeding(data -> testContext.verify(() -> {
             assertThat(data.size()).isEqualTo(3);
             assertThat(data.hasNext()).isFalse();
 
@@ -1215,7 +1215,7 @@ class MapLogRepositoryTest {
     @Test
     @DisplayName("DB Test: findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging#6")
     void findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging_NM6(Vertx vertx, VertxTestContext testContext) {
-        repo.findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging(USER_IDENTITY,2, 2000, null, null, 4, 1, emptyList()).onComplete(testContext.succeeding(data -> testContext.verify(() -> {
+        repo.findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging(USER_IDENTITY,2, 2000, null, null, 4, 1, SortType.ASC, emptyList()).onComplete(testContext.succeeding(data -> testContext.verify(() -> {
             assertThat(data.size()).isEqualTo(1);
             assertThat(data.hasNext()).isTrue();
 
@@ -1235,7 +1235,7 @@ class MapLogRepositoryTest {
                 Filter.of(FilterType.UNIT, FilterAttribute.SPEED, null, FilterOperation.NE, 0f)
         );
 
-        repo.findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging(USER_IDENTITY,2, 2000, null, null, 0, 1, filters).onComplete(testContext.succeeding(data -> testContext.verify(() -> {
+        repo.findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging(USER_IDENTITY,2, 2000, null, null, 0, 1, SortType.ASC, filters).onComplete(testContext.succeeding(data -> testContext.verify(() -> {
             assertThat(data.size()).isEqualTo(1);
             assertThat(data.hasNext()).isTrue();
 
@@ -1258,7 +1258,7 @@ class MapLogRepositoryTest {
                 Filter.of(FilterType.UNIT, FilterAttribute.LATITUDE, null, FilterOperation.LT, 45.f)
         );
 
-        repo.findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging(USER_IDENTITY,2, 2000, null, null, 0, 1, filters).onComplete(testContext.succeeding(data -> testContext.verify(() -> {
+        repo.findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging(USER_IDENTITY,2, 2000, null, null, 0, 1, SortType.ASC, filters).onComplete(testContext.succeeding(data -> testContext.verify(() -> {
             assertThat(data.size()).isEqualTo(1);
 
             UnitTelemetry t = data.data().get(0);
@@ -1276,7 +1276,7 @@ class MapLogRepositoryTest {
         // No observations before the date 'to'.
         OffsetDateTime to = OffsetDateTime.of(2023, 12, 20, 0, 0, 0, 0, UTC);
 
-        repo.findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging(USER_IDENTITY,2, 2000, null, to, 0, 1, emptyList()).onComplete(testContext.succeeding(data -> testContext.verify(() -> {
+        repo.findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging(USER_IDENTITY,2, 2000, null, to, 0, 1, SortType.ASC, emptyList()).onComplete(testContext.succeeding(data -> testContext.verify(() -> {
             assertThat(data.size()).isEqualTo(0);
             assertThat(data.hasNext()).isFalse();
 
@@ -1291,7 +1291,7 @@ class MapLogRepositoryTest {
 
         OffsetDateTime from = OffsetDateTime.of(2024, 2, 1, 0, 0, 0, 0, UTC);
 
-        repo.findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging(USER_IDENTITY,2, 2000, from, null, 0, 1, emptyList()).onComplete(testContext.succeeding(data -> testContext.verify(() -> {
+        repo.findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging(USER_IDENTITY,2, 2000, from, null, 0, 1, SortType.ASC, emptyList()).onComplete(testContext.succeeding(data -> testContext.verify(() -> {
             assertThat(data.size()).isEqualTo(1);
             assertThat(data.hasNext()).isFalse();
 
@@ -1307,7 +1307,7 @@ class MapLogRepositoryTest {
     @DisplayName("DB Test: findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging#11")
     void findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging_NM11(Vertx vertx, VertxTestContext testContext) {
         //  Campaign ID 10 does not exist.
-        repo.findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging(USER_IDENTITY,10, 2000, null, null, 0, 1, emptyList()).onComplete(testContext.succeeding(data -> testContext.verify(() -> {
+        repo.findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging(USER_IDENTITY,10, 2000, null, null, 0, 1, SortType.ASC, emptyList()).onComplete(testContext.succeeding(data -> testContext.verify(() -> {
             assertThat(data.size()).isEqualTo(0);
             assertThat(data.hasNext()).isFalse();
 
@@ -1319,7 +1319,7 @@ class MapLogRepositoryTest {
     @DisplayName("DB Test: findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging#12")
     void findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging_NM12(Vertx vertx, VertxTestContext testContext) {
         // No access to the unit 1000 from the campaign 2
-        repo.findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging(USER_IDENTITY,2, 1000, null, null, 0, 1, emptyList()).onComplete(testContext.succeeding(data -> testContext.verify(() -> {
+        repo.findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging(USER_IDENTITY,2, 1000, null, null, 0, 1, SortType.ASC, emptyList()).onComplete(testContext.succeeding(data -> testContext.verify(() -> {
             assertThat(data.size()).isEqualTo(0);
             assertThat(data.hasNext()).isFalse();
 
@@ -1331,7 +1331,7 @@ class MapLogRepositoryTest {
     @DisplayName("DB Test: findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging#13")
     void findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging_NM13(Vertx vertx, VertxTestContext testContext) {
         // No user's access to the campaign 1
-        repo.findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging(USER_IDENTITY,1, 3000, null, null, 0, 1, emptyList()).onComplete(testContext.succeeding(data -> testContext.verify(() -> {
+        repo.findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging(USER_IDENTITY,1, 3000, null, null, 0, 1, SortType.ASC, emptyList()).onComplete(testContext.succeeding(data -> testContext.verify(() -> {
             assertThat(data.size()).isEqualTo(0);
             assertThat(data.hasNext()).isFalse();
 
@@ -1348,7 +1348,7 @@ class MapLogRepositoryTest {
                 Filter.of(FilterType.SENSOR, FilterAttribute.ID, "2002", FilterOperation.EQ, 22)
         );
 
-        repo.findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging(USER_IDENTITY, 2, 2000, null, null, 0, 10, filters).onComplete(testContext.succeeding(data -> testContext.verify(() -> {
+        repo.findObservationsByIdentityAndCampaignIdAndUnitIdWithPaging(USER_IDENTITY, 2, 2000, null, null, 0, 10, SortType.ASC, filters).onComplete(testContext.succeeding(data -> testContext.verify(() -> {
             assertThat(data.size()).isEqualTo(1);
 
             UnitTelemetry t = data.data().get(0);

+ 9 - 8
src/test/java/cz/senslog/telemetry/server/Fm4exSocketHandlerTest.java

@@ -13,6 +13,7 @@ import io.vertx.core.Vertx;
 import io.vertx.core.json.JsonObject;
 import org.junit.jupiter.api.Test;
 import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
 
 import java.time.OffsetDateTime;
 import java.util.List;
@@ -40,19 +41,19 @@ class Fm4exSocketHandlerTest {
 
 
         MapLogRepository repo = mock(MapLogRepository.class);
-        when(repo.findUnitByIMEI(anyString()))
+        Mockito.when(repo.findUnitByIMEI(anyString()))
                 .thenReturn(Future.succeededFuture(Unit.of(TEST_UNIT_ID, datasetIMEI.getObject())));
 
-        when(repo.findSensorsByUnitIdGroupById(anyLong()))
+        Mockito.when(repo.findSensorsByUnitIdGroupById(anyLong()))
                 .thenReturn(Future.succeededFuture(Map.of(
-                        239L, Sensor.of(239, "sensor_id", 239),
-                        67L, Sensor.of(67, "sensor_id", 67),
-                        66L, Sensor.of(66, "sensor_id", 66),
-                        24L, Sensor.of(24, "sensor_id", 24),
-                        78L, Sensor.of(78, "sensor_id", 78)
+                        239, Sensor.of(239, "sensor_id", 239, 1.0),
+                        67, Sensor.of(67, "sensor_id", 67, 0.1),
+                        66, Sensor.of(66, "sensor_id", 66, 1.0),
+                        24, Sensor.of(24, "sensor_id", 24, 1.0),
+                        78, Sensor.of(78, "sensor_id", 78, 1.0)
                 )));
 
-        when(repo.saveAllTelemetry(obsTelemetryCapture.capture()))
+        Mockito.when(repo.saveAllTelemetry(obsTelemetryCapture.capture()))
                 .thenReturn(Future.succeededFuture(Stream.of(datasetAVLData8Ex.getObject()).map(o -> UnitTelemetry.of(
                         1L,
                         1L,

+ 53 - 14
src/test/java/cz/senslog/telemetry/server/ws/OpenAPIHandlerTest.java

@@ -15,6 +15,7 @@ import io.vertx.core.http.HttpClientResponse;
 import io.vertx.core.http.HttpHeaders;
 import io.vertx.core.http.HttpMethod;
 import io.vertx.core.http.RequestOptions;
+import io.vertx.core.json.JsonArray;
 import io.vertx.core.json.JsonObject;
 import io.vertx.junit5.VertxExtension;
 import io.vertx.junit5.VertxTestContext;
@@ -565,7 +566,7 @@ class OpenAPIHandlerTest {
     private static Stream<Arguments> campaignIdUnitIdSensorIdGET_dataSource() {
         return Stream.of(
                 Arguments.of(Future.succeededFuture(
-                        Sensor.of(105, "mock(name)", "M", 98, Phenomenon.of(15, "mock(phenomenon)"), "mock(description)")),
+                        Sensor.of(105, "mock(name)", "M", 98, 1.0, Phenomenon.of(15, "mock(phenomenon)"), "mock(description)")),
                         SUCCESS, JSON
                 ),
                 Arguments.of(
@@ -598,7 +599,7 @@ class OpenAPIHandlerTest {
     }
 
     private static Stream<Arguments> campaignIdUnitIdSensorIdGET_Params_dataSource() {
-        Future<Sensor> future = Future.succeededFuture(Sensor.of(105, "mock(name)", "M", 98, Phenomenon.of(15, "mock(phenomenon)"), "mock(description)"));
+        Future<Sensor> future = Future.succeededFuture(Sensor.of(105, "mock(name)", "M", 98, 1.0, Phenomenon.of(15, "mock(phenomenon)"), "mock(description)"));
         return Stream.of(
                 Arguments.of(future, false),
                 Arguments.of(future, true)
@@ -985,9 +986,9 @@ class OpenAPIHandlerTest {
     private static Stream<Arguments> sensorsGET_dataSource() {
         return Stream.of(
                 Arguments.of(Future.succeededFuture(List.of(
-                                        Sensor.of(105, "mock(name)", "M", 98, Phenomenon.of(15, "mock(phenomenon)"), "mock(description)"),
-                                        Sensor.of(106, "mock(name)", "M", 99, Phenomenon.of(16, "mock(phenomenon)"), "mock(description)"),
-                                        Sensor.of(107, "mock(name)", "M", 100, Phenomenon.of(17, "mock(phenomenon)"), "mock(description)"))
+                                        Sensor.of(105, "mock(name)", "M", 98, 1.0, Phenomenon.of(15, "mock(phenomenon)"), "mock(description)"),
+                                        Sensor.of(106, "mock(name)", "M", 99, 1.0, Phenomenon.of(16, "mock(phenomenon)"), "mock(description)"),
+                                        Sensor.of(107, "mock(name)", "M", 100, 1.0, Phenomenon.of(17, "mock(phenomenon)"), "mock(description)"))
                         ),
                         SUCCESS, JSON
                 ),
@@ -1024,7 +1025,7 @@ class OpenAPIHandlerTest {
     }
 
     private static Stream<Arguments> sensorsGET_Params_dataSource() {
-        Future<List<Sensor>> future = Future.succeededFuture(List.of(Sensor.of(105, "mock(name)", "M", 98, Phenomenon.of(15, "mock(phenomenon)"), "mock(description)")));
+        Future<List<Sensor>> future = Future.succeededFuture(List.of(Sensor.of(105, "mock(name)", "M", 98, 1.0, Phenomenon.of(15, "mock(phenomenon)"), "mock(description)")));
         return Stream.of(
                 Arguments.of(future, false),
                 Arguments.of(future, true)
@@ -1059,7 +1060,7 @@ class OpenAPIHandlerTest {
     private static Stream<Arguments> sensorIdGET_dataSource() {
         return Stream.of(
                 Arguments.of(Future.succeededFuture(
-                                Sensor.of(105, "mock(name)", "M", 98, Phenomenon.of(15, "mock(phenomenon)"), "mock(description)")
+                                Sensor.of(105, "mock(name)", "M", 98, 1.0, Phenomenon.of(15, "mock(phenomenon)"), "mock(description)")
                         ),
                         SUCCESS, JSON
                 ),
@@ -1093,7 +1094,7 @@ class OpenAPIHandlerTest {
     }
 
     private static Stream<Arguments> sensorIdGET_Params_dataSource() {
-        Future<Sensor> future = Future.succeededFuture(Sensor.of(105, "mock(name)", "M", 98, Phenomenon.of(15, "mock(phenomenon)"), "mock(description)"));
+        Future<Sensor> future = Future.succeededFuture(Sensor.of(105, "mock(name)", "M", 98, 1.0, Phenomenon.of(15, "mock(phenomenon)"), "mock(description)"));
         return Stream.of(
                 Arguments.of(future, false),
                 Arguments.of(future, true)
@@ -1344,9 +1345,9 @@ class OpenAPIHandlerTest {
         return Stream.of(
                 Arguments.of(
                         Future.succeededFuture(List.of(
-                                Sensor.of(105, "mock(name)", "M", 98, Phenomenon.of(15, "mock(phenomenon)"), "mock(description)"),
-                                Sensor.of(106, "mock(name)", "M", 99, Phenomenon.of(17, "mock(phenomenon)"), "mock(description)"),
-                                Sensor.of(107, "mock(name)", "M", 100, Phenomenon.of(18, "mock(phenomenon)"), "mock(description)")
+                                Sensor.of(105, "mock(name)", "M", 98, 1.0, Phenomenon.of(15, "mock(phenomenon)"), "mock(description)"),
+                                Sensor.of(106, "mock(name)", "M", 99, 1.0, Phenomenon.of(17, "mock(phenomenon)"), "mock(description)"),
+                                Sensor.of(107, "mock(name)", "M", 100, 1.0, Phenomenon.of(18, "mock(phenomenon)"), "mock(description)")
                         )),
                         SUCCESS, JSON
                 ),
@@ -1383,7 +1384,7 @@ class OpenAPIHandlerTest {
     }
 
     private static Stream<Arguments> phenomenonIdSensorsGET_Params_dataSource() {
-        Future<List<Sensor>> future = Future.succeededFuture(List.of(Sensor.of(105, "mock(name)", "M", 98, Phenomenon.of(15, "mock(phenomenon)"), "mock(description)")));
+        Future<List<Sensor>> future = Future.succeededFuture(List.of(Sensor.of(105, "mock(name)", "M", 98, 1.0, Phenomenon.of(15, "mock(phenomenon)"), "mock(description)")));
         return Stream.of(
                 Arguments.of(future, false),
                 Arguments.of(future, true)
@@ -2174,7 +2175,7 @@ class OpenAPIHandlerTest {
     @DisplayName("API Spec Test: entityIdUnitIdActionIdEventsGET")
     void entityIdUnitIdActionIdEventsGET_bySpec(Future<List<Event>> dbFuture, OpenAPIResponseTyp responseTyp, ContentType contentType, Vertx vertx, VertxTestContext testContext) {
 
-        Mockito.when(repo.findEventsByEntityIdAndUnitIdAndActionId(anyInt(), anyLong(), anyInt())).thenReturn(dbFuture);
+        Mockito.when(repo.findEventsByEntityIdAndUnitIdAndActionId(anyInt(), anyLong(), anyInt(), any(), any())).thenReturn(dbFuture);
 
         Operation operation = OPEN_API.getOperationById("entityIdUnitIdActionIdEventsGET");
         VISITED_TEST_NODES.add(operation.getOperationId());
@@ -2224,7 +2225,7 @@ class OpenAPIHandlerTest {
     @DisplayName("API Params Test: entityIdUnitIdActionIdEventsGET")
     void entityIdUnitIdActionIdEventsGET_byParams(Future<List<Event>> dbFuture, String zoneParam, Tuple<String, String> expectedTimezones, boolean navigationLinks, Vertx vertx, VertxTestContext testContext) {
 
-        Mockito.when(repo.findEventsByEntityIdAndUnitIdAndActionId(anyInt(), anyLong(), anyInt())).thenReturn(dbFuture);
+        Mockito.when(repo.findEventsByEntityIdAndUnitIdAndActionId(anyInt(), anyLong(), anyInt(), any(), any())).thenReturn(dbFuture);
 
         RequestOptions reqOpt = new RequestOptions()
                 .setMethod(HttpMethod.GET).setPort(serverPort).setHost(HOST).setURI(
@@ -2880,6 +2881,44 @@ class OpenAPIHandlerTest {
     }
 
     @Test
+    @DisplayName("API Spec Test: campaignIdUnitsObservationsPOST")
+    void campaignIdUnitsObservationsPOST_bySpec(Vertx vertx, VertxTestContext testContext) {
+
+        // TODO
+        OffsetDateTime baseTimestamp = OffsetDateTime.ofInstant(BASE_INSTANT_TIMESTAMP, ZoneOffset.UTC);
+        Mockito.when(repo.findCampaignById(anyLong()))
+                .thenReturn(Future.succeededFuture(Campaign.of(1, "mock(name)", "mock(description)", baseTimestamp, baseTimestamp.plusYears(10))));
+        Mockito.when(repo.findSensorsByCampaignIdGroupByUnitId(anyLong()))
+                .thenReturn(Future.succeededFuture(Map.of(25L, List.of(Sensor.of(105, "mock(name)")))));
+        Mockito.when(repo.saveAllTelemetry(anyList()))
+                .thenReturn(Future.succeededFuture(emptyList()));
+
+
+        RequestOptions reqOpt = new RequestOptions()
+                .setMethod(HttpMethod.POST).setPort(serverPort).setHost(HOST).setURI("/campaigns/1/units/observations")
+                .setHeaders(MultiMap.caseInsensitiveMultiMap()
+                        .add(HttpHeaders.ACCEPT, JSON.contentType())
+                        .add(HttpHeaders.CONTENT_TYPE, JSON.contentType()));
+
+        JsonArray reqBody = JsonArray.of(JsonObject.of(
+                "unitId", 25L,
+                "timestamp",  "2024-08-02T22:40:00+02:00", // "2024-08-02 22:40:00+02",
+                    "observedValues", JsonObject.of(
+                            "105", 10.35
+                )
+        ));
+
+        vertx.createHttpClient().request(reqOpt)
+                .compose(req -> req.send(reqBody.toBuffer()).compose(HttpClientResponse::body))
+                .onComplete(testContext.succeeding(b -> testContext.verify(() -> {
+                    String resBody = b.toString();
+                    System.out.println(resBody);
+                    testContext.completeNow();
+                })));
+
+    }
+
+    @Test
     @DisplayName("API Spec Test: legacyInsertObservationsGET")
     void legacyInsertObservationsGET_bySpec(Vertx vertx, VertxTestContext testContext) {
 

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio