Procházet zdrojové kódy

Implemented Alerting system

Lukas Cerny před 1 rokem
rodič
revize
5a4e79ab93

+ 401 - 317
doc/openAPI_NavigationLinks.svg

@@ -4,712 +4,796 @@
 <!-- Generated by graphviz version 2.43.0 (0)
  -->
 <!-- Title: regexp Pages: 1 -->
-<svg width="2959pt" height="908pt"
- viewBox="0.00 0.00 2959.21 908.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
-<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 904)">
+<svg width="3038pt" height="1124pt"
+ viewBox="0.00 0.00 3037.81 1124.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1120)">
 <title>regexp</title>
-<polygon fill="white" stroke="transparent" points="-4,4 -4,-904 2955.21,-904 2955.21,4 -4,4"/>
+<polygon fill="white" stroke="transparent" points="-4,4 -4,-1120 3033.81,-1120 3033.81,4 -4,4"/>
 <!-- infoGET -->
 <g id="node1" class="node">
 <title>infoGET</title>
-<ellipse fill="none" stroke="black" cx="787.18" cy="-882" rx="27.9" ry="18"/>
-<text text-anchor="middle" x="787.18" y="-878.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/info</text>
+<ellipse fill="none" stroke="black" cx="2157.18" cy="-1098" rx="27.9" ry="18"/>
+<text text-anchor="middle" x="2157.18" y="-1094.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/info</text>
 </g>
 <!-- campaignsGET -->
 <g id="node2" class="node">
 <title>campaignsGET</title>
-<ellipse fill="none" stroke="black" cx="889.18" cy="-882" rx="55.79" ry="18"/>
-<text text-anchor="middle" x="889.18" y="-878.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/campaigns</text>
+<ellipse fill="none" stroke="black" cx="2259.18" cy="-1098" rx="55.79" ry="18"/>
+<text text-anchor="middle" x="2259.18" y="-1094.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/campaigns</text>
 </g>
 <!-- campaignIdGET -->
 <g id="node3" class="node">
 <title>campaignIdGET</title>
-<ellipse fill="none" stroke="black" cx="889.18" cy="-810" rx="110.48" ry="18"/>
-<text text-anchor="middle" x="889.18" y="-806.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/campaigns/{campaignId}</text>
+<ellipse fill="none" stroke="black" cx="2259.18" cy="-1026" rx="110.48" ry="18"/>
+<text text-anchor="middle" x="2259.18" y="-1022.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/campaigns/{campaignId}</text>
 </g>
 <!-- campaignsGET&#45;&gt;campaignIdGET -->
 <g id="edge1" class="edge">
 <title>campaignsGET&#45;&gt;campaignIdGET</title>
-<path fill="none" stroke="black" d="M889.18,-863.7C889.18,-855.98 889.18,-846.71 889.18,-838.11"/>
-<polygon fill="black" stroke="black" points="892.68,-838.1 889.18,-828.1 885.68,-838.1 892.68,-838.1"/>
+<path fill="none" stroke="black" d="M2259.18,-1079.7C2259.18,-1071.98 2259.18,-1062.71 2259.18,-1054.11"/>
+<polygon fill="black" stroke="black" points="2262.68,-1054.1 2259.18,-1044.1 2255.68,-1054.1 2262.68,-1054.1"/>
 </g>
 <!-- campaignIdGET&#45;&gt;campaignIdGET -->
 <g id="edge2" class="edge">
 <title>campaignIdGET&#45;&gt;campaignIdGET</title>
-<path fill="none" stroke="black" d="M963.12,-823.42C992.78,-824.18 1017.67,-819.7 1017.67,-810 1017.67,-801.43 998.27,-796.94 973.32,-796.53"/>
-<polygon fill="black" stroke="black" points="973.1,-793.03 963.12,-796.58 973.14,-800.03 973.1,-793.03"/>
+<path fill="none" stroke="black" d="M2333.12,-1039.42C2362.78,-1040.18 2387.67,-1035.7 2387.67,-1026 2387.67,-1017.43 2368.27,-1012.94 2343.32,-1012.53"/>
+<polygon fill="black" stroke="black" points="2343.1,-1009.03 2333.12,-1012.58 2343.14,-1016.03 2343.1,-1009.03"/>
 </g>
 <!-- campaignIdUnitsGET -->
 <g id="node4" class="node">
 <title>campaignIdUnitsGET</title>
-<ellipse fill="none" stroke="black" cx="1482.18" cy="-738" rx="131.88" ry="18"/>
-<text text-anchor="middle" x="1482.18" y="-734.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/campaigns/{campaignId}/units</text>
+<ellipse fill="none" stroke="black" cx="1993.18" cy="-954" rx="131.88" ry="18"/>
+<text text-anchor="middle" x="1993.18" y="-950.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/campaigns/{campaignId}/units</text>
 </g>
 <!-- campaignIdGET&#45;&gt;campaignIdUnitsGET -->
 <g id="edge5" class="edge">
 <title>campaignIdGET&#45;&gt;campaignIdUnitsGET</title>
-<path fill="none" stroke="black" d="M976.26,-798.87C1067.29,-788.27 1214.27,-771.09 1341.18,-756 1352.75,-754.62 1364.86,-753.18 1376.9,-751.73"/>
-<polygon fill="black" stroke="black" points="1377.49,-755.19 1387,-750.52 1376.65,-748.24 1377.49,-755.19"/>
+<path fill="none" stroke="black" d="M2203.74,-1010.41C2162.26,-999.5 2105.32,-984.51 2061.11,-972.88"/>
+<polygon fill="black" stroke="black" points="2061.87,-969.46 2051.31,-970.3 2060.09,-976.23 2061.87,-969.46"/>
 </g>
 <!-- campaignIdUnitsObservationsGET -->
 <g id="node5" class="node">
 <title>campaignIdUnitsObservationsGET</title>
-<ellipse fill="none" stroke="black" cx="668.18" cy="-738" rx="184.67" ry="18"/>
-<text text-anchor="middle" x="668.18" y="-734.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/campaigns/{campaignId}/units/observations</text>
+<ellipse fill="none" stroke="black" cx="2366.18" cy="-954" rx="184.67" ry="18"/>
+<text text-anchor="middle" x="2366.18" y="-950.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/campaigns/{campaignId}/units/observations</text>
 </g>
 <!-- campaignIdGET&#45;&gt;campaignIdUnitsObservationsGET -->
 <g id="edge3" class="edge">
 <title>campaignIdGET&#45;&gt;campaignIdUnitsObservationsGET</title>
-<path fill="none" stroke="black" d="M836.67,-794.15C802.86,-783.85 759.02,-770.02 724.49,-758.78"/>
-<polygon fill="black" stroke="black" points="725.15,-755.32 714.56,-755.54 722.98,-761.97 725.15,-755.32"/>
+<path fill="none" stroke="black" d="M2278.95,-1008.23C2292.14,-998.95 2309.8,-987.29 2325.82,-977.26"/>
+<polygon fill="black" stroke="black" points="2327.86,-980.11 2334.52,-971.87 2324.17,-974.16 2327.86,-980.11"/>
 </g>
 <!-- campaignIdUnitsObservationsLocationsGET -->
 <g id="node6" class="node">
 <title>campaignIdUnitsObservationsLocationsGET</title>
-<ellipse fill="none" stroke="black" cx="1110.18" cy="-738" rx="221.76" ry="18"/>
-<text text-anchor="middle" x="1110.18" y="-734.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/campaigns/{campaignId}/units/observations/locations</text>
+<ellipse fill="none" stroke="black" cx="2808.18" cy="-954" rx="221.76" ry="18"/>
+<text text-anchor="middle" x="2808.18" y="-950.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/campaigns/{campaignId}/units/observations/locations</text>
 </g>
 <!-- campaignIdGET&#45;&gt;campaignIdUnitsObservationsLocationsGET -->
 <g id="edge4" class="edge">
 <title>campaignIdGET&#45;&gt;campaignIdUnitsObservationsLocationsGET</title>
-<path fill="none" stroke="black" d="M932.77,-793.38C964.11,-783.06 1006.89,-769.47 1042.62,-758.46"/>
-<polygon fill="black" stroke="black" points="1043.96,-761.71 1052.49,-755.43 1041.91,-755.02 1043.96,-761.71"/>
+<path fill="none" stroke="black" d="M2341.08,-1013.91C2430.85,-1002.21 2575.93,-983.68 2680.62,-970.53"/>
+<polygon fill="black" stroke="black" points="2681.14,-974 2690.62,-969.28 2680.26,-967.05 2681.14,-974"/>
 </g>
 <!-- campaignIdUnitIdGET -->
 <g id="node7" class="node">
 <title>campaignIdUnitIdGET</title>
-<ellipse fill="none" stroke="black" cx="773.18" cy="-666" rx="161.67" ry="18"/>
-<text text-anchor="middle" x="773.18" y="-662.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/campaigns/{campaignId}/units/{unitId}</text>
+<ellipse fill="none" stroke="black" cx="1554.18" cy="-882" rx="161.67" ry="18"/>
+<text text-anchor="middle" x="1554.18" y="-878.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/campaigns/{campaignId}/units/{unitId}</text>
 </g>
 <!-- campaignIdUnitsGET&#45;&gt;campaignIdUnitIdGET -->
 <g id="edge7" class="edge">
 <title>campaignIdUnitsGET&#45;&gt;campaignIdUnitIdGET</title>
-<path fill="none" stroke="black" d="M1388.17,-725.31C1372.49,-723.46 1356.38,-721.63 1341.18,-720 1192.89,-704.15 1022.53,-688.6 907.37,-678.49"/>
-<polygon fill="black" stroke="black" points="907.36,-674.98 897.09,-677.59 906.75,-681.95 907.36,-674.98"/>
+<path fill="none" stroke="black" d="M1910.86,-939.87C1837.68,-928.21 1730.54,-911.12 1652.8,-898.73"/>
+<polygon fill="black" stroke="black" points="1652.94,-895.2 1642.51,-897.08 1651.83,-902.12 1652.94,-895.2"/>
 </g>
 <!-- unitIdGET -->
 <g id="node14" class="node">
 <title>unitIdGET</title>
-<ellipse fill="none" stroke="black" cx="1512.18" cy="-594" rx="61.99" ry="18"/>
-<text text-anchor="middle" x="1512.18" y="-590.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/units/{unitId}</text>
+<ellipse fill="none" stroke="black" cx="2045.18" cy="-810" rx="61.99" ry="18"/>
+<text text-anchor="middle" x="2045.18" y="-806.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/units/{unitId}</text>
 </g>
 <!-- campaignIdUnitsGET&#45;&gt;unitIdGET -->
 <g id="edge6" class="edge">
 <title>campaignIdUnitsGET&#45;&gt;unitIdGET</title>
-<path fill="none" stroke="black" d="M1485.8,-719.87C1490.93,-695.56 1500.38,-650.82 1506.47,-622.01"/>
-<polygon fill="black" stroke="black" points="1509.9,-622.7 1508.55,-612.19 1503.05,-621.25 1509.9,-622.7"/>
+<path fill="none" stroke="black" d="M2006.11,-935.68C2012.99,-925.7 2021.07,-912.66 2026.18,-900 2034.2,-880.13 2039.05,-856.33 2041.84,-838.35"/>
+<polygon fill="black" stroke="black" points="2045.34,-838.64 2043.29,-828.25 2038.41,-837.65 2045.34,-838.64"/>
 </g>
 <!-- campaignIdUnitsObservationsGET&#45;&gt;campaignIdGET -->
-<g id="edge20" class="edge">
+<g id="edge21" class="edge">
 <title>campaignIdUnitsObservationsGET&#45;&gt;campaignIdGET</title>
-<path fill="none" stroke="black" d="M725.06,-755.18C759.26,-765.64 802.44,-779.29 835.96,-790.22"/>
-<polygon fill="black" stroke="black" points="834.99,-793.59 845.59,-793.38 837.17,-786.94 834.99,-793.59"/>
+<path fill="none" stroke="black" d="M2346.02,-972.04C2332.67,-981.42 2314.85,-993.17 2298.79,-1003.21"/>
+<polygon fill="black" stroke="black" points="2296.75,-1000.35 2290.08,-1008.59 2300.43,-1006.31 2296.75,-1000.35"/>
 </g>
 <!-- campaignIdUnitsObservationsGET&#45;&gt;campaignIdUnitsObservationsGET -->
-<g id="edge21" class="edge">
+<g id="edge22" class="edge">
 <title>campaignIdUnitsObservationsGET&#45;&gt;campaignIdUnitsObservationsGET</title>
-<path fill="none" stroke="black" d="M790.56,-751.49C834.72,-751.79 870.76,-747.3 870.76,-738 870.76,-729.43 840.13,-724.94 800.75,-724.53"/>
-<polygon fill="black" stroke="black" points="800.57,-721.03 790.56,-724.51 800.56,-728.03 800.57,-721.03"/>
+<path fill="none" stroke="black" d="M2488.56,-967.49C2532.72,-967.79 2568.76,-963.3 2568.76,-954 2568.76,-945.43 2538.13,-940.94 2498.75,-940.53"/>
+<polygon fill="black" stroke="black" points="2498.57,-937.03 2488.56,-940.51 2498.56,-944.03 2498.57,-937.03"/>
 </g>
 <!-- campaignIdUnitsObservationsLocationsGET&#45;&gt;campaignIdGET -->
-<g id="edge31" class="edge">
+<g id="edge32" class="edge">
 <title>campaignIdUnitsObservationsLocationsGET&#45;&gt;campaignIdGET</title>
-<path fill="none" stroke="black" d="M1063.42,-755.66C1030.81,-766.36 987.03,-780.24 951.48,-791.16"/>
-<polygon fill="black" stroke="black" points="950.22,-787.88 941.68,-794.15 952.27,-794.58 950.22,-787.88"/>
+<path fill="none" stroke="black" d="M2698.46,-969.71C2598.04,-982.72 2450.96,-1001.45 2355.12,-1013.43"/>
+<polygon fill="black" stroke="black" points="2354.63,-1009.96 2345.14,-1014.67 2355.5,-1016.91 2354.63,-1009.96"/>
 </g>
 <!-- campaignIdUnitIdGET&#45;&gt;campaignIdGET -->
 <g id="edge10" class="edge">
 <title>campaignIdUnitIdGET&#45;&gt;campaignIdGET</title>
-<path fill="none" stroke="black" d="M615.31,-670.08C519.21,-677.58 423.8,-698.78 474.18,-756 494.01,-778.53 661.27,-794.06 777.29,-802.24"/>
-<polygon fill="black" stroke="black" points="777.08,-805.73 787.3,-802.94 777.57,-798.75 777.08,-805.73"/>
+<path fill="none" stroke="black" d="M1600.29,-899.3C1658.35,-919.23 1761.48,-952.68 1852.18,-972 1953.37,-993.55 2070.7,-1007.71 2153.31,-1015.92"/>
+<polygon fill="black" stroke="black" points="2153.17,-1019.42 2163.46,-1016.92 2153.85,-1012.46 2153.17,-1019.42"/>
 </g>
 <!-- campaignIdUnitIdGET&#45;&gt;campaignIdUnitIdGET -->
 <g id="edge8" class="edge">
 <title>campaignIdUnitIdGET&#45;&gt;campaignIdUnitIdGET</title>
-<path fill="none" stroke="black" d="M880.71,-679.48C920.41,-679.88 953.01,-675.39 953.01,-666 953.01,-657.42 925.77,-652.93 890.77,-652.53"/>
-<polygon fill="black" stroke="black" points="890.71,-649.03 880.71,-652.52 890.7,-656.03 890.71,-649.03"/>
+<path fill="none" stroke="black" d="M1661.71,-895.48C1701.41,-895.88 1734.01,-891.39 1734.01,-882 1734.01,-873.42 1706.77,-868.93 1671.77,-868.53"/>
+<polygon fill="black" stroke="black" points="1671.71,-865.03 1661.71,-868.52 1671.7,-872.03 1671.71,-865.03"/>
 </g>
 <!-- campaignIdUnitIdObservationsGET -->
 <g id="node8" class="node">
 <title>campaignIdUnitIdObservationsGET</title>
-<ellipse fill="none" stroke="black" cx="1200.18" cy="-594" rx="214.46" ry="18"/>
-<text text-anchor="middle" x="1200.18" y="-590.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/campaigns/{campaignId}/units/{unitId}/observations</text>
+<ellipse fill="none" stroke="black" cx="1554.18" cy="-810" rx="214.46" ry="18"/>
+<text text-anchor="middle" x="1554.18" y="-806.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/campaigns/{campaignId}/units/{unitId}/observations</text>
 </g>
 <!-- campaignIdUnitIdGET&#45;&gt;campaignIdUnitIdObservationsGET -->
 <g id="edge12" class="edge">
 <title>campaignIdUnitIdGET&#45;&gt;campaignIdUnitIdObservationsGET</title>
-<path fill="none" stroke="black" d="M856.06,-650.5C923.43,-639.21 1018.94,-623.54 1092.27,-611.73"/>
-<polygon fill="black" stroke="black" points="1093.15,-615.13 1102.46,-610.09 1092.03,-608.22 1093.15,-615.13"/>
+<path fill="none" stroke="black" d="M1548.26,-863.7C1547.47,-855.98 1547.24,-846.71 1547.58,-838.11"/>
+<polygon fill="black" stroke="black" points="1551.08,-838.32 1548.28,-828.1 1544.09,-837.84 1551.08,-838.32"/>
 </g>
 <!-- campaignIdUnitIdLocationsGET -->
 <g id="node9" class="node">
 <title>campaignIdUnitIdLocationsGET</title>
-<ellipse fill="none" stroke="black" cx="252.18" cy="-594" rx="252.36" ry="18"/>
-<text text-anchor="middle" x="252.18" y="-590.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/campaigns/{campaignId}/units/{unitId}/observations/locations</text>
+<ellipse fill="none" stroke="black" cx="252.18" cy="-810" rx="252.36" ry="18"/>
+<text text-anchor="middle" x="252.18" y="-806.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/campaigns/{campaignId}/units/{unitId}/observations/locations</text>
 </g>
 <!-- campaignIdUnitIdGET&#45;&gt;campaignIdUnitIdLocationsGET -->
 <g id="edge13" class="edge">
 <title>campaignIdUnitIdGET&#45;&gt;campaignIdUnitIdLocationsGET</title>
-<path fill="none" stroke="black" d="M671.01,-652C584.7,-640.62 461.07,-624.03 370.48,-611.68"/>
-<polygon fill="black" stroke="black" points="370.82,-608.19 360.44,-610.31 369.87,-615.13 370.82,-608.19"/>
+<path fill="none" stroke="black" d="M1408.35,-874.16C1208.31,-864.43 837.79,-845.79 522.18,-828 496.08,-826.53 468.61,-824.92 441.59,-823.31"/>
+<polygon fill="black" stroke="black" points="441.45,-819.79 431.26,-822.69 441.03,-826.78 441.45,-819.79"/>
 </g>
 <!-- campaignIdUnitIdSensorsGET -->
 <g id="node10" class="node">
 <title>campaignIdUnitIdSensorsGET</title>
-<ellipse fill="none" stroke="black" cx="773.18" cy="-594" rx="194.97" ry="18"/>
-<text text-anchor="middle" x="773.18" y="-590.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/campaigns/{campaignId}/units/{unitId}/sensors</text>
+<ellipse fill="none" stroke="black" cx="735.18" cy="-810" rx="194.97" ry="18"/>
+<text text-anchor="middle" x="735.18" y="-806.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/campaigns/{campaignId}/units/{unitId}/sensors</text>
 </g>
 <!-- campaignIdUnitIdGET&#45;&gt;campaignIdUnitIdSensorsGET -->
 <g id="edge11" class="edge">
 <title>campaignIdUnitIdGET&#45;&gt;campaignIdUnitIdSensorsGET</title>
-<path fill="none" stroke="black" d="M773.18,-647.7C773.18,-639.98 773.18,-630.71 773.18,-622.11"/>
-<polygon fill="black" stroke="black" points="776.68,-622.1 773.18,-612.1 769.68,-622.1 776.68,-622.1"/>
+<path fill="none" stroke="black" d="M1430.23,-870.41C1283.54,-857.87 1039.49,-837.01 883.06,-823.64"/>
+<polygon fill="black" stroke="black" points="883.15,-820.13 872.89,-822.77 882.55,-827.11 883.15,-820.13"/>
 </g>
 <!-- campaignIdUnitIdGET&#45;&gt;unitIdGET -->
 <g id="edge9" class="edge">
 <title>campaignIdUnitIdGET&#45;&gt;unitIdGET</title>
-<path fill="none" stroke="black" d="M924.86,-659.7C1060.77,-653.36 1265.23,-639.87 1441.18,-612 1446.65,-611.13 1452.33,-610.06 1457.98,-608.89"/>
-<polygon fill="black" stroke="black" points="1459.06,-612.24 1468.08,-606.68 1457.56,-605.4 1459.06,-612.24"/>
+<path fill="none" stroke="black" d="M1649.43,-867.42C1747.11,-853.49 1896.35,-832.22 1980.9,-820.16"/>
+<polygon fill="black" stroke="black" points="1981.47,-823.62 1990.88,-818.74 1980.48,-816.69 1981.47,-823.62"/>
+</g>
+<!-- campaignIdUnitIdAlertsGET -->
+<g id="node42" class="node">
+<title>campaignIdUnitIdAlertsGET</title>
+<ellipse fill="none" stroke="black" cx="1135.18" cy="-810" rx="186.57" ry="18"/>
+<text text-anchor="middle" x="1135.18" y="-806.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/campaigns/{campaignId}/units/{unitId}/alerts</text>
+</g>
+<!-- campaignIdUnitIdGET&#45;&gt;campaignIdUnitIdAlertsGET -->
+<g id="edge14" class="edge">
+<title>campaignIdUnitIdGET&#45;&gt;campaignIdUnitIdAlertsGET</title>
+<path fill="none" stroke="black" d="M1468.63,-866.71C1400.62,-855.35 1305.4,-839.44 1234.12,-827.53"/>
+<polygon fill="black" stroke="black" points="1234.66,-824.07 1224.22,-825.88 1233.51,-830.98 1234.66,-824.07"/>
 </g>
 <!-- campaignIdUnitIdObservationsGET&#45;&gt;campaignIdUnitIdGET -->
