@fleetbase/fleetops-engine 0.6.20 → 0.6.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/addon/components/custom-entity/form.hbs +14 -14
  2. package/addon/components/device/card.hbs +1 -0
  3. package/addon/components/device/card.js +3 -0
  4. package/addon/components/device/details.hbs +92 -43
  5. package/addon/components/device/form.hbs +108 -60
  6. package/addon/components/device/form.js +36 -8
  7. package/addon/components/device/manager.hbs +29 -0
  8. package/addon/components/device/manager.js +95 -0
  9. package/addon/components/device/panel-header.hbs +32 -0
  10. package/addon/components/device/panel-header.js +3 -0
  11. package/addon/components/device/pill.hbs +16 -0
  12. package/addon/components/device/pill.js +3 -0
  13. package/addon/components/driver/details.hbs +4 -0
  14. package/addon/components/driver/details.js +19 -1
  15. package/addon/components/driver/form.hbs +14 -3
  16. package/addon/components/driver/form.js +49 -47
  17. package/addon/components/driver/pill.hbs +17 -0
  18. package/addon/components/driver/pill.js +3 -0
  19. package/addon/components/entity/form.hbs +7 -5
  20. package/addon/components/layout/fleet-ops-sidebar.js +12 -12
  21. package/addon/components/map/drawer/device-event-listing.hbs +64 -0
  22. package/addon/components/map/drawer/device-event-listing.js +181 -0
  23. package/addon/components/map/drawer/position-listing.hbs +100 -0
  24. package/addon/components/map/drawer/position-listing.js +455 -0
  25. package/addon/components/map/drawer.js +2 -0
  26. package/addon/components/map/leaflet-live-map.hbs +7 -2
  27. package/addon/components/modals/attach-device.hbs +18 -0
  28. package/addon/components/modals/attach-device.js +3 -0
  29. package/addon/components/order/details/detail.hbs +2 -54
  30. package/addon/components/order/details/detail.js +1 -0
  31. package/addon/components/order/details/payload.hbs +6 -4
  32. package/addon/components/order/details/payload.js +2 -0
  33. package/addon/components/order/pill.hbs +34 -0
  34. package/addon/components/order/pill.js +3 -0
  35. package/addon/components/order-config-manager/custom-fields.js +1 -1
  36. package/addon/components/positions-replay.hbs +339 -0
  37. package/addon/components/positions-replay.js +409 -0
  38. package/addon/components/sensor/details.hbs +64 -38
  39. package/addon/components/sensor/form.hbs +112 -63
  40. package/addon/components/sensor/form.js +36 -24
  41. package/addon/components/sensor/panel-header.hbs +32 -0
  42. package/addon/components/sensor/panel-header.js +3 -0
  43. package/addon/components/telematic/details.hbs +40 -16
  44. package/addon/components/telematic/form.hbs +63 -64
  45. package/addon/components/telematic/form.js +73 -4
  46. package/addon/components/vehicle/card.hbs +2 -2
  47. package/addon/components/vehicle/details.hbs +4 -0
  48. package/addon/components/vehicle/details.js +19 -1
  49. package/addon/components/vehicle/form.hbs +4 -0
  50. package/addon/components/vehicle/pill.hbs +34 -0
  51. package/addon/components/vehicle/pill.js +3 -0
  52. package/addon/controllers/analytics/reports/index/edit.js +1 -1
  53. package/addon/controllers/connectivity/devices/index/details.js +22 -1
  54. package/addon/controllers/connectivity/devices/index/edit.js +66 -1
  55. package/addon/controllers/connectivity/devices/index.js +51 -9
  56. package/addon/controllers/connectivity/events/index.js +65 -16
  57. package/addon/controllers/connectivity/sensors/index/details.js +22 -1
  58. package/addon/controllers/connectivity/sensors/index/edit.js +66 -1
  59. package/addon/controllers/connectivity/sensors/index.js +66 -6
  60. package/addon/controllers/connectivity/telematics/index/details.js +22 -1
  61. package/addon/controllers/connectivity/telematics/index/edit.js +66 -1
  62. package/addon/controllers/connectivity/telematics/index.js +20 -11
  63. package/addon/controllers/management/fleets/index/details.js +26 -21
  64. package/addon/controllers/management/fleets/index/edit.js +9 -6
  65. package/addon/controllers/management/vehicles/index/details.js +26 -13
  66. package/addon/controllers/settings/custom-fields.js +6 -0
  67. package/addon/helpers/get-fleet-ops-option-label.js +11 -0
  68. package/addon/routes/connectivity/devices/index/details.js +27 -1
  69. package/addon/routes/connectivity/devices/index/edit.js +27 -1
  70. package/addon/routes/connectivity/sensors/index/details.js +27 -1
  71. package/addon/routes/connectivity/sensors/index/edit.js +27 -1
  72. package/addon/routes/connectivity/telematics/index/details.js +27 -1
  73. package/addon/routes/connectivity/telematics/index/edit.js +27 -1
  74. package/addon/routes/management/drivers/index/details/positions.js +3 -0
  75. package/addon/routes/management/vehicles/index/details/positions.js +3 -0
  76. package/addon/routes.js +4 -0
  77. package/addon/services/movement-tracker.js +81 -30
  78. package/addon/services/position-playback.js +486 -0
  79. package/addon/services/resource-metadata.js +46 -0
  80. package/addon/styles/fleetops-engine.css +157 -0
  81. package/addon/templates/connectivity/devices/index/details/index.hbs +2 -2
  82. package/addon/templates/connectivity/devices/index/details.hbs +15 -2
  83. package/addon/templates/connectivity/devices/index/edit.hbs +1 -1
  84. package/addon/templates/connectivity/events/index.hbs +1 -1
  85. package/addon/templates/connectivity/sensors/index/details/index.hbs +2 -2
  86. package/addon/templates/connectivity/sensors/index/details.hbs +15 -2
  87. package/addon/templates/connectivity/sensors/index/edit.hbs +1 -1
  88. package/addon/templates/connectivity/telematics/index/details/index.hbs +2 -2
  89. package/addon/templates/connectivity/telematics/index/details.hbs +14 -2
  90. package/addon/templates/connectivity/telematics/index/edit.hbs +1 -1
  91. package/addon/templates/management/drivers/index/details/positions.hbs +2 -0
  92. package/addon/templates/management/vehicles/index/details/devices.hbs +1 -2
  93. package/addon/templates/management/vehicles/index/details/positions.hbs +1 -0
  94. package/addon/utils/fleet-ops-options.js +95 -0
  95. package/app/components/device/card.js +1 -0
  96. package/app/components/device/manager.js +1 -0
  97. package/app/components/device/panel-header.js +1 -0
  98. package/app/components/device/pill.js +1 -0
  99. package/app/components/driver/pill.js +1 -0
  100. package/app/components/map/drawer/device-event-listing.js +1 -0
  101. package/app/components/map/drawer/position-listing.js +1 -0
  102. package/app/components/modals/attach-device.js +1 -0
  103. package/app/components/order/pill.js +1 -0
  104. package/app/components/positions-replay.js +1 -0
  105. package/app/components/sensor/panel-header.js +1 -0
  106. package/app/components/vehicle/pill.js +1 -0
  107. package/app/helpers/get-fleet-ops-option-label.js +1 -0
  108. package/app/routes/management/drivers/index/details/positions.js +1 -0
  109. package/app/routes/management/vehicles/index/details/positions.js +1 -0
  110. package/app/services/position-playback.js +1 -0
  111. package/app/services/resource-metadata.js +1 -0
  112. package/app/templates/management/drivers/index/details/positions.js +1 -0
  113. package/app/templates/management/vehicles/index/details/positions.js +1 -0
  114. package/composer.json +1 -1
  115. package/extension.json +1 -1
  116. package/package.json +4 -4
  117. package/server/config/telematics.php +111 -0
  118. package/server/migrations/2025_10_27_000001_add_telematics_integration_fields.php +70 -0
  119. package/server/migrations/2025_10_27_171322_fix_device_column_names.php +107 -0
  120. package/server/migrations/2025_10_27_203023_add_company_uuid_to_device_events_table.php +28 -0
  121. package/server/src/Console/Commands/ReplayVehicleLocations.php +225 -0
  122. package/server/src/Contracts/TelematicProviderDescriptor.php +72 -0
  123. package/server/src/Contracts/TelematicProviderInterface.php +119 -0
  124. package/server/src/Exceptions/TelematicProviderException.php +14 -0
  125. package/server/src/Exceptions/TelematicRateLimitExceededException.php +12 -0
  126. package/server/src/Http/Controllers/Api/v1/DriverController.php +24 -14
  127. package/server/src/Http/Controllers/Api/v1/VehicleController.php +27 -7
  128. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +22 -0
  129. package/server/src/Http/Controllers/Internal/v1/PositionController.php +240 -0
  130. package/server/src/Http/Controllers/Internal/v1/SensorController.php +11 -0
  131. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +141 -0
  132. package/server/src/Http/Controllers/TelematicWebhookController.php +169 -0
  133. package/server/src/Http/Filter/DeviceEventFilter.php +68 -0
  134. package/server/src/Http/Filter/PositionFilter.php +35 -0
  135. package/server/src/Http/Resources/v1/Position.php +44 -0
  136. package/server/src/Jobs/ReplayPositions.php +64 -0
  137. package/server/src/Jobs/SendPositionReplay.php +65 -0
  138. package/server/src/Jobs/SyncTelematicDevicesJob.php +106 -0
  139. package/server/src/Jobs/TestTelematicConnectionJob.php +102 -0
  140. package/server/src/Models/Asset.php +10 -8
  141. package/server/src/Models/Device.php +79 -12
  142. package/server/src/Models/DeviceEvent.php +33 -3
  143. package/server/src/Models/Driver.php +28 -1
  144. package/server/src/Models/Maintenance.php +15 -12
  145. package/server/src/Models/Part.php +2 -0
  146. package/server/src/Models/Payload.php +0 -1
  147. package/server/src/Models/Place.php +4 -1
  148. package/server/src/Models/Position.php +27 -17
  149. package/server/src/Models/Sensor.php +78 -13
  150. package/server/src/Models/Telematic.php +116 -6
  151. package/server/src/Models/TrackingNumber.php +3 -1
  152. package/server/src/Models/Vehicle.php +8 -11
  153. package/server/src/Models/WorkOrder.php +8 -5
  154. package/server/src/Providers/FleetOpsServiceProvider.php +2 -0
  155. package/server/src/Support/Telematics/Providers/AbstractProvider.php +151 -0
  156. package/server/src/Support/Telematics/Providers/FlespiProvider.php +182 -0
  157. package/server/src/Support/Telematics/Providers/GeotabProvider.php +181 -0
  158. package/server/src/Support/Telematics/Providers/SamsaraProvider.php +177 -0
  159. package/server/src/Support/Telematics/TelematicProviderRegistry.php +147 -0
  160. package/server/src/Support/Telematics/TelematicService.php +223 -0
  161. package/server/src/Support/Utils.php +1 -1
  162. package/server/src/routes.php +24 -1
