@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.
- package/addon/components/custom-entity/form.hbs +14 -14
- package/addon/components/device/card.hbs +1 -0
- package/addon/components/device/card.js +3 -0
- package/addon/components/device/details.hbs +92 -43
- package/addon/components/device/form.hbs +108 -60
- package/addon/components/device/form.js +36 -8
- package/addon/components/device/manager.hbs +29 -0
- package/addon/components/device/manager.js +95 -0
- package/addon/components/device/panel-header.hbs +32 -0
- package/addon/components/device/panel-header.js +3 -0
- package/addon/components/device/pill.hbs +16 -0
- package/addon/components/device/pill.js +3 -0
- package/addon/components/driver/details.hbs +4 -0
- package/addon/components/driver/details.js +19 -1
- package/addon/components/driver/form.hbs +14 -3
- package/addon/components/driver/form.js +49 -47
- package/addon/components/driver/pill.hbs +17 -0
- package/addon/components/driver/pill.js +3 -0
- package/addon/components/entity/form.hbs +7 -5
- package/addon/components/layout/fleet-ops-sidebar.js +12 -12
- package/addon/components/map/drawer/device-event-listing.hbs +64 -0
- package/addon/components/map/drawer/device-event-listing.js +181 -0
- package/addon/components/map/drawer/position-listing.hbs +100 -0
- package/addon/components/map/drawer/position-listing.js +455 -0
- package/addon/components/map/drawer.js +2 -0
- package/addon/components/map/leaflet-live-map.hbs +7 -2
- package/addon/components/modals/attach-device.hbs +18 -0
- package/addon/components/modals/attach-device.js +3 -0
- package/addon/components/order/details/detail.hbs +2 -54
- package/addon/components/order/details/detail.js +1 -0
- package/addon/components/order/details/payload.hbs +6 -4
- package/addon/components/order/details/payload.js +2 -0
- package/addon/components/order/pill.hbs +34 -0
- package/addon/components/order/pill.js +3 -0
- package/addon/components/order-config-manager/custom-fields.js +1 -1
- package/addon/components/positions-replay.hbs +339 -0
- package/addon/components/positions-replay.js +409 -0
- package/addon/components/sensor/details.hbs +64 -38
- package/addon/components/sensor/form.hbs +112 -63
- package/addon/components/sensor/form.js +36 -24
- package/addon/components/sensor/panel-header.hbs +32 -0
- package/addon/components/sensor/panel-header.js +3 -0
- package/addon/components/telematic/details.hbs +40 -16
- package/addon/components/telematic/form.hbs +63 -64
- package/addon/components/telematic/form.js +73 -4
- package/addon/components/vehicle/card.hbs +2 -2
- package/addon/components/vehicle/details.hbs +4 -0
- package/addon/components/vehicle/details.js +19 -1
- package/addon/components/vehicle/form.hbs +4 -0
- package/addon/components/vehicle/pill.hbs +34 -0
- package/addon/components/vehicle/pill.js +3 -0
- package/addon/controllers/analytics/reports/index/edit.js +1 -1
- package/addon/controllers/connectivity/devices/index/details.js +22 -1
- package/addon/controllers/connectivity/devices/index/edit.js +66 -1
- package/addon/controllers/connectivity/devices/index.js +51 -9
- package/addon/controllers/connectivity/events/index.js +65 -16
- package/addon/controllers/connectivity/sensors/index/details.js +22 -1
- package/addon/controllers/connectivity/sensors/index/edit.js +66 -1
- package/addon/controllers/connectivity/sensors/index.js +66 -6
- package/addon/controllers/connectivity/telematics/index/details.js +22 -1
- package/addon/controllers/connectivity/telematics/index/edit.js +66 -1
- package/addon/controllers/connectivity/telematics/index.js +20 -11
- package/addon/controllers/management/fleets/index/details.js +26 -21
- package/addon/controllers/management/fleets/index/edit.js +9 -6
- package/addon/controllers/management/vehicles/index/details.js +26 -13
- package/addon/controllers/settings/custom-fields.js +6 -0
- package/addon/helpers/get-fleet-ops-option-label.js +11 -0
- package/addon/routes/connectivity/devices/index/details.js +27 -1
- package/addon/routes/connectivity/devices/index/edit.js +27 -1
- package/addon/routes/connectivity/sensors/index/details.js +27 -1
- package/addon/routes/connectivity/sensors/index/edit.js +27 -1
- package/addon/routes/connectivity/telematics/index/details.js +27 -1
- package/addon/routes/connectivity/telematics/index/edit.js +27 -1
- package/addon/routes/management/drivers/index/details/positions.js +3 -0
- package/addon/routes/management/vehicles/index/details/positions.js +3 -0
- package/addon/routes.js +4 -0
- package/addon/services/movement-tracker.js +81 -30
- package/addon/services/position-playback.js +486 -0
- package/addon/services/resource-metadata.js +46 -0
- package/addon/styles/fleetops-engine.css +157 -0
- package/addon/templates/connectivity/devices/index/details/index.hbs +2 -2
- package/addon/templates/connectivity/devices/index/details.hbs +15 -2
- package/addon/templates/connectivity/devices/index/edit.hbs +1 -1
- package/addon/templates/connectivity/events/index.hbs +1 -1
- package/addon/templates/connectivity/sensors/index/details/index.hbs +2 -2
- package/addon/templates/connectivity/sensors/index/details.hbs +15 -2
- package/addon/templates/connectivity/sensors/index/edit.hbs +1 -1
- package/addon/templates/connectivity/telematics/index/details/index.hbs +2 -2
- package/addon/templates/connectivity/telematics/index/details.hbs +14 -2
- package/addon/templates/connectivity/telematics/index/edit.hbs +1 -1
- package/addon/templates/management/drivers/index/details/positions.hbs +2 -0
- package/addon/templates/management/vehicles/index/details/devices.hbs +1 -2
- package/addon/templates/management/vehicles/index/details/positions.hbs +1 -0
- package/addon/utils/fleet-ops-options.js +95 -0
- package/app/components/device/card.js +1 -0
- package/app/components/device/manager.js +1 -0
- package/app/components/device/panel-header.js +1 -0
- package/app/components/device/pill.js +1 -0
- package/app/components/driver/pill.js +1 -0
- package/app/components/map/drawer/device-event-listing.js +1 -0
- package/app/components/map/drawer/position-listing.js +1 -0
- package/app/components/modals/attach-device.js +1 -0
- package/app/components/order/pill.js +1 -0
- package/app/components/positions-replay.js +1 -0
- package/app/components/sensor/panel-header.js +1 -0
- package/app/components/vehicle/pill.js +1 -0
- package/app/helpers/get-fleet-ops-option-label.js +1 -0
- package/app/routes/management/drivers/index/details/positions.js +1 -0
- package/app/routes/management/vehicles/index/details/positions.js +1 -0
- package/app/services/position-playback.js +1 -0
- package/app/services/resource-metadata.js +1 -0
- package/app/templates/management/drivers/index/details/positions.js +1 -0
- package/app/templates/management/vehicles/index/details/positions.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +4 -4
- package/server/config/telematics.php +111 -0
- package/server/migrations/2025_10_27_000001_add_telematics_integration_fields.php +70 -0
- package/server/migrations/2025_10_27_171322_fix_device_column_names.php +107 -0
- package/server/migrations/2025_10_27_203023_add_company_uuid_to_device_events_table.php +28 -0
- package/server/src/Console/Commands/ReplayVehicleLocations.php +225 -0
- package/server/src/Contracts/TelematicProviderDescriptor.php +72 -0
- package/server/src/Contracts/TelematicProviderInterface.php +119 -0
- package/server/src/Exceptions/TelematicProviderException.php +14 -0
- package/server/src/Exceptions/TelematicRateLimitExceededException.php +12 -0
- package/server/src/Http/Controllers/Api/v1/DriverController.php +24 -14
- package/server/src/Http/Controllers/Api/v1/VehicleController.php +27 -7
- package/server/src/Http/Controllers/Internal/v1/DeviceController.php +22 -0
- package/server/src/Http/Controllers/Internal/v1/PositionController.php +240 -0
- package/server/src/Http/Controllers/Internal/v1/SensorController.php +11 -0
- package/server/src/Http/Controllers/Internal/v1/TelematicController.php +141 -0
- package/server/src/Http/Controllers/TelematicWebhookController.php +169 -0
- package/server/src/Http/Filter/DeviceEventFilter.php +68 -0
- package/server/src/Http/Filter/PositionFilter.php +35 -0
- package/server/src/Http/Resources/v1/Position.php +44 -0
- package/server/src/Jobs/ReplayPositions.php +64 -0
- package/server/src/Jobs/SendPositionReplay.php +65 -0
- package/server/src/Jobs/SyncTelematicDevicesJob.php +106 -0
- package/server/src/Jobs/TestTelematicConnectionJob.php +102 -0
- package/server/src/Models/Asset.php +10 -8
- package/server/src/Models/Device.php +79 -12
- package/server/src/Models/DeviceEvent.php +33 -3
- package/server/src/Models/Driver.php +28 -1
- package/server/src/Models/Maintenance.php +15 -12
- package/server/src/Models/Part.php +2 -0
- package/server/src/Models/Payload.php +0 -1
- package/server/src/Models/Place.php +4 -1
- package/server/src/Models/Position.php +27 -17
- package/server/src/Models/Sensor.php +78 -13
- package/server/src/Models/Telematic.php +116 -6
- package/server/src/Models/TrackingNumber.php +3 -1
- package/server/src/Models/Vehicle.php +8 -11
- package/server/src/Models/WorkOrder.php +8 -5
- package/server/src/Providers/FleetOpsServiceProvider.php +2 -0
- package/server/src/Support/Telematics/Providers/AbstractProvider.php +151 -0
- package/server/src/Support/Telematics/Providers/FlespiProvider.php +182 -0
- package/server/src/Support/Telematics/Providers/GeotabProvider.php +181 -0
- package/server/src/Support/Telematics/Providers/SamsaraProvider.php +177 -0
- package/server/src/Support/Telematics/TelematicProviderRegistry.php +147 -0
- package/server/src/Support/Telematics/TelematicService.php +223 -0
- package/server/src/Support/Utils.php +1 -1
- 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
|
+
}
|
|
@@ -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
|
-
$
|
|
347
|
-
'location'
|
|
348
|
-
'
|
|
349
|
-
'
|
|
350
|
-
'
|
|
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
|
-
|
|
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
|
-
$
|
|
239
|
-
'location'
|
|
240
|
-
'
|
|
241
|
-
'
|
|
242
|
-
'
|
|
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
|
}
|