-<g id="edge22" class="edge">
+<g id="edge23" class="edge">
 <title>campaignIdUnitIdObservationsGET&#45;&gt;campaignIdUnitIdGET</title>
-<path fill="none" stroke="black" d="M1111.63,-610.45C1042.36,-622.03 946.05,-637.82 873.85,-649.43"/>
-<polygon fill="black" stroke="black" points="873.14,-646 863.83,-651.04 874.25,-652.91 873.14,-646"/>
+<path fill="none" stroke="black" d="M1560.07,-828.1C1560.88,-835.79 1561.11,-845.05 1560.78,-853.67"/>
+<polygon fill="black" stroke="black" points="1557.29,-853.48 1560.09,-863.7 1564.27,-853.96 1557.29,-853.48"/>
 </g>
 <!-- campaignIdUnitIdObservationsGET&#45;&gt;campaignIdUnitIdObservationsGET -->
-<g id="edge23" class="edge">
+<g id="edge24" class="edge">
 <title>campaignIdUnitIdObservationsGET&#45;&gt;campaignIdUnitIdObservationsGET</title>
-<path fill="none" stroke="black" d="M1342.34,-607.49C1392.23,-607.69 1432.66,-603.19 1432.66,-594 1432.66,-585.44 1397.57,-580.95 1352.43,-580.53"/>
-<polygon fill="black" stroke="black" points="1352.35,-577.03 1342.34,-580.51 1352.33,-584.03 1352.35,-577.03"/>
+<path fill="none" stroke="black" d="M1696.34,-823.49C1746.23,-823.69 1786.66,-819.19 1786.66,-810 1786.66,-801.44 1751.57,-796.95 1706.43,-796.53"/>
+<polygon fill="black" stroke="black" points="1706.35,-793.03 1696.34,-796.51 1706.33,-800.03 1706.35,-793.03"/>
 </g>
 <!-- campaignIdUnitIdLocationsGET&#45;&gt;campaignIdGET -->
-<g id="edge28" class="edge">
+<g id="edge29" class="edge">
 <title>campaignIdUnitIdLocationsGET&#45;&gt;campaignIdGET</title>
-<path fill="none" stroke="black" d="M267.43,-612.04C299.01,-646.06 375.48,-722.04 457.18,-756 513.25,-779.31 669.21,-794.24 777.52,-802.13"/>
-<polygon fill="black" stroke="black" points="777.3,-805.62 787.52,-802.84 777.8,-798.64 777.3,-805.62"/>
+<path fill="none" stroke="black" d="M379.9,-825.58C634.17,-854.48 1221.82,-920.69 1716.18,-972 1869.21,-987.88 2046.64,-1004.95 2155.91,-1015.3"/>
+<polygon fill="black" stroke="black" points="2155.66,-1018.79 2165.94,-1016.25 2156.32,-1011.82 2155.66,-1018.79"/>
 </g>
 <!-- campaignIdUnitIdLocationsGET&#45;&gt;campaignIdUnitIdGET -->
-<g id="edge29" class="edge">
+<g id="edge30" class="edge">
 <title>campaignIdUnitIdLocationsGET&#45;&gt;campaignIdUnitIdGET</title>
-<path fill="none" stroke="black" d="M369.45,-610C458.91,-621.82 581.02,-638.23 667.48,-650.04"/>
-<polygon fill="black" stroke="black" points="667.19,-653.53 677.58,-651.42 668.14,-646.6 667.19,-653.53"/>
+<path fill="none" stroke="black" d="M439.23,-822.1C472.88,-824.12 507.59,-826.16 540.18,-828 845.16,-845.19 1201.42,-863.17 1400.13,-873.15"/>
+<polygon fill="black" stroke="black" points="1400.19,-876.65 1410.35,-873.66 1400.54,-869.66 1400.19,-876.65"/>
 </g>
 <!-- campaignIdUnitIdLocationsGET&#45;&gt;campaignIdUnitIdLocationsGET -->
-<g id="edge30" class="edge">
+<g id="edge31" class="edge">
 <title>campaignIdUnitIdLocationsGET&#45;&gt;campaignIdUnitIdLocationsGET</title>
-<path fill="none" stroke="black" d="M419.06,-607.5C476.26,-607.6 522.36,-603.11 522.36,-594 522.36,-585.45 481.68,-580.96 429.32,-580.53"/>
-<polygon fill="black" stroke="black" points="429.07,-577.03 419.06,-580.5 429.04,-584.03 429.07,-577.03"/>
+<path fill="none" stroke="black" d="M419.06,-823.5C476.26,-823.6 522.36,-819.11 522.36,-810 522.36,-801.45 481.68,-796.96 429.32,-796.53"/>
+<polygon fill="black" stroke="black" points="429.07,-793.03 419.06,-796.5 429.04,-800.03 429.07,-793.03"/>
 </g>
 <!-- campaignIdUnitIdSensorIdGET -->
 <g id="node11" class="node">
 <title>campaignIdUnitIdSensorIdGET</title>
-<ellipse fill="none" stroke="black" cx="722.18" cy="-522" rx="237.46" ry="18"/>
-<text text-anchor="middle" x="722.18" y="-518.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/campaigns/{campaignId}/units/{unitId}/sensors/{sensorId}</text>
+<ellipse fill="none" stroke="black" cx="1333.18" cy="-738" rx="237.46" ry="18"/>
+<text text-anchor="middle" x="1333.18" y="-734.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/campaigns/{campaignId}/units/{unitId}/sensors/{sensorId}</text>
 </g>
 <!-- campaignIdUnitIdSensorsGET&#45;&gt;campaignIdUnitIdSensorIdGET -->
-<g id="edge14" class="edge">
+<g id="edge15" class="edge">
 <title>campaignIdUnitIdSensorsGET&#45;&gt;campaignIdUnitIdSensorIdGET</title>
-<path fill="none" stroke="black" d="M760.57,-575.7C754.58,-567.47 747.3,-557.48 740.7,-548.42"/>
-<polygon fill="black" stroke="black" points="743.36,-546.13 734.64,-540.1 737.7,-550.25 743.36,-546.13"/>
+<path fill="none" stroke="black" d="M850.83,-795.46C950.82,-783.76 1095.37,-766.84 1200.13,-754.57"/>
+<polygon fill="black" stroke="black" points="1200.62,-758.04 1210.14,-753.4 1199.8,-751.09 1200.62,-758.04"/>
 </g>
 <!-- campaignIdUnitIdSensorIdGET&#45;&gt;campaignIdUnitIdGET -->
-<g id="edge17" class="edge">
+<g id="edge18" class="edge">
 <title>campaignIdUnitIdSensorIdGET&#45;&gt;campaignIdUnitIdGET</title>
-<path fill="none" stroke="black" d="M644.66,-539.03C594.08,-553.13 542.06,-576.94 569.18,-612 582,-628.58 624.21,-640.94 666.93,-649.53"/>
-<polygon fill="black" stroke="black" points="666.43,-653 676.91,-651.47 667.76,-646.12 666.43,-653"/>
+<path fill="none" stroke="black" d="M1522.13,-748.93C1641.42,-757.19 1776.47,-771.02 1795.18,-792 1834.74,-836.36 1767.8,-858.65 1694.53,-869.83"/>
+<polygon fill="black" stroke="black" points="1694.02,-866.37 1684.63,-871.27 1695.03,-873.3 1694.02,-866.37"/>
 </g>
 <!-- campaignIdUnitIdSensorIdGET&#45;&gt;campaignIdUnitIdSensorIdGET -->
-<g id="edge15" class="edge">
+<g id="edge16" class="edge">
 <title>campaignIdUnitIdSensorIdGET&#45;&gt;campaignIdUnitIdSensorIdGET</title>
-<path fill="none" stroke="black" d="M879.2,-535.5C933.53,-535.64 977.41,-531.14 977.41,-522 977.41,-513.43 938.84,-508.94 889.25,-508.53"/>
-<polygon fill="black" stroke="black" points="889.21,-505.03 879.2,-508.5 889.19,-512.03 889.21,-505.03"/>
+<path fill="none" stroke="black" d="M1490.2,-751.5C1544.53,-751.64 1588.41,-747.14 1588.41,-738 1588.41,-729.43 1549.84,-724.94 1500.25,-724.53"/>
+<polygon fill="black" stroke="black" points="1500.21,-721.03 1490.2,-724.5 1500.19,-728.03 1500.21,-721.03"/>
 </g>
 <!-- campaignIdUnitIdSensorIdObservationsGET -->
 <g id="node12" class="node">
 <title>campaignIdUnitIdSensorIdObservationsGET</title>
-<ellipse fill="none" stroke="black" cx="524.18" cy="-450" rx="289.75" ry="18"/>
-<text text-anchor="middle" x="524.18" y="-446.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/campaigns/{campaignId}/units/{unitId}/sensors/{sensorId}/observations</text>
+<ellipse fill="none" stroke="black" cx="1421.18" cy="-666" rx="289.75" ry="18"/>
+<text text-anchor="middle" x="1421.18" y="-662.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/campaigns/{campaignId}/units/{unitId}/sensors/{sensorId}/observations</text>
 </g>
 <!-- campaignIdUnitIdSensorIdGET&#45;&gt;campaignIdUnitIdSensorIdObservationsGET -->
-<g id="edge19" class="edge">
+<g id="edge20" class="edge">
 <title>campaignIdUnitIdSensorIdGET&#45;&gt;campaignIdUnitIdSensorIdObservationsGET</title>
-<path fill="none" stroke="black" d="M669.41,-504.41C640.28,-494.49 604.32,-481.82 575.43,-471.31"/>
-<polygon fill="black" stroke="black" points="576.52,-467.98 565.93,-467.83 574.12,-474.56 576.52,-467.98"/>
+<path fill="none" stroke="black" d="M1349.01,-719.7C1359.29,-710.8 1372.92,-699.82 1385.62,-690.2"/>
+<polygon fill="black" stroke="black" points="1387.86,-692.89 1393.78,-684.1 1383.67,-687.28 1387.86,-692.89"/>
 </g>
 <!-- sensorIdGET -->
 <g id="node19" class="node">
 <title>sensorIdGET</title>
-<ellipse fill="none" stroke="black" cx="1136.18" cy="-450" rx="86.38" ry="18"/>
-<text text-anchor="middle" x="1136.18" y="-446.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/sensors/{sensorId}</text>
+<ellipse fill="none" stroke="black" cx="1031.18" cy="-522" rx="86.38" ry="18"/>
+<text text-anchor="middle" x="1031.18" y="-518.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/sensors/{sensorId}</text>
 </g>
 <!-- campaignIdUnitIdSensorIdGET&#45;&gt;sensorIdGET -->
-<g id="edge16" class="edge">
+<g id="edge17" class="edge">
 <title>campaignIdUnitIdSensorIdGET&#45;&gt;sensorIdGET</title>
-<path fill="none" stroke="black" d="M815.23,-505.43C877.95,-494.91 962.55,-480.69 1037.18,-468 1045.12,-466.65 1053.42,-465.23 1061.7,-463.82"/>
-<polygon fill="black" stroke="black" points="1062.48,-467.23 1071.75,-462.1 1061.3,-460.33 1062.48,-467.23"/>
+<path fill="none" stroke="black" d="M1196.19,-723.28C1169.17,-715.2 1142.83,-702.83 1122.18,-684 1084.58,-649.72 1111.29,-619.06 1084.18,-576 1077.41,-565.25 1068.09,-554.98 1059.15,-546.4"/>
+<polygon fill="black" stroke="black" points="1061.52,-543.83 1051.8,-539.62 1056.78,-548.97 1061.52,-543.83"/>
 </g>
 <!-- phenomenonIdGET -->
 <g id="node22" class="node">
 <title>phenomenonIdGET</title>
-<ellipse fill="none" stroke="black" cx="860.18" cy="-378" rx="137.58" ry="18"/>
-<text text-anchor="middle" x="860.18" y="-374.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/phenomenons/{phenomenonId}</text>
+<ellipse fill="none" stroke="black" cx="919.18" cy="-666" rx="137.58" ry="18"/>
+<text text-anchor="middle" x="919.18" y="-662.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/phenomenons/{phenomenonId}</text>
 </g>
 <!-- campaignIdUnitIdSensorIdGET&#45;&gt;phenomenonIdGET -->
-<g id="edge18" class="edge">
+<g id="edge19" class="edge">
 <title>campaignIdUnitIdSensorIdGET&#45;&gt;phenomenonIdGET</title>
-<path fill="none" stroke="black" d="M787.2,-504.66C807.15,-496.66 827.44,-484.96 841.18,-468 855.21,-450.68 859.55,-425.42 860.61,-406.23"/>
-<polygon fill="black" stroke="black" points="864.12,-406.11 860.91,-396.01 857.12,-405.9 864.12,-406.11"/>
+<path fill="none" stroke="black" d="M1240.78,-721.38C1171.85,-709.72 1078.15,-693.88 1009.67,-682.3"/>
+<polygon fill="black" stroke="black" points="1010.2,-678.84 999.76,-680.62 1009.03,-685.74 1010.2,-678.84"/>
 </g>
 <!-- campaignIdUnitIdSensorIdObservationsGET&#45;&gt;campaignIdUnitIdSensorIdGET -->
-<g id="edge24" class="edge">
+<g id="edge25" class="edge">
 <title>campaignIdUnitIdSensorIdObservationsGET&#45;&gt;campaignIdUnitIdSensorIdGET</title>
-<path fill="none" stroke="black" d="M577.42,-467.75C606.54,-477.67 642.38,-490.3 671.15,-500.77"/>
-<polygon fill="black" stroke="black" points="670.02,-504.09 680.61,-504.23 672.42,-497.51 670.02,-504.09"/>
+<path fill="none" stroke="black" d="M1405.65,-684.03C1395.32,-693.01 1381.48,-704.15 1368.62,-713.89"/>
+<polygon fill="black" stroke="black" points="1366.28,-711.27 1360.36,-720.05 1370.47,-716.88 1366.28,-711.27"/>
 </g>
 <!-- campaignIdUnitIdSensorIdObservationsGET&#45;&gt;campaignIdUnitIdSensorIdObservationsGET -->
-<g id="edge25" class="edge">
+<g id="edge26" class="edge">
 <title>campaignIdUnitIdSensorIdObservationsGET&#45;&gt;campaignIdUnitIdSensorIdObservationsGET</title>
-<path fill="none" stroke="black" d="M716.22,-463.5C780.54,-463.52 832.05,-459.02 832.05,-450 832.05,-441.46 785.83,-436.97 726.29,-436.53"/>
-<polygon fill="black" stroke="black" points="726.24,-433.03 716.22,-436.5 726.21,-440.03 726.24,-433.03"/>
+<path fill="none" stroke="black" d="M1613.22,-679.5C1677.54,-679.52 1729.05,-675.02 1729.05,-666 1729.05,-657.46 1682.83,-652.97 1623.29,-652.53"/>
+<polygon fill="black" stroke="black" points="1623.24,-649.03 1613.22,-652.5 1623.21,-656.03 1623.24,-649.03"/>
 </g>
 <!-- unitsGET -->
 <g id="node13" class="node">
 <title>unitsGET</title>
-<ellipse fill="none" stroke="black" cx="1542.18" cy="-666" rx="31.7" ry="18"/>
-<text text-anchor="middle" x="1542.18" y="-662.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/units</text>
+<ellipse fill="none" stroke="black" cx="1985.18" cy="-882" rx="31.7" ry="18"/>
+<text text-anchor="middle" x="1985.18" y="-878.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/units</text>
 </g>
 <!-- unitsGET&#45;&gt;unitIdGET -->
-<g id="edge34" class="edge">
+<g id="edge35" class="edge">
 <title>unitsGET&#45;&gt;unitIdGET</title>
-<path fill="none" stroke="black" d="M1535.07,-648.41C1531.61,-640.34 1527.36,-630.43 1523.47,-621.35"/>
-<polygon fill="black" stroke="black" points="1526.6,-619.77 1519.44,-611.96 1520.17,-622.53 1526.6,-619.77"/>
+<path fill="none" stroke="black" d="M1998.49,-865.46C2006.01,-856.69 2015.58,-845.53 2024.1,-835.6"/>
+<polygon fill="black" stroke="black" points="2026.79,-837.83 2030.64,-827.96 2021.47,-833.28 2026.79,-837.83"/>
 </g>
 <!-- unitIdGET&#45;&gt;unitIdGET -->
-<g id="edge35" class="edge">
+<g id="edge36" class="edge">
 <title>unitIdGET&#45;&gt;unitIdGET</title>
-<path fill="none" stroke="black" d="M1554.43,-607.2C1574.39,-608.74 1591.92,-604.34 1591.92,-594 1591.92,-585.44 1579.9,-580.95 1564.43,-580.53"/>
-<polygon fill="black" stroke="black" points="1564.34,-577.03 1554.43,-580.8 1564.52,-584.03 1564.34,-577.03"/>
+<path fill="none" stroke="black" d="M2087.43,-823.2C2107.39,-824.74 2124.92,-820.34 2124.92,-810 2124.92,-801.44 2112.9,-796.95 2097.43,-796.53"/>
+<polygon fill="black" stroke="black" points="2097.34,-793.03 2087.43,-796.8 2097.52,-800.03 2097.34,-793.03"/>
 </g>
 <!-- unitIdSensorsGET -->
 <g id="node15" class="node">
 <title>unitIdSensorsGET</title>
-<ellipse fill="none" stroke="black" cx="1110.18" cy="-522" rx="94.78" ry="18"/>
-<text text-anchor="middle" x="1110.18" y="-518.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/units/{unitId}/sensors</text>
+<ellipse fill="none" stroke="black" cx="1880.18" cy="-666" rx="94.78" ry="18"/>
+<text text-anchor="middle" x="1880.18" y="-662.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/units/{unitId}/sensors</text>
 </g>
 <!-- unitIdGET&#45;&gt;unitIdSensorsGET -->
-<g id="edge36" class="edge">
+<g id="edge37" class="edge">
 <title>unitIdGET&#45;&gt;unitIdSensorsGET</title>
-<path fill="none" stroke="black" d="M1466.84,-581.59C1458.34,-579.61 1449.51,-577.66 1441.18,-576 1340.99,-556.09 1315.03,-556.24 1214.18,-540 1206.22,-538.72 1197.91,-537.37 1189.62,-536.03"/>
-<polygon fill="black" stroke="black" points="1189.97,-532.54 1179.54,-534.38 1188.85,-539.45 1189.97,-532.54"/>
+<path fill="none" stroke="black" d="M1989.54,-802.1C1958.63,-795.38 1921.93,-782.13 1899.18,-756 1884.47,-739.11 1880.22,-713.64 1879.36,-694.27"/>
+<polygon fill="black" stroke="black" points="1882.86,-694.16 1879.19,-684.22 1875.86,-694.28 1882.86,-694.16"/>
 </g>
 <!-- unitIdCampaignsGET -->
 <g id="node16" class="node">
 <title>unitIdCampaignsGET</title>
-<ellipse fill="none" stroke="black" cx="1772.18" cy="-522" rx="107.48" ry="18"/>
-<text text-anchor="middle" x="1772.18" y="-518.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/units/{unitId}/campaigns</text>
+<ellipse fill="none" stroke="black" cx="2468.18" cy="-738" rx="107.48" ry="18"/>
+<text text-anchor="middle" x="2468.18" y="-734.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/units/{unitId}/campaigns</text>
 </g>
 <!-- unitIdGET&#45;&gt;unitIdCampaignsGET -->
-<g id="edge37" class="edge">
+<g id="edge38" class="edge">
 <title>unitIdGET&#45;&gt;unitIdCampaignsGET</title>
-<path fill="none" stroke="black" d="M1555.99,-581.2C1597.6,-570 1660.66,-553.02 1707.96,-540.29"/>
-<polygon fill="black" stroke="black" points="1709.05,-543.62 1717.79,-537.64 1707.23,-536.86 1709.05,-543.62"/>
+<path fill="none" stroke="black" d="M2095.88,-799.65C2109.9,-797.11 2125.12,-794.4 2139.18,-792 2221.63,-777.9 2315.88,-762.83 2382.31,-752.37"/>
+<polygon fill="black" stroke="black" points="2382.98,-755.81 2392.32,-750.8 2381.9,-748.89 2382.98,-755.81"/>
 </g>
 <!-- unitIdDriversGET -->
 <g id="node17" class="node">
 <title>unitIdDriversGET</title>
-<ellipse fill="none" stroke="black" cx="1458.18" cy="-522" rx="90.98" ry="18"/>
-<text text-anchor="middle" x="1458.18" y="-518.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/units/{unitId}/drivers</text>
+<ellipse fill="none" stroke="black" cx="2097.18" cy="-738" rx="90.98" ry="18"/>
+<text text-anchor="middle" x="2097.18" y="-734.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/units/{unitId}/drivers</text>
 </g>
 <!-- unitIdGET&#45;&gt;unitIdDriversGET -->