@@ -0,0 +1,119 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Contracts;
4
+
5
+ use Fleetbase\FleetOps\Models\Telematic;
6
+
7
+ /**
8
+ * Interface TelematicProviderInterface.
9
+ *
10
+ * Core contract that all telematics providers must implement.
11
+ * Defines the standard methods for authentication, device discovery,
12
+ * webhook handling, and data normalization.
13
+ */
14
+ interface TelematicProviderInterface
15
+ {
16
+ /**
17
+ * Connect to the provider using the given telematic configuration.
18
+ */
19
+ public function connect(Telematic $telematic): void;
20
+
21
+ /**
22
+ * Test the connection to the provider.
23
+ *
24
+ * @param array $credentials Provider credentials
25
+ *
26
+ * @return array ['success' => bool, 'message' => string, 'metadata' => array]
27
+ */
28
+ public function testConnection(array $credentials): array;
29
+
30
+ /**
31
+ * Fetch devices from the provider.
32
+ *
33
+ * @param array $options Options including limit, cursor, filters
34
+ *
35
+ * @return array ['devices' => array, 'next_cursor' => string|null, 'has_more' => bool]
36
+ */
37
+ public function fetchDevices(array $options = []): array;
38
+
39
+ /**
40
+ * Fetch detailed information for a specific device.
41
+ *
42
+ * @param string $externalId Provider's device identifier
43
+ *
44
+ * @return array Device details
45
+ */
46
+ public function fetchDeviceDetails(string $externalId): array;
47
+
48
+ /**
49
+ * Normalize a device payload from the provider into FleetOps format.
50
+ *
51
+ * @param array $payload Raw device data from provider
52
+ *
53
+ * @return array Normalized device data
54
+ */
55
+ public function normalizeDevice(array $payload): array;
56
+
57
+ /**
58
+ * Normalize an event payload from the provider into FleetOps format.
59
+ *
60
+ * @param array $payload Raw event data from provider
61
+ *
62
+ * @return array Normalized event data
63
+ */
64
+ public function normalizeEvent(array $payload): array;
65
+
66
+ /**
67
+ * Normalize sensor data from the provider into FleetOps format.
68
+ *
69
+ * @param array $payload Raw sensor data from provider
70
+ *
71
+ * @return array Normalized sensor data
72
+ */
73
+ public function normalizeSensor(array $payload): array;
74
+
75
+ /**
76
+ * Validate a webhook signature.
77
+ *
78
+ * @param string $payload Raw webhook payload
79
+ * @param string $signature Signature from webhook headers
80
+ * @param array $credentials Provider credentials
81
+ *
82
+ * @return bool True if signature is valid
83
+ */
84
+ public function validateWebhookSignature(string $payload, string $signature, array $credentials): bool;
85
+
86
+ /**
87
+ * Process a webhook payload from the provider.
88
+ *
89
+ * @param array $payload Webhook payload
90
+ * @param array $headers Webhook headers
91
+ *
92
+ * @return array ['devices' => array, 'events' => array, 'sensors' => array]
93
+ */
94
+ public function processWebhook(array $payload, array $headers = []): array;
95
+
96
+ /**
97
+ * Get the provider's credential schema.
98
+ *
99
+ * @return array Array of field definitions
100
+ */
101
+ public function getCredentialSchema(): array;
102
+
103
+ /**
104
+ * Check if the provider supports webhooks.
105
+ */
106
+ public function supportsWebhooks(): bool;
107
+
108
+ /**
109
+ * Check if the provider supports device discovery.
110
+ */
111
+ public function supportsDiscovery(): bool;
112
+
113
+ /**
114
+ * Get rate limit information for the provider.
115
+ *
116
+ * @return array ['requests_per_minute' => int, 'burst_size' => int]
117
+ */
118
+ public function getRateLimits(): array;
119
+ }
@@ -0,0 +1,14 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Exceptions;
4
+
5
+ use Exception;
6
+
7
+ /**
8
+ * Class TelematicProviderException.
9
+ *
10
+ * Base exception for provider-related errors.
11
+ */
12
+ class TelematicProviderException extends \Exception
13
+ {
14
+ }
@@ -0,0 +1,12 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Exceptions;
4
+
5
+ /**
6
+ * Class TelematicRateLimitExceededException.
7
+ *
8
+ * Exception thrown when provider rate limit is exceeded.
9
+ */
10
+ class TelematicRateLimitExceededException extends ProviderException
11
+ {
12
+ }
@@ -343,23 +343,34 @@ class DriverController extends Controller
343
343
  // check if driver needs a geocoded update to set city and country they are currently in
