@fleetbase/fleetops-engine 0.6.19 → 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/kanban.hbs +12 -10
- package/addon/components/order/kanban.js +27 -3
- 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/operations/orders/index/new.js +4 -2
- package/addon/controllers/operations/orders/index.js +50 -45
- 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/operations/orders/index.js +0 -3
- package/addon/routes.js +1 -0
- package/addon/services/movement-tracker.js +81 -30
- package/addon/services/order-creation.js +4 -8
- package/addon/services/order-validation.js +3 -3
- package/addon/styles/fleetops-engine.css +192 -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/templates/operations/orders/index.hbs +26 -2
- package/addon/utils/fleet-ops-options.js +95 -0
- package/addon/utils/setup-customer-portal.js +7 -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/OrderController.php +50 -68
- 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 +11 -3
- package/server/src/Models/Place.php +9 -2
- 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 +104 -1
- 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
|
@@ -8,6 +8,7 @@ use Fleetbase\FleetOps\Casts\Point;
|
|
|
8
8
|
use Fleetbase\FleetOps\Support\Utils;
|
|
9
9
|
use Fleetbase\FleetOps\Support\VehicleData;
|
|
10
10
|
use Fleetbase\LaravelMysqlSpatial\Eloquent\SpatialTrait;
|
|
11
|
+
use Fleetbase\LaravelMysqlSpatial\Types\Point as SpatialPoint;
|
|
11
12
|
use Fleetbase\Models\Category;
|
|
12
13
|
use Fleetbase\Models\File;
|
|
13
14
|
use Fleetbase\Models\Model;
|
|
@@ -23,6 +24,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
|
23
24
|
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
|
24
25
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
|
25
26
|
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
|
27
|
+
use Illuminate\Support\Arr;
|
|
26
28
|
use Illuminate\Support\Str;
|
|
27
29
|
use Spatie\Activitylog\LogOptions;
|
|
28
30
|
use Spatie\Activitylog\Traits\LogsActivity;
|
|
@@ -61,7 +63,7 @@ class Vehicle extends Model
|
|
|
61
63
|
*
|
|
62
64
|
* @var array
|
|
63
65
|
*/
|
|
64
|
-
protected $searchableColumns = ['make', 'model', 'year', 'plate_number', 'vin', 'public_id'];
|
|
66
|
+
protected $searchableColumns = ['name', 'description', 'make', 'model', 'trim', 'model_type', 'body_type', 'body_sub_type', 'year', 'plate_number', 'vin', 'call_sign', 'public_id'];
|
|
65
67
|
|
|
66
68
|
/**
|
|
67
69
|
* Attributes that is filterable on this model.
|
|
@@ -533,6 +535,31 @@ class Vehicle extends Model
|
|
|
533
535
|
return ($isFirstPosition || $isPast50Meters) ? Position::create($positionData) : null;
|
|
534
536
|
}
|
|
535
537
|
|
|
538
|
+
/**
|
|
539
|
+
* Creates a new position for the vehicle.
|
|
540
|
+
*/
|
|
541
|
+
public function createPosition(array $attributes = [], Model|string|null $destination = null): ?Position
|
|
542
|
+
{
|
|
543
|
+
if (!isset($attributes['coordinates']) && isset($attributes['location'])) {
|
|
544
|
+
$attributes['coordinates'] = $attributes['location'];
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (!isset($attributes['coordinates']) && isset($attributes['latitude']) && isset($attributes['longitude'])) {
|
|
548
|
+
$attributes['coordinates'] = new SpatialPoint($attributes['latitude'], $attributes['longitude']);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// handle destination if set
|
|
552
|
+
$destinationUuid = Str::isUuid($destination) ? $destination : data_get($destination, 'uuid');
|
|
553
|
+
|
|
554
|
+
return Position::create([
|
|
555
|
+
...Arr::only($attributes, ['coordinates', 'heading', 'bearing', 'speed', 'altitude', 'order_uuid']),
|
|
556
|
+
'subject_uuid' => $this->uuid,
|
|
557
|
+
'subject_type' => $this->getMorphClass(),
|
|
558
|
+
'company_uuid' => $this->company_uuid,
|
|
559
|
+
'destination_uuid' => $destinationUuid,
|
|
560
|
+
]);
|
|
561
|
+
}
|
|
562
|
+
|
|
536
563
|
public static function createFromImport(array $row, bool $saveInstance = false): Vehicle
|
|
537
564
|
{
|
|
538
565
|
// Filter array for null key values
|
|
@@ -660,4 +687,80 @@ class Vehicle extends Model
|
|
|
660
687
|
|
|
661
688
|
return $details;
|
|
662
689
|
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Set or update a single key/value pair in the `specs` JSON column.
|
|
693
|
+
*
|
|
694
|
+
* Uses Laravel's `data_set` helper to allow dot notation for nested keys.
|
|
695
|
+
*
|
|
696
|
+
* @param string|array $key the key (or array path) to set within the specs
|
|
697
|
+
* @param mixed $value the value to assign to the given key
|
|
698
|
+
*
|
|
699
|
+
* @return array the updated specs array
|
|
700
|
+
*/
|
|
701
|
+
public function setSpec(string|array $key, mixed $value): array
|
|
702
|
+
{
|
|
703
|
+
$specs = is_array($this->specs) ? $this->specs : (array) $this->specs;
|
|
704
|
+
data_set($specs, $key, $value);
|
|
705
|
+
$this->specs = $specs;
|
|
706
|
+
|
|
707
|
+
return $specs;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Merge multiple values into the `specs` JSON column.
|
|
712
|
+
*
|
|
713
|
+
* By default this performs a shallow merge (overwrites duplicate keys).
|
|
714
|
+
* Use `array_replace_recursive` if you need nested merges.
|
|
715
|
+
*
|
|
716
|
+
* @param array $newSpecs key/value pairs to merge into specs
|
|
717
|
+
*
|
|
718
|
+
* @return array the updated specs array
|
|
719
|
+
*/
|
|
720
|
+
public function setSpecs(array $newSpecs = []): array
|
|
721
|
+
{
|
|
722
|
+
$specs = is_array($this->specs) ? $this->specs : (array) $this->specs;
|
|
723
|
+
$specs = array_merge($specs, $newSpecs);
|
|
724
|
+
$this->specs = $specs;
|
|
725
|
+
|
|
726
|
+
return $specs;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Set or update a single key/value pair in the `vin_data` JSON column.
|
|
731
|
+
*
|
|
732
|
+
* Uses Laravel's `data_set` helper to allow dot notation for nested keys.
|
|
733
|
+
*
|
|
734
|
+
* @param string|array $key the key (or array path) to set within the VIN data
|
|
735
|
+
* @param mixed $value the value to assign to the given key
|
|
736
|
+
*
|
|
737
|
+
* @return array the updated vin_data array
|
|
738
|
+
*/
|
|
739
|
+
public function setVinData(string|array $key, mixed $value): array
|
|
740
|
+
{
|
|
741
|
+
$vinData = is_array($this->vin_data) ? $this->vin_data : (array) $this->vin_data;
|
|
742
|
+
data_set($vinData, $key, $value);
|
|
743
|
+
$this->vin_data = $vinData;
|
|
744
|
+
|
|
745
|
+
return $vinData;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Merge multiple values into the `vin_data` JSON column.
|
|
750
|
+
*
|
|
751
|
+
* By default this performs a shallow merge (overwrites duplicate keys).
|
|
752
|
+
* Use `array_replace_recursive` if you need nested merges.
|
|
753
|
+
*
|
|
754
|
+
* @param array $newVinData key/value pairs to merge into vin_data
|
|
755
|
+
*
|
|
756
|
+
* @return array the updated vin_data array
|
|
757
|
+
*/
|
|
758
|
+
public function setVinDatas(array $newVinData = []): array
|
|
759
|
+
{
|
|
760
|
+
$vinData = is_array($this->vin_data) ? $this->vin_data : (array) $this->vin_data;
|
|
761
|
+
$vinData = array_merge($vinData, $newVinData);
|
|
762
|
+
$this->vin_data = $vinData;
|
|
763
|
+
|
|
764
|
+
return $vinData;
|
|
765
|
+
}
|
|
663
766
|
}
|
|
@@ -59,6 +59,7 @@ class FleetOpsServiceProvider extends CoreServiceProvider
|
|
|
59
59
|
\Fleetbase\FleetOps\Console\Commands\DebugOrderTracker::class,
|
|
60
60
|
\Fleetbase\FleetOps\Console\Commands\PurgeUnpurchasedServiceQuotes::class,
|
|
61
61
|
\Fleetbase\FleetOps\Console\Commands\SendDriverNotification::class,
|
|
62
|
+
\Fleetbase\FleetOps\Console\Commands\ReplayVehicleLocations::class,
|
|
62
63
|
];
|
|
63
64
|
|
|
64
65
|
/**
|
|
@@ -103,6 +104,7 @@ class FleetOpsServiceProvider extends CoreServiceProvider
|
|
|
103
104
|
$this->loadMigrationsFrom(__DIR__ . '/../../migrations');
|
|
104
105
|
$this->loadViewsFrom(__DIR__ . '/../../resources/views', 'fleetops');
|
|
105
106
|
$this->mergeConfigFrom(__DIR__ . '/../../config/fleetops.php', 'fleetops');
|
|
107
|
+
$this->mergeConfigFrom(__DIR__ . '/../../config/telematics.php', 'telematics');
|
|
106
108
|
$this->mergeConfigFrom(__DIR__ . '/../../config/api.php', 'api');
|
|
107
109
|
$this->mergeConfigFrom(__DIR__ . '/../../config/cache.stores.php', 'cache.stores');
|
|
108
110
|
$this->mergeConfigFrom(__DIR__ . '/../../config/geocoder.php', 'geocoder');
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Support\Telematics\Providers;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Contracts\TelematicProviderInterface;
|
|
6
|
+
use Fleetbase\FleetOps\Exceptions\TelematicRateLimitExceededException;
|
|
7
|
+
use Fleetbase\FleetOps\Models\Telematic;
|
|
8
|
+
use Illuminate\Support\Facades\Cache;
|
|
9
|
+
use Illuminate\Support\Facades\Crypt;
|
|
10
|
+
use Illuminate\Support\Facades\Http;
|
|
11
|
+
use Illuminate\Support\Facades\Log;
|
|
12
|
+
use Illuminate\Support\Str;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Class AbstractProvider.
|
|
16
|
+
*
|
|
17
|
+
* Base implementation for all telematics providers.
|
|
18
|
+
* Provides common functionality for HTTP requests, rate limiting,
|
|
19
|
+
* and credential management.
|
|
20
|
+
*/
|
|
21
|
+
abstract class AbstractProvider implements TelematicProviderInterface
|
|
22
|
+
{
|
|
23
|
+
protected Telematic $telematic;
|
|
24
|
+
protected array $credentials = [];
|
|
25
|
+
protected array $headers = [];
|
|
26
|
+
protected string $baseUrl = '';
|
|
27
|
+
protected int $requestsPerMinute = 60;
|
|
28
|
+
protected int $burstSize = 10;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Connect to the provider.
|
|
32
|
+
*/
|
|
33
|
+
public function connect(Telematic $telematic): void
|
|
34
|
+
{
|
|
35
|
+
$this->telematic = $telematic;
|
|
36
|
+
$this->credentials = json_decode(Crypt::decryptString($telematic->credentials), true);
|
|
37
|
+
$this->prepareAuthentication();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Prepare authentication headers/tokens.
|
|
42
|
+
* Override this in provider implementations.
|
|
43
|
+
*/
|
|
44
|
+
abstract protected function prepareAuthentication(): void;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Make an HTTP request to the provider API.
|
|
48
|
+
*
|
|
49
|
+
* @throws TelematicRateLimitExceededException
|
|
50
|
+
*/
|
|
51
|
+
protected function request(string $method, string $endpoint, array $data = []): array
|
|
52
|
+
{
|
|
53
|
+
$this->checkRateLimit();
|
|
54
|
+
|
|
55
|
+
$url = $this->baseUrl . $endpoint;
|
|
56
|
+
$correlationId = Str::uuid()->toString();
|
|
57
|
+
|
|
58
|
+
Log::info('Provider API request', [
|
|
59
|
+
'correlation_id' => $correlationId,
|
|
60
|
+
'provider' => class_basename($this),
|
|
61
|
+
'method' => $method,
|
|
62
|
+
'url' => $url,
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
$response = Http::withHeaders($this->headers)
|
|
66
|
+
->timeout(30)
|
|
67
|
+
->{strtolower($method)}($url, $data);
|
|
68
|
+
|
|
69
|
+
$this->recordRequest();
|
|
70
|
+
|
|
71
|
+
if ($response->failed()) {
|
|
72
|
+
Log::error('Provider API request failed', [
|
|
73
|
+
'correlation_id' => $correlationId,
|
|
74
|
+
'status' => $response->status(),
|
|
75
|
+
'body' => $response->body(),
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
throw new \Exception('API request failed: ' . $response->body());
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return $response->json();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check rate limit using token bucket algorithm.
|
|
86
|
+
*
|
|
87
|
+
* @throws TelematicRateLimitExceededException
|
|
88
|
+
*/
|
|
89
|
+
protected function checkRateLimit(): void
|
|
90
|
+
{
|
|
91
|
+
$key = 'rate_limit:' . class_basename($this) . ':' . $this->telematic->uuid;
|
|
92
|
+
$tokens = Cache::get($key, $this->burstSize);
|
|
93
|
+
|
|
94
|
+
if ($tokens <= 0) {
|
|
95
|
+
throw new TelematicRateLimitExceededException('Rate limit exceeded for provider');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
Cache::put($key, $tokens - 1, 60);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Record a request for metrics.
|
|
103
|
+
*/
|
|
104
|
+
protected function recordRequest(): void
|
|
105
|
+
{
|
|
106
|
+
$key = 'rate_limit:' . class_basename($this) . ':' . $this->telematic->uuid;
|
|
107
|
+
$tokens = Cache::get($key, 0);
|
|
108
|
+
|
|
109
|
+
// Refill tokens gradually
|
|
110
|
+
if ($tokens < $this->burstSize) {
|
|
111
|
+
Cache::put($key, min($tokens + 1, $this->burstSize), 60);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public function supportsWebhooks(): bool
|
|
116
|
+
{
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
public function supportsDiscovery(): bool
|
|
121
|
+
{
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
public function getRateLimits(): array
|
|
126
|
+
{
|
|
127
|
+
return [
|
|
128
|
+
'requests_per_minute' => $this->requestsPerMinute,
|
|
129
|
+
'burst_size' => $this->burstSize,
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public function validateWebhookSignature(string $payload, string $signature, array $credentials): bool
|
|
134
|
+
{
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
public function processWebhook(array $payload, array $headers = []): array
|
|
139
|
+
{
|
|
140
|
+
return [
|
|
141
|
+
'devices' => [],
|
|
142
|
+
'events' => [],
|
|
143
|
+
'sensors' => [],
|
|
144
|
+
];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
public function getCredentialSchema(): array
|
|
148
|
+
{
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -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
|
+
}
|