-<g id="edge38" class="edge">
+<g id="edge39" class="edge">
 <title>unitIdGET&#45;&gt;unitIdDriversGET</title>
-<path fill="none" stroke="black" d="M1499.11,-576.05C1492.7,-567.75 1484.86,-557.58 1477.75,-548.38"/>
-<polygon fill="black" stroke="black" points="1480.38,-546.06 1471.5,-540.28 1474.84,-550.33 1480.38,-546.06"/>
+<path fill="none" stroke="black" d="M2057.77,-792.05C2063.93,-783.75 2071.49,-773.58 2078.33,-764.38"/>
+<polygon fill="black" stroke="black" points="2081.19,-766.39 2084.34,-756.28 2075.57,-762.22 2081.19,-766.39"/>
 </g>
 <!-- unitIdSensorsGET&#45;&gt;sensorIdGET -->
-<g id="edge39" class="edge">
+<g id="edge40" class="edge">
 <title>unitIdSensorsGET&#45;&gt;sensorIdGET</title>
-<path fill="none" stroke="black" d="M1116.6,-503.7C1119.53,-495.81 1123.06,-486.3 1126.32,-477.55"/>
-<polygon fill="black" stroke="black" points="1129.62,-478.7 1129.82,-468.1 1123.06,-476.26 1129.62,-478.7"/>
+<path fill="none" stroke="black" d="M1811.42,-653.5C1654.14,-627.19 1269.19,-562.81 1106.13,-535.54"/>
+<polygon fill="black" stroke="black" points="1106.62,-532.07 1096.18,-533.87 1105.47,-538.97 1106.62,-532.07"/>
 </g>
 <!-- unitIdCampaignsGET&#45;&gt;campaignIdGET -->
-<g id="edge40" class="edge">
+<g id="edge41" class="edge">
 <title>unitIdCampaignsGET&#45;&gt;campaignIdGET</title>
-<path fill="none" stroke="black" d="M1767.18,-540.08C1753.07,-584.78 1708.38,-705.28 1623.18,-756 1571.75,-786.62 1202.3,-800.81 1007.98,-806.22"/>
-<polygon fill="black" stroke="black" points="1007.62,-802.73 997.72,-806.5 1007.81,-809.72 1007.62,-802.73"/>
+<path fill="none" stroke="black" d="M2421,-754.25C2351.76,-779.49 2225.08,-837.24 2172.18,-936 2164.62,-950.1 2164.34,-958.05 2172.18,-972 2179.92,-985.79 2192.87,-996.41 2206.44,-1004.39"/>
+<polygon fill="black" stroke="black" points="2204.95,-1007.56 2215.4,-1009.27 2208.29,-1001.42 2204.95,-1007.56"/>
 </g>
 <!-- driverIdGET -->
 <g id="node25" class="node">
 <title>driverIdGET</title>
-<ellipse fill="none" stroke="black" cx="1762.18" cy="-450" rx="77.19" ry="18"/>
-<text text-anchor="middle" x="1762.18" y="-446.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/drivers/{driverId}</text>
+<ellipse fill="none" stroke="black" cx="2083.18" cy="-666" rx="77.19" ry="18"/>
+<text text-anchor="middle" x="2083.18" y="-662.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/drivers/{driverId}</text>
 </g>
 <!-- unitIdDriversGET&#45;&gt;driverIdGET -->
-<g id="edge41" class="edge">
+<g id="edge42" class="edge">
 <title>unitIdDriversGET&#45;&gt;driverIdGET</title>
-<path fill="none" stroke="black" d="M1515,-507.92C1567.42,-495.85 1645.11,-477.96 1699.35,-465.47"/>
-<polygon fill="black" stroke="black" points="1700.24,-468.85 1709.2,-463.2 1698.66,-462.03 1700.24,-468.85"/>
+<path fill="none" stroke="black" d="M2093.72,-719.7C2092.17,-711.98 2090.32,-702.71 2088.6,-694.11"/>
+<polygon fill="black" stroke="black" points="2091.99,-693.22 2086.6,-684.1 2085.13,-694.6 2091.99,-693.22"/>
 </g>
 <!-- sensorsGET -->
 <g id="node18" class="node">
 <title>sensorsGET</title>
-<ellipse fill="none" stroke="black" cx="1267.18" cy="-522" rx="44.39" ry="18"/>
-<text text-anchor="middle" x="1267.18" y="-518.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/sensors</text>
+<ellipse fill="none" stroke="black" cx="1031.18" cy="-594" rx="44.39" ry="18"/>
+<text text-anchor="middle" x="1031.18" y="-590.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/sensors</text>
 </g>
 <!-- sensorsGET&#45;&gt;sensorIdGET -->
-<g id="edge47" class="edge">
+<g id="edge49" class="edge">
 <title>sensorsGET&#45;&gt;sensorIdGET</title>
-<path fill="none" stroke="black" d="M1241.3,-507.17C1222.38,-497.06 1196.39,-483.17 1174.99,-471.74"/>
-<polygon fill="black" stroke="black" points="1176.6,-468.63 1166.13,-467.01 1173.3,-474.81 1176.6,-468.63"/>
+<path fill="none" stroke="black" d="M1031.18,-575.7C1031.18,-567.98 1031.18,-558.71 1031.18,-550.11"/>
+<polygon fill="black" stroke="black" points="1034.68,-550.1 1031.18,-540.1 1027.68,-550.1 1034.68,-550.1"/>
 </g>
 <!-- sensorIdGET&#45;&gt;sensorIdGET -->
-<g id="edge48" class="edge">
+<g id="edge50" class="edge">
 <title>sensorIdGET&#45;&gt;sensorIdGET</title>
-<path fill="none" stroke="black" d="M1194.44,-463.35C1219.35,-464.4 1240.62,-459.95 1240.62,-450 1240.62,-441.45 1224.91,-436.96 1204.68,-436.53"/>
-<polygon fill="black" stroke="black" points="1204.4,-433.04 1194.44,-436.65 1204.48,-440.04 1204.4,-433.04"/>
+<path fill="none" stroke="black" d="M1089.44,-535.35C1114.35,-536.4 1135.62,-531.95 1135.62,-522 1135.62,-513.45 1119.91,-508.96 1099.68,-508.53"/>
+<polygon fill="black" stroke="black" points="1099.4,-505.04 1089.44,-508.65 1099.48,-512.04 1099.4,-505.04"/>
 </g>
 <!-- sensorIdUnitsGET -->
 <g id="node20" class="node">
 <title>sensorIdUnitsGET</title>
-<ellipse fill="none" stroke="black" cx="1141.18" cy="-378" rx="107.48" ry="18"/>
-<text text-anchor="middle" x="1141.18" y="-374.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/sensors/{sensorId}/units</text>
+<ellipse fill="none" stroke="black" cx="1031.18" cy="-450" rx="107.48" ry="18"/>
+<text text-anchor="middle" x="1031.18" y="-446.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/sensors/{sensorId}/units</text>
 </g>
 <!-- sensorIdGET&#45;&gt;sensorIdUnitsGET -->
-<g id="edge49" class="edge">
+<g id="edge51" class="edge">
 <title>sensorIdGET&#45;&gt;sensorIdUnitsGET</title>
-<path fill="none" stroke="black" d="M1137.41,-431.7C1137.96,-423.98 1138.63,-414.71 1139.24,-406.11"/>
-<polygon fill="black" stroke="black" points="1142.73,-406.33 1139.96,-396.1 1135.75,-405.83 1142.73,-406.33"/>
+<path fill="none" stroke="black" d="M1031.18,-503.7C1031.18,-495.98 1031.18,-486.71 1031.18,-478.11"/>
+<polygon fill="black" stroke="black" points="1034.68,-478.1 1031.18,-468.1 1027.68,-478.1 1034.68,-478.1"/>
 </g>
 <!-- sensorIdGET&#45;&gt;phenomenonIdGET -->
-<g id="edge50" class="edge">
+<g id="edge52" class="edge">
 <title>sensorIdGET&#45;&gt;phenomenonIdGET</title>
-<path fill="none" stroke="black" d="M1083.61,-435.67C1040.1,-424.63 977.9,-408.86 930.18,-396.75"/>
-<polygon fill="black" stroke="black" points="930.78,-393.29 920.22,-394.23 929.05,-400.08 930.78,-393.29"/>
+<path fill="none" stroke="black" d="M1012.58,-539.83C1001.97,-549.86 988.72,-563.11 978.18,-576 961.71,-596.13 945.58,-620.93 934.35,-639.23"/>
+<polygon fill="black" stroke="black" points="931.29,-637.53 929.1,-647.9 937.28,-641.16 931.29,-637.53"/>
 </g>
 <!-- phenomenonsGET -->
 <g id="node21" class="node">
 <title>phenomenonsGET</title>
-<ellipse fill="none" stroke="black" cx="958.18" cy="-450" rx="69.59" ry="18"/>
-<text text-anchor="middle" x="958.18" y="-446.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/phenomenons</text>
+<ellipse fill="none" stroke="black" cx="919.18" cy="-738" rx="69.59" ry="18"/>
+<text text-anchor="middle" x="919.18" y="-734.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/phenomenons</text>
 </g>
 <!-- phenomenonsGET&#45;&gt;phenomenonIdGET -->
-<g id="edge51" class="edge">
+<g id="edge53" class="edge">
 <title>phenomenonsGET&#45;&gt;phenomenonIdGET</title>
-<path fill="none" stroke="black" d="M935.45,-432.76C922.62,-423.6 906.38,-412 892.27,-401.92"/>
-<polygon fill="black" stroke="black" points="893.98,-398.84 883.81,-395.88 889.91,-404.54 893.98,-398.84"/>
+<path fill="none" stroke="black" d="M919.18,-719.7C919.18,-711.98 919.18,-702.71 919.18,-694.11"/>
+<polygon fill="black" stroke="black" points="922.68,-694.1 919.18,-684.1 915.68,-694.1 922.68,-694.1"/>
 </g>
 <!-- phenomenonIdGET&#45;&gt;phenomenonIdGET -->
-<g id="edge52" class="edge">
+<g id="edge54" class="edge">
 <title>phenomenonIdGET&#45;&gt;phenomenonIdGET</title>
-<path fill="none" stroke="black" d="M951.78,-391.45C986.87,-392.01 1015.97,-387.53 1015.97,-378 1015.97,-369.4 992.27,-364.91 961.86,-364.53"/>
-<polygon fill="black" stroke="black" points="961.77,-361.03 951.78,-364.55 961.78,-368.03 961.77,-361.03"/>
+<path fill="none" stroke="black" d="M1010.78,-679.45C1045.87,-680.01 1074.97,-675.53 1074.97,-666 1074.97,-657.4 1051.27,-652.91 1020.86,-652.53"/>
+<polygon fill="black" stroke="black" points="1020.77,-649.03 1010.78,-652.55 1020.78,-656.03 1020.77,-649.03"/>
 </g>
 <!-- phenomenonIdSensorsGET -->
 <g id="node23" class="node">
 <title>phenomenonIdSensorsGET</title>
-<ellipse fill="none" stroke="black" cx="860.18" cy="-306" rx="170.87" ry="18"/>
-<text text-anchor="middle" x="860.18" y="-302.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/phenomenons/{phenomenonId}/sensors</text>
+<ellipse fill="none" stroke="black" cx="760.18" cy="-594" rx="170.87" ry="18"/>
+<text text-anchor="middle" x="760.18" y="-590.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/phenomenons/{phenomenonId}/sensors</text>
 </g>
 <!-- phenomenonIdGET&#45;&gt;phenomenonIdSensorsGET -->
-<g id="edge53" class="edge">
+<g id="edge55" class="edge">
 <title>phenomenonIdGET&#45;&gt;phenomenonIdSensorsGET</title>
-<path fill="none" stroke="black" d="M854.26,-359.7C853.47,-351.98 853.24,-342.71 853.58,-334.11"/>
-<polygon fill="black" stroke="black" points="857.08,-334.32 854.28,-324.1 850.09,-333.84 857.08,-334.32"/>
+<path fill="none" stroke="black" d="M881.9,-648.59C859.62,-638.78 831.1,-626.22 807.27,-615.73"/>
+<polygon fill="black" stroke="black" points="808.61,-612.5 798.05,-611.67 805.79,-618.91 808.61,-612.5"/>
 </g>
 <!-- phenomenonIdSensorsGET&#45;&gt;sensorIdGET -->
-<g id="edge55" class="edge">
+<g id="edge56" class="edge">
 <title>phenomenonIdSensorsGET&#45;&gt;sensorIdGET</title>
-<path fill="none" stroke="black" d="M998.63,-316.6C1106.21,-325.51 1239.97,-340.35 1257.18,-360 1267.72,-372.04 1265.93,-382.61 1257.18,-396 1245.53,-413.82 1226.42,-425.68 1206.8,-433.57"/>
-<polygon fill="black" stroke="black" points="1205.32,-430.38 1197.15,-437.13 1207.75,-436.95 1205.32,-430.38"/>
-</g>
-<!-- phenomenonIdSensorsGET&#45;&gt;phenomenonIdGET -->
-<g id="edge54" class="edge">
-<title>phenomenonIdSensorsGET&#45;&gt;phenomenonIdGET</title>
-<path fill="none" stroke="black" d="M866.07,-324.1C866.88,-331.79 867.11,-341.05 866.78,-349.67"/>
-<polygon fill="black" stroke="black" points="863.29,-349.48 866.09,-359.7 870.27,-349.96 863.29,-349.48"/>
+<path fill="none" stroke="black" d="M821.67,-577.12C865.81,-565.72 925.06,-550.41 969.28,-538.99"/>
+<polygon fill="black" stroke="black" points="970.24,-542.36 979.05,-536.47 968.49,-535.58 970.24,-542.36"/>
 </g>
 <!-- driversGET -->
 <g id="node24" class="node">
 <title>driversGET</title>
-<ellipse fill="none" stroke="black" cx="1607.18" cy="-522" rx="39.79" ry="18"/>
-<text text-anchor="middle" x="1607.18" y="-518.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/drivers</text>
+<ellipse fill="none" stroke="black" cx="1948.18" cy="-738" rx="39.79" ry="18"/>
+<text text-anchor="middle" x="1948.18" y="-734.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/drivers</text>
 </g>
 <!-- driversGET&#45;&gt;driverIdGET -->
-<g id="edge56" class="edge">
+<g id="edge57" class="edge">
 <title>driversGET&#45;&gt;driverIdGET</title>
-<path fill="none" stroke="black" d="M1634.18,-508.81C1657.61,-498.22 1691.95,-482.72 1719.16,-470.43"/>
-<polygon fill="black" stroke="black" points="1720.79,-473.53 1728.46,-466.23 1717.91,-467.15 1720.79,-473.53"/>
+<path fill="none" stroke="black" d="M1973.25,-724C1993.09,-713.71 2021.15,-699.16 2043.95,-687.34"/>
+<polygon fill="black" stroke="black" points="2045.78,-690.34 2053.04,-682.62 2042.56,-684.12 2045.78,-690.34"/>
 </g>
 <!-- driverIdGET&#45;&gt;driverIdGET -->
-<g id="edge57" class="edge">
+<g id="edge58" class="edge">
 <title>driverIdGET&#45;&gt;driverIdGET</title>
-<path fill="none" stroke="black" d="M1814.64,-463.31C1837.69,-464.49 1857.52,-460.05 1857.52,-450 1857.52,-441.48 1843.27,-436.99 1824.89,-436.54"/>
-<polygon fill="black" stroke="black" points="1824.59,-433.04 1814.64,-436.69 1824.69,-440.04 1824.59,-433.04"/>
+<path fill="none" stroke="black" d="M2135.64,-679.31C2158.69,-680.49 2178.52,-676.05 2178.52,-666 2178.52,-657.48 2164.27,-652.99 2145.89,-652.54"/>
+<polygon fill="black" stroke="black" points="2145.59,-649.04 2135.64,-652.69 2145.69,-656.04 2145.59,-649.04"/>
 </g>
 <!-- driverIdUnitsGET -->
 <g id="node26" class="node">
 <title>driverIdUnitsGET</title>
-<ellipse fill="none" stroke="black" cx="1762.18" cy="-378" rx="98.58" ry="18"/>
-<text text-anchor="middle" x="1762.18" y="-374.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/drivers/{driverId}/units</text>
+<ellipse fill="none" stroke="black" cx="1840.18" cy="-594" rx="98.58" ry="18"/>
+<text text-anchor="middle" x="1840.18" y="-590.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/drivers/{driverId}/units</text>
 </g>
 <!-- driverIdGET&#45;&gt;driverIdUnitsGET -->
-<g id="edge58" class="edge">
+<g id="edge59" class="edge">
 <title>driverIdGET&#45;&gt;driverIdUnitsGET</title>
-<path fill="none" stroke="black" d="M1762.18,-431.7C1762.18,-423.98 1762.18,-414.71 1762.18,-406.11"/>
-<polygon fill="black" stroke="black" points="1765.68,-406.1 1762.18,-396.1 1758.68,-406.1 1765.68,-406.1"/>
+<path fill="none" stroke="black" d="M2036.61,-651.59C1997.95,-640.45 1942.69,-624.53 1900.62,-612.41"/>
+<polygon fill="black" stroke="black" points="1901.36,-608.98 1890.78,-609.58 1899.42,-615.71 1901.36,-608.98"/>
 </g>
 <!-- driverIdActionsGET -->
 <g id="node29" class="node">
 <title>driverIdActionsGET</title>
-<ellipse fill="none" stroke="black" cx="1537.18" cy="-378" rx="107.78" ry="18"/>
-<text text-anchor="middle" x="1537.18" y="-374.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/drivers/{driverId}/actions</text>
+<ellipse fill="none" stroke="black" cx="2083.18" cy="-594" rx="107.78" ry="18"/>
+<text text-anchor="middle" x="2083.18" y="-590.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/drivers/{driverId}/actions</text>
 </g>
 <!-- driverIdGET&#45;&gt;driverIdActionsGET -->
-<g id="edge59" class="edge">
+<g id="edge60" class="edge">
 <title>driverIdGET&#45;&gt;driverIdActionsGET</title>
-<path fill="none" stroke="black" d="M1717.72,-435.17C1682.81,-424.31 1633.88,-409.09 1595.77,-397.23"/>
-<polygon fill="black" stroke="black" points="1596.42,-393.76 1585.83,-394.14 1594.34,-400.45 1596.42,-393.76"/>
+<path fill="none" stroke="black" d="M2083.18,-647.7C2083.18,-639.98 2083.18,-630.71 2083.18,-622.11"/>
+<polygon fill="black" stroke="black" points="2086.68,-622.1 2083.18,-612.1 2079.68,-622.1 2086.68,-622.1"/>
 </g>
 <!-- driverIdUnitsGET&#45;&gt;driverIdUnitsGET -->
-<g id="edge60" class="edge">
+<g id="edge61" class="edge">
 <title>driverIdUnitsGET&#45;&gt;driverIdUnitsGET</title>
-<path fill="none" stroke="black" d="M1828.51,-391.39C1855.85,-392.27 1878.97,-387.81 1878.97,-378 1878.97,-369.42 1861.27,-364.93 1838.53,-364.53"/>
-<polygon fill="black" stroke="black" points="1838.48,-361.03 1828.51,-364.61 1838.53,-368.03 1838.48,-361.03"/>
+<path fill="none" stroke="black" d="M1906.51,-607.39C1933.85,-608.27 1956.97,-603.81 1956.97,-594 1956.97,-585.42 1939.27,-580.93 1916.53,-580.53"/>
+<polygon fill="black" stroke="black" points="1916.48,-577.03 1906.51,-580.61 1916.53,-584.03 1916.48,-577.03"/>
 </g>
 <!-- driverIdUnitIdGET -->
 <g id="node27" class="node">
 <title>driverIdUnitIdGET</title>
-<ellipse fill="none" stroke="black" cx="2102.18" cy="-666" rx="128.88" ry="18"/>
-<text text-anchor="middle" x="2102.18" y="-662.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/drivers/{driverId}/units/{unitId}</text>
+<ellipse fill="none" stroke="black" cx="2214.18" cy="-162" rx="128.88" ry="18"/>
+<text text-anchor="middle" x="2214.18" y="-158.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/drivers/{driverId}/units/{unitId}</text>
 </g>
 <!-- driverIdUnitIdGET&#45;&gt;unitIdGET -->
-<g id="edge62" class="edge">
+<g id="edge63" class="edge">
 <title>driverIdUnitIdGET&#45;&gt;unitIdGET</title>
-<path fill="none" stroke="black" d="M2007.38,-653.75C1887.28,-639.5 1683.33,-615.31 1578.6,-602.88"/>
-<polygon fill="black" stroke="black" points="1578.81,-599.38 1568.47,-601.68 1577.99,-606.33 1578.81,-599.38"/>
+<path fill="none" stroke="black" d="M2242.38,-179.75C2277.57,-203.02 2333.18,-248.71 2333.18,-305 2333.18,-667 2333.18,-667 2333.18,-667 2333.18,-763.83 2200.6,-794.63 2115.35,-804.43"/>
+<polygon fill="black" stroke="black" points="2114.77,-800.97 2105.2,-805.52 2115.52,-807.93 2114.77,-800.97"/>
 </g>
 <!-- driverIdUnitIdGET&#45;&gt;driverIdUnitIdGET -->