344
344
  $isGeocodable = Carbon::parse($driver->updated_at)->diffInMinutes(Carbon::now(), false) > 10 || empty($driver->country) || empty($driver->city);
345
345
 
346
- $driver->update([
347
- 'location' => new Point($latitude, $longitude),
348
- 'altitude' => $altitude,
349
- 'heading' => $heading,
350
- 'speed' => $speed,
351
- ]);
346
+ $positionData = [
347
+ 'location' => new Point($latitude, $longitude),
348
+ 'latitude' => $latitude,
349
+ 'longitude' => $longitude,
350
+ 'altitude' => $altitude,
351
+ 'heading' => $heading,
352
+ 'speed' => $speed,
353
+ ];
354
+
355
+ // Append current order to data if applicable
356
+ $order = $driver->getCurrentOrder();
357
+ if ($order) {
358
+ $positionData['order_uuid'] = $order->uuid;
359
+ // Get destination
360
+ $destination = $order->payload?->getPickupOrCurrentWaypoint();
361
+ if ($destination) {
362
+ $positionData['destination_uuid'] = $destination->uuid;
363
+ }
364
+ }
365
+
366
+ $driver->update($positionData);
367
+ $driver->createPosition($positionData);
352
368
 
353
369
  // If vehicle is assigned to driver load it and sync position data
