@fleetbase/fleetops-engine 0.6.20 → 0.6.21
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/details.hbs +92 -43
- package/addon/components/device/form.hbs +108 -60
- package/addon/components/device/form.js +36 -8
- package/addon/components/device/panel-header.hbs +32 -0
- package/addon/components/device/panel-header.js +3 -0
- package/addon/components/driver/form.hbs +1 -1
- package/addon/components/driver/form.js +49 -47
- 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 +58 -0
- package/addon/components/map/drawer/device-event-listing.js +181 -0
- package/addon/components/map/drawer/position-listing.hbs +84 -0
- package/addon/components/map/drawer/position-listing.js +289 -0
- package/addon/components/map/drawer.js +2 -0
- package/addon/components/map/leaflet-live-map.hbs +7 -2
- package/addon/components/order/details/payload.hbs +6 -4
- package/addon/components/order/details/payload.js +2 -0
- package/addon/components/order-config-manager/custom-fields.js +1 -1
- package/addon/components/positions-replay.hbs +333 -0
- package/addon/components/positions-replay.js +372 -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 +1 -1
- 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 +21 -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/vehicles/index/details/positions.js +3 -0
- package/addon/routes.js +1 -0
- package/addon/services/movement-tracker.js +81 -30
- 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/vehicles/index/details/positions.hbs +1 -0
- package/addon/utils/fleet-ops-options.js +95 -0
- package/app/components/device/panel-header.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/positions-replay.js +1 -0
- package/app/components/sensor/panel-header.js +1 -0
- package/app/helpers/get-fleet-ops-option-label.js +1 -0
- package/app/routes/management/vehicles/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/Internal/v1/TelematicWebhookController.php +170 -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/Device.php +72 -10
- package/server/src/Models/DeviceEvent.php +7 -0
- package/server/src/Models/Driver.php +28 -1
- package/server/src/Models/Payload.php +0 -1
- package/server/src/Models/Place.php +4 -1
- package/server/src/Models/Position.php +17 -17
- package/server/src/Models/Sensor.php +78 -13
- package/server/src/Models/Telematic.php +116 -6
- package/server/src/Models/Vehicle.php +8 -11
- 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 +12 -1
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Support\Telematics\Providers;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Class FlespiProvider.
|
|
7
|
+
*
|
|
8
|
+
* Flespi telematics provider implementation.
|
|
9
|
+
* https://flespi.com/
|
|
10
|
+
*/
|
|
11
|
+
class FlespiProvider extends AbstractProvider
|
|
12
|
+
{
|
|
13
|
+
protected string $baseUrl = 'https://flespi.io/gw';
|
|
14
|
+
protected int $requestsPerMinute = 100;
|
|
15
|
+
|
|
16
|
+
protected function prepareAuthentication(): void
|
|
17
|
+
{
|
|
18
|
+
$this->headers = [
|
|
19
|
+
'Authorization' => 'FlespiToken ' . $this->credentials['token'],
|
|
20
|
+
'Accept' => 'application/json',
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public function testConnection(array $credentials): array
|
|
25
|
+
{
|
|
26
|
+
try {
|
|
27
|
+
$this->credentials = $credentials;
|
|
28
|
+
$this->prepareAuthentication();
|
|
29
|
+
|
|
30
|
+
// Test by fetching channels
|
|
31
|
+
$response = $this->request('GET', '/channels/all');
|
|
32
|
+
|
|
33
|
+
return [
|
|
34
|
+
'success' => true,
|
|
35
|
+
'message' => 'Connection successful',
|
|
36
|
+
'metadata' => [
|
|
37
|
+
'channels_count' => count($response['result'] ?? []),
|
|
38
|
+
],
|
|
39
|
+
];
|
|
40
|
+
} catch (\Exception $e) {
|
|
41
|
+
return [
|
|
42
|
+
'success' => false,
|
|
43
|
+
'message' => $e->getMessage(),
|
|
44
|
+
'metadata' => [],
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public function fetchDevices(array $options = []): array
|
|
50
|
+
{
|
|
51
|
+
$limit = $options['limit'] ?? 100;
|
|
52
|
+
$cursor = $options['cursor'] ?? null;
|
|
53
|
+
|
|
54
|
+
$params = ['count' => $limit];
|
|
55
|
+
if ($cursor) {
|
|
56
|
+
$params['offset'] = $cursor;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
$response = $this->request('GET', '/devices/all', $params);
|
|
60
|
+
|
|
61
|
+
$devices = $response['result'] ?? [];
|
|
62
|
+
$nextCursor = count($devices) >= $limit ? ((int) ($cursor ?? 0) + $limit) : null;
|
|
63
|
+
|
|
64
|
+
return [
|
|
65
|
+
'devices' => $devices,
|
|
66
|
+
'next_cursor' => $nextCursor,
|
|
67
|
+
'has_more' => $nextCursor !== null,
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public function fetchDeviceDetails(string $externalId): array
|
|
72
|
+
{
|
|
73
|
+
$response = $this->request('GET', "/devices/{$externalId}");
|
|
74
|
+
|
|
75
|
+
return $response['result'][0] ?? [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public function normalizeDevice(array $payload): array
|
|
79
|
+
{
|
|
80
|
+
return [
|
|
81
|
+
'external_id' => $payload['id'],
|
|
82
|
+
'device_name' => $payload['name'] ?? 'Unknown Device',
|
|
83
|
+
'device_provider' => 'flespi',
|
|
84
|
+
'device_model' => $payload['device_type_id'] ?? null,
|
|
85
|
+
'imei' => $payload['configuration']['ident'] ?? null,
|
|
86
|
+
'phone' => $payload['configuration']['phone'] ?? null,
|
|
87
|
+
'status' => isset($payload['telemetry']) ? 'active' : 'inactive',
|
|
88
|
+
'location' => [
|
|
89
|
+
'lat' => $payload['telemetry']['position.latitude'] ?? null,
|
|
90
|
+
'lng' => $payload['telemetry']['position.longitude'] ?? null,
|
|
91
|
+
],
|
|
92
|
+
'meta' => $payload,
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public function normalizeEvent(array $payload): array
|
|
97
|
+
{
|
|
98
|
+
return [
|
|
99
|
+
'external_id' => $payload['id'] ?? null,
|
|
100
|
+
'event_type' => $payload['event.enum'] ?? 'telemetry_update',
|
|
101
|
+
'occurred_at' => isset($payload['timestamp']) ? date('Y-m-d H:i:s', $payload['timestamp']) : now(),
|
|
102
|
+
'location' => [
|
|
103
|
+
'lat' => $payload['position.latitude'] ?? null,
|
|
104
|
+
'lng' => $payload['position.longitude'] ?? null,
|
|
105
|
+
],
|
|
106
|
+
'meta' => $payload,
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public function normalizeSensor(array $payload): array
|
|
111
|
+
{
|
|
112
|
+
return [
|
|
113
|
+
'sensor_type' => $payload['sensor_type'] ?? 'generic',
|
|
114
|
+
'value' => $payload['value'] ?? null,
|
|
115
|
+
'unit' => $payload['unit'] ?? null,
|
|
116
|
+
'recorded_at' => isset($payload['timestamp']) ? date('Y-m-d H:i:s', $payload['timestamp']) : now(),
|
|
117
|
+
'meta' => $payload,
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public function validateWebhookSignature(string $payload, string $signature, array $credentials): bool
|
|
122
|
+
{
|
|
123
|
+
if (!isset($credentials['webhook_secret'])) {
|
|
124
|
+
return true; // No secret configured, skip validation
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
$expectedSignature = hash_hmac('sha256', $payload, $credentials['webhook_secret']);
|
|
128
|
+
|
|
129
|
+
return hash_equals($expectedSignature, $signature);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
public function processWebhook(array $payload, array $headers = []): array
|
|
133
|
+
{
|
|
134
|
+
$devices = [];
|
|
135
|
+
$events = [];
|
|
136
|
+
|
|
137
|
+
// Flespi sends array of messages
|
|
138
|
+
foreach ($payload as $message) {
|
|
139
|
+
if (isset($message['device.id'])) {
|
|
140
|
+
$devices[] = $this->normalizeDevice([
|
|
141
|
+
'id' => $message['device.id'],
|
|
142
|
+
'name' => $message['device.name'] ?? null,
|
|
143
|
+
'telemetry' => $message,
|
|
144
|
+
]);
|
|
145
|
+
|
|
146
|
+
$events[] = $this->normalizeEvent($message);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return [
|
|
151
|
+
'devices' => $devices,
|
|
152
|
+
'events' => $events,
|
|
153
|
+
'sensors' => [],
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
public function getCredentialSchema(): array
|
|
158
|
+
{
|
|
159
|
+
return [
|
|
160
|
+
[
|
|
161
|
+
'name' => 'token',
|
|
162
|
+
'label' => 'Flespi Token',
|
|
163
|
+
'type' => 'password',
|
|
164
|
+
'placeholder' => 'Enter your Flespi API token',
|
|
165
|
+
'required' => true,
|
|
166
|
+
'validation' => 'required|string|min:20',
|
|
167
|
+
],
|
|
168
|
+
[
|
|
169
|
+
'name' => 'webhook_secret',
|
|
170
|
+
'label' => 'Webhook Secret (Optional)',
|
|
171
|
+
'type' => 'password',
|
|
172
|
+
'placeholder' => 'Enter webhook secret for signature validation',
|
|
173
|
+
'required' => false,
|
|
174
|
+
],
|
|
175
|
+
];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
public function supportsWebhooks(): bool
|
|
179
|
+
{
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Support\Telematics\Providers;
|
|
4
|
+
|
|
5
|
+
use Illuminate\Support\Facades\Http;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Class GeotabProvider.
|
|
9
|
+
*
|
|
10
|
+
* Geotab telematics provider implementation.
|
|
11
|
+
* https://www.geotab.com/
|
|
12
|
+
*/
|
|
13
|
+
class GeotabProvider extends AbstractProvider
|
|
14
|
+
{
|
|
15
|
+
protected string $baseUrl = 'https://my.geotab.com/apiv1';
|
|
16
|
+
protected int $requestsPerMinute = 50;
|
|
17
|
+
protected ?string $sessionId = null;
|
|
18
|
+
|
|
19
|
+
protected function prepareAuthentication(): void
|
|
20
|
+
{
|
|
21
|
+
// Geotab uses session-based authentication
|
|
22
|
+
if (!$this->sessionId) {
|
|
23
|
+
$this->authenticate();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
$this->headers = [
|
|
27
|
+
'Content-Type' => 'application/json',
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Authenticate with Geotab and get session ID.
|
|
33
|
+
*/
|
|
34
|
+
protected function authenticate(): void
|
|
35
|
+
{
|
|
36
|
+
$response = Http::post($this->baseUrl, [
|
|
37
|
+
'method' => 'Authenticate',
|
|
38
|
+
'params' => [
|
|
39
|
+
'database' => $this->credentials['database'],
|
|
40
|
+
'userName' => $this->credentials['username'],
|
|
41
|
+
'password' => $this->credentials['password'],
|
|
42
|
+
],
|
|
43
|
+
])->json();
|
|
44
|
+
|
|
45
|
+
if (isset($response['result']['credentials']['sessionId'])) {
|
|
46
|
+
$this->sessionId = $response['result']['credentials']['sessionId'];
|
|
47
|
+
} else {
|
|
48
|
+
throw new \Exception('Geotab authentication failed');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public function testConnection(array $credentials): array
|
|
53
|
+
{
|
|
54
|
+
try {
|
|
55
|
+
$this->credentials = $credentials;
|
|
56
|
+
$this->authenticate();
|
|
57
|
+
|
|
58
|
+
return [
|
|
59
|
+
'success' => true,
|
|
60
|
+
'message' => 'Connection successful',
|
|
61
|
+
'metadata' => [
|
|
62
|
+
'session_id' => substr($this->sessionId, 0, 10) . '...',
|
|
63
|
+
],
|
|
64
|
+
];
|
|
65
|
+
} catch (\Exception $e) {
|
|
66
|
+
return [
|
|
67
|
+
'success' => false,
|
|
68
|
+
'message' => $e->getMessage(),
|
|
69
|
+
'metadata' => [],
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public function fetchDevices(array $options = []): array
|
|
75
|
+
{
|
|
76
|
+
$limit = $options['limit'] ?? 100;
|
|
77
|
+
|
|
78
|
+
$response = Http::post($this->baseUrl, [
|
|
79
|
+
'method' => 'Get',
|
|
80
|
+
'params' => [
|
|
81
|
+
'credentials' => [
|
|
82
|
+
'database' => $this->credentials['database'],
|
|
83
|
+
'sessionId' => $this->sessionId,
|
|
84
|
+
],
|
|
85
|
+
'typeName' => 'Device',
|
|
86
|
+
'resultsLimit' => $limit,
|
|
87
|
+
],
|
|
88
|
+
])->json();
|
|
89
|
+
|
|
90
|
+
$devices = $response['result'] ?? [];
|
|
91
|
+
|
|
92
|
+
return [
|
|
93
|
+
'devices' => $devices,
|
|
94
|
+
'next_cursor' => null,
|
|
95
|
+
'has_more' => false,
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public function fetchDeviceDetails(string $externalId): array
|
|
100
|
+
{
|
|
101
|
+
$response = Http::post($this->baseUrl, [
|
|
102
|
+
'method' => 'Get',
|
|
103
|
+
'params' => [
|
|
104
|
+
'credentials' => [
|
|
105
|
+
'database' => $this->credentials['database'],
|
|
106
|
+
'sessionId' => $this->sessionId,
|
|
107
|
+
],
|
|
108
|
+
'typeName' => 'Device',
|
|
109
|
+
'search' => ['id' => $externalId],
|
|
110
|
+
],
|
|
111
|
+
])->json();
|
|
112
|
+
|
|
113
|
+
return $response['result'][0] ?? [];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
public function normalizeDevice(array $payload): array
|
|
117
|
+
{
|
|
118
|
+
return [
|
|
119
|
+
'external_id' => $payload['id'],
|
|
120
|
+
'device_name' => $payload['name'] ?? 'Unknown Device',
|
|
121
|
+
'device_provider' => 'geotab',
|
|
122
|
+
'device_model' => $payload['deviceType'] ?? null,
|
|
123
|
+
'imei' => $payload['serialNumber'] ?? null,
|
|
124
|
+
'vin' => $payload['vehicleIdentificationNumber'] ?? null,
|
|
125
|
+
'status' => 'active',
|
|
126
|
+
'meta' => $payload,
|
|
127
|
+
];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
public function normalizeEvent(array $payload): array
|
|
131
|
+
{
|
|
132
|
+
return [
|
|
133
|
+
'external_id' => $payload['id'] ?? null,
|
|
134
|
+
'event_type' => $payload['type'] ?? 'status_data',
|
|
135
|
+
'occurred_at' => $payload['dateTime'] ?? now(),
|
|
136
|
+
'meta' => $payload,
|
|
137
|
+
];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
public function normalizeSensor(array $payload): array
|
|
141
|
+
{
|
|
142
|
+
return [
|
|
143
|
+
'sensor_type' => $payload['diagnosticType'] ?? 'generic',
|
|
144
|
+
'value' => $payload['data'] ?? null,
|
|
145
|
+
'recorded_at' => $payload['dateTime'] ?? now(),
|
|
146
|
+
'meta' => $payload,
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
public function getCredentialSchema(): array
|
|
151
|
+
{
|
|
152
|
+
return [
|
|
153
|
+
[
|
|
154
|
+
'name' => 'database',
|
|
155
|
+
'label' => 'Database Name',
|
|
156
|
+
'type' => 'text',
|
|
157
|
+
'placeholder' => 'Enter your Geotab database name',
|
|
158
|
+
'required' => true,
|
|
159
|
+
],
|
|
160
|
+
[
|
|
161
|
+
'name' => 'username',
|
|
162
|
+
'label' => 'Username',
|
|
163
|
+
'type' => 'text',
|
|
164
|
+
'placeholder' => 'Enter your Geotab username',
|
|
165
|
+
'required' => true,
|
|
166
|
+
],
|
|
167
|
+
[
|
|
168
|
+
'name' => 'password',
|
|
169
|
+
'label' => 'Password',
|
|
170
|
+
'type' => 'password',
|
|
171
|
+
'placeholder' => 'Enter your Geotab password',
|
|
172
|
+
'required' => true,
|
|
173
|
+
],
|
|
174
|
+
];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
public function supportsWebhooks(): bool
|
|
178
|
+
{
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Support\Telematics\Providers;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Class SamsaraProvider.
|
|
7
|
+
*
|
|
8
|
+
* Samsara telematics provider implementation.
|
|
9
|
+
* https://www.samsara.com/
|
|
10
|
+
*/
|
|
11
|
+
class SamsaraProvider extends AbstractProvider
|
|
12
|
+
{
|
|
13
|
+
protected string $baseUrl = 'https://api.samsara.com';
|
|
14
|
+
protected int $requestsPerMinute = 60;
|
|
15
|
+
|
|
16
|
+
protected function prepareAuthentication(): void
|
|
17
|
+
{
|
|
18
|
+
$this->headers = [
|
|
19
|
+
'Authorization' => 'Bearer ' . $this->credentials['api_token'],
|
|
20
|
+
'Accept' => 'application/json',
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public function testConnection(array $credentials): array
|
|
25
|
+
{
|
|
26
|
+
try {
|
|
27
|
+
$this->credentials = $credentials;
|
|
28
|
+
$this->prepareAuthentication();
|
|
29
|
+
|
|
30
|
+
// Test by fetching user info
|
|
31
|
+
$response = $this->request('GET', '/fleet/users');
|
|
32
|
+
|
|
33
|
+
return [
|
|
34
|
+
'success' => true,
|
|
35
|
+
'message' => 'Connection successful',
|
|
36
|
+
'metadata' => [
|
|
37
|
+
'users_count' => count($response['data'] ?? []),
|
|
38
|
+
],
|
|
39
|
+
];
|
|
40
|
+
} catch (\Exception $e) {
|
|
41
|
+
return [
|
|
42
|
+
'success' => false,
|
|
43
|
+
'message' => $e->getMessage(),
|
|
44
|
+
'metadata' => [],
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public function fetchDevices(array $options = []): array
|
|
50
|
+
{
|
|
51
|
+
$limit = $options['limit'] ?? 100;
|
|
52
|
+
$cursor = $options['cursor'] ?? null;
|
|
53
|
+
|
|
54
|
+
$params = ['limit' => $limit];
|
|
55
|
+
if ($cursor) {
|
|
56
|
+
$params['after'] = $cursor;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
$response = $this->request('GET', '/fleet/vehicles', $params);
|
|
60
|
+
|
|
61
|
+
$devices = $response['data'] ?? [];
|
|
62
|
+
$nextCursor = $response['pagination']['endCursor'] ?? null;
|
|
63
|
+
$hasMore = $response['pagination']['hasNextPage'] ?? false;
|
|
64
|
+
|
|
65
|
+
return [
|
|
66
|
+
'devices' => $devices,
|
|
67
|
+
'next_cursor' => $hasMore ? $nextCursor : null,
|
|
68
|
+
'has_more' => $hasMore,
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public function fetchDeviceDetails(string $externalId): array
|
|
73
|
+
{
|
|
74
|
+
$response = $this->request('GET', "/fleet/vehicles/{$externalId}");
|
|
75
|
+
|
|
76
|
+
return $response['data'] ?? [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public function normalizeDevice(array $payload): array
|
|
80
|
+
{
|
|
81
|
+
return [
|
|
82
|
+
'external_id' => $payload['id'],
|
|
83
|
+
'device_name' => $payload['name'] ?? 'Unknown Device',
|
|
84
|
+
'device_provider' => 'samsara',
|
|
85
|
+
'device_model' => $payload['make'] ?? null,
|
|
86
|
+
'vin' => $payload['vin'] ?? null,
|
|
87
|
+
'license_plate' => $payload['licensePlate'] ?? null,
|
|
88
|
+
'status' => 'active',
|
|
89
|
+
'meta' => $payload,
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public function normalizeEvent(array $payload): array
|
|
94
|
+
{
|
|
95
|
+
return [
|
|
96
|
+
'external_id' => $payload['id'] ?? null,
|
|
97
|
+
'event_type' => $payload['eventType'] ?? 'vehicle_update',
|
|
98
|
+
'occurred_at' => $payload['time'] ?? now(),
|
|
99
|
+
'location' => [
|
|
100
|
+
'lat' => $payload['location']['latitude'] ?? null,
|
|
101
|
+
'lng' => $payload['location']['longitude'] ?? null,
|
|
102
|
+
],
|
|
103
|
+
'meta' => $payload,
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public function normalizeSensor(array $payload): array
|
|
108
|
+
{
|
|
109
|
+
return [
|
|
110
|
+
'sensor_type' => $payload['sensorType'] ?? 'generic',
|
|
111
|
+
'value' => $payload['value'] ?? null,
|
|
112
|
+
'unit' => $payload['unit'] ?? null,
|
|
113
|
+
'recorded_at' => $payload['time'] ?? now(),
|
|
114
|
+
'meta' => $payload,
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
public function validateWebhookSignature(string $payload, string $signature, array $credentials): bool
|
|
119
|
+
{
|
|
120
|
+
if (!isset($credentials['webhook_secret'])) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
$expectedSignature = hash_hmac('sha256', $payload, $credentials['webhook_secret']);
|
|
125
|
+
|
|
126
|
+
return hash_equals($expectedSignature, $signature);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public function processWebhook(array $payload, array $headers = []): array
|
|
130
|
+
{
|
|
131
|
+
$devices = [];
|
|
132
|
+
$events = [];
|
|
133
|
+
|
|
134
|
+
// Samsara webhook structure
|
|
135
|
+
if (isset($payload['data'])) {
|
|
136
|
+
foreach ($payload['data'] as $item) {
|
|
137
|
+
if (isset($item['vehicle'])) {
|
|
138
|
+
$devices[] = $this->normalizeDevice($item['vehicle']);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
$events[] = $this->normalizeEvent($item);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return [
|
|
146
|
+
'devices' => $devices,
|
|
147
|
+
'events' => $events,
|
|
148
|
+
'sensors' => [],
|
|
149
|
+
];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
public function getCredentialSchema(): array
|
|
153
|
+
{
|
|
154
|
+
return [
|
|
155
|
+
[
|
|
156
|
+
'name' => 'api_token',
|
|
157
|
+
'label' => 'API Token',
|
|
158
|
+
'type' => 'password',
|
|
159
|
+
'placeholder' => 'Enter your Samsara API token',
|
|
160
|
+
'required' => true,
|
|
161
|
+
'validation' => 'required|string|min:20',
|
|
162
|
+
],
|
|
163
|
+
[
|
|
164
|
+
'name' => 'webhook_secret',
|
|
165
|
+
'label' => 'Webhook Secret (Optional)',
|
|
166
|
+
'type' => 'password',
|
|
167
|
+
'placeholder' => 'Enter webhook secret for signature validation',
|
|
168
|
+
'required' => false,
|
|
169
|
+
],
|
|
170
|
+
];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
public function supportsWebhooks(): bool
|
|
174
|
+
{
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Support\Telematics;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Contracts\TelematicProviderDescriptor;
|
|
6
|
+
use Fleetbase\FleetOps\Contracts\TelematicProviderInterface;
|
|
7
|
+
use Illuminate\Support\Collection;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Class ProviderRegistry.
|
|
11
|
+
*
|
|
12
|
+
* Central registry for all telematics providers.
|
|
13
|
+
* Manages provider discovery, registration, and instantiation.
|
|
14
|
+
*/
|
|
15
|
+
class TelematicProviderRegistry
|
|
16
|
+
{
|
|
17
|
+
/**
|
|
18
|
+
* @var Collection<TelematicProviderDescriptor>
|
|
19
|
+
*/
|
|
20
|
+
protected Collection $providers;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a new ProviderRegistry instance.
|
|
24
|
+
*/
|
|
25
|
+
public function __construct()
|
|
26
|
+
{
|
|
27
|
+
$this->providers = collect();
|
|
28
|
+
$this->loadNativeProviders();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Load native providers from configuration.
|
|
33
|
+
*/
|
|
34
|
+
protected function loadNativeProviders(): void
|
|
35
|
+
{
|
|
36
|
+
$config = config('telematics.providers', []);
|
|
37
|
+
|
|
38
|
+
foreach ($config as $providerConfig) {
|
|
39
|
+
$descriptor = new TelematicProviderDescriptor($providerConfig);
|
|
40
|
+
$this->register($descriptor);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Register a provider.
|
|
46
|
+
*/
|
|
47
|
+
public function register(TelematicProviderDescriptor $descriptor): void
|
|
48
|
+
{
|
|
49
|
+
$this->providers->put($descriptor->key, $descriptor);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get all registered providers.
|
|
54
|
+
*
|
|
55
|
+
* @return Collection<TelematicProviderDescriptor>
|
|
56
|
+
*/
|
|
57
|
+
public function all(): Collection
|
|
58
|
+
{
|
|
59
|
+
return $this->providers;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Find a provider by key.
|
|
64
|
+
*/
|
|
65
|
+
public function findByKey(string $key): ?TelematicProviderDescriptor
|
|
66
|
+
{
|
|
67
|
+
return $this->providers->get($key);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if a provider exists.
|
|
72
|
+
*/
|
|
73
|
+
public function has(string $key): bool
|
|
74
|
+
{
|
|
75
|
+
return $this->providers->has($key);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Resolve a provider instance by key.
|
|
80
|
+
*
|
|
81
|
+
* @throws \InvalidArgumentException
|
|
82
|
+
*/
|
|
83
|
+
public function resolve(string $key): TelematicProviderInterface
|
|
84
|
+
{
|
|
85
|
+
$descriptor = $this->findByKey($key);
|
|
86
|
+
|
|
87
|
+
if (!$descriptor) {
|
|
88
|
+
throw new \InvalidArgumentException("Provider '{$key}' not found in registry.");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!$descriptor->driverClass) {
|
|
92
|
+
throw new \InvalidArgumentException("Provider '{$key}' does not have a driver class.");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!class_exists($descriptor->driverClass)) {
|
|
96
|
+
throw new \InvalidArgumentException("Provider driver class '{$descriptor->driverClass}' does not exist.");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
$provider = app($descriptor->driverClass);
|
|
100
|
+
|
|
101
|
+
if (!$provider instanceof TelematicProviderInterface) {
|
|
102
|
+
throw new \InvalidArgumentException('Provider driver class must implement TelematicProviderInterface.');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return $provider;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get providers that support webhooks.
|
|
110
|
+
*
|
|
111
|
+
* @return Collection<TelematicProviderDescriptor>
|
|
112
|
+
*/
|
|
113
|
+
public function getWebhookProviders(): Collection
|
|
114
|
+
{
|
|
115
|
+
return $this->providers->filter(fn ($p) => $p->supportsWebhooks);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get providers that support discovery.
|
|
120
|
+
*
|
|
121
|
+
* @return Collection<TelematicProviderDescriptor>
|
|
122
|
+
*/
|
|
123
|
+
public function getDiscoveryProviders(): Collection
|
|
124
|
+
{
|
|
125
|
+
return $this->providers->filter(fn ($p) => $p->supportsDiscovery);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get native providers only.
|
|
130
|
+
*
|
|
131
|
+
* @return Collection<TelematicProviderDescriptor>
|
|
132
|
+
*/
|
|
133
|
+
public function getNativeProviders(): Collection
|
|
134
|
+
{
|
|
135
|
+
return $this->providers->filter(fn ($p) => $p->type === 'native');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get custom providers only.
|
|
140
|
+
*
|
|
141
|
+
* @return Collection<TelematicProviderDescriptor>
|
|
142
|
+
*/
|
|
143
|
+
public function getCustomProviders(): Collection
|
|
144
|
+
{
|
|
145
|
+
return $this->providers->filter(fn ($p) => $p->type === 'custom');
|
|
146
|
+
}
|
|
147
|
+
}
|