-<g id="edge61" class="edge">
+<g id="edge62" class="edge">
 <title>driverIdUnitIdGET&#45;&gt;driverIdUnitIdGET</title>
-<path fill="none" stroke="black" d="M2188.06,-679.45C2221.27,-680.04 2248.87,-675.56 2248.87,-666 2248.87,-657.45 2226.78,-652.96 2198.35,-652.53"/>
-<polygon fill="black" stroke="black" points="2198.05,-649.03 2188.06,-652.55 2198.07,-656.03 2198.05,-649.03"/>
+<path fill="none" stroke="black" d="M2300.06,-175.45C2333.27,-176.04 2360.87,-171.56 2360.87,-162 2360.87,-153.45 2338.78,-148.96 2310.35,-148.53"/>
+<polygon fill="black" stroke="black" points="2310.05,-145.03 2300.06,-148.55 2310.07,-152.03 2310.05,-145.03"/>
 </g>
 <!-- driverIdUnitIdActionsGET -->
 <g id="node28" class="node">
 <title>driverIdUnitIdActionsGET</title>
-<ellipse fill="none" stroke="black" cx="2230.18" cy="-594" rx="159.47" ry="18"/>
-<text text-anchor="middle" x="2230.18" y="-590.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/drivers/{driverId}/units/{unitId}/actions</text>
+<ellipse fill="none" stroke="black" cx="2095.18" cy="-90" rx="159.47" ry="18"/>
+<text text-anchor="middle" x="2095.18" y="-86.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/drivers/{driverId}/units/{unitId}/actions</text>
 </g>
 <!-- driverIdUnitIdGET&#45;&gt;driverIdUnitIdActionsGET -->
-<g id="edge63" class="edge">
+<g id="edge64" class="edge">
 <title>driverIdUnitIdGET&#45;&gt;driverIdUnitIdActionsGET</title>
-<path fill="none" stroke="black" d="M2132.51,-648.41C2149.92,-638.89 2171.97,-626.83 2190.72,-616.58"/>
-<polygon fill="black" stroke="black" points="2192.52,-619.58 2199.62,-611.71 2189.16,-613.44 2192.52,-619.58"/>
+<path fill="none" stroke="black" d="M2185.98,-144.41C2169.94,-134.98 2149.66,-123.05 2132.34,-112.86"/>
+<polygon fill="black" stroke="black" points="2133.98,-109.77 2123.59,-107.71 2130.43,-115.8 2133.98,-109.77"/>
 </g>
 <!-- driverIdUnitIdActionIdGET -->
 <g id="node33" class="node">
 <title>driverIdUnitIdActionIdGET</title>
-<ellipse fill="none" stroke="black" cx="2171.18" cy="-522" rx="198.47" ry="18"/>
-<text text-anchor="middle" x="2171.18" y="-518.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/drivers/{driverId}/units/{unitId}/actions/{actionId}</text>
+<ellipse fill="none" stroke="black" cx="2188.18" cy="-18" rx="198.47" ry="18"/>
+<text text-anchor="middle" x="2188.18" y="-14.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/drivers/{driverId}/units/{unitId}/actions/{actionId}</text>
 </g>
 <!-- driverIdUnitIdActionsGET&#45;&gt;driverIdUnitIdActionIdGET -->
-<g id="edge65" class="edge">
+<g id="edge66" class="edge">
 <title>driverIdUnitIdActionsGET&#45;&gt;driverIdUnitIdActionIdGET</title>
-<path fill="none" stroke="black" d="M2215.9,-576.05C2208.83,-567.67 2200.15,-557.38 2192.33,-548.1"/>
-<polygon fill="black" stroke="black" points="2194.86,-545.67 2185.74,-540.28 2189.51,-550.18 2194.86,-545.67"/>
+<path fill="none" stroke="black" d="M2117.69,-72.05C2129.6,-63.09 2144.39,-51.96 2157.35,-42.2"/>
+<polygon fill="black" stroke="black" points="2159.67,-44.84 2165.55,-36.03 2155.46,-39.25 2159.67,-44.84"/>
 </g>
 <!-- driverIdActionIdGET -->
 <g id="node30" class="node">
 <title>driverIdActionIdGET</title>
-<ellipse fill="none" stroke="black" cx="1907.18" cy="-306" rx="147.57" ry="18"/>
-<text text-anchor="middle" x="1907.18" y="-302.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/drivers/{driverId}/actions/{actionId}</text>
+<ellipse fill="none" stroke="black" cx="1985.18" cy="-522" rx="147.57" ry="18"/>
+<text text-anchor="middle" x="1985.18" y="-518.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/drivers/{driverId}/actions/{actionId}</text>
 </g>
 <!-- driverIdActionsGET&#45;&gt;driverIdActionIdGET -->
-<g id="edge64" class="edge">
+<g id="edge65" class="edge">
 <title>driverIdActionsGET&#45;&gt;driverIdActionIdGET</title>
-<path fill="none" stroke="black" d="M1605.9,-364C1666.63,-352.51 1755.47,-335.7 1820.95,-323.31"/>
-<polygon fill="black" stroke="black" points="1821.67,-326.74 1830.84,-321.44 1820.37,-319.86 1821.67,-326.74"/>
+<path fill="none" stroke="black" d="M2059.95,-576.41C2047.2,-567.3 2031.2,-555.87 2017.27,-545.92"/>
+<polygon fill="black" stroke="black" points="2019.09,-542.92 2008.92,-539.96 2015.02,-548.62 2019.09,-542.92"/>
 </g>
 <!-- driverIdActionIdGET&#45;&gt;driverIdGET -->
-<g id="edge67" class="edge">
+<g id="edge68" class="edge">
 <title>driverIdActionIdGET&#45;&gt;driverIdGET</title>
-<path fill="none" stroke="black" d="M1907.99,-324.07C1908,-343.61 1905.1,-375.55 1888.18,-396 1873.02,-414.31 1850.43,-426.41 1828.55,-434.35"/>
-<polygon fill="black" stroke="black" points="1827.26,-431.09 1818.9,-437.61 1829.5,-437.72 1827.26,-431.09"/>
+<path fill="none" stroke="black" d="M1854.44,-530.41C1772.02,-539.98 1688.7,-561.87 1732.18,-612 1769.24,-654.73 1928.3,-639.21 1984.18,-648 1993.64,-649.49 2003.6,-651.12 2013.4,-652.77"/>
+<polygon fill="black" stroke="black" points="2013.1,-656.26 2023.54,-654.48 2014.27,-649.36 2013.1,-656.26"/>
 </g>
 <!-- driverIdActionIdGET&#45;&gt;driverIdActionIdGET -->
-<g id="edge66" class="edge">
+<g id="edge67" class="edge">
 <title>driverIdActionIdGET&#45;&gt;driverIdActionIdGET</title>
-<path fill="none" stroke="black" d="M2005.33,-319.47C2042.25,-319.95 2072.71,-315.46 2072.71,-306 2072.71,-297.43 2047.7,-292.94 2015.53,-292.53"/>
-<polygon fill="black" stroke="black" points="2015.33,-289.03 2005.33,-292.53 2015.34,-296.03 2015.33,-289.03"/>
+<path fill="none" stroke="black" d="M2083.33,-535.47C2120.25,-535.95 2150.71,-531.46 2150.71,-522 2150.71,-513.43 2125.7,-508.94 2093.53,-508.53"/>
+<polygon fill="black" stroke="black" points="2093.33,-505.03 2083.33,-508.53 2093.34,-512.03 2093.33,-505.03"/>
 </g>
 <!-- driverIdActionIdUnitsGET -->
 <g id="node31" class="node">
 <title>driverIdActionIdUnitsGET</title>
-<ellipse fill="none" stroke="black" cx="1941.18" cy="-234" rx="168.17" ry="18"/>
-<text text-anchor="middle" x="1941.18" y="-230.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/drivers/{driverId}/actions/{actionId}/units</text>
+<ellipse fill="none" stroke="black" cx="1985.18" cy="-450" rx="168.17" ry="18"/>
+<text text-anchor="middle" x="1985.18" y="-446.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/drivers/{driverId}/actions/{actionId}/units</text>
 </g>
 <!-- driverIdActionIdGET&#45;&gt;driverIdActionIdUnitsGET -->
-<g id="edge68" class="edge">
+<g id="edge69" class="edge">
 <title>driverIdActionIdGET&#45;&gt;driverIdActionIdUnitsGET</title>
-<path fill="none" stroke="black" d="M1915.58,-287.7C1919.45,-279.73 1924.13,-270.1 1928.42,-261.26"/>
-<polygon fill="black" stroke="black" points="1931.65,-262.63 1932.87,-252.1 1925.35,-259.57 1931.65,-262.63"/>
+<path fill="none" stroke="black" d="M1985.18,-503.7C1985.18,-495.98 1985.18,-486.71 1985.18,-478.11"/>
+<polygon fill="black" stroke="black" points="1988.68,-478.1 1985.18,-468.1 1981.68,-478.1 1988.68,-478.1"/>
 </g>
 <!-- driverIdActionIdUnitIdGET -->
 <g id="node32" class="node">
 <title>driverIdActionIdUnitIdGET</title>
-<ellipse fill="none" stroke="black" cx="1843.18" cy="-162" rx="198.47" ry="18"/>
-<text text-anchor="middle" x="1843.18" y="-158.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/drivers/{driverId}/actions/{actionId}/units/{unitId}</text>
+<ellipse fill="none" stroke="black" cx="1926.18" cy="-378" rx="198.47" ry="18"/>
+<text text-anchor="middle" x="1926.18" y="-374.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/drivers/{driverId}/actions/{actionId}/units/{unitId}</text>
 </g>
 <!-- driverIdActionIdUnitsGET&#45;&gt;driverIdActionIdUnitIdGET -->
-<g id="edge42" class="edge">
+<g id="edge43" class="edge">
 <title>driverIdActionIdUnitsGET&#45;&gt;driverIdActionIdUnitIdGET</title>
-<path fill="none" stroke="black" d="M1917.45,-216.05C1904.79,-207.01 1889.02,-195.74 1875.27,-185.93"/>
-<polygon fill="black" stroke="black" points="1877.19,-183 1867.02,-180.03 1873.12,-188.69 1877.19,-183"/>
+<path fill="none" stroke="black" d="M1970.9,-432.05C1963.83,-423.67 1955.15,-413.38 1947.33,-404.1"/>
+<polygon fill="black" stroke="black" points="1949.86,-401.67 1940.74,-396.28 1944.51,-406.18 1949.86,-401.67"/>
 </g>
 <!-- driverIdActionIdUnitIdGET&#45;&gt;unitIdGET -->
-<g id="edge46" class="edge">
+<g id="edge47" class="edge">
 <title>driverIdActionIdUnitIdGET&#45;&gt;unitIdGET</title>
-<path fill="none" stroke="black" d="M1704.12,-174.88C1554.94,-191.57 1339.18,-228.87 1339.18,-305 1339.18,-451 1339.18,-451 1339.18,-451 1339.18,-491.45 1331.36,-509.72 1358.18,-540 1371.43,-554.96 1417.01,-569.56 1455.17,-579.64"/>
-<polygon fill="black" stroke="black" points="1454.63,-583.11 1465.19,-582.23 1456.38,-576.34 1454.63,-583.11"/>
+<path fill="none" stroke="black" d="M2053.64,-391.82C2090.59,-399.6 2129.7,-412.06 2162.18,-432 2241.27,-480.56 2295.18,-500.19 2295.18,-593 2295.18,-667 2295.18,-667 2295.18,-667 2295.18,-751.74 2185.24,-786.54 2111.17,-800.36"/>
+<polygon fill="black" stroke="black" points="2110.2,-796.98 2100.97,-802.18 2111.43,-803.87 2110.2,-796.98"/>
 </g>
 <!-- driverIdActionIdUnitIdGET&#45;&gt;driverIdActionIdGET -->
-<g id="edge44" class="edge">
+<g id="edge45" class="edge">
 <title>driverIdActionIdUnitIdGET&#45;&gt;driverIdActionIdGET</title>
-<path fill="none" stroke="black" d="M1803.33,-179.64C1788.23,-188.15 1772.65,-200.12 1764.18,-216 1756.65,-230.12 1755.04,-238.87 1764.18,-252 1775.63,-268.45 1792.81,-279.81 1811.34,-287.65"/>
-<polygon fill="black" stroke="black" points="1810.23,-290.97 1820.82,-291.32 1812.76,-284.45 1810.23,-290.97"/>
+<path fill="none" stroke="black" d="M1857.07,-394.9C1838,-402.8 1819.46,-414.56 1808.18,-432 1799.49,-445.43 1798.62,-455.17 1808.18,-468 1819.39,-483.06 1857.34,-495.42 1895.25,-504.41"/>
+<polygon fill="black" stroke="black" points="1894.86,-507.92 1905.39,-506.74 1896.43,-501.09 1894.86,-507.92"/>
 </g>
 <!-- driverIdActionIdUnitIdGET&#45;&gt;driverIdActionIdUnitIdGET -->
-<g id="edge43" class="edge">
+<g id="edge44" class="edge">
 <title>driverIdActionIdUnitIdGET&#45;&gt;driverIdActionIdUnitIdGET</title>
-<path fill="none" stroke="black" d="M1974.61,-175.49C2021.37,-175.74 2059.41,-171.25 2059.41,-162 2059.41,-153.44 2026.81,-148.95 1984.86,-148.53"/>
-<polygon fill="black" stroke="black" points="1984.62,-145.03 1974.61,-148.51 1984.6,-152.03 1984.62,-145.03"/>
+<path fill="none" stroke="black" d="M2057.61,-391.49C2104.37,-391.74 2142.41,-387.25 2142.41,-378 2142.41,-369.44 2109.81,-364.95 2067.86,-364.53"/>
+<polygon fill="black" stroke="black" points="2067.62,-361.03 2057.61,-364.51 2067.6,-368.03 2067.62,-361.03"/>
 </g>
 <!-- driverIdUnitIdActionIdEventsGET -->
 <g id="node34" class="node">
 <title>driverIdUnitIdActionIdEventsGET</title>
-<ellipse fill="none" stroke="black" cx="2171.18" cy="-90" rx="226.66" ry="18"/>
-<text text-anchor="middle" x="2171.18" y="-86.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/drivers/{driverId}/units/{unitId}/actions/{actionId}/events</text>
+<ellipse fill="none" stroke="black" cx="1926.18" cy="-306" rx="226.66" ry="18"/>
+<text text-anchor="middle" x="1926.18" y="-302.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/drivers/{driverId}/units/{unitId}/actions/{actionId}/events</text>
 </g>
 <!-- driverIdActionIdUnitIdGET&#45;&gt;driverIdUnitIdActionIdEventsGET -->
-<g id="edge45" class="edge">
+<g id="edge46" class="edge">
 <title>driverIdActionIdUnitIdGET&#45;&gt;driverIdUnitIdActionIdEventsGET</title>
-<path fill="none" stroke="black" d="M1917.2,-145.2C1967.34,-134.5 2033.74,-120.33 2086.07,-109.16"/>
-<polygon fill="black" stroke="black" points="2086.96,-112.55 2096.01,-107.04 2085.5,-105.71 2086.96,-112.55"/>
+<path fill="none" stroke="black" d="M1926.18,-359.7C1926.18,-351.98 1926.18,-342.71 1926.18,-334.11"/>
+<polygon fill="black" stroke="black" points="1929.68,-334.1 1926.18,-324.1 1922.68,-334.1 1929.68,-334.1"/>
+</g>
+<!-- driverIdActionIdUnitIdAlertsGET -->
+<g id="node43" class="node">
+<title>driverIdActionIdUnitIdAlertsGET</title>
+<ellipse fill="none" stroke="black" cx="841.18" cy="-306" rx="222.86" ry="18"/>
+<text text-anchor="middle" x="841.18" y="-302.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/drivers/{driverId}/actions/{actionId}/units/{unitId}/alerts</text>
+</g>
+<!-- driverIdActionIdUnitIdGET&#45;&gt;driverIdActionIdUnitIdAlertsGET -->
+<g id="edge48" class="edge">
+<title>driverIdActionIdUnitIdGET&#45;&gt;driverIdActionIdUnitIdAlertsGET</title>
+<path fill="none" stroke="black" d="M1765,-367.52C1593.45,-357.3 1314.11,-340.31 1073.18,-324 1052.39,-322.59 1030.57,-321.06 1008.96,-319.52"/>
+<polygon fill="black" stroke="black" points="1008.94,-316.01 998.72,-318.78 1008.44,-322.99 1008.94,-316.01"/>
 </g>
 <!-- driverIdUnitIdActionIdGET&#45;&gt;driverIdGET -->
-<g id="edge70" class="edge">
+<g id="edge71" class="edge">
 <title>driverIdUnitIdActionIdGET&#45;&gt;driverIdGET</title>
-<path fill="none" stroke="black" d="M2082.68,-505.85C2007.68,-493.02 1901.58,-474.86 1832.33,-463.01"/>
-<polygon fill="black" stroke="black" points="1832.84,-459.54 1822.39,-461.31 1831.66,-466.44 1832.84,-459.54"/>
+<path fill="none" stroke="black" d="M2220.41,-35.99C2238.4,-45.87 2260.99,-58.91 2280.18,-72 2322.49,-100.87 2347.99,-97.83 2370.18,-144 2377.11,-158.42 2372.79,-164.21 2370.18,-180 2353.62,-280.05 2293.66,-539.09 2223.18,-612 2204.44,-631.38 2177.9,-643.76 2152.96,-651.63"/>
+<polygon fill="black" stroke="black" points="2151.74,-648.34 2143.14,-654.53 2153.72,-655.05 2151.74,-648.34"/>
 </g>
 <!-- driverIdUnitIdActionIdGET&#45;&gt;driverIdUnitIdGET -->
-<g id="edge71" class="edge">
+<g id="edge72" class="edge">
 <title>driverIdUnitIdActionIdGET&#45;&gt;driverIdUnitIdGET</title>
-<path fill="none" stroke="black" d="M2109.22,-539.16C2090.9,-547.14 2072.89,-558.88 2062.18,-576 2049.4,-596.42 2063.16,-621.62 2077.92,-639.99"/>
-<polygon fill="black" stroke="black" points="2075.53,-642.57 2084.68,-647.9 2080.85,-638.03 2075.53,-642.57"/>
+<path fill="none" stroke="black" d="M2225.67,-35.86C2240.13,-44.45 2255.11,-56.43 2263.18,-72 2270.53,-86.21 2269.01,-93.1 2263.18,-108 2258.95,-118.81 2251.36,-128.74 2243.4,-137"/>
+<polygon fill="black" stroke="black" points="2240.82,-134.63 2236.07,-144.11 2245.69,-139.66 2240.82,-134.63"/>
 </g>
 <!-- driverIdUnitIdActionIdGET&#45;&gt;driverIdUnitIdActionIdGET -->
-<g id="edge69" class="edge">
+<g id="edge70" class="edge">
 <title>driverIdUnitIdActionIdGET&#45;&gt;driverIdUnitIdActionIdGET</title>
-<path fill="none" stroke="black" d="M2302.61,-535.49C2349.37,-535.74 2387.41,-531.25 2387.41,-522 2387.41,-513.44 2354.81,-508.95 2312.86,-508.53"/>
-<polygon fill="black" stroke="black" points="2312.62,-505.03 2302.61,-508.51 2312.6,-512.03 2312.62,-505.03"/>
+<path fill="none" stroke="black" d="M2319.61,-31.49C2366.37,-31.74 2404.41,-27.25 2404.41,-18 2404.41,-9.44 2371.81,-4.95 2329.86,-4.53"/>
+<polygon fill="black" stroke="black" points="2329.62,-1.03 2319.61,-4.51 2329.6,-8.03 2329.62,-1.03"/>
 </g>
 <!-- driverIdUnitIdActionIdGET&#45;&gt;driverIdUnitIdActionIdEventsGET -->
-<g id="edge72" class="edge">
-<title>driverIdUnitIdActionIdGET&#45;&gt;driverIdUnitIdActionIdEventsGET</title>
-<path fill="none" stroke="black" d="M2171.18,-503.95C2171.18,-477.29 2171.18,-424.11 2171.18,-379 2171.18,-379 2171.18,-379 2171.18,-233 2171.18,-193 2171.18,-146.65 2171.18,-118.08"/>
-<polygon fill="black" stroke="black" points="2174.68,-118.05 2171.18,-108.05 2167.68,-118.05 2174.68,-118.05"/>
-</g>
-<!-- driverIdUnitIdActionIdEventIdGET -->
-<g id="node38" class="node">
-<title>driverIdUnitIdActionIdEventIdGET</title>
-<ellipse fill="none" stroke="black" cx="2371.18" cy="-18" rx="142.97" ry="18"/>
-<text text-anchor="middle" x="2371.18" y="-14.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">driverIdUnitIdActionIdEventIdGET</text>
-</g>
-<!-- driverIdUnitIdActionIdEventsGET&#45;&gt;driverIdUnitIdActionIdEventIdGET -->
 <g id="edge73" class="edge">