354
370
  $driver->loadMissing('vehicle');
355
371
  if ($driver->vehicle) {
356
- $driver->vehicle->update([
357
- 'location' => new Point($latitude, $longitude),
358
- 'altitude' => $altitude,
359
- 'heading' => $heading,
360
- 'speed' => $speed,
361
- ]);
362
- $driver->vehicle->createPositionWithOrderContext();
372
+ $driver->vehicle->update($positionData);
373
+ $driver->vehicle->createPosition($positionData);
363
374
  broadcast(new VehicleLocationChanged($driver->vehicle, ['driver' => $driver->public_id]));
364
375
  }
365
376
 
@@ -376,7 +387,6 @@ class DriverController extends Controller
376
387
  }
377
388
 
378
389
  broadcast(new DriverLocationChanged($driver));
379
- $driver->createPositionWithOrderContext();
380
390
 
381
391
  return new DriverResource($driver);
382
392
  }
@@ -235,15 +235,35 @@ class VehicleController extends Controller
235
235
  );
236
236
  }
237
237
 
238
- $vehicle->update([
239
- 'location' => new Point($latitude, $longitude),
240
- 'altitude' => $altitude,
241
- 'heading' => $heading,
242
- 'speed' => $speed,
243
- ]);
238
+ $positionData = [
239
+ 'location' => new Point($latitude, $longitude),
240
+ 'latitude' => $latitude,
241
+ 'longitude' => $longitude,
242
+ 'altitude' => $altitude,
243
+ 'heading' => $heading,
244
+ 'speed' => $speed,
245
+ ];
246
+
247
+ // Get vehicle driver
248
+ $vehicle->loadMissing('driver');
249
+ $driver = $vehicle->driver;
250
+ if ($driver) {
251
+ // Append current order to data if applicable
252
+ $order = $driver->getCurrentOrder();
253
+ if ($order) {
254
+ $positionData['order_uuid'] = $order->uuid;
255
+ // Get destination
256
+ $destination = $order->payload?->getPickupOrCurrentWaypoint();
257
+ if ($destination) {
258
+ $positionData['destination_uuid'] = $destination->uuid;
259
+ }
260
+ }
261
+ }
262
+
263
+ $vehicle->update($positionData);
264
+ $vehicle->createPosition($positionData);
244
265
 
