@fleetbase/fleetops-engine 0.6.17 → 0.6.18
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/layout/fleet-ops-sidebar.hbs +25 -0
- package/addon/templates/virtual.hbs +3 -3
- package/composer.json +3 -2
- package/extension.json +1 -1
- package/package.json +1 -1
- package/server/migrations/2025_08_28_054920_create_warranties_table.php +56 -0
- package/server/migrations/2025_08_28_054921_create_telematics_table.php +60 -0
- package/server/migrations/2025_08_28_054922_create_assets_table.php +94 -0
- package/server/migrations/2025_08_28_054925_create_devices_table.php +190 -0
- package/server/migrations/2025_08_28_054926_create_device_events_table.php +99 -0
- package/server/migrations/2025_08_28_054926_create_sensors_table.php +62 -0
- package/server/migrations/2025_08_28_054927_create_parts_table.php +73 -0
- package/server/migrations/2025_08_28_054929_create_equipments_table.php +58 -0
- package/server/migrations/2025_08_28_054930_create_work_orders_table.php +85 -0
- package/server/migrations/2025_08_28_054931_create_maintenances_table.php +79 -0
- package/server/migrations/2025_08_28_082002_update_vehicles_table_telematics.php +60 -0
- package/server/src/Http/Controllers/Api/v1/OrderController.php +6 -1
- package/server/src/Http/Resources/v1/Order.php +111 -60
- package/server/src/Models/Asset.php +548 -0
- package/server/src/Models/Contact.php +2 -0
- package/server/src/Models/Device.php +435 -0
- package/server/src/Models/DeviceEvent.php +501 -0
- package/server/src/Models/Driver.php +2 -0
- package/server/src/Models/Entity.php +2 -0
- package/server/src/Models/Equipment.php +483 -0
- package/server/src/Models/Fleet.php +2 -0
- package/server/src/Models/FuelReport.php +2 -0
- package/server/src/Models/Issue.php +2 -0
- package/server/src/Models/Maintenance.php +549 -0
- package/server/src/Models/Order.php +32 -112
- package/server/src/Models/OrderConfig.php +8 -0
- package/server/src/Models/Part.php +502 -0
- package/server/src/Models/Payload.php +101 -20
- package/server/src/Models/Place.php +10 -4
- package/server/src/Models/Sensor.php +510 -0
- package/server/src/Models/ServiceArea.php +1 -1
- package/server/src/Models/Telematic.php +336 -0
- package/server/src/Models/Vehicle.php +45 -1
- package/server/src/Models/VehicleDevice.php +1 -1
- package/server/src/Models/Vendor.php +2 -0
- package/server/src/Models/Warranty.php +413 -0
- package/server/src/Models/WorkOrder.php +532 -0
- package/server/src/Support/Utils.php +5 -0
- package/server/src/Traits/Maintainable.php +307 -0
|
@@ -308,6 +308,34 @@ class Payload extends Model
|
|
|
308
308
|
return $this;
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
+
/**
|
|
312
|
+
* Set waypoints for the current entity by resolving or creating Places and
|
|
313
|
+
* upserting Waypoint records in order.
|
|
314
|
+
*
|
|
315
|
+
* Input supports multiple shapes per item:
|
|
316
|
+
* - Top-level fields (preferred): 'type', 'place_uuid', 'id' (public_id), 'uuid' (temp search UUID),
|
|
317
|
+
* 'customer_uuid' (preferred), 'customer_id' (customer public_id), 'customer_type'
|
|
318
|
+
* - Or wrapped place payload: { type, place: {...} }
|
|
319
|
+
*
|
|
320
|
+
* Place resolution order:
|
|
321
|
+
* 1) attributes.place_uuid (must exist in DB)
|
|
322
|
+
* 2) attributes.id (public_id) -> resolve to uuid
|
|
323
|
+
* 3) Place::createFromMixed($attributes)
|
|
324
|
+
* - If attributes.uuid is present and differs from created uuid, stores it as meta: search_uuid
|
|
325
|
+
*
|
|
326
|
+
* Customer resolution (optional):
|
|
327
|
+
* - Uses 'customer_type' (default: 'fleetops:contact') to determine the model via Utils::getMutationType()
|
|
328
|
+
* - Tries 'customer_uuid' first (must exist)
|
|
329
|
+
* - If not found, tries 'customer_id' (public_id) and resolves to uuid
|
|
330
|
+
* - Only sets customer_uuid/customer_type if a valid record is found
|
|
331
|
+
*
|
|
332
|
+
* Uniqueness for each waypoint is defined by: (payload_uuid, place_uuid, order).
|
|
333
|
+
* Updatable fields include: type, customer_uuid, customer_type.
|
|
334
|
+
*
|
|
335
|
+
* @param array<int, array<string,mixed>> $waypoints list of waypoint attribute maps
|
|
336
|
+
*
|
|
337
|
+
* @return static
|
|
338
|
+
*/
|
|
311
339
|
public function setWaypoints($waypoints = [])
|
|
312
340
|
{
|
|
313
341
|
if (!is_array($waypoints)) {
|
|
@@ -315,45 +343,98 @@ class Payload extends Model
|
|
|
315
343
|
}
|
|
316
344
|
|
|
317
345
|
foreach ($waypoints as $index => $attributes) {
|
|
318
|
-
|
|
346
|
+
// Keep a copy to safely read top-level fields regardless of { place: {...} } normalization.
|
|
347
|
+
$raw = $attributes;
|
|
319
348
|
|
|
349
|
+
// Read top-level fields BEFORE normalizing the place shape.
|
|
350
|
+
$type = data_get($raw, 'type', 'dropoff');
|
|
351
|
+
$customerUuidIn = data_get($raw, 'customer_uuid'); // preferred
|
|
352
|
+
$customerPubIdIn = data_get($raw, 'customer_id'); // public_id fallback
|
|
353
|
+
$customerType = data_get($raw, 'customer_type', 'fleetops:contact');
|
|
354
|
+
|
|
355
|
+
// Normalize { place: {...} } shape if present.
|
|
320
356
|
if (Utils::isset($attributes, 'place') && is_array(Utils::get($attributes, 'place'))) {
|
|
321
357
|
$attributes = Utils::get($attributes, 'place');
|
|
322
358
|
}
|
|
323
359
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
360
|
+
// -------- Resolve Place (uuid) --------
|
|
361
|
+
$placeUuid = null;
|
|
362
|
+
|
|
363
|
+
// Path 1: explicit place_uuid, ensure it exists
|
|
364
|
+
if (
|
|
365
|
+
is_array($attributes)
|
|
366
|
+
&& isset($attributes['place_uuid'])
|
|
367
|
+
&& Place::where('uuid', $attributes['place_uuid'])->exists()
|
|
368
|
+
) {
|
|
369
|
+
$placeUuid = $attributes['place_uuid'];
|
|
370
|
+
|
|
371
|
+
// Path 2: public_id under "id" -> resolve to uuid
|
|
372
|
+
} elseif (
|
|
373
|
+
is_array($attributes)
|
|
374
|
+
&& isset($attributes['id'])
|
|
375
|
+
&& ($resolvedUuid = Place::where('public_id', $attributes['id'])->value('uuid'))
|
|
376
|
+
) {
|
|
377
|
+
$placeUuid = $resolvedUuid;
|
|
378
|
+
|
|
379
|
+
// Path 3: create from mixed payload
|
|
330
380
|
} else {
|
|
331
381
|
$place = Place::createFromMixed($attributes);
|
|
332
382
|
|
|
333
|
-
//
|
|
383
|
+
// Store temp search UUID for traceability if present and different
|
|
334
384
|
if ($place instanceof Place && isset($attributes['uuid']) && $place->uuid !== $attributes['uuid']) {
|
|
335
385
|
$place->updateMeta('search_uuid', $attributes['uuid']);
|
|
336
386
|
}
|
|
337
387
|
|
|
338
|
-
$
|
|
388
|
+
$placeUuid = $place->uuid;
|
|
339
389
|
}
|
|
340
390
|
|
|
341
|
-
//
|
|
342
|
-
$
|
|
343
|
-
$
|
|
344
|
-
|
|
391
|
+
// -------- Resolve Customer (uuid) --------
|
|
392
|
+
$customerUuid = null;
|
|
393
|
+
$customerTypeNamespace = null;
|
|
394
|
+
|
|
395
|
+
if ($customerType) {
|
|
345
396
|
$customerTypeNamespace = Utils::getMutationType($customerType);
|
|
346
|
-
$
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
397
|
+
$customerModel = app($customerTypeNamespace);
|
|
398
|
+
|
|
399
|
+
// Try by UUID first (preferred)
|
|
400
|
+
if ($customerUuidIn && $customerModel->where('uuid', $customerUuidIn)->exists()) {
|
|
401
|
+
$customerUuid = $customerUuidIn;
|
|
402
|
+
}
|
|
403
|
+
// If not found by UUID, try by public_id
|
|
404
|
+
if (!$customerUuid && $customerPubIdIn) {
|
|
405
|
+
$maybeUuid = $customerModel->where('public_id', $customerPubIdIn)->value('uuid');
|
|
406
|
+
if ($maybeUuid) {
|
|
407
|
+
$customerUuid = $maybeUuid;
|
|
408
|
+
} else {
|
|
409
|
+
// If neither lookup succeeds, drop the type namespace to avoid FK/type mismatch
|
|
410
|
+
$customerTypeNamespace = null;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// If no valid uuid after both attempts, clear the type
|
|
415
|
+
if (!$customerUuid) {
|
|
416
|
+
$customerTypeNamespace = null;
|
|
350
417
|
}
|
|
351
418
|
}
|
|
352
419
|
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
$
|
|
420
|
+
// -------- Upsert Waypoint --------
|
|
421
|
+
// Uniqueness: payload + place + order for deterministic row per position.
|
|
422
|
+
$unique = [
|
|
423
|
+
'payload_uuid' => $this->payload_uuid,
|
|
424
|
+
'place_uuid' => $placeUuid,
|
|
425
|
+
'order' => $index,
|
|
426
|
+
];
|
|
427
|
+
|
|
428
|
+
// Only mutable/updatable fields go here.
|
|
429
|
+
$values = array_filter([
|
|
430
|
+
'type' => $type,
|
|
431
|
+
'customer_uuid' => $customerUuid,
|
|
432
|
+
'customer_type' => $customerTypeNamespace,
|
|
433
|
+
], fn ($v) => !is_null($v));
|
|
434
|
+
|
|
435
|
+
$waypointRecord = Waypoint::updateOrCreate($unique, $values);
|
|
356
436
|
|
|
437
|
+
// Track it (assumes this is a Collection)
|
|
357
438
|
$this->waypointMarkers->push($waypointRecord);
|
|
358
439
|
}
|
|
359
440
|
|
|
@@ -431,12 +431,10 @@ class Place extends Model
|
|
|
431
431
|
|
|
432
432
|
$results = \Geocoder\Laravel\Facades\Geocoder::reverse($latitude, $longitude)->get();
|
|
433
433
|
|
|
434
|
-
if (
|
|
435
|
-
|
|
434
|
+
if (!$results->isEmpty()) {
|
|
435
|
+
$instance->fillWithGoogleAddress($results->first());
|
|
436
436
|
}
|
|
437
437
|
|
|
438
|
-
$instance->fillWithGoogleAddress($results->first());
|
|
439
|
-
|
|
440
438
|
if ($saveInstance) {
|
|
441
439
|
$instance->save();
|
|
442
440
|
}
|
|
@@ -541,6 +539,14 @@ class Place extends Model
|
|
|
541
539
|
return $existingPlace;
|
|
542
540
|
}
|
|
543
541
|
|
|
542
|
+
// Get public_id if supplied if set
|
|
543
|
+
$id = data_get($place, 'id') || data_get($place, 'public_id');
|
|
544
|
+
|
|
545
|
+
// If $place has a valid uuid and a matching Place object exists, return the uuid
|
|
546
|
+
if (Utils::isPublicId($id) && $existingPlace = static::where('public_id', $id)->first()) {
|
|
547
|
+
return $existingPlace;
|
|
548
|
+
}
|
|
549
|
+
|
|
544
550
|
// If has $attributes['address']
|
|
545
551
|
if (!empty($place['address'])) {
|
|
546
552
|
return static::createFromGeocodingLookup($place['address'], $saveInstance);
|
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Models;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\Casts\Json;
|
|
6
|
+
use Fleetbase\Models\Alert;
|
|
7
|
+
use Fleetbase\Models\Model;
|
|
8
|
+
use Fleetbase\Models\User;
|
|
9
|
+
use Fleetbase\Traits\HasApiModelBehavior;
|
|
10
|
+
use Fleetbase\Traits\HasCustomFields;
|
|
11
|
+
use Fleetbase\Traits\HasMetaAttributes;
|
|
12
|
+
use Fleetbase\Traits\HasPublicId;
|
|
13
|
+
use Fleetbase\Traits\HasUuid;
|
|
14
|
+
use Fleetbase\Traits\Searchable;
|
|
15
|
+
use Fleetbase\Traits\TracksApiCredential;
|
|
16
|
+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
17
|
+
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
18
|
+
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
|
19
|
+
use Spatie\Activitylog\LogOptions;
|
|
20
|
+
use Spatie\Activitylog\Traits\LogsActivity;
|
|
21
|
+
use Spatie\Sluggable\HasSlug;
|
|
22
|
+
use Spatie\Sluggable\SlugOptions;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Class Sensor.
|
|
26
|
+
*
|
|
27
|
+
* Represents a sensor that can measure and report various metrics like temperature,
|
|
28
|
+
* door status, fuel level, tire pressure, etc. Sensors can be attached to devices,
|
|
29
|
+
* assets, or other entities in the fleet management system.
|
|
30
|
+
*/
|
|
31
|
+
class Sensor extends Model
|
|
32
|
+
{
|
|
33
|
+
use HasUuid;
|
|
34
|
+
use HasPublicId;
|
|
35
|
+
use TracksApiCredential;
|
|
36
|
+
use HasApiModelBehavior;
|
|
37
|
+
use HasSlug;
|
|
38
|
+
use LogsActivity;
|
|
39
|
+
use HasMetaAttributes;
|
|
40
|
+
use Searchable;
|
|
41
|
+
use HasCustomFields;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The database table used by the model.
|
|
45
|
+
*
|
|
46
|
+
* @var string
|
|
47
|
+
*/
|
|
48
|
+
protected $table = 'sensors';
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The type of public Id to generate.
|
|
52
|
+
*
|
|
53
|
+
* @var string
|
|
54
|
+
*/
|
|
55
|
+
protected $publicIdType = 'sensor';
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The attributes that can be queried.
|
|
59
|
+
*
|
|
60
|
+
* @var array
|
|
61
|
+
*/
|
|
62
|
+
protected $searchableColumns = ['name', 'sensor_type', 'unit', 'public_id'];
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The attributes that can be used for filtering.
|
|
66
|
+
*
|
|
67
|
+
* @var array
|
|
68
|
+
*/
|
|
69
|
+
protected $filterParams = ['sensor_type', 'status', 'device_uuid', 'warranty_uuid', 'sensorable_type'];
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The attributes that are mass assignable.
|
|
73
|
+
*
|
|
74
|
+
* @var array
|
|
75
|
+
*/
|
|
76
|
+
protected $fillable = [
|
|
77
|
+
'company_uuid',
|
|
78
|
+
'device_uuid',
|
|
79
|
+
'warranty_uuid',
|
|
80
|
+
'name',
|
|
81
|
+
'sensor_type',
|
|
82
|
+
'unit',
|
|
83
|
+
'min_threshold',
|
|
84
|
+
'max_threshold',
|
|
85
|
+
'threshold_inclusive',
|
|
86
|
+
'last_reading_at',
|
|
87
|
+
'last_value',
|
|
88
|
+
'calibration',
|
|
89
|
+
'report_frequency_sec',
|
|
90
|
+
'sensorable_type',
|
|
91
|
+
'sensorable_uuid',
|
|
92
|
+
'status',
|
|
93
|
+
'meta',
|
|
94
|
+
'slug',
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Dynamic attributes that are appended to object.
|
|
99
|
+
*
|
|
100
|
+
* @var array
|
|
101
|
+
*/
|
|
102
|
+
protected $appends = [
|
|
103
|
+
'device_name',
|
|
104
|
+
'warranty_name',
|
|
105
|
+
'attached_to_name',
|
|
106
|
+
'is_active',
|
|
107
|
+
'threshold_status',
|
|
108
|
+
'last_reading_formatted',
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The attributes excluded from the model's JSON form.
|
|
113
|
+
*
|
|
114
|
+
* @var array
|
|
115
|
+
*/
|
|
116
|
+
protected $hidden = ['device', 'warranty', 'sensorable'];
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The attributes that should be cast to native types.
|
|
120
|
+
*
|
|
121
|
+
* @var array
|
|
122
|
+
*/
|
|
123
|
+
protected $casts = [
|
|
124
|
+
'min_threshold' => 'float',
|
|
125
|
+
'max_threshold' => 'float',
|
|
126
|
+
'threshold_inclusive' => 'boolean',
|
|
127
|
+
'last_reading_at' => 'datetime',
|
|
128
|
+
'report_frequency_sec' => 'integer',
|
|
129
|
+
'calibration' => Json::class,
|
|
130
|
+
'meta' => Json::class,
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Properties which activity needs to be logged.
|
|
135
|
+
*
|
|
136
|
+
* @var array
|
|
137
|
+
*/
|
|
138
|
+
protected static $logAttributes = '*';
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Do not log empty changed.
|
|
142
|
+
*
|
|
143
|
+
* @var bool
|
|
144
|
+
*/
|
|
145
|
+
protected static $submitEmptyLogs = false;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The name of the subject to log.
|
|
149
|
+
*
|
|
150
|
+
* @var string
|
|
151
|
+
*/
|
|
152
|
+
protected static $logName = 'sensor';
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get the options for generating the slug.
|
|
156
|
+
*/
|
|
157
|
+
public function getSlugOptions(): SlugOptions
|
|
158
|
+
{
|
|
159
|
+
return SlugOptions::create()
|
|
160
|
+
->generateSlugsFrom(['name', 'sensor_type'])
|
|
161
|
+
->saveSlugsTo('slug');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get the activity log options for the model.
|
|
166
|
+
*/
|
|
167
|
+
public function getActivitylogOptions(): LogOptions
|
|
168
|
+
{
|
|
169
|
+
return LogOptions::defaults()->logAll();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
public function device(): BelongsTo
|
|
173
|
+
{
|
|
174
|
+
return $this->belongsTo(Device::class, 'device_uuid', 'uuid');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
public function warranty(): BelongsTo
|
|
178
|
+
{
|
|
179
|
+
return $this->belongsTo(Warranty::class, 'warranty_uuid', 'uuid');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
public function createdBy(): BelongsTo
|
|
183
|
+
{
|
|
184
|
+
return $this->belongsTo(User::class, 'created_by_uuid', 'uuid');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
public function updatedBy(): BelongsTo
|
|
188
|
+
{
|
|
189
|
+
return $this->belongsTo(User::class, 'updated_by_uuid', 'uuid');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
public function sensorable(): MorphTo
|
|
193
|
+
{
|
|
194
|
+
return $this->morphTo();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
public function alerts(): HasMany
|
|
198
|
+
{
|
|
199
|
+
return $this->hasMany(Alert::class, 'subject_uuid', 'uuid')
|
|
200
|
+
->where('subject_type', static::class);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get the device name.
|
|
205
|
+
*/
|
|
206
|
+
public function getDeviceNameAttribute(): ?string
|
|
207
|
+
{
|
|
208
|
+
return $this->device?->name;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get the warranty name.
|
|
213
|
+
*/
|
|
214
|
+
public function getWarrantyNameAttribute(): ?string
|
|
215
|
+
{
|
|
216
|
+
return $this->warranty?->name;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get the name of what the sensor is attached to.
|
|
221
|
+
*/
|
|
222
|
+
public function getAttachedToNameAttribute(): ?string
|
|
223
|
+
{
|
|
224
|
+
if ($this->sensorable) {
|
|
225
|
+
return $this->sensorable->name ?? $this->sensorable->display_name ?? null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Check if the sensor is currently active.
|
|
233
|
+
*/
|
|
234
|
+
public function getIsActiveAttribute(): bool
|
|
235
|
+
{
|
|
236
|
+
if ($this->status !== 'active') {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Consider active if we've received a reading within the expected frequency
|
|
241
|
+
if ($this->last_reading_at && $this->report_frequency_sec) {
|
|
242
|
+
$expectedNextReading = $this->last_reading_at->addSeconds($this->report_frequency_sec * 2); // Allow 2x frequency
|
|
243
|
+
|
|
244
|
+
return now()->lte($expectedNextReading);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return $this->status === 'active';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Get the threshold status of the last reading.
|
|
252
|
+
*/
|
|
253
|
+
public function getThresholdStatusAttribute(): string
|
|
254
|
+
{
|
|
255
|
+
if (!$this->last_value || (!$this->min_threshold && !$this->max_threshold)) {
|
|
256
|
+
return 'normal';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
$value = (float) $this->last_value;
|
|
260
|
+
|
|
261
|
+
if ($this->min_threshold && $this->max_threshold) {
|
|
262
|
+
if ($this->threshold_inclusive) {
|
|
263
|
+
if ($value < $this->min_threshold || $value > $this->max_threshold) {
|
|
264
|
+
return 'out_of_range';
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
if ($value <= $this->min_threshold || $value >= $this->max_threshold) {
|
|
268
|
+
return 'out_of_range';
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
} elseif ($this->min_threshold) {
|
|
272
|
+
if ($this->threshold_inclusive ? $value < $this->min_threshold : $value <= $this->min_threshold) {
|
|
273
|
+
return 'below_minimum';
|
|
274
|
+
}
|
|
275
|
+
} elseif ($this->max_threshold) {
|
|
276
|
+
if ($this->threshold_inclusive ? $value > $this->max_threshold : $value >= $this->max_threshold) {
|
|
277
|
+
return 'above_maximum';
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return 'normal';
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Get the last reading with unit formatting.
|
|
286
|
+
*/
|
|
287
|
+
public function getLastReadingFormattedAttribute(): ?string
|
|
288
|
+
{
|
|
289
|
+
if (!$this->last_value) {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return $this->last_value . ($this->unit ? ' ' . $this->unit : '');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Scope to get sensors by type.
|
|
298
|
+
*
|
|
299
|
+
* @param \Illuminate\Database\Eloquent\Builder $query
|
|
300
|
+
*
|
|
301
|
+
* @return \Illuminate\Database\Eloquent\Builder
|
|
302
|
+
*/
|
|
303
|
+
public function scopeByType($query, string $type)
|
|
304
|
+
{
|
|
305
|
+
return $query->where('sensor_type', $type);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Scope to get active sensors.
|
|
310
|
+
*
|
|
311
|
+
* @param \Illuminate\Database\Eloquent\Builder $query
|
|
312
|
+
*
|
|
313
|
+
* @return \Illuminate\Database\Eloquent\Builder
|
|
314
|
+
*/
|
|
315
|
+
public function scopeActive($query)
|
|
316
|
+
{
|
|
317
|
+
return $query->where('status', 'active');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Scope to get sensors with recent readings.
|
|
322
|
+
*
|
|
323
|
+
* @param \Illuminate\Database\Eloquent\Builder $query
|
|
324
|
+
*
|
|
325
|
+
* @return \Illuminate\Database\Eloquent\Builder
|
|
326
|
+
*/
|
|
327
|
+
public function scopeWithRecentReadings($query, int $minutes = 60)
|
|
328
|
+
{
|
|
329
|
+
return $query->where('last_reading_at', '>=', now()->subMinutes($minutes));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Scope to get sensors that are out of threshold.
|
|
334
|
+
*
|
|
335
|
+
* @param \Illuminate\Database\Eloquent\Builder $query
|
|
336
|
+
*
|
|
337
|
+
* @return \Illuminate\Database\Eloquent\Builder
|
|
338
|
+
*/
|
|
339
|
+
public function scopeOutOfThreshold($query)
|
|
340
|
+
{
|
|
341
|
+
return $query->where(function ($q) {
|
|
342
|
+
$q->whereRaw('CAST(last_value AS DECIMAL) < min_threshold')
|
|
343
|
+
->orWhereRaw('CAST(last_value AS DECIMAL) > max_threshold');
|
|
344
|
+
})->whereNotNull('last_value');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Record a new sensor reading.
|
|
349
|
+
*/
|
|
350
|
+
public function recordReading($value, ?\DateTime $timestamp = null): bool
|
|
351
|
+
{
|
|
352
|
+
$timestamp = $timestamp ?? now();
|
|
353
|
+
|
|
354
|
+
$updated = $this->update([
|
|
355
|
+
'last_value' => $value,
|
|
356
|
+
'last_reading_at' => $timestamp,
|
|
357
|
+
]);
|
|
358
|
+
|
|
359
|
+
if ($updated) {
|
|
360
|
+
// Check if the reading is out of threshold and create alert if needed
|
|
361
|
+
$this->checkThresholdAndCreateAlert($value);
|
|
362
|
+
|
|
363
|
+
activity('sensor_reading')
|
|
364
|
+
->performedOn($this)
|
|
365
|
+
->withProperties([
|
|
366
|
+
'value' => $value,
|
|
367
|
+
'unit' => $this->unit,
|
|
368
|
+
'threshold_status' => $this->threshold_status,
|
|
369
|
+
'timestamp' => $timestamp,
|
|
370
|
+
])
|
|
371
|
+
->log('Sensor reading recorded');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return $updated;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Check threshold and create alert if needed.
|
|
379
|
+
*/
|
|
380
|
+
protected function checkThresholdAndCreateAlert($value): void
|
|
381
|
+
{
|
|
382
|
+
$thresholdStatus = $this->threshold_status;
|
|
383
|
+
|
|
384
|
+
if ($thresholdStatus !== 'normal') {
|
|
385
|
+
// Check if there's already an open alert for this sensor
|
|
386
|
+
$existingAlert = $this->alerts()
|
|
387
|
+
->where('status', 'open')
|
|
388
|
+
->where('type', 'sensor_threshold')
|
|
389
|
+
->first();
|
|
390
|
+
|
|
391
|
+
if (!$existingAlert) {
|
|
392
|
+
Alert::create([
|
|
393
|
+
'company_uuid' => $this->company_uuid,
|
|
394
|
+
'type' => 'sensor_threshold',
|
|
395
|
+
'severity' => $this->getSeverityForThresholdStatus($thresholdStatus),
|
|
396
|
+
'status' => 'open',
|
|
397
|
+
'subject_type' => static::class,
|
|
398
|
+
'subject_uuid' => $this->uuid,
|
|
399
|
+
'message' => $this->generateThresholdAlertMessage($value, $thresholdStatus),
|
|
400
|
+
'context' => [
|
|
401
|
+
'sensor_name' => $this->name,
|
|
402
|
+
'sensor_type' => $this->sensor_type,
|
|
403
|
+
'value' => $value,
|
|
404
|
+
'unit' => $this->unit,
|
|
405
|
+
'threshold_status' => $thresholdStatus,
|
|
406
|
+
'min_threshold' => $this->min_threshold,
|
|
407
|
+
'max_threshold' => $this->max_threshold,
|
|
408
|
+
],
|
|
409
|
+
'triggered_at' => now(),
|
|
410
|
+
]);
|
|
411
|
+
}
|
|
412
|
+
} else {
|
|
413
|
+
// Resolve any open threshold alerts for this sensor
|
|
414
|
+
$this->alerts()
|
|
415
|
+
->where('status', 'open')
|
|
416
|
+
->where('type', 'sensor_threshold')
|
|
417
|
+
->update([
|
|
418
|
+
'status' => 'resolved',
|
|
419
|
+
'resolved_at' => now(),
|
|
420
|
+
]);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Get severity level for threshold status.
|
|
426
|
+
*/
|
|
427
|
+
protected function getSeverityForThresholdStatus(string $thresholdStatus): string
|
|
428
|
+
{
|
|
429
|
+
switch ($thresholdStatus) {
|
|
430
|
+
case 'out_of_range':
|
|
431
|
+
return 'high';
|
|
432
|
+
case 'above_maximum':
|
|
433
|
+
case 'below_minimum':
|
|
434
|
+
return 'medium';
|
|
435
|
+
default:
|
|
436
|
+
return 'low';
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Generate alert message for threshold violation.
|
|
442
|
+
*/
|
|
443
|
+
protected function generateThresholdAlertMessage($value, string $thresholdStatus): string
|
|
444
|
+
{
|
|
445
|
+
$formattedValue = $value . ($this->unit ? ' ' . $this->unit : '');
|
|
446
|
+
|
|
447
|
+
switch ($thresholdStatus) {
|
|
448
|
+
case 'out_of_range':
|
|
449
|
+
return "Sensor '{$this->name}' reading ({$formattedValue}) is out of acceptable range ({$this->min_threshold}-{$this->max_threshold} {$this->unit})";
|
|
450
|
+
case 'above_maximum':
|
|
451
|
+
return "Sensor '{$this->name}' reading ({$formattedValue}) exceeds maximum threshold ({$this->max_threshold} {$this->unit})";
|
|
452
|
+
case 'below_minimum':
|
|
453
|
+
return "Sensor '{$this->name}' reading ({$formattedValue}) is below minimum threshold ({$this->min_threshold} {$this->unit})";
|
|
454
|
+
default:
|
|
455
|
+
return "Sensor '{$this->name}' threshold violation detected";
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Calibrate the sensor with offset and scale factors.
|
|
461
|
+
*/
|
|
462
|
+
public function calibrate(float $offset = 0, float $scale = 1): bool
|
|
463
|
+
{
|
|
464
|
+
$calibration = [
|
|
465
|
+
'offset' => $offset,
|
|
466
|
+
'scale' => $scale,
|
|
467
|
+
'calibrated_at' => now(),
|
|
468
|
+
'calibrated_by' => auth()->id(),
|
|
469
|
+
];
|
|
470
|
+
|
|
471
|
+
return $this->update(['calibration' => $calibration]);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Apply calibration to a raw sensor value.
|
|
476
|
+
*/
|
|
477
|
+
public function applyCalibratedValue(float $rawValue): float
|
|
478
|
+
{
|
|
479
|
+
$calibration = $this->calibration ?? [];
|
|
480
|
+
$offset = $calibration['offset'] ?? 0;
|
|
481
|
+
$scale = $calibration['scale'] ?? 1;
|
|
482
|
+
|
|
483
|
+
return ($rawValue * $scale) + $offset;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Get sensor reading history.
|
|
488
|
+
*/
|
|
489
|
+
public function getReadingHistory(int $limit = 100, int $hours = 24): array
|
|
490
|
+
{
|
|
491
|
+
// This would typically query a separate sensor_readings table
|
|
492
|
+
// For now, return a placeholder structure
|
|
493
|
+
return [
|
|
494
|
+
'sensor_uuid' => $this->uuid,
|
|
495
|
+
'sensor_name' => $this->name,
|
|
496
|
+
'period' => [
|
|
497
|
+
'start' => now()->subHours($hours),
|
|
498
|
+
'end' => now(),
|
|
499
|
+
],
|
|
500
|
+
'readings' => [], // Would contain actual reading data
|
|
501
|
+
'summary' => [
|
|
502
|
+
'count' => 0,
|
|
503
|
+
'min' => null,
|
|
504
|
+
'max' => null,
|
|
505
|
+
'avg' => null,
|
|
506
|
+
'last' => $this->last_value,
|
|
507
|
+
],
|
|
508
|
+
];
|
|
509
|
+
}
|
|
510
|
+
}
|