-<title>driverIdUnitIdActionIdEventsGET&#45;&gt;driverIdUnitIdActionIdEventIdGET</title>
-<path fill="none" stroke="black" d="M2218.83,-72.32C2247.92,-62.14 2285.23,-49.08 2315.65,-38.43"/>
-<polygon fill="black" stroke="black" points="2316.99,-41.68 2325.27,-35.07 2314.67,-35.07 2316.99,-41.68"/>
+<title>driverIdUnitIdActionIdGET&#45;&gt;driverIdUnitIdActionIdEventsGET</title>
+<path fill="none" stroke="black" d="M1996.04,-22.48C1791.02,-30.74 1485.73,-57.65 1412.18,-144 1401.8,-156.18 1402.29,-167.42 1412.18,-180 1457.75,-237.98 1648.45,-272.41 1784.24,-290.02"/>
+<polygon fill="black" stroke="black" points="1783.96,-293.51 1794.33,-291.31 1784.85,-286.57 1783.96,-293.51"/>
 </g>
 <!-- eventIdGET -->
 <g id="node35" class="node">
 <title>eventIdGET</title>
-<ellipse fill="none" stroke="black" cx="2396.18" cy="-738" rx="76.89" ry="18"/>
-<text text-anchor="middle" x="2396.18" y="-734.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/events/{eventId}</text>
+<ellipse fill="none" stroke="black" cx="1920.18" cy="-234" rx="76.89" ry="18"/>
+<text text-anchor="middle" x="1920.18" y="-230.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/events/{eventId}</text>
+</g>
+<!-- driverIdUnitIdActionIdEventsGET&#45;&gt;eventIdGET -->
+<g id="edge74" class="edge">
+<title>driverIdUnitIdActionIdEventsGET&#45;&gt;eventIdGET</title>
+<path fill="none" stroke="black" d="M1924.69,-287.7C1924.03,-279.98 1923.24,-270.71 1922.5,-262.11"/>
+<polygon fill="black" stroke="black" points="1925.99,-261.77 1921.64,-252.1 1919.01,-262.37 1925.99,-261.77"/>
 </g>
 <!-- eventIdGET&#45;&gt;driverIdGET -->
-<g id="edge75" class="edge">
+<g id="edge76" class="edge">
 <title>eventIdGET&#45;&gt;driverIdGET</title>
-<path fill="none" stroke="black" d="M2322.22,-733.18C2198.41,-726.12 1959.84,-709.39 1931.18,-684 1869.61,-629.46 1941.39,-566.72 1888.18,-504 1872.73,-485.79 1849.96,-473.71 1828.02,-465.75"/>
-<polygon fill="black" stroke="black" points="1828.95,-462.37 1818.35,-462.48 1826.7,-469 1828.95,-462.37"/>
+<path fill="none" stroke="black" d="M1990.96,-241.06C2060.18,-248.36 2159.27,-262.88 2188.18,-288 2219.79,-315.48 2219.18,-335.11 2219.18,-377 2219.18,-523 2219.18,-523 2219.18,-523 2219.18,-563.45 2225.54,-580.5 2200.18,-612 2186.23,-629.32 2165.4,-641.19 2145.16,-649.22"/>
+<polygon fill="black" stroke="black" points="2143.78,-646 2135.62,-652.76 2146.22,-652.56 2143.78,-646"/>
 </g>
 <!-- eventIdGET&#45;&gt;driverIdUnitIdGET -->
-<g id="edge76" class="edge">
+<g id="edge77" class="edge">
 <title>eventIdGET&#45;&gt;driverIdUnitIdGET</title>
-<path fill="none" stroke="black" d="M2344.29,-724.65C2297.06,-713.4 2226.86,-696.69 2174.2,-684.15"/>
-<polygon fill="black" stroke="black" points="2174.79,-680.69 2164.25,-681.78 2173.17,-687.5 2174.79,-680.69"/>
+<path fill="none" stroke="black" d="M1972.06,-220.65C2019.3,-209.4 2089.49,-192.69 2142.15,-180.15"/>
+<polygon fill="black" stroke="black" points="2143.19,-183.5 2152.11,-177.78 2141.57,-176.69 2143.19,-183.5"/>
 </g>
 <!-- eventIdGET&#45;&gt;driverIdActionIdGET -->
-<g id="edge77" class="edge">
+<g id="edge78" class="edge">
 <title>eventIdGET&#45;&gt;driverIdActionIdGET</title>
-<path fill="none" stroke="black" d="M2319.2,-737.91C2209.15,-737.53 2014.55,-730.11 1964.18,-684 1934.34,-656.69 1945.18,-635.45 1945.18,-595 1945.18,-595 1945.18,-595 1945.18,-449 1945.18,-407.63 1929.94,-361.72 1918.77,-333.61"/>
-<polygon fill="black" stroke="black" points="1921.9,-332.02 1914.87,-324.09 1915.42,-334.67 1921.9,-332.02"/>
+<path fill="none" stroke="black" d="M1991.26,-240.85C2054.79,-247.72 2141.01,-261.7 2162.18,-288 2212.33,-350.33 2209.95,-403.83 2162.18,-468 2150.97,-483.06 2113.01,-495.42 2075.11,-504.41"/>
+<polygon fill="black" stroke="black" points="2073.93,-501.09 2064.97,-506.74 2075.5,-507.92 2073.93,-501.09"/>
+</g>
+<!-- eventIdGET&#45;&gt;eventIdGET -->
+<g id="edge75" class="edge">
+<title>eventIdGET&#45;&gt;eventIdGET</title>
+<path fill="none" stroke="black" d="M1972.28,-247.31C1995.17,-248.49 2014.87,-244.05 2014.87,-234 2014.87,-225.48 2000.72,-220.99 1982.46,-220.54"/>
+<polygon fill="black" stroke="black" points="1982.23,-217.04 1972.28,-220.69 1982.33,-224.04 1982.23,-217.04"/>
 </g>
 <!-- eventIdObservationsGET -->
 <g id="node36" class="node">
 <title>eventIdObservationsGET</title>
-<ellipse fill="none" stroke="black" cx="2396.18" cy="-666" rx="129.18" ry="18"/>
-<text text-anchor="middle" x="2396.18" y="-662.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/events/{eventId}/observations</text>
+<ellipse fill="none" stroke="black" cx="1920.18" cy="-162" rx="129.18" ry="18"/>
+<text text-anchor="middle" x="1920.18" y="-158.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/events/{eventId}/observations</text>
 </g>
 <!-- eventIdGET&#45;&gt;eventIdObservationsGET -->
-<g id="edge78" class="edge">
+<g id="edge79" class="edge">
 <title>eventIdGET&#45;&gt;eventIdObservationsGET</title>
-<path fill="none" stroke="black" d="M2390.26,-719.7C2389.47,-711.98 2389.24,-702.71 2389.58,-694.11"/>
-<polygon fill="black" stroke="black" points="2393.08,-694.32 2390.28,-684.1 2386.09,-693.84 2393.08,-694.32"/>
+<path fill="none" stroke="black" d="M1914.26,-215.7C1913.47,-207.98 1913.24,-198.71 1913.58,-190.11"/>
+<polygon fill="black" stroke="black" points="1917.08,-190.32 1914.28,-180.1 1910.09,-189.84 1917.08,-190.32"/>
 </g>
 <!-- eventIdLocationsGET -->
 <g id="node37" class="node">
 <title>eventIdLocationsGET</title>
-<ellipse fill="none" stroke="black" cx="2766.18" cy="-666" rx="167.07" ry="18"/>
-<text text-anchor="middle" x="2766.18" y="-662.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/events/{eventId}/observations/locations</text>
+<ellipse fill="none" stroke="black" cx="1588.18" cy="-162" rx="167.07" ry="18"/>
+<text text-anchor="middle" x="1588.18" y="-158.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/events/{eventId}/observations/locations</text>
 </g>
 <!-- eventIdGET&#45;&gt;eventIdLocationsGET -->
-<g id="edge79" class="edge">
+<g id="edge80" class="edge">
 <title>eventIdGET&#45;&gt;eventIdLocationsGET</title>
-<path fill="none" stroke="black" d="M2451.71,-725.52C2510.22,-714.1 2603.25,-696.45 2673.01,-683.52"/>
-<polygon fill="black" stroke="black" points="2673.93,-686.91 2683.13,-681.65 2672.66,-680.03 2673.93,-686.91"/>
-</g>
-<!-- eventIdGET&#45;&gt;driverIdUnitIdActionIdEventIdGET -->
-<g id="edge74" class="edge">
-<title>eventIdGET&#45;&gt;driverIdUnitIdActionIdEventIdGET</title>
-<path fill="none" stroke="black" d="M2463.75,-729.39C2494.85,-722.45 2529.75,-709.22 2552.18,-684 2579.05,-653.77 2571.18,-635.45 2571.18,-595 2571.18,-595 2571.18,-595 2571.18,-161 2571.18,-95.69 2499.39,-57.66 2441.83,-37.67"/>
-<polygon fill="black" stroke="black" points="2442.59,-34.24 2432,-34.38 2440.37,-40.88 2442.59,-34.24"/>
+<path fill="none" stroke="black" d="M1862.86,-221.99C1808.39,-210.9 1726.16,-193.62 1665.84,-180.63"/>
+<polygon fill="black" stroke="black" points="1666.49,-177.19 1655.98,-178.5 1665.01,-184.03 1666.49,-177.19"/>
 </g>
 <!-- eventIdObservationsGET&#45;&gt;eventIdGET -->
-<g id="edge26" class="edge">
+<g id="edge27" class="edge">
 <title>eventIdObservationsGET&#45;&gt;eventIdGET</title>
-<path fill="none" stroke="black" d="M2402.07,-684.1C2402.88,-691.79 2403.11,-701.05 2402.78,-709.67"/>
-<polygon fill="black" stroke="black" points="2399.29,-709.48 2402.09,-719.7 2406.27,-709.96 2399.29,-709.48"/>
+<path fill="none" stroke="black" d="M1926.07,-180.1C1926.88,-187.79 1927.11,-197.05 1926.78,-205.67"/>
+<polygon fill="black" stroke="black" points="1923.29,-205.48 1926.09,-215.7 1930.27,-205.96 1923.29,-205.48"/>
 </g>
 <!-- eventIdObservationsGET&#45;&gt;eventIdObservationsGET -->
-<g id="edge27" class="edge">
+<g id="edge28" class="edge">
 <title>eventIdObservationsGET&#45;&gt;eventIdObservationsGET</title>
-<path fill="none" stroke="black" d="M2482.44,-679.45C2515.79,-680.04 2543.52,-675.56 2543.52,-666 2543.52,-657.45 2521.33,-652.96 2492.78,-652.53"/>
-<polygon fill="black" stroke="black" points="2492.43,-649.03 2482.44,-652.55 2492.45,-656.03 2492.43,-649.03"/>
+<path fill="none" stroke="black" d="M2006.44,-175.45C2039.79,-176.04 2067.52,-171.56 2067.52,-162 2067.52,-153.45 2045.33,-148.96 2016.78,-148.53"/>
+<polygon fill="black" stroke="black" points="2016.43,-145.03 2006.44,-148.55 2016.45,-152.03 2016.43,-145.03"/>
 </g>
 <!-- eventIdLocationsGET&#45;&gt;eventIdGET -->
-<g id="edge32" class="edge">
+<g id="edge33" class="edge">
 <title>eventIdLocationsGET&#45;&gt;eventIdGET</title>
-<path fill="none" stroke="black" d="M2691.89,-682.13C2626.07,-694.88 2530.74,-712.89 2466.22,-724.75"/>
-<polygon fill="black" stroke="black" points="2465.34,-721.35 2456.14,-726.6 2466.61,-728.23 2465.34,-721.35"/>
+<path fill="none" stroke="black" d="M1665.16,-178.04C1723.68,-190.06 1803.1,-206.82 1857.72,-218.66"/>
+<polygon fill="black" stroke="black" points="1857.1,-222.1 1867.61,-220.81 1858.59,-215.26 1857.1,-222.1"/>
 </g>
 <!-- eventIdLocationsGET&#45;&gt;eventIdLocationsGET -->
-<g id="edge33" class="edge">
+<g id="edge34" class="edge">
 <title>eventIdLocationsGET&#45;&gt;eventIdLocationsGET</title>
-<path fill="none" stroke="black" d="M2877.27,-679.48C2917.92,-679.84 2951.21,-675.35 2951.21,-666 2951.21,-657.45 2923.39,-652.96 2887.57,-652.53"/>
-<polygon fill="black" stroke="black" points="2887.28,-649.03 2877.27,-652.52 2887.27,-656.03 2887.28,-649.03"/>
+<path fill="none" stroke="black" d="M1699.27,-175.48C1739.92,-175.84 1773.21,-171.35 1773.21,-162 1773.21,-153.45 1745.39,-148.96 1709.57,-148.53"/>
+<polygon fill="black" stroke="black" points="1709.28,-145.03 1699.27,-148.52 1709.27,-152.03 1709.28,-145.03"/>
+</g>
+<!-- alertIdGET -->
+<g id="node38" class="node">
+<title>alertIdGET</title>
+<ellipse fill="none" stroke="black" cx="1040.18" cy="-234" rx="68.79" ry="18"/>
+<text text-anchor="middle" x="1040.18" y="-230.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/alerts/{alertId}</text>
+</g>
+<!-- alertIdGET&#45;&gt;campaignIdUnitIdGET -->
+<g id="edge86" class="edge">
+<title>alertIdGET&#45;&gt;campaignIdUnitIdGET</title>
+<path fill="none" stroke="black" d="M973.28,-238.43C858.23,-245.02 632.87,-261.21 609.18,-288 598.58,-299.98 599.04,-311.62 609.18,-324 763.73,-512.66 912.41,-365.95 1147.18,-432 1416.39,-507.74 1527.02,-464.63 1738.18,-648 1787.41,-690.75 1838.18,-799.86 1815.18,-828 1798.41,-848.52 1744.34,-861.29 1689.73,-869.13"/>
+<polygon fill="black" stroke="black" points="1688.96,-865.71 1679.53,-870.54 1689.92,-872.64 1688.96,-865.71"/>
+</g>
+<!-- alertIdGET&#45;&gt;alertIdGET -->
+<g id="edge85" class="edge">
+<title>alertIdGET&#45;&gt;alertIdGET</title>
+<path fill="none" stroke="black" d="M1087.11,-247.26C1108.49,-248.62 1127.07,-244.2 1127.07,-234 1127.07,-225.48 1114.08,-220.99 1097.33,-220.54"/>
+<polygon fill="black" stroke="black" points="1097.04,-217.04 1087.11,-220.74 1097.17,-224.04 1097.04,-217.04"/>
+</g>
+<!-- alertIdEventsGET -->
+<g id="node44" class="node">
+<title>alertIdEventsGET</title>
+<ellipse fill="none" stroke="black" cx="1040.18" cy="-162" rx="81.49" ry="18"/>
+<text text-anchor="middle" x="1040.18" y="-158.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">alertIdEventsGET</text>
+</g>
+<!-- alertIdGET&#45;&gt;alertIdEventsGET -->
+<g id="edge87" class="edge">
+<title>alertIdGET&#45;&gt;alertIdEventsGET</title>
+<path fill="none" stroke="black" d="M1040.18,-215.7C1040.18,-207.98 1040.18,-198.71 1040.18,-190.11"/>
+<polygon fill="black" stroke="black" points="1043.68,-190.1 1040.18,-180.1 1036.68,-190.1 1043.68,-190.1"/>
+</g>
+<!-- eventIdAlertsGET -->
+<g id="node39" class="node">
+<title>eventIdAlertsGET</title>
+<ellipse fill="none" stroke="black" cx="2434.18" cy="-1098" rx="100.98" ry="18"/>
+<text text-anchor="middle" x="2434.18" y="-1094.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/events/{eventId}/alerts</text>
+</g>
+<!-- driverIdActionIdUnitIdAlertIdGET -->
+<g id="node40" class="node">
+<title>driverIdActionIdUnitIdAlertIdGET</title>
+<ellipse fill="none" stroke="black" cx="1547.18" cy="-306" rx="134.58" ry="18"/>
+<text text-anchor="middle" x="1547.18" y="-302.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/events/{eventId}/alerts/{alertId}</text>
+</g>
+<!-- driverIdActionIdUnitIdAlertIdGET&#45;&gt;eventIdGET -->
+<g id="edge84" class="edge">
+<title>driverIdActionIdUnitIdAlertIdGET&#45;&gt;eventIdGET</title>
+<path fill="none" stroke="black" d="M1621.76,-291C1688.96,-278.39 1786.93,-260.01 1852.06,-247.78"/>
+<polygon fill="black" stroke="black" points="1853.03,-251.16 1862.22,-245.88 1851.74,-244.28 1853.03,-251.16"/>
+</g>
+<!-- driverIdActionIdUnitIdAlertIdGET&#45;&gt;alertIdGET -->
+<g id="edge83" class="edge">
+<title>driverIdActionIdUnitIdAlertIdGET&#45;&gt;alertIdGET</title>
+<path fill="none" stroke="black" d="M1457.12,-292.57C1357.8,-278.85 1200.12,-257.08 1109.81,-244.61"/>
+<polygon fill="black" stroke="black" points="1110.06,-241.12 1099.68,-243.22 1109.11,-248.05 1110.06,-241.12"/>
+</g>
+<!-- campaignIdUnitsAlertsGET -->
+<g id="node41" class="node">
+<title>campaignIdUnitsAlertsGET</title>
+<ellipse fill="none" stroke="black" cx="1238.18" cy="-306" rx="155.97" ry="18"/>
+<text text-anchor="middle" x="1238.18" y="-302.3" font-family="Helvetica,Arial,sans-serif" font-size="14.00">/campaigns/{campaignId}/units/alerts</text>
+</g>
+<!-- campaignIdUnitsAlertsGET&#45;&gt;alertIdGET -->
+<g id="edge81" class="edge">
+<title>campaignIdUnitsAlertsGET&#45;&gt;alertIdGET</title>
+<path fill="none" stroke="black" d="M1192.25,-288.76C1161.3,-277.82 1120.51,-263.4 1088.97,-252.25"/>
+<polygon fill="black" stroke="black" points="1090.1,-248.94 1079.5,-248.9 1087.76,-255.54 1090.1,-248.94"/>
+</g>
+<!-- driverIdActionIdUnitIdAlertsGET&#45;&gt;alertIdGET -->
+<g id="edge82" class="edge">
+<title>driverIdActionIdUnitIdAlertsGET&#45;&gt;alertIdGET</title>
+<path fill="none" stroke="black" d="M888.59,-288.32C919.5,-277.45 959.76,-263.29 991.01,-252.29"/>
+<polygon fill="black" stroke="black" points="992.55,-255.46 1000.82,-248.84 990.23,-248.86 992.55,-255.46"/>
 </g>
 </g>
 </svg>

+ 33 - 3
init.sql

@@ -50,7 +50,8 @@ CREATE TABLE maplog.campaign (
     name TEXT NOT NULL,
     description TEXT NOT NULL,
     from_time TIMESTAMP WITH TIME ZONE NOT NULL,
-    to_time TIMESTAMP WITH TIME ZONE NOT NULL
+    to_time TIMESTAMP WITH TIME ZONE NOT NULL,
+    CONSTRAINT campaign_check_time CHECK (from_time < to_time)
 );
 ALTER TABLE maplog.campaign OWNER TO senslog;
 