245
266
  broadcast(new VehicleLocationChanged($vehicle));
246
- $vehicle->createPositionWithOrderContext();
247
267
 
248
268
  return new VehicleResource($vehicle);
249
269
  }
@@ -12,4 +12,26 @@ class DeviceController extends FleetOpsController
12
12
  * @var string
13
13
  */
14
14
  public $resource = 'device';
15
+
16
+ /**
17
+ * Query callback when querying record.
18
+ *
19
+ * @param \Illuminate\Database\Query\Builder $query
20
+ * @param Request $request
21
+ */
22
+ public static function onQueryRecord($query, $request): void
23
+ {
24
+ $query->with(['telematic', 'warranty']);
25
+ }
26
+
27
+ /**
28
+ * Query callback when finding record.
29
+ *
30
+ * @param \Illuminate\Database\Query\Builder $query
31
+ * @param Request $request
32
+ */
33
+ public static function onFindRecord($query, $request): void
34
+ {
35
+ $query->with(['telematic', 'warranty']);
36
+ }
15
37
  }
@@ -0,0 +1,240 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1;
4
+
5
+ use Fleetbase\FleetOps\Http\Controllers\FleetOpsController;
6
+ use Fleetbase\FleetOps\Jobs\ReplayPositions;
7
+ use Fleetbase\FleetOps\Models\Position;
8
+ use Fleetbase\FleetOps\Support\Utils;
9
+ use Illuminate\Http\Request;
10
+
11
+ class PositionController extends FleetOpsController
12
+ {
13
+ /**
14
+ * The resource to query.
15
+ *
16
+ * @var string
17
+ */
18
+ public $resource = 'position';
19
+
20
+ /**
21
+ * Replay positions on a custom channel.
22
+ *
23
+ * @return \Illuminate\Http\Response
24
+ */
25
+ public function replay(Request $request)
26
+ {
27
+ $positionIds = $request->input('position_ids', []);
28
+ $channelId = $request->input('channel_id');
29
+ $speed = max((float) $request->input('speed', 1), 0.1); // avoid division by zero
30
+ $subjectUuid = $request->input('subject_uuid');
31
+
32
+ if (!$channelId) {
33
+ return response()->error('Channel ID is required');
34
+ }
35
+
36
+ if (empty($positionIds)) {
37
+ return response()->error('Position IDs are required');
38
+ }
39
+
40
+ $positions = Position::whereIn('uuid', $positionIds)
41
+ ->where('company_uuid', session('company'))
42
+ ->orderBy('created_at')
43
+ ->get();
44
+
45
+ if ($positions->isEmpty()) {
46
+ return response()->error('No positions found');
47
+ }
48
+
49
+ // Dispatch async job (will handle replay logic)
50
+ ReplayPositions::dispatch($positions, $channelId, $speed, $subjectUuid);
51
+
52
+ return response()->json([
53
+ 'status' => 'ok',
54
+ 'message' => 'Replay started',
55
+ 'channel_id' => $channelId,
56
+ 'total_positions' => $positions->count(),
57
+ ]);
58
+ }
59
+
60
+ /**
61
+ * Get position statistics/metrics.
62
+ *
63
+ * @return \Illuminate\Http\Response
64
+ */
65
+ public function metrics(Request $request)
66
+ {
67
+ $positionIds = $request->input('position_ids', []);
68
+
69
+ if (empty($positionIds)) {
70
+ return response()->error('Position IDs are required');
71
+ }
72
+
73
+ $positions = Position::whereIn('uuid', $positionIds)
74
+ ->where('company_uuid', session('company'))
75
+ ->orderBy('created_at', 'asc')
76
+ ->get();
77
+
78
+ if ($positions->isEmpty()) {
79
+ return response()->json([
80
+ 'metrics' => [],
81
+ ]);
82
+ }
83
+
84
+ // Calculate metrics
85
+ $metrics = $this->calculateMetrics($positions);
86
+
87
+ return response()->json([
88
+ 'metrics' => $metrics,
89
+ ]);
90
+ }
91
+
92
+ /**
93
+ * Calculate metrics from positions.
94
+ *
95
+ * @param \Illuminate\Support\Collection $positions
96
+ *
97
+ * @return array
98
+ */
99
+ private function calculateMetrics($positions)
100
+ {
101
+ $totalDistance = 0;
102
+ $maxSpeed = 0;
103
+ $avgSpeed = 0;
104
+ $speedingEvents = [];
105
+ $dwellTimes = [];
106
+ $accelerationEvents = [];
107
+ $speedLimit = 100; // km/h - configurable
108
+ $dwellThreshold = 300; // 5 minutes in seconds
109
+ $accelerationThreshold = 2.5; // m/s²
110
+
111
+ $previousPosition = null;
112
+ $previousSpeed = null;
113
+ $dwellStart = null;
114
+
115
+ foreach ($positions as $index => $position) {
116
+ $speed = $position->speed ?? 0;
117
+
118
+ // Track max speed
119
+ if ($speed > $maxSpeed) {
120
+ $maxSpeed = $speed;
121
+ }
122
+
123
+ // Check for speeding (convert m/s to km/h)
124
+ $speedKmh = $speed * 3.6;
125
+ if ($speedKmh > $speedLimit) {
126
+ $speedingEvents[] = [
127
+ 'position_uuid' => $position->uuid,
128
+ 'speed' => round($speedKmh, 2),
129
+ 'timestamp' => $position->created_at->toDateTimeString(),
130
+ ];
131
+ }
132
+
133
+ // Calculate distance and dwell time
134
+ if ($previousPosition) {
135
+ // Calculate distance using Haversine formula
136
+ // $distance = $this->calculateDistance($previousPosition, $position);
137
+ $distance = Utils::vincentyGreatCircleDistance($previousPosition->coordinates, $position->coordinates);
138
+ // dd($previousPosition, $position, $distance);
139
+ $totalDistance += $distance;
140
+
141
+ // Check for dwell (low speed or no movement)
142
+ if ($speed < 0.5) { // Less than 0.5 m/s
143
+ if ($dwellStart === null) {
144
+ $dwellStart = $previousPosition->created_at;
145
+ }
146
+ } else {
147
+ if ($dwellStart !== null) {
148
+ $dwellDuration = $previousPosition->created_at->diffInSeconds($dwellStart);
149
+ if ($dwellDuration >= $dwellThreshold) {
150
+ $dwellTimes[] = [
151
+ 'start' => $dwellStart->toDateTimeString(),
152
+ 'end' => $previousPosition->created_at->toDateTimeString(),
153
+ 'duration' => $dwellDuration,
154
+ ];
155
+ }
156
+ $dwellStart = null;
157
+ }
158
+ }
159
+
160
+ // Calculate acceleration
161
+ if ($previousSpeed !== null) {
162
+ $timeDiff = $position->created_at->diffInSeconds($previousPosition->created_at);
163
+ if ($timeDiff > 0) {
164
+ $acceleration = abs($speed - $previousSpeed) / $timeDiff;
165
+ if ($acceleration > $accelerationThreshold) {
166
+ $accelerationEvents[] = [
167
+ 'position_uuid' => $position->uuid,
168
+ 'acceleration' => round($acceleration, 2),
169
+ 'type' => $speed > $previousSpeed ? 'acceleration' : 'deceleration',
170
+ 'timestamp' => $position->created_at->toDateTimeString(),
171
+ ];
172
+ }
173
+ }
174
+ }
175
+ }
176
+
177
+ $previousPosition = $position;
178
+ $previousSpeed = $speed;
179
+ }
180
+
181
+ // Calculate average speed
182
+ $speeds = $positions->pluck('speed')->filter()->toArray();
183
+ $avgSpeed = count($speeds) > 0 ? array_sum($speeds) / count($speeds) : 0;
184
+
185
+ // Calculate total duration
186
+ $firstPosition = $positions->first();
187
+ $lastPosition = $positions->last();
188
+ $totalDuration = $lastPosition->created_at->diffInSeconds($firstPosition->created_at);
189
+
190
+ return [
191
+ 'total_distance' => round($totalDistance / 1000, 2), // Convert to km
192
+ 'total_duration' => $totalDuration, // seconds
193
+ 'max_speed' => round($maxSpeed * 3.6, 2), // km/h
194
+ 'avg_speed' => round($avgSpeed * 3.6, 2), // km/h
195
+ 'speeding_events' => $speedingEvents,
196
+ 'speeding_count' => count($speedingEvents),
197
+ 'dwell_times' => $dwellTimes,
198
+ 'dwell_count' => count($dwellTimes),
199
+ 'acceleration_events' => $accelerationEvents,
200
+ 'acceleration_count' => count($accelerationEvents),
201
+ 'total_positions' => $positions->count(),
202
+ ];
203
+ }
204
+
205
+ /**
206
+ * Calculate distance between two positions using Haversine formula.
207
+ *
208
+ * @param Position $pos1
209
+ * @param Position $pos2
210
+ *
211
+ * @return float Distance in meters
212
+ */
213
+ private function calculateDistance($pos1, $pos2)
214
+ {
215
+ $coords1 = [$pos1->latitude, $pos1->longitude];
216
+ $coords2 = [$pos2->latitude, $pos2->longitude];
217
+
218
+ if (!isset($coords1[0]) || !isset($coords2[0]) || !isset($coords1[1]) || !isset($coords2[1])) {
219
+ return 0;
220
+ }
221
+
222
+ $lat1 = deg2rad($coords1[0]);
223
+ $lon1 = deg2rad($coords1[1]);
224
+ $lat2 = deg2rad($coords2[0]);
225
+ $lon2 = deg2rad($coords2[1]);
226
+
227
+ $earthRadius = 6371000; // meters
228
+
229
+ $dLat = $lat2 - $lat1;
230
+ $dLon = $lon2 - $lon1;
231
+
232
+ $a = sin($dLat / 2) * sin($dLat / 2) +
233
+ cos($lat1) * cos($lat2) *
234
+ sin($dLon / 2) * sin($dLon / 2);
235
+
236
+ $c = 2 * atan2(sqrt($a), sqrt(1 - $a));
237
+
238
+ return $earthRadius * $c;
239
+ }
240
+ }
@@ -12,4 +12,15 @@ class SensorController extends FleetOpsController
12
12
  * @var string
13
13
  */
14
14
  public $resource = 'sensor';
15
+
16
+ /**
17
+ * Query callback when querying record.
18
+ *
19
+ * @param \Illuminate\Database\Query\Builder $query
20
+ * @param Request $request
21
+ */
22
+ public static function onQueryRecord($query, $request): void
23
+ {
24
+ $query->with(['telematic', 'warranty']);
25
+ }
15
26
  }
@@ -3,6 +3,11 @@
3
3
  namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1;
4
4
 
5
5
  use Fleetbase\FleetOps\Http\Controllers\FleetOpsController;
6
+ use Fleetbase\FleetOps\Models\Telematic;
7
+ use Fleetbase\FleetOps\Support\Telematics\TelematicProviderRegistry;
8
+ use Fleetbase\FleetOps\Support\Telematics\TelematicService;
9
+ use Illuminate\Http\JsonResponse;
10
+ use Illuminate\Http\Request;
6
11
 
7
12
  class TelematicController extends FleetOpsController
8
13
  {
@@ -12,4 +17,140 @@ class TelematicController extends FleetOpsController
12
17
  * @var string
13
18
  */
14
19
  public $resource = 'telematic';
20
+
21
+ protected TelematicService $telematicService;
22
+ protected TelematicProviderRegistry $registry;
23
+
24
+ public function __construct(TelematicService $service, TelematicProviderRegistry $registry)
25
+ {
26
+ parent::__construct();
27
+ $this->telematicService = $service;
28
+ $this->registry = $registry;
29
+ }
30
+
31
+ /**
32
+ * Query callback when querying record.
33
+ *
34
+ * @param \Illuminate\Database\Query\Builder $query
35
+ * @param Request $request
36
+ */
37
+ public static function onQueryRecord($query, $request): void
38
+ {
39
+ $query->with(['warranty']);
40
+ }
41
+
42
+ /**
43
+ * List available providers.
44
+ */
45
+ public function providers(): JsonResponse
46
+ {
47
+ $providers = $this->registry->all()->map(fn ($p) => $p->toArray());
48
+
49
+ return response()->json($providers->values());
50
+ }
51
+
52
+ /**
53
+ * Test connection to provider.
54
+ */
55
+ public function testConnection(Request $request, string $id): JsonResponse
56
+ {
57
+ $telematic = Telematic::where('uuid', $id)
58
+ ->where('company_uuid', session('company'))
59
+ ->firstOrFail();
60
+
61
+ $async = $request->input('async', false);
62
+
63
+ $result = $this->telematicService->testConnection($telematic, $async);
64
+
65
+ if ($async) {
66
+ return response()->json($result, 202);
67
+ }
68
+
69
+ return response()->json($result);
70
+ }
71
+
72
+ /**
73
+ * Test connection to provider.
74
+ */
75
+ public function testCredentials(Request $request, string $key): JsonResponse
76
+ {
77
+ $credentials = $request->array('credentials', []);
78
+ $async = $request->input('async', false);
79
+
80
+ try {
81
+ $provider = $this->registry->resolve($key);
82
+ if (!$provider) {
83
+ return response()->error('Unable to resolve telematic provider.');
84
+ }
85
+ $result = $provider->testConnection($credentials);
86
+ } catch (\Exception $e) {
87
+ return response()->error($e->getMessage());
88
+ }
89
+
90
+ if ($async) {
91
+ return response()->json($result, 202);
92
+ }
93
+
94
+ return response()->json($result);
95
+ }
96
+
97
+ /**
98
+ * Discover devices from provider.
99
+ */
100
+ public function discover(Request $request, string $id): JsonResponse
101
+ {
102
+ $telematic = Telematic::where('uuid', $id)
103
+ ->where('company_uuid', session('company'))
104
+ ->firstOrFail();
105
+
106
+ $jobId = $this->telematicService->discoverDevices($telematic, [
107
+ 'limit' => $request->input('limit', 100),
108
+ 'filters' => $request->input('filters', []),
109
+ ]);
110
+
111
+ return response()->json([
112
+ 'job_id' => $jobId,
113
+ 'message' => 'Device discovery initiated',
114
+ ], 202);
115
+ }
116
+
117
+ /**
118
+ * Get devices for a telematic.
119
+ */
120
+ public function devices(Request $request, string $id): JsonResponse
121
+ {
122
+ $telematic = Telematic::where('uuid', $id)
123
+ ->where('company_uuid', session('company'))
124
+ ->firstOrFail();
125
+
126
+ $devices = $this->telematicService->getDevices($telematic, [
127
+ 'status' => $request->input('status'),
128
+ 'search' => $request->input('search'),
129
+ ]);
130
+
131
+ return response()->json([
132
+ 'data' => $devices,
133
+ ]);
134
+ }
135
+
136
+ /**
137
+ * Link a device to a telematic.
138
+ */
139
+ public function linkDevice(Request $request, string $id): JsonResponse
140
+ {
141
+ $telematic = Telematic::where('uuid', $id)
142
+ ->where('company_uuid', session('company'))
143
+ ->firstOrFail();
144
+
145
+ $request->validate([
146
+ 'external_id' => 'required|string',
147
+ 'device_name' => 'required|string',
148
+ ]);
149
+
150
+ $device = $this->telematicService->linkDevice($telematic, $request->all());
151
+
152
+ return response()->json([
153
+ 'device' => $device,
154
+ ], 201);
155
+ }
15
156
  }