@@ -100,13 +101,14 @@ CREATE TABLE maplog.action (
 ALTER TABLE maplog.action OWNER TO senslog;
 
 
-CREATE TABLE maplog.driver_to_action (
+CREATE TABLE maplog.driver_to_action ( -- rename to event --
     id          BIGINT NOT NULL PRIMARY KEY,
     driver_id   INTEGER NOT NULL,
     action_id   INTEGER NOT NULL,
     unit_id     BIGINT NOT NULL,
     from_time   TIMESTAMP WITH TIME ZONE NOT NULL,
-    to_time     TIMESTAMP WITH TIME ZONE
+    to_time     TIMESTAMP WITH TIME ZONE,
+    CONSTRAINT dta_check_time CHECK (from_time < to_time)
 );
 ALTER TABLE maplog.driver_to_action OWNER TO senslog;
 
@@ -216,6 +218,28 @@ CREATE TABLE maplog.user_to_campaign (
 ALTER TABLE maplog.user_to_campaign OWNER TO senslog;
 
 
+CREATE TYPE alert_status AS ENUM ('CREATED', 'INFORMED', 'IN_PROCESS', 'SOLVED', 'DELETED');
+
+CREATE TABLE maplog.alert (
+    id BIGINT NOT NULL PRIMARY KEY,
+    time_stamp TIMESTAMP WITH TIME ZONE NOT NULL,
+    unit_id BIGINT NOT NULL,
+    message TEXT NOT NULL,
+    status alert_status NOT NULL,
+    time_received TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
+);
+
+ALTER TABLE maplog.alert OWNER TO senslog;
+
+CREATE SEQUENCE maplog.alert_id_seq START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1;
+
+ALTER TABLE maplog.alert_id_seq OWNER TO senslog;
+
+ALTER SEQUENCE maplog.alert_id_seq OWNED BY maplog.alert.id;
+
+ALTER TABLE ONLY maplog.alert ALTER COLUMN id SET DEFAULT nextval('maplog.alert_id_seq'::regclass);
+
+
 
 CREATE INDEX fki_obss_unitid_fk ON maplog.obs_telemetry USING btree (unit_id);
 
@@ -245,6 +269,9 @@ CREATE INDEX fki_dr2ac_actionid_fk ON maplog.driver_to_action USING btree (actio
 
 CREATE INDEX fki_dr2ac_unitid_fk ON maplog.driver_to_action USING btree (unit_id);
 
+CREATE INDEX fki_alert_unitid_fk ON maplog.alert USING btree (unit_id);
+
+
 
 ALTER TABLE ONLY maplog.obs_telemetry ADD CONSTRAINT obss_unitid_fk FOREIGN KEY (unit_id) REFERENCES maplog.unit(unit_id) ON UPDATE CASCADE ON DELETE CASCADE;
 
@@ -270,5 +297,8 @@ ALTER TABLE ONLY maplog.driver_to_action ADD CONSTRAINT dta_driverid_fk FOREIGN
 
 ALTER TABLE ONLY maplog.driver_to_action ADD CONSTRAINT dta_actionid_fk FOREIGN KEY (action_id) REFERENCES maplog.action(action_id) ON UPDATE CASCADE ON DELETE CASCADE;
 
+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;
+
+
 REVOKE USAGE ON SCHEMA public FROM PUBLIC;
 GRANT ALL ON SCHEMA public TO PUBLIC;

+ 41 - 0
src/main/java/cz/senslog/telemetry/database/domain/Alert.java

@@ -0,0 +1,41 @@
+package cz.senslog.telemetry.database.domain;
+
+import java.time.OffsetDateTime;
+
+public class Alert {
+    private final long id;
+    private final String message;
+    private final OffsetDateTime timestamp;
+    private final AlertStatus status;
+
+    public static Alert of(long id, String message, OffsetDateTime timestamp, AlertStatus status) {
+        return new Alert(id, message, timestamp, status);
+    }
+
+    public static Alert of(String message, OffsetDateTime timestamp, AlertStatus status) {
+        return new Alert(-1, message, timestamp, status);
+    }
+
+    public Alert(long id, String message, OffsetDateTime timestamp, AlertStatus status) {
+        this.id = id;
+        this.message = message;
+        this.timestamp = timestamp;
+        this.status = status;
+    }
+
+    public long getId() {
+        return id;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public OffsetDateTime getTimestamp() {
+        return timestamp;
+    }
+
+    public AlertStatus getStatus() {
+        return status;
+    }
+}

+ 5 - 0
src/main/java/cz/senslog/telemetry/database/domain/AlertStatus.java

@@ -0,0 +1,5 @@
+package cz.senslog.telemetry.database.domain;
+
+public enum AlertStatus {
+    CREATED, INFORMED, IN_PROCESS, SOLVED, DELETED;
+}

+ 25 - 0
src/main/java/cz/senslog/telemetry/database/domain/CampaignUnitAlert.java

@@ -0,0 +1,25 @@
+package cz.senslog.telemetry.database.domain;
+
+import java.time.OffsetDateTime;
+
+public class CampaignUnitAlert extends UnitAlert {
+
+    private final long campaignId;
+
+    public static CampaignUnitAlert of(long id, long campaignId, long unitId, String message, OffsetDateTime timestamp, AlertStatus status) {
+        return new CampaignUnitAlert(id, campaignId, unitId, message, timestamp, status);
+    }
+
+    public static CampaignUnitAlert of(long unitId, long campaignId, String message, OffsetDateTime timestamp, AlertStatus status) {
+        return new CampaignUnitAlert(-1, campaignId, unitId, message, timestamp, status);
+    }
+
+    public CampaignUnitAlert(long id, long campaignId, long unitId, String message, OffsetDateTime timestamp, AlertStatus status) {
+        super(id, unitId, message, timestamp, status);
+        this.campaignId = campaignId;
+    }
+
+    public long getCampaignId() {
+        return campaignId;
+    }
+}

+ 16 - 6
src/main/java/cz/senslog/telemetry/database/domain/Event.java

@@ -5,34 +5,40 @@ import java.time.OffsetDateTime;
 public class Event {
 
     private final long id;
-    private final int driverId;
-    private final int actionId;
+    private final long driverId;
+    private final long actionId;
     private final long unitId;
     private final OffsetDateTime fromTime;
     private final OffsetDateTime toTime;
+    private final EventStatus status;
 
-    public static Event of(long id, int driverId, int actionId, long unitId, OffsetDateTime fromTime, OffsetDateTime toTime) {
+    public static Event of(long id, long driverId, long actionId, long unitId, OffsetDateTime fromTime, OffsetDateTime toTime) {
         return new Event(id, driverId, actionId, unitId, fromTime, toTime);
     }
 
-    private Event(long id, int driverId, int actionId, long unitId, OffsetDateTime fromTime, OffsetDateTime toTime) {
+    public static Event of(long driverId, long actionId, long unitId, OffsetDateTime fromTime, OffsetDateTime toTime) {
+        return new Event(-1, driverId, actionId, unitId, fromTime, toTime);
+    }
+
+    private Event(long id, long driverId, long actionId, long unitId, OffsetDateTime fromTime, OffsetDateTime toTime) {
         this.id = id;
         this.driverId = driverId;
         this.actionId = actionId;
         this.unitId = unitId;
         this.fromTime = fromTime;
         this.toTime = toTime;
+        this.status = toTime == null ? EventStatus.ONGOING : EventStatus.FINISHED;
     }
 
     public long getId() {
         return id;
     }
 
-    public int getDriverId() {
+    public long getDriverId() {
         return driverId;
     }
 
-    public int getActionId() {
+    public long getActionId() {
         return actionId;
     }
 
@@ -47,4 +53,8 @@ public class Event {
     public OffsetDateTime getToTime() {
         return toTime;
     }
+
+    public EventStatus getStatus() {
+        return status;
+    }
 }

+ 21 - 0
src/main/java/cz/senslog/telemetry/database/domain/EventAlert.java

@@ -0,0 +1,21 @@
+package cz.senslog.telemetry.database.domain;
+
+import java.time.OffsetDateTime;
+
+public class EventAlert extends UnitAlert {
+
+    private final long eventId;
+
+    public static EventAlert of(long id, long eventId, long unitId, String message, OffsetDateTime timestamp, AlertStatus status) {
+        return new EventAlert(id, eventId, unitId, message, timestamp, status);
+    }
+
+    public EventAlert(long id, long eventId, long unitId, String message, OffsetDateTime timestamp, AlertStatus status) {
+        super(id, unitId, message, timestamp, status);
+        this.eventId = eventId;
+    }
+
+    public long getEventId() {
+        return eventId;
+    }
+}

+ 5 - 0
src/main/java/cz/senslog/telemetry/database/domain/EventStatus.java

@@ -0,0 +1,5 @@
+package cz.senslog.telemetry.database.domain;
+
+public enum EventStatus {
+    ONGOING, FINISHED
+}

+ 25 - 0
src/main/java/cz/senslog/telemetry/database/domain/UnitAlert.java

@@ -0,0 +1,25 @@
+package cz.senslog.telemetry.database.domain;
+
+import java.time.OffsetDateTime;
+
+public class UnitAlert extends Alert {
+
+    private final long unitId;
+
+    public static UnitAlert of(long id, long unitId, String message, OffsetDateTime timestamp, AlertStatus status) {
+        return new UnitAlert(id, unitId, message, timestamp, status);
+    }
+
+    public static UnitAlert of(long unitId, String message, OffsetDateTime timestamp, AlertStatus status) {
+        return new UnitAlert(-1, unitId, message, timestamp, status);
+    }
+
+    public UnitAlert(long id, long unitId, String message, OffsetDateTime timestamp, AlertStatus status) {
+        super(id, message, timestamp, status);
+        this.unitId = unitId;
+    }
+
+    public long getUnitId() {
+        return unitId;
+    }
+}

+ 262 - 1
src/main/java/cz/senslog/telemetry/database/repository/MapLogRepository.java

@@ -35,7 +35,7 @@ public class MapLogRepository implements SensLogRepository {
     }
 
     @Override
-    public Future<Integer> updateActionByDriverUnit(DriverAction data) {
+    public Future<Integer> updateEvent(DriverAction data) {
         Tuple params = Tuple.of(data.getTimestamp(), data.getDriverId(), data.getActionId(), data.getUnitId());
         return client.withTransaction(conn -> conn.preparedQuery("UPDATE maplog.driver_to_action SET to_time = $1 " +
                         "WHERE driver_id = $2 AND action_id = $3 AND unit_id = $4 AND to_time IS NULL AND from_time < $1 RETURNING id")
@@ -62,6 +62,50 @@ public class MapLogRepository implements SensLogRepository {
     }
 
     @Override
+    public Future<Long> updateEvent(long eventId, OffsetDateTime toTime) {
+        return client.withTransaction(conn -> conn.preparedQuery("UPDATE maplog.driver_to_action SET to_time = $2 WHERE id = $1 AND to_time IS NULL RETURNING id")
+                .execute(Tuple.of(eventId, toTime))
+                .map(RowSet::iterator)
+                .map(it -> it.hasNext() ? it.next().getLong(0) : null)
+                .map(Optional::ofNullable)
+                .map(p -> p.orElseThrow(() -> new IllegalStateException("Event<%d> was not updated due to no such ongoing event.")))
+        );
+    }
+
+    @Override
+    public Future<Long> saveEvent(Event event) {
+        return client.withTransaction(conn ->
+                conn.preparedQuery("INSERT INTO maplog.driver_to_action(driver_id, action_id, unit_id, from_time, to_time) " +
+                        "SELECT $1, $2, $3, $4, $5  WHERE EXISTS(SELECT action_id FROM maplog.action WHERE action_id = $2) RETURNING id")
+                        .execute(Tuple.of(event.getDriverId(), event.getActionId(), event.getUnitId(), event.getFromTime(), event.getToTime()))
+                        .map(RowSet::iterator)
+                        .map(it -> it.hasNext() ? it.next().getLong(0) : null)
+                        .map(Optional::ofNullable)
+                        .map(p -> p.orElseThrow(() -> new IllegalStateException("Event was not saved successfully.")))
+        );
+    }
+
+    @Override
+    public Future<List<Long>> saveEvents(List<Event> events) {
+        if (events == null || events.isEmpty()) {
+            return Future.succeededFuture(Collections.emptyList());
+        }
+
+        List<Tuple> tuples = events.stream().map(e -> Tuple.of(
+                e.getDriverId(),
+                e.getActionId(),
+                e.getUnitId(),
+                e.getFromTime(),
+                e.getToTime())
+        ).toList();
+
+        return client.withTransaction(conn -> conn.preparedQuery("INSERT INTO maplog.driver_to_action(driver_id, action_id, unit_id, from_time, to_time) " +
+                "SELECT $1, $2, $3, $4, $5  WHERE EXISTS(SELECT action_id FROM maplog.action WHERE action_id = $2) RETURNING id")
+                .executeBatch(tuples)
+                .map(rs -> StreamSupport.stream(rs.spliterator(), false).map(r -> r.getLong(0)).collect(toList())));
+    }
+
+    @Override
     public Future<Integer> saveTelemetry(UnitTelemetry data) {
         return client.preparedQuery("INSERT INTO maplog.obs_telemetry(unit_id, time_stamp, the_geom, speed, observed_values) " +
                         "VALUES ($1, $2, ST_SetSRID(ST_MakePoint($3, $4, $5, $6), 4326), $7, $8::jsonb) RETURNING (id)")
@@ -1258,4 +1302,221 @@ public class MapLogRepository implements SensLogRepository {
                         .collect(toList())
                 );
     }
+
+    private static final Function<Row, CampaignUnitAlert> ROW_TO_ALERT = (row) -> CampaignUnitAlert.of(
+        row.getLong("id"),
+        row.getLong("campaign_id"),
+        row.getLong("unit_id"),
+        row.getString("message"),
+        row.getOffsetDateTime("time_stamp"),
+        AlertStatus.valueOf(row.getString("status"))
+    );
+
+    @Override
+    public Future<CampaignUnitAlert> findAlertById(long alertId) {
+        return client.preparedQuery("SELECT a.id, utc.campaign_id, a.unit_id, a.message, a.time_stamp, a.status FROM maplog.alert AS a " +
+                        "JOIN maplog.unit u on u.unit_id = a.unit_id " +
+                        "JOIN maplog.unit_to_campaign utc on u.unit_id = utc.unit_id " +
+                        "WHERE utc.from_time <= a.time_stamp AND utc.to_time >= a.time_stamp AND a.id = $1")
+                .execute(Tuple.of(alertId))
+                .map(RowSet::iterator)
+                .map(it -> it.hasNext() ? ROW_TO_ALERT.apply(it.next()) : null)
+                .map(Optional::ofNullable)
+                .map(p -> p.orElseThrow(() -> new DataNotFoundException(format("Alert ID '%d' not found.", alertId))));
+    }
+
+    @Override
+    public Future<Long> updateAlert(long alertId, AlertStatus status) {
+        return client.withTransaction(conn -> conn.preparedQuery("UPDATE maplog.alert SET status = $2 WHERE id = $1 RETURNING id")
+                .execute(Tuple.of(alertId, status.name()))
+                .map(RowSet::iterator)
+                .map(it -> it.hasNext() ? it.next().getLong(0) : null)
+                .map(Optional::ofNullable)
+                .map(p -> p.orElseThrow(() -> new IllegalStateException(format("Alert<%s> was not updated.", alertId))))
+        );
+    }
+
+    @Override
+    public Future<Long> saveAlert(UnitAlert alert) {
+        return client.withTransaction(conn ->
+                conn.preparedQuery("INSERT INTO maplog.alert(time_stamp, unit_id, message, status) VALUES ($1, $2, $3, $4) RETURNING id")
+                .execute(Tuple.of(alert.getTimestamp(), alert.getUnitId(), alert.getMessage(), alert.getStatus()))
+                .map(RowSet::iterator)
+                .map(it -> it.hasNext() ? it.next().getLong(0) : null)
+                .map(Optional::ofNullable)
+                .map(p -> p.orElseThrow(() -> new IllegalStateException("Alert was not saved successfully.")))
+        );
+    }
+
+    @Override
+    public Future<List<Long>> saveAlerts(List<UnitAlert> alerts) {
+        if (alerts == null || alerts.isEmpty()) {
+            return Future.succeededFuture(Collections.emptyList());
+        }
+
+        List<Tuple> tuples = alerts.stream().map(a -> Tuple.of(
+                a.getTimestamp(), a.getUnitId(), a.getMessage(), a.getStatus()
+        )).toList();
+
+        return client.withTransaction(conn ->
+                conn.preparedQuery("INSERT INTO maplog.alert(time_stamp, unit_id, message, status) VALUES ($1, $2, $3, $4) RETURNING id;")
+                        .executeBatch(tuples).map(rs -> StreamSupport.stream(rs.spliterator(), false).map(r -> r.getLong(0)).toList())
+        );
+    }
+
+    @Override
+    public Future<List<Alert>> findAlertsByEventId(long eventId, Set<AlertStatus> statusFilter, SortType sortType) {
+
+        if (statusFilter.isEmpty()) {
+            statusFilter = Set.of(AlertStatus.CREATED, AlertStatus.INFORMED, AlertStatus.IN_PROCESS, AlertStatus.SOLVED);
+        }
+
+        String statuses = String.join(",", statusFilter.stream().map(s -> "'"+s.name()+"'").collect(toSet()));
+
+        return client.preparedQuery("SELECT a.id, a.message, a.status, a.time_stamp FROM maplog.alert AS a " +
+                        "JOIN maplog.driver_to_action AS dta ON dta.unit_id = a.unit_id " +
+                        "WHERE dta.from_time <= a.time_stamp AND (CASE WHEN dta.to_time IS NULL THEN now() ELSE dta.to_time END) >= a.time_stamp " +
+                            "AND dta.id = $1 AND a.status IN ("+statuses+") " + // TODO better solution IN (array)
+                        "ORDER BY a.time_stamp "+sortType.name())
+                .execute(Tuple.of(eventId))
+                .map(rs -> StreamSupport.stream(rs.spliterator(), false)
+                        .map(r -> Alert.of(
+                                r.getLong("id"),
+                                r.getString("message"),
+                                r.getOffsetDateTime("time_stamp"),
+                                AlertStatus.valueOf(r.getString("status"))
+                        ))
+                        .collect(toList())
+                );
+    }
+
+    private static final Function<Row, EventAlert> ROW_TO_EVENT_ALERT = (row) -> EventAlert.of(
+        row.getLong("id"),
+        row.getLong("event_id"),
+        row.getLong("unit_id"),
+        row.getString("message"),
+        row.getOffsetDateTime("time_stamp"),
+        AlertStatus.valueOf(row.getString("status"))
+    );
+
+    @Override
+    public Future<EventAlert> findAlertByIdAndEventId(long alertId, long eventId) {
+        return client.preparedQuery("SELECT a.id, dta.id AS event_id, a.unit_id, a.message, a.status, a.time_stamp FROM maplog.alert AS a " +
+                        "JOIN maplog.driver_to_action AS dta ON dta.unit_id = a.unit_id WHERE a.id = $1 AND dta.id = $2")
+                .execute(Tuple.of(alertId, eventId))
+                .map(RowSet::iterator)
+                .map(it -> it.hasNext() ? ROW_TO_EVENT_ALERT.apply(it.next()) : null)
+                .map(Optional::ofNullable)
+                .map(p -> p.orElseThrow(() -> new DataNotFoundException(format("Alert ID '%d' not found.", alertId))));
+    }
+
+    @Override
+    public Future<List<CampaignUnitAlert>> findAlertsByCampaignId(long campaignId, OffsetDateTime from, OffsetDateTime to, Set<AlertStatus> statusFilter, SortType sort) {
+
+        if (statusFilter.isEmpty()) {
+            statusFilter = Set.of(AlertStatus.CREATED, AlertStatus.INFORMED, AlertStatus.IN_PROCESS, AlertStatus.SOLVED);
+        }
+
+        String statuses = String.join(",", statusFilter.stream().map(s -> "'"+s.name()+"'").collect(toSet()));
+
+        String whereTimestampClause;
+        Tuple tupleParams;
+        if (from != null && to != null) {
+            whereTimestampClause = "WHERE utc.campaign_id = $1 AND a.status IN ("+statuses+") AND a.time_stamp >= (CASE WHEN utc.from_time > $2 THEN utc.from_time ELSE $2 END) AND a.time_stamp <= (CASE WHEN utc.to_time < $3 THEN utc.to_time ELSE $3 END)";
+            tupleParams = Tuple.of(campaignId, from, to);
+        } else if (from != null) {
+            whereTimestampClause = "WHERE utc.campaign_id = $1 AND a.status IN ("+statuses+") AND a.time_stamp >= (CASE WHEN utc.from_time > $2 THEN utc.from_time ELSE $2 END) AND a.time_stamp <= utc.to_time";
+            tupleParams = Tuple.of(campaignId, from);
+        } else if (to != null) {
+            whereTimestampClause = "WHERE utc.campaign_id = $1 AND a.status IN ("+statuses+") AND a.time_stamp >= utc.from_time AND a.time_stamp <= (CASE WHEN utc.to_time < $2 THEN utc.to_time ELSE $2 END)";
+            tupleParams = Tuple.of(campaignId, to);
+        } else {
+            whereTimestampClause = "WHERE utc.campaign_id = $1 AND a.status IN ("+statuses+")";
+            tupleParams = Tuple.of(campaignId);
+        }
+
+        return client.preparedQuery("SELECT a.id, utc.campaign_id, a.unit_id, a.message, a.time_stamp, a.status FROM maplog.alert AS a " +
+                        "JOIN maplog.unit_to_campaign utc on a.unit_id = utc.unit_id " + whereTimestampClause + " ORDER BY a.time_stamp "+sort.name())
+                .execute(tupleParams)
+                .map(rs -> StreamSupport.stream(rs.spliterator(), false)
+                        .map(r -> CampaignUnitAlert.of(
+                                r.getLong("id"),
+                                r.getLong("campaign_id"),
+                                r.getLong("unit_id"),
+                                r.getString("message"),
+                                r.getOffsetDateTime("time_stamp"),
+                                AlertStatus.valueOf(r.getString("status"))
+                        ))
+                        .collect(toList())
+                );
+    }
+
+    @Override
+    public Future<List<UnitAlert>> findAlertsByCampaignIdAndUnitId(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, Set<AlertStatus> statusFilter, SortType sort) {
+
+        if (statusFilter.isEmpty()) {
+            statusFilter = Set.of(AlertStatus.CREATED, AlertStatus.INFORMED, AlertStatus.IN_PROCESS, AlertStatus.SOLVED);
+        }
+
+        String statuses = String.join(",", statusFilter.stream().map(s -> "'"+s.name()+"'").collect(toSet()));
+
+        String whereTimestampClause;
+        Tuple tupleParams;
+        if (from != null && to != null) {
+            whereTimestampClause = "WHERE utc.campaign_id = $1 AND utc.unit_id = $2 AND a.status IN ("+statuses+") AND a.time_stamp >= (CASE WHEN utc.from_time > $3 THEN utc.from_time ELSE $3 END) AND a.time_stamp <= (CASE WHEN utc.to_time < $4 THEN utc.to_time ELSE $4 END)";
+            tupleParams = Tuple.of(campaignId, unitId, from, to);
+        } else if (from != null) {
+            whereTimestampClause = "WHERE utc.campaign_id = $1 AND utc.unit_id = $2 AND a.status IN ("+statuses+") AND a.time_stamp >= (CASE WHEN utc.from_time > $3 THEN utc.from_time ELSE $3 END) AND a.time_stamp <= utc.to_time";
+            tupleParams = Tuple.of(campaignId, unitId, from);
+        } else if (to != null) {
+            whereTimestampClause = "WHERE utc.campaign_id = $1 AND utc.unit_id = $2 AND a.status IN ("+statuses+") AND a.time_stamp >= utc.from_time AND a.time_stamp <= (CASE WHEN utc.to_time < $3 THEN utc.to_time ELSE $3 END)";
+            tupleParams = Tuple.of(campaignId, unitId, to);
+        } else {
+            whereTimestampClause = "WHERE utc.campaign_id = $1 AND utc.unit_id = $2 AND a.status IN ("+statuses+")";
+            tupleParams = Tuple.of(campaignId, unitId);
+        }
+
+        return client.preparedQuery("SELECT a.id, a.unit_id, a.message, a.time_stamp, a.status FROM maplog.alert AS a " +
+                        "JOIN maplog.unit_to_campaign utc on a.unit_id = utc.unit_id " + whereTimestampClause + " ORDER BY a.time_stamp "+sort.name())
+                .execute(tupleParams)
+                .map(rs -> StreamSupport.stream(rs.spliterator(), false)
+                        .map(r -> UnitAlert.of(
+                                r.getLong("id"),
+                                r.getLong("unit_id"),
+                                r.getString("message"),
+                                r.getOffsetDateTime("time_stamp"),
+                                AlertStatus.valueOf(r.getString("status"))
+                        ))
+                        .collect(toList())
+                );
+    }
+
+    @Override
+    public Future<List<Alert>> findAlertsByDriverIdAndActionIdAndUnitId(long driverId, long actionId, long unitId, Set<AlertStatus> statusFilter, SortType sort) {
+
+        if (statusFilter.isEmpty()) {
+            statusFilter = Set.of(AlertStatus.CREATED, AlertStatus.INFORMED, AlertStatus.IN_PROCESS, AlertStatus.SOLVED);
+        }
+
+        String statuses = String.join(",", statusFilter.stream().map(s -> "'"+s.name()+"'").collect(toSet()));
+
+        return client.preparedQuery("SELECT a.id, a.message, a.time_stamp, a.status FROM maplog.alert AS a " +
+                        "JOIN maplog.driver_to_action dta on a.unit_id = dta.unit_id " +
+                        "WHERE dta.driver_id = $1 AND dta.action_id = $2 AND dta.unit_id = $3 " +
+                            "AND dta.from_time <= a.time_stamp AND (CASE WHEN dta.to_time IS NULL THEN now() ELSE dta.to_time END) >= a.time_stamp " +
+                            "AND a.status IN ("+statuses+")" +
+                        "ORDER BY a.time_stamp "+sort.name())
+                .execute(Tuple.of(driverId, actionId, unitId))
+                .map(rs -> StreamSupport.stream(rs.spliterator(), false)
+                        .map(r -> Alert.of(
+                                r.getLong("id"),
+                                r.getString("message"),
+                                r.getOffsetDateTime("time_stamp"),
+                                AlertStatus.valueOf(r.getString("status"))
+                        ))
+                        .collect(toList())
+                );
+    }
+
+
 }

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

@@ -14,7 +14,11 @@ import java.util.Set;
 
 public interface SensLogRepository {
 
-    Future<Integer> updateActionByDriverUnit(DriverAction data);
+    Future<Integer> updateEvent(DriverAction data);
+    Future<Long> updateEvent(long eventId, OffsetDateTime toTime);
+    Future<Long> saveEvent(Event event);
+    Future<List<Long>> saveEvents(List<Event> events);
+
     Future<Integer> saveTelemetry(UnitTelemetry data);
     Future<Integer> saveAllTelemetry(List<UnitTelemetry> data);
 
@@ -85,4 +89,14 @@ public interface SensLogRepository {
     Future<PagingRetrieve<List<UnitLocation>>> findLocationsByEventIdWithPaging(long eventId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters);
 
     Future<List<UnitLocation>> findUnitsLocationsByCampaignId(long campaignId, int limitPerUnit, OffsetDateTime from, OffsetDateTime to, ZoneId zone, SortType sort, List<Filter> filters);
+
+    Future<CampaignUnitAlert> findAlertById(long alertId);
+    Future<Long> updateAlert(long alertId, AlertStatus status);
+    Future<Long> saveAlert(UnitAlert alert);
+    Future<List<Long>> saveAlerts(List<UnitAlert> alerts);
+    Future<List<Alert>> findAlertsByEventId(long eventId, Set<AlertStatus> statusFilter, SortType sortType);
+    Future<EventAlert> findAlertByIdAndEventId(long alertId, long eventId);
+    Future<List<CampaignUnitAlert>> findAlertsByCampaignId(long campaignId, OffsetDateTime from, OffsetDateTime to, Set<AlertStatus> statusFilter, SortType sort);
+    Future<List<UnitAlert>> findAlertsByCampaignIdAndUnitId(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, Set<AlertStatus> statusFilter, SortType sort);
+    Future<List<Alert>> findAlertsByDriverIdAndActionIdAndUnitId(long driverId, long actionId, long unitId, Set<AlertStatus> statusFilter, SortType sort);
 }

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

@@ -145,7 +145,7 @@ public class Fm4exSocketHandler {
             }
 
             OffsetDateTime timestamp = avlPacket.getTimestamp().atZone(UTC).toOffsetDateTime();
-            return repo.updateActionByDriverUnit(new DriverAction(contextUnit.getUnitId(), driverId, actionId, timestamp))
+            return repo.updateEvent(new DriverAction(contextUnit.getUnitId(), driverId, actionId, timestamp))
                     .map(res -> res > 0 ? SUCCESS : ERROR)
                     .onSuccess(res -> logger.info("[{}] AVL Driver Activity Packet was saved successfully.", socketId));
         }
@@ -167,7 +167,7 @@ public class Fm4exSocketHandler {
                     for (IOProperty io : avl.getIoProperties()) {
                         Sensor sensor = ioToSensor.get((long)io.getId());
                         if (sensor != null) {
-                            observedValues.put(Long.toString(sensor.getSensorId()), io.getValue());
+                            observedValues.put(sensor.getName(), io.getValue());
                         } else {
                             logger.error("Sensor for the IO Property <{}> does not exist.", io.getId());
                         }

+ 16 - 5
src/main/java/cz/senslog/telemetry/server/HttpVertxServer.java

@@ -1,7 +1,6 @@
 package cz.senslog.telemetry.server;
 
 import cz.senslog.telemetry.database.repository.SensLogRepository;
-import cz.senslog.telemetry.server.ws.AuthorizationType;
 import cz.senslog.telemetry.server.ws.ExceptionHandler;
 import cz.senslog.telemetry.server.ws.OpenAPIHandler;
 import cz.senslog.telemetry.utils.ResourcesUtils;
@@ -14,15 +13,13 @@ import io.vertx.ext.auth.jwt.JWTAuth;
 import io.vertx.ext.auth.jwt.JWTAuthOptions;
 import io.vertx.ext.web.Router;
 import io.vertx.ext.web.handler.*;
-import io.vertx.ext.web.openapi.OpenAPILoaderOptions;
 import io.vertx.ext.web.openapi.RouterBuilder;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
 import java.nio.file.Path;
-import java.util.Optional;
 
-import static cz.senslog.telemetry.server.ws.AuthorizationType.OAUTH;
+import static cz.senslog.telemetry.server.ws.AuthorizationType.BEARER;
 import static java.util.Objects.requireNonNull;
 
 public final class HttpVertxServer extends AbstractVerticle {
@@ -48,6 +45,8 @@ public final class HttpVertxServer extends AbstractVerticle {
                     openAPIRouterBuilder.rootHandler(CorsHandler.create()
                             .allowedMethod(HttpMethod.GET)
                             .allowedMethod(HttpMethod.POST)
+                            .allowedMethod(HttpMethod.PUT)
+                            .allowedMethod(HttpMethod.DELETE)
 
                             .allowedHeader("x-requested-with")
                             .allowedHeader("Access-Control-Allow-Origin")
@@ -66,7 +65,7 @@ public final class HttpVertxServer extends AbstractVerticle {
                                         .setType(authConfig.getString("keystore.type"))
                                         .setPassword(authConfig.getString("keystore.password")))));
 
-                        openAPIRouterBuilder.securityHandler(OAUTH.getSecuritySchemaKey(), authHandler);
+                        openAPIRouterBuilder.securityHandler(BEARER.getSecuritySchemaKey(), authHandler);
                     }
 
 
@@ -115,11 +114,23 @@ public final class HttpVertxServer extends AbstractVerticle {
                     openAPIRouterBuilder.operation("driverIdActionIdUnitIdGET").handler(apiHandler::driverIdActionIdUnitIdGET);
                     openAPIRouterBuilder.operation("driverIdUnitIdActionIdGET").handler(apiHandler::driverIdUnitIdActionIdGET);
                     openAPIRouterBuilder.operation("driverIdUnitIdActionIdEventsGET").handler(apiHandler::driverIdUnitIdActionIdEventsGET);
+                    openAPIRouterBuilder.operation("driverIdUnitIdActionIdEventsPOST").handler(apiHandler::driverIdUnitIdActionIdEventsPOST);
 
                     openAPIRouterBuilder.operation("eventIdGET").handler(apiHandler::eventIdGET);
+                    openAPIRouterBuilder.operation("eventIdPUT").handler(apiHandler::eventIdPUT);
                     openAPIRouterBuilder.operation("eventIdObservationsGET").handler(apiHandler::eventIdObservationsGET);
                     openAPIRouterBuilder.operation("eventIdLocationsGET").handler(apiHandler::eventIdLocationsGET);
 
+                    openAPIRouterBuilder.operation("alertIdGET").handler(apiHandler::alertIdGET);
+                    openAPIRouterBuilder.operation("alertIdPUT").handler(apiHandler::alertIdPUT);
+                    openAPIRouterBuilder.operation("alertIdDELETE").handler(apiHandler::alertIdDELETE);
+                    openAPIRouterBuilder.operation("eventIdAlertsGET").handler(apiHandler::eventIdAlertsGET);
+                    openAPIRouterBuilder.operation("driverIdActionIdUnitIdAlertIdGET").handler(apiHandler::driverIdActionIdUnitIdAlertIdGET);
+                    openAPIRouterBuilder.operation("campaignIdUnitsAlertsGET").handler(apiHandler::campaignIdUnitsAlertsGET);
+                    openAPIRouterBuilder.operation("campaignIdUnitsAlertsPOST").handler(apiHandler::campaignIdUnitsAlertsPOST);
+                    openAPIRouterBuilder.operation("campaignIdUnitIdAlertsGET").handler(apiHandler::campaignIdUnitIdAlertsGET);
+                    openAPIRouterBuilder.operation("driverIdActionIdUnitIdAlertsGET").handler(apiHandler::driverIdActionIdUnitIdAlertsGET);
+
                     Router mainRouter = openAPIRouterBuilder.createRouter();
 
                     mainRouter.route().failureHandler(ExceptionHandler.createAsJSON());

+ 2 - 1
src/main/java/cz/senslog/telemetry/server/ws/AuthorizationType.java

@@ -1,7 +1,8 @@
 package cz.senslog.telemetry.server.ws;
 
 public enum AuthorizationType {
-    OAUTH   ("bearerAuth")
+    NONE(null),
+    BEARER("bearerAuth")
 
     ;
     private final String securitySchemaKey;

+ 337 - 10
src/main/java/cz/senslog/telemetry/server/ws/OpenAPIHandler.java

@@ -1,17 +1,17 @@
 package cz.senslog.telemetry.server.ws;
 
 import cz.senslog.telemetry.app.Application;
+import cz.senslog.telemetry.app.PropertyConfig;
 import cz.senslog.telemetry.database.SortType;
-import cz.senslog.telemetry.database.domain.Location;
-import cz.senslog.telemetry.database.domain.UnitLocation;
-import cz.senslog.telemetry.database.domain.UnitTelemetry;
+import cz.senslog.telemetry.database.domain.*;
 import cz.senslog.telemetry.database.repository.SensLogRepository;
-import cz.senslog.telemetry.database.domain.Filter;
 import cz.senslog.telemetry.utils.TernaryCondition;
 import io.vertx.core.http.HttpServerRequest;
+import io.vertx.core.json.Json;
 import io.vertx.core.json.JsonArray;
 import io.vertx.core.json.JsonObject;
 import io.vertx.ext.web.RoutingContext;
+import io.vertx.json.schema.common.JsonUtil;
 
 
 import java.time.OffsetDateTime;
@@ -33,8 +33,7 @@ import static java.time.OffsetDateTime.ofInstant;
 import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME;
 import static java.util.Collections.emptyList;
 import static java.util.Comparator.comparing;
-import static java.util.stream.Collectors.groupingBy;
-import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.*;
 
 public class OpenAPIHandler {
 
@@ -44,7 +43,7 @@ public class OpenAPIHandler {
     private static final ContentType DEFAULT_RESPONSE_FORMAT = JSON;
 
     private static final BiFunction<OffsetDateTime, ZoneId, String> DATE_TIME_FORMATTER = (dateTime, zoneId) ->
-            ofInstant(dateTime.toInstant(), zoneId).format(ISO_OFFSET_DATE_TIME);
+            dateTime != null ? ofInstant(dateTime.toInstant(), zoneId).format(ISO_OFFSET_DATE_TIME) : null;
 
     private final SensLogRepository repo;
 
@@ -71,7 +70,7 @@ public class OpenAPIHandler {
                             "build", Application.BUILD_VERSION,
                             "uptime", Application.uptimeFormatted(),
                             "uptimeMillis", Application.uptime(),
-                            "authType", AuthorizationType.OAUTH.name()
+                            "authType", PropertyConfig.getInstance().authConfig().getDisabled() ? AuthorizationType.NONE : AuthorizationType.BEARER
                         ).encode()));
     }
 
@@ -414,7 +413,7 @@ public class OpenAPIHandler {
 
         List<String> paramSort = rc.queryParam("sort");
         SortType sortType = TernaryCondition.<SortType>ternaryIf(paramSort::isEmpty, SortType.ASC, () -> {
-           paramsJson.put("sort", paramLimitPerUnit.get(0));
+           paramsJson.put("sort", paramSort.get(0));
            return SortType.of(paramSort.get(0));
         });
 
@@ -823,7 +822,8 @@ public class OpenAPIHandler {
                                 "Campaign@NavigationLink", String.format("%s/campaigns/%d", host, u.getCampaignId()),
                                 "Sensors@NavigationLink", String.format("%s/campaigns/%d/units/%d/sensors", host, u.getCampaignId(), u.getUnitId()),
                                 "Observations@NavigationLink", String.format("%s/campaigns/%d/units/%d/observations", host, u.getCampaignId(), u.getUnitId()),
-                                "Locations@NavigationLink", String.format("%s/campaigns/%d/units/%d/observations/locations", host, u.getCampaignId(), u.getUnitId())
+                                "Locations@NavigationLink", String.format("%s/campaigns/%d/units/%d/observations/locations", host, u.getCampaignId(), u.getUnitId()),
+                                "Alerts@NavigationLink", String.format("%s/campaigns/%d/units/%d/alerts", host, u.getCampaignId(), u.getUnitId())
                         ) : JsonObject.of()).mergeIn(JsonObject.of(
                                 "unitId", u.getUnitId(),
                                 "name", u.getName(),
@@ -1246,6 +1246,7 @@ public class OpenAPIHandler {
                                 "Event@NavigationLink", String.format("%s/events/%d",host, e.getId())
                         ) : JsonObject.of()).mergeIn(JsonObject.of(
                                 "id", e.getId(),
+                                "status", e.getStatus(),
                                 "fromTime", DATE_TIME_FORMATTER.apply(e.getFromTime(), zone),
                                 "toTime", DATE_TIME_FORMATTER.apply(e.getToTime(), zone)
                         ))).collect(toList())).encode()))
@@ -1276,6 +1277,7 @@ public class OpenAPIHandler {
                                 "driverId", e.getDriverId(),
                                 "actionId", e.getActionId(),
                                 "unitId", e.getUnitId(),
+                                "status", e.getStatus(),
                                 "fromTime", DATE_TIME_FORMATTER.apply(e.getFromTime(), zone),
                                 "toTime", DATE_TIME_FORMATTER.apply(e.getToTime(), zone)
                         )).encode()))
@@ -1581,4 +1583,329 @@ public class OpenAPIHandler {
                     .ifPresent(validateAndSave);
         }
     }
+
+    public void alertsGET(RoutingContext rc) {
+        String host =  hostURLFull(rc.request());
+        JsonObject paramsJson = new JsonObject();
+
+        List<String> paramFrom = rc.queryParam("from");
+        OffsetDateTime from = TernaryCondition.<OffsetDateTime>ternaryIf(paramFrom::isEmpty, () -> null, () -> {
+            paramsJson.put("from", paramFrom.get(0));
+            return OffsetDateTime.parse(paramFrom.get(0));
+        });
+
+        List<String> paramTo = rc.queryParam("to");
+        OffsetDateTime to = TernaryCondition.<OffsetDateTime>ternaryIf(paramTo::isEmpty, () -> null,() -> {
+            paramsJson.put("to", paramTo.get(0));
+            return OffsetDateTime.parse(paramTo.get(0));
+        });
+
+        List<String> paramZone = rc.queryParam("zone");
+        ZoneId zone = TernaryCondition.<ZoneId>ternaryIf(paramZone::isEmpty, ZoneId.of("UTC"), () -> {
+            paramsJson.put("zone", paramZone.get(0));
+            return ZoneId.of(paramZone.get(0));
+        });
+
+        List<String> paramOffset = rc.queryParam("offset");
+        int offset = TernaryCondition.<Integer>ternaryIf(paramOffset::isEmpty, 0, () -> {
+            paramsJson.put("offset", paramOffset.get(0));
+            return Integer.parseInt(paramOffset.get(0));
+        });
+
+        List<String> paramLimit = rc.queryParam("limit");
+        int limit = TernaryCondition.<Integer>ternaryIf(paramLimit::isEmpty, DEFAULT_MAX_DATA_LIMIT, () -> {
+            paramsJson.put("limit", paramLimit.get(0));
+            return Integer.parseInt(paramLimit.get(0));
+        });
+
+        List<String> paramSort = rc.queryParam("sort");
+        SortType sort = TernaryCondition.<SortType>ternaryIf(paramSort::isEmpty, SortType.ASC, () -> {
+            paramsJson.put("sort", paramSort.get(0));
+            return SortType.of(paramSort.get(0));
+        });
+
+        List<String> paramNavigationLinks = rc.queryParam("navigationLinks");
+        boolean navigationLinks = TernaryCondition.<Boolean>ternaryIf(paramNavigationLinks::isEmpty, true, () -> {
+            paramsJson.put("navigationLinks", paramNavigationLinks.get(0));
+            return parseBoolean(paramNavigationLinks.get(0));
+        });
+    }
+
+    public void alertIdGET(RoutingContext rc) {
+        String host =  hostURLFull(rc.request());
+
+        long alertId = Long.parseLong(rc.pathParam("alertId"));
+
+        List<String> paramZone = rc.queryParam("zone");
+        ZoneId zone = paramZone.isEmpty() ? DEFAULT_ZONE_ID : ZoneId.of(paramZone.get(0));
+
+        List<String> paramNavigationLinks = rc.queryParam("navigationLinks");
+        boolean navigationLinks = paramNavigationLinks.isEmpty() ? DEFAULT_NAVIGATION_LINKS : parseBoolean(paramNavigationLinks.get(0));
+
+        repo.findAlertById(alertId)
+                .onSuccess(a -> rc.response().end((navigationLinks ? JsonObject.of(
+                        "self@NavigationLink", String.format("%s/alerts/%d", host, a.getId()),
+                        "CampaignUnit@NavigationLink", String.format("%s/campaigns/%d/units/%s", host, a.getCampaignId(), a.getUnitId()),
+                        "Events@NavigationLink", String.format("%s/alerts/%s/events", host, a.getId())
+                ) : JsonObject.of()).mergeIn(JsonObject.of(
+                        "id", a.getId(),
+                        "unitId", a.getUnitId(),
+                        "message", a.getMessage(),
+                        "status", a.getStatus(),
+                        "timestamp", DATE_TIME_FORMATTER.apply(a.getTimestamp(), zone)
+                )).encode()))
+                .onFailure(rc::fail);
+    }
+
+    public void eventIdAlertsGET(RoutingContext rc) {
+        String host =  hostURLFull(rc.request());
+
+        long eventId = Long.parseLong(rc.pathParam("eventId"));
+
+        List<String> paramZone = rc.queryParam("zone");
+        ZoneId zone = paramZone.isEmpty() ? DEFAULT_ZONE_ID : ZoneId.of(paramZone.get(0));
+
+        Set<AlertStatus> statuses = rc.queryParam("status").stream().map(AlertStatus::valueOf).collect(toSet());
+
+        List<String> paramSort = rc.queryParam("sort");
+        SortType sort = TernaryCondition.<SortType>ternaryIf(paramSort::isEmpty, SortType.ASC, () -> SortType.of(paramSort.get(0)));
+
+        List<String> paramNavigationLinks = rc.queryParam("navigationLinks");
+        boolean navigationLinks = paramNavigationLinks.isEmpty() ? DEFAULT_NAVIGATION_LINKS : parseBoolean(paramNavigationLinks.get(0));
+
+        repo.findAlertsByEventId(eventId, statuses, sort)
+                .onSuccess(data -> rc.response().end(new JsonArray(
+                        data.stream().map(a -> (navigationLinks ? JsonObject.of(
+                                "Alert@NavigationLink", String.format("%s/alerts/%d", host, a.getId())
+                        ) : JsonObject.of()).mergeIn(JsonObject.of(
+                                "id", a.getId(),
+                                "message", a.getMessage(),
+                                "status", a.getStatus(),
+                                "timestamp", DATE_TIME_FORMATTER.apply(a.getTimestamp(), zone)
+                        ))).collect(toList())).encode()))
+                .onFailure(rc::fail);
+    }
+
+    public void driverIdActionIdUnitIdAlertIdGET(RoutingContext rc) {
+        String host =  hostURLFull(rc.request());
+
+        long eventId = Long.parseLong(rc.pathParam("eventId"));
+        long alertId = Long.parseLong(rc.pathParam("alertId"));
+
+        List<String> paramZone = rc.queryParam("zone");
+        ZoneId zone = paramZone.isEmpty() ? DEFAULT_ZONE_ID : ZoneId.of(paramZone.get(0));
+
+        List<String> paramNavigationLinks = rc.queryParam("navigationLinks");
+        boolean navigationLinks = paramNavigationLinks.isEmpty() ? DEFAULT_NAVIGATION_LINKS : parseBoolean(paramNavigationLinks.get(0));
+
+        repo.findAlertByIdAndEventId(alertId, eventId)
+                .onSuccess(a -> rc.response().end((navigationLinks ? JsonObject.of(
+                        "self@NavigationLink", String.format("%s/alerts/%d", host, a.getId()),
+                        "Event@NavigationLink", String.format("%s/events/%d", host, a.getEventId())
+                ) : JsonObject.of()).mergeIn(JsonObject.of(
+                        "id", a.getId(),
+                        "message", a.getMessage(),
+                        "status", a.getStatus(),
+                        "timestamp", DATE_TIME_FORMATTER.apply(a.getTimestamp(), zone)
+                )).encode()))
+                .onFailure(rc::fail);
+    }
+
+    public void campaignIdUnitsAlertsGET(RoutingContext rc) {
+        String host =  hostURLFull(rc.request());
+
+        long campaignId = Long.parseLong(rc.pathParam("campaignId"));
+
+        List<String> paramFrom = rc.queryParam("from");
+        OffsetDateTime from = TernaryCondition.<OffsetDateTime>ternaryIf(paramFrom::isEmpty, () -> null, () -> OffsetDateTime.parse(paramFrom.get(0)));
+
+        List<String> paramTo = rc.queryParam("to");
+        OffsetDateTime to = TernaryCondition.<OffsetDateTime>ternaryIf(paramTo::isEmpty, () -> null,() -> OffsetDateTime.parse(paramTo.get(0)));
+
+        List<String> paramZone = rc.queryParam("zone");
+        ZoneId zone = TernaryCondition.<ZoneId>ternaryIf(paramZone::isEmpty, ZoneId.of("UTC"), () -> ZoneId.of(paramZone.get(0)));
+
+        Set<AlertStatus> statuses = rc.queryParam("status").stream().map(AlertStatus::valueOf).collect(toSet());
+
+        List<String> paramSort = rc.queryParam("sort");
+        SortType sort = TernaryCondition.<SortType>ternaryIf(paramSort::isEmpty, SortType.ASC, () -> SortType.of(paramSort.get(0)));
+
+        List<String> paramNavigationLinks = rc.queryParam("navigationLinks");
+        boolean navigationLinks = paramNavigationLinks.isEmpty() ? DEFAULT_NAVIGATION_LINKS : parseBoolean(paramNavigationLinks.get(0));
+
+        repo.findAlertsByCampaignId(campaignId, from, to, statuses, sort)
+                .onSuccess(data -> rc.response().end(new JsonArray(
+                        data.stream().map(a -> (navigationLinks ? JsonObject.of(
+                                "Alert@NavigationLink", String.format("%s/alerts/%d", host, a.getId())
+                        ) : JsonObject.of()).mergeIn(JsonObject.of(
+                                "id", a.getId(),
+                                "unitId", a.getUnitId(),
+                                "message", a.getMessage(),
+                                "status", a.getStatus(),
+                                "timestamp", DATE_TIME_FORMATTER.apply(a.getTimestamp(), zone)
+                        ))).collect(toList())).encode()))
+                .onFailure(rc::fail);
+    }
+
+    public void campaignIdUnitsAlertsPOST(RoutingContext rc) {
+        long campaignId = Long.parseLong(rc.pathParam("campaignId"));
+
+        final Function<JsonObject, UnitAlert> jsonToAlert = json -> UnitAlert.of(
+                json.getLong("unitId"),
+                json.getString("message"),
+                OffsetDateTime.parse(json.getString("timestamp")),
+                AlertStatus.CREATED
+        );
+
+        Object reqBody = Json.decodeValue(rc.body().buffer());
+        if (JsonUtil.isArray(reqBody)) {
+            List<UnitAlert> alerts = ((JsonArray)reqBody).stream().map(JsonObject.class::cast).map(jsonToAlert).toList();
+            // TODO send notifications
+            repo.saveAlerts(alerts)
+                    .onSuccess(ids -> rc.response().end(JsonObject.of(
+                        "ids", new JsonArray(ids),
+                        "message", String.format("Saved: %s/%s", ids.size(), alerts.size())
+                    ).encode()))
+                    .onFailure(rc::fail);
+
+        } else if (JsonUtil.isObject(reqBody)) {
+            UnitAlert alert = jsonToAlert.apply((JsonObject) reqBody);
+            // TODO send notifications
+            repo.saveAlert(alert)
+                    .onSuccess(id -> rc.response().end(JsonObject.of(
+                            "id", 15
+                    ).encode()))
+                    .onFailure(rc::fail);
+
+        } else {
+            throw new HttpIllegalArgumentException(400, "Request body is not either JSON Object or JSON Array.");
+        }
+    }
+
+    public void campaignIdUnitIdAlertsGET(RoutingContext rc) {
+        String host =  hostURLFull(rc.request());
+
+        long campaignId = Long.parseLong(rc.pathParam("campaignId"));
+        long unitId = Long.parseLong(rc.pathParam("unitId"));
+
+        List<String> paramFrom = rc.queryParam("from");
+        OffsetDateTime from = TernaryCondition.<OffsetDateTime>ternaryIf(paramFrom::isEmpty, () -> null, () -> OffsetDateTime.parse(paramFrom.get(0)));
+
+        List<String> paramTo = rc.queryParam("to");
+        OffsetDateTime to = TernaryCondition.<OffsetDateTime>ternaryIf(paramTo::isEmpty, () -> null,() -> OffsetDateTime.parse(paramTo.get(0)));
+
+        List<String> paramZone = rc.queryParam("zone");
+        ZoneId zone = TernaryCondition.<ZoneId>ternaryIf(paramZone::isEmpty, ZoneId.of("UTC"), () -> ZoneId.of(paramZone.get(0)));
+
+        Set<AlertStatus> statuses = rc.queryParam("status").stream().map(AlertStatus::valueOf).collect(toSet());
+
+        List<String> paramSort = rc.queryParam("sort");
+        SortType sort = TernaryCondition.<SortType>ternaryIf(paramSort::isEmpty, SortType.ASC, () -> SortType.of(paramSort.get(0)));
+
+        List<String> paramNavigationLinks = rc.queryParam("navigationLinks");
+        boolean navigationLinks = paramNavigationLinks.isEmpty() ? DEFAULT_NAVIGATION_LINKS : parseBoolean(paramNavigationLinks.get(0));
+
+        repo.findAlertsByCampaignIdAndUnitId(campaignId, unitId, from, to, statuses, sort)
+                .onSuccess(data -> rc.response().end(new JsonArray(
+                        data.stream().map(a -> (navigationLinks ? JsonObject.of(
+                                "Alert@NavigationLink", String.format("%s/alerts/%d", host, a.getId())
+                        ) : JsonObject.of()).mergeIn(JsonObject.of(
+                                "id", a.getId(),
+                                "message", a.getMessage(),
+                                "status", a.getStatus(),
+                                "timestamp", DATE_TIME_FORMATTER.apply(a.getTimestamp(), zone)
+                        ))).collect(toList())).encode()))
+                .onFailure(rc::fail);
+    }
+
+    public void driverIdActionIdUnitIdAlertsGET(RoutingContext rc) {
+        String host =  hostURLFull(rc.request());
+
+        long driverId = Long.parseLong(rc.pathParam("driverId"));
+        long actionId = Long.parseLong(rc.pathParam("actionId"));
+        long unitId = Long.parseLong(rc.pathParam("unitId"));
+
+        List<String> paramZone = rc.queryParam("zone");
+        ZoneId zone = TernaryCondition.<ZoneId>ternaryIf(paramZone::isEmpty, ZoneId.of("UTC"), () -> ZoneId.of(paramZone.get(0)));
+
+        Set<AlertStatus> statuses = rc.queryParam("status").stream().map(AlertStatus::valueOf).collect(toSet());
+
+        List<String> paramSort = rc.queryParam("sort");
+        SortType sort = TernaryCondition.<SortType>ternaryIf(paramSort::isEmpty, SortType.ASC, () -> SortType.of(paramSort.get(0)));
+
+        List<String> paramNavigationLinks = rc.queryParam("navigationLinks");
+        boolean navigationLinks = paramNavigationLinks.isEmpty() ? DEFAULT_NAVIGATION_LINKS : parseBoolean(paramNavigationLinks.get(0));
+
+        repo.findAlertsByDriverIdAndActionIdAndUnitId(driverId, actionId, unitId, statuses, sort)
+                .onSuccess(data -> rc.response().end(new JsonArray(
+                        data.stream().map(a -> (navigationLinks ? JsonObject.of(
+                                "Alert@NavigationLink", String.format("%s/alerts/%d", host, a.getId())
+                        ) : JsonObject.of()).mergeIn(JsonObject.of(
+                                "id", a.getId(),
+                                "message", a.getMessage(),
+                                "status", a.getStatus(),
+                                "timestamp", DATE_TIME_FORMATTER.apply(a.getTimestamp(), zone)
+                        ))).collect(toList())).encode()))
+                .onFailure(rc::fail);
+    }
+
+    public void driverIdUnitIdActionIdEventsPOST(RoutingContext rc) {
+
+        long driverId = Long.parseLong(rc.pathParam("driverId"));
+        long actionId = Long.parseLong(rc.pathParam("actionId"));
+        long unitId = Long.parseLong(rc.pathParam("unitId"));
+
+        final Function<JsonObject, Event> jsonToEvent = json -> Event.of(
+                driverId, actionId, unitId,
+                OffsetDateTime.parse(json.getString("fromTime")),
+                json.containsKey("toTime") ? OffsetDateTime.parse(json.getString("toTime")) : null
+        );
+
+        Object reqBody = Json.decodeValue(rc.body().buffer());
+        if (JsonUtil.isArray(reqBody)) {
+            List<Event> events = rc.body().asJsonArray().stream().map(JsonObject.class::cast).map(jsonToEvent).toList();
+            repo.saveEvents(events)
+                    .onSuccess(ids -> rc.response().end(JsonObject.of(
+                            "ids", new JsonArray(ids),
+                            "message", String.format("Saved: %d/%d", ids.size(), events.size())
+                    ).encode()))
+                    .onFailure(rc::fail);
+
+        } else if (JsonUtil.isObject(reqBody)) {
+            repo.saveEvent(jsonToEvent.apply(rc.body().asJsonObject()))
+                    .onSuccess(id -> rc.response().end(JsonObject.of(
+                            "id", id
+                    ).encode()))
+                    .onFailure(rc::fail);
+        } else {
+            throw new HttpIllegalArgumentException(400, "Request body is not either JSON Object or JSON Array.");
+        }
+    }
+
+    public void eventIdPUT(RoutingContext rc) {
+        long eventId = Long.parseLong(rc.pathParam("eventId"));
+        repo.updateEvent(eventId, OffsetDateTime.parse(rc.body().asJsonObject().getString("toTime")))
+                .onSuccess(id -> rc.response().end(JsonObject.of(
+                        "id", id
+                ).encode()))
+                .onFailure(rc::fail);
+    }
+
+    public void alertIdPUT(RoutingContext rc) {
+        long alertId = Long.parseLong(rc.pathParam("alertId"));
+        repo.updateAlert(alertId, AlertStatus.valueOf(rc.body().asJsonObject().getString("status")))
+                .onSuccess(id -> rc.response().end(JsonObject.of(
+                        "id", id
+                ).encode()))
+                .onFailure(rc::fail);
+    }
+
+    public void alertIdDELETE(RoutingContext rc) {
+        long alertId = Long.parseLong(rc.pathParam("alertId"));
+        repo.updateAlert(alertId, AlertStatus.DELETED)
+                .onSuccess(id -> rc.response().end(JsonObject.of(
+                        "id", id
+                ).encode()))
+                .onFailure(rc::fail);
+    }
 }

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 459 - 40
src/main/resources/openAPISpec.yaml


+ 67 - 1
src/test/java/cz/senslog/telemetry/MockSensLogRepository.java

@@ -17,11 +17,26 @@ public class MockSensLogRepository implements SensLogRepository {
     private static final Instant BASE_INSTANT_TIMESTAMP = LocalDateTime.of(2023, 1, 1, 0, 0).toInstant(ZoneOffset.UTC);
 
     @Override
-    public Future<Integer> updateActionByDriverUnit(DriverAction data) {
+    public Future<Integer> updateEvent(DriverAction data) {
         return Future.succeededFuture(1);
     }
 
     @Override
+    public Future<Long> updateEvent(long eventId, OffsetDateTime endTime) {
+        return Future.succeededFuture(1L);
+    }
+
+    @Override
+    public Future<Long> saveEvent(Event event) {
+        return Future.succeededFuture(1L);
+    }
+
+    @Override
+    public Future<List<Long>> saveEvents(List<Event> events) {
+        return Future.succeededFuture(List.of(1L,2L,3L));
+    }
+
+    @Override
     public Future<Integer> saveTelemetry(UnitTelemetry data) {
         return Future.succeededFuture(1);
     }
@@ -400,4 +415,55 @@ public class MockSensLogRepository implements SensLogRepository {
     public Future<List<UnitLocation>> findUnitsLocationsByCampaignId(long campaignId, int limitPerUnit, OffsetDateTime from, OffsetDateTime to, ZoneId zone, SortType sort, List<Filter> filters) {
         return Future.succeededFuture(mockUnitLocations());
     }
+
+    @Override
+    public Future<CampaignUnitAlert> findAlertById(long alertId) {
+        OffsetDateTime baseTimestamp = OffsetDateTime.ofInstant(BASE_INSTANT_TIMESTAMP, ZoneOffset.UTC);
+        return Future.succeededFuture(CampaignUnitAlert.of(1, 1, 1000, "mock(message)", baseTimestamp, AlertStatus.CREATED));
+    }
+
+    @Override
+    public Future<Long> updateAlert(long alertId, AlertStatus status) {
+        return Future.succeededFuture(1L);
+    }
+
+    @Override
+    public Future<Long> saveAlert(UnitAlert alert) {
+        return Future.succeededFuture(1L);
+    }
+
+    @Override
+    public Future<List<Long>> saveAlerts(List<UnitAlert> alerts) {
+        return Future.succeededFuture(List.of(1L, 2L, 3L));
+    }
+
+    @Override
+    public Future<List<Alert>> findAlertsByEventId(long eventId, Set<AlertStatus> statusFilter, SortType sortType) {
+        OffsetDateTime baseTimestamp = OffsetDateTime.ofInstant(BASE_INSTANT_TIMESTAMP, ZoneOffset.UTC);
+        return Future.succeededFuture(List.of(Alert.of(1, "mock(message)", baseTimestamp, AlertStatus.CREATED)));
+    }
+
+    @Override
+    public Future<EventAlert> findAlertByIdAndEventId(long alertId, long eventId) {
+        OffsetDateTime baseTimestamp = OffsetDateTime.ofInstant(BASE_INSTANT_TIMESTAMP, ZoneOffset.UTC);
+        return Future.succeededFuture(EventAlert.of(1, 1, 1000, "mock(message)", baseTimestamp, AlertStatus.CREATED));
+    }
+
+    @Override
+    public Future<List<CampaignUnitAlert>> findAlertsByCampaignId(long campaignId, OffsetDateTime from, OffsetDateTime to, Set<AlertStatus> statusFilter, SortType sort) {
+        OffsetDateTime baseTimestamp = OffsetDateTime.ofInstant(BASE_INSTANT_TIMESTAMP, ZoneOffset.UTC);
+        return Future.succeededFuture(List.of(CampaignUnitAlert.of(1, 1, 1000, "mock(message)", baseTimestamp, AlertStatus.CREATED)));
+    }
+
+    @Override
+    public Future<List<UnitAlert>> findAlertsByCampaignIdAndUnitId(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, Set<AlertStatus> statusFilter, SortType sort) {
+        OffsetDateTime baseTimestamp = OffsetDateTime.ofInstant(BASE_INSTANT_TIMESTAMP, ZoneOffset.UTC);
+        return Future.succeededFuture(List.of(UnitAlert.of(1, 1, "mock(message)", baseTimestamp, AlertStatus.CREATED)));
+    }
+
+    @Override
+    public Future<List<Alert>> findAlertsByDriverIdAndActionIdAndUnitId(long driverId, long actionId, long unitId, Set<AlertStatus> statusFilter, SortType sort) {
+        OffsetDateTime baseTimestamp = OffsetDateTime.ofInstant(BASE_INSTANT_TIMESTAMP, ZoneOffset.UTC);
+        return Future.succeededFuture(List.of(Alert.of(1, "mock(message)", baseTimestamp, AlertStatus.CREATED)));
+    }
 }

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

@@ -85,7 +85,7 @@ class MapLogRepositoryTest {
 
         DriverAction dr = new DriverAction(1000, driverId, actionId, timestamp);
 
-        repo.updateActionByDriverUnit(dr).onComplete(testContext.succeeding(res -> testContext.verify(() -> {
+        repo.updateEvent(dr).onComplete(testContext.succeeding(res -> testContext.verify(() -> {
             assertThat(res).isGreaterThan(0);
             testContext.completeNow();
         })));

+ 2 - 2
testing/access_token.json

@@ -1,6 +1,6 @@
 {
-  "access_token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ii0xbW13UG9vM25ZeEF1akt2LVRieCJ9.eyJpc3MiOiJodHRwczovL2Rldi0wdXF2eHJyemJseWlyeXIyLnVzLmF1dGgwLmNvbS8iLCJzdWIiOiJmQW4xNlgzbkFhWFVkTnZYVTZmSjhtaEdzOTY1MHpCOEBjbGllbnRzIiwiYXVkIjoiaHR0cDovLzEyNy4wLjAuMTo4MDgwIiwiaWF0IjoxNzA2NTM3NDEwLCJleHAiOjE3MDkxMjk0MTAsImF6cCI6ImZBbjE2WDNuQWFYVWROdlhVNmZKOG1oR3M5NjUwekI4Iiwic2NvcGUiOiJ0ZWxlbWV0cnk6cmVhZCIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyIsInBlcm1pc3Npb25zIjpbInRlbGVtZXRyeTpyZWFkIl19.g-XGG0qUFyOj2lVdDlL58FgyASfneIc1RQHS6AtRyrl-CwdrFqge1ClSARd7tAuSFlMe_sFy3IJZVgrTXnlxiNoyCu_y85wM6tsQ_ZqgpnpM-q2fokb3k7Tsi4kKxe3maRoZXzOAWofCCYkWzpsl3IXiu4SL1kniyCWea9D3T5vXytnzzpjhplqFgbUH2q367pGV5_IgJ_GN7WokB6z8cJL9gWWCquCZdNXO85v3SGW-OLYd7dCJUYlVAsp69TNaZwZRiZ3K5J2U7sSqwWhvyBHYlHQVFUP1MyRYhFyoK0Z-2z2XEf1zHGLe8qmbJm7V6xHtnUhUvEX6Kakj-Fjpow",
-  "scope":"telemetry:read",
+  "access_token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ii0xbW13UG9vM25ZeEF1akt2LVRieCJ9.eyJpc3MiOiJodHRwczovL2Rldi0wdXF2eHJyemJseWlyeXIyLnVzLmF1dGgwLmNvbS8iLCJzdWIiOiJmQW4xNlgzbkFhWFVkTnZYVTZmSjhtaEdzOTY1MHpCOEBjbGllbnRzIiwiYXVkIjoiaHR0cDovLzEyNy4wLjAuMTo4MDgwIiwiaWF0IjoxNzA5MjA5MTY5LCJleHAiOjE3MTE4MDExNjksImF6cCI6ImZBbjE2WDNuQWFYVWROdlhVNmZKOG1oR3M5NjUwekI4Iiwic2NvcGUiOiJ0ZWxlbWV0cnk6cmVhZCB0ZWxlbWV0cnk6d3JpdGUiLCJndHkiOiJjbGllbnQtY3JlZGVudGlhbHMiLCJwZXJtaXNzaW9ucyI6WyJ0ZWxlbWV0cnk6cmVhZCIsInRlbGVtZXRyeTp3cml0ZSJdfQ.SwU9Lr-IdpwaemGdqlSN7qLdaYjy2TQwUEBZSh2cIujes9-HS9I82qheS6BRm5tBoByWfRJKUMg2ifoqLfF_MH59vd_71PHUhqHketcic7hP5iNzOvWLd0Ji1NrFE0-1JBV-ZZZhIx_YwCamjV0BAhCXm_-8936yW0PuRGKgltNYB9Q1S5emdYrOnWENyrd6al4gPcl7Ta5fj64-SV6hnkfzRjuNyatwsJh9MIczSOlYZG_NWq1Rfje1roaUGTpqKBhXIzMWIGsYpLarw_s1lPif9Udwf-g1h9ErCtZ6nx-xD2Cy3nuF4aHaE_eDy0H1RDcyN0mSf9BsXKt--HBMFg",
+  "scope":"telemetry:read telemetry:write",
   "expires_in":2592000,
   "token_type":"Bearer"
 }

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů