Sensor   F
last analyzed

Complexity

Total Complexity 60

Size/Duplication

Total Lines 622
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 60
eloc 253
c 0
b 0
f 0
dl 0
loc 622
rs 3.6

24 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 54 2
A save() 0 33 5
A fetch() 0 18 2
A getUniqueId() 0 3 1
A getTypes() 0 3 1
A recordSensorData() 0 39 4
A fetchSnmpData() 0 21 3
B poll() 0 46 6
A clean() 0 21 4
A getPollingMethod() 0 3 1
A checkForDuplicateSensors() 0 16 3
A isValid() 0 3 1
A escapeNull() 0 5 2
A pollSensorType() 0 17 2
A getDiscoveryInterface() 0 3 1
A discoverType() 0 26 5
A getOidsFromSensors() 0 11 1
B processSensorData() 0 38 8
A sync() 0 13 3
A runDiscovery() 0 2 1
A getPollingInterface() 0 3 1
A getDiscoveryMethod() 0 3 1
A getTable() 0 3 1
A toArray() 0 19 1

How to fix   Complexity   

Complex Class

Complex classes like Sensor often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Sensor, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Sensor.php
4
 *
5
 * Base Sensor class
6
 *
7
 * This program is free software: you can redistribute it and/or modify
8
 * it under the terms of the GNU General Public License as published by
9
 * the Free Software Foundation, either version 3 of the License, or
10
 * (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
15
 * GNU General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU General Public License
18
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19
 *
20
 * @link       https://www.librenms.org
21
 *
22
 * @copyright  2017 Tony Murray
23
 * @author     Tony Murray <[email protected]>
24
 */
25
26
namespace LibreNMS\Device;
27
28
use LibreNMS\Config;
29
use LibreNMS\Interfaces\Discovery\DiscoveryModule;
30
use LibreNMS\Interfaces\Polling\PollerModule;
31
use LibreNMS\OS;
32
use LibreNMS\RRD\RrdDefinition;
33
34
class Sensor implements DiscoveryModule, PollerModule
35
{
36
    protected static $name = 'Sensor';
37
    protected static $table = 'sensors';
38
    protected static $data_name = 'sensor';
39
    protected static $translation_prefix = 'sensors';
40
41
    private $valid = true;
42
43
    private $sensor_id;
44
45
    private $type;
46
    private $device_id;
47
    private $oids;
48
    private $subtype;
49
    private $index;
50
    private $description;
51
    private $current;
52
    private $multiplier;
53
    private $divisor;
54
    private $aggregator;
55
    private $high_limit;
56
    private $low_limit;
57
    private $high_warn;
58
    private $low_warn;
59
    private $entPhysicalIndex;
60
    private $entPhysicalMeasured;
61
62
    /**
63
     * Sensor constructor. Create a new sensor to be discovered.
64
     *
65
     * @param  string  $type  Class of this sensor, must be a supported class
66
     * @param  int  $device_id  the device_id of the device that owns this sensor
67
     * @param  array|string  $oids  an array or single oid that contains the data for this sensor
68
     * @param  string  $subtype  the type of sensor an additional identifier to separate out sensors of the same class, generally this is the os name
69
     * @param  int|string  $index  the index of this sensor, must be stable, generally the index of the oid
70
     * @param  string  $description  A user visible description of this sensor, may be truncated in some places (like graphs)
71
     * @param  int|float  $current  The current value of this sensor, will seed the db and may be used to guess limits
72
     * @param  int  $multiplier  a number to multiply the value(s) by
73
     * @param  int  $divisor  a number to divide the value(s) by
74
     * @param  string  $aggregator  an operation to combine multiple numbers. Supported: sum, avg
75
     * @param  int|float  $high_limit  Alerting: Maximum value
76
     * @param  int|float  $low_limit  Alerting: Minimum value
77
     * @param  int|float  $high_warn  Alerting: High warning value
78
     * @param  int|float  $low_warn  Alerting: Low warning value
79
     * @param  int|float  $entPhysicalIndex  The entPhysicalIndex this sensor is associated, often a port
80
     * @param  int|float  $entPhysicalMeasured  the table to look for the entPhysicalIndex, for example 'ports' (maybe unused)
81
     */
82
    public function __construct(
83
        $type,
84
        $device_id,
85
        $oids,
86
        $subtype,
87
        $index,
88
        $description,
89
        $current = null,
90
        $multiplier = 1,
91
        $divisor = 1,
92
        $aggregator = 'sum',
93
        $high_limit = null,
94
        $low_limit = null,
95
        $high_warn = null,
96
        $low_warn = null,
97
        $entPhysicalIndex = null,
98
        $entPhysicalMeasured = null
99
    ) {
100
        $this->type = $type;
101
        $this->device_id = $device_id;
102
        $this->oids = (array) $oids;
103
        $this->subtype = $subtype;
104
        $this->index = $index;
105
        $this->description = $description;
106
        $this->current = $current;
107
        $this->multiplier = $multiplier;
108
        $this->divisor = $divisor;
109
        $this->aggregator = $aggregator;
110
        $this->entPhysicalIndex = $entPhysicalIndex;
111
        $this->entPhysicalMeasured = $entPhysicalMeasured;
112
        $this->high_limit = $high_limit;
113
        $this->low_limit = $low_limit;
114
        $this->high_warn = $high_warn;
115
        $this->low_warn = $low_warn;
116
117
        // ensure leading dots
118
        array_walk($this->oids, function (&$oid) {
119
            $oid = '.' . ltrim($oid, '.');
120
        });
121
122
        $sensor = $this->toArray();
123
        // validity not checked yet
124
        if (is_null($this->current)) {
125
            $sensor['sensor_oids'] = $this->oids;
126
            $sensors = [$sensor];
127
128
            $prefetch = self::fetchSnmpData(device_by_id_cache($device_id), $sensors);
129
            $data = static::processSensorData($sensors, $prefetch);
130
131
            $this->current = current($data);
132
            $this->valid = is_numeric($this->current);
133
        }
134
135
        d_echo('Discovered ' . get_called_class() . ' ' . print_r($sensor, true));
0 ignored issues
show
Bug introduced by
Are you sure print_r($sensor, true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

135
        d_echo('Discovered ' . get_called_class() . ' ' . /** @scrutinizer ignore-type */ print_r($sensor, true));
Loading history...
136
    }
137
138
    /**
139
     * Save this sensor to the database.
140
     *
141
     * @return int the sensor_id of this sensor in the database
142
     */
143
    final public function save()
144
    {
145
        $db_sensor = $this->fetch();
146
147
        $new_sensor = $this->toArray();
148
        if ($db_sensor) {
149
            unset($new_sensor['sensor_current']); // if updating, don't check sensor_current
150
            $update = array_diff_assoc($new_sensor, $db_sensor);
151
152
            if ($db_sensor['sensor_custom'] == 'Yes') {
153
                unset($update['sensor_limit']);
154
                unset($update['sensor_limit_warn']);
155
                unset($update['sensor_limit_low']);
156
                unset($update['sensor_limit_low_warn']);
157
            }
158
159
            if (empty($update)) {
160
                echo '.';
161
            } else {
162
                dbUpdate($this->escapeNull($update), $this->getTable(), '`sensor_id`=?', [$this->sensor_id]);
163
                echo 'U';
164
            }
165
        } else {
166
            $this->sensor_id = dbInsert($this->escapeNull($new_sensor), $this->getTable());
167
            if ($this->sensor_id !== null) {
168
                $name = static::$name;
169
                $message = "$name Discovered: {$this->type} {$this->subtype} {$this->index} {$this->description}";
170
                log_event($message, $this->device_id, static::$table, 3, $this->sensor_id);
171
                echo '+';
172
            }
173
        }
174
175
        return $this->sensor_id;
176
    }
177
178
    /**
179
     * Fetch the sensor from the database.
180
     * If it doesn't exist, returns null.
181
     *
182
     * @return array|null
183
     */
184
    private function fetch()
185
    {
186
        $table = $this->getTable();
187
        if (isset($this->sensor_id)) {
188
            return dbFetchRow(
189
                "SELECT `$table` FROM ? WHERE `sensor_id`=?",
190
                [$this->sensor_id]
191
            );
192
        }
193
194
        $sensor = dbFetchRow(
195
            "SELECT * FROM `$table` " .
196
            'WHERE `device_id`=? AND `sensor_class`=? AND `sensor_type`=? AND `sensor_index`=?',
197
            [$this->device_id, $this->type, $this->subtype, $this->index]
198
        );
199
        $this->sensor_id = $sensor['sensor_id'];
200
201
        return $sensor;
202
    }
203
204
    /**
205
     * Get the table for this sensor
206
     *
207
     * @return string
208
     */
209
    public function getTable()
210
    {
211
        return static::$table;
212
    }
213
214
    /**
215
     * Get an array of this sensor with fields that line up with the database.
216
     * Excludes sensor_id and current
217
     *
218
     * @return array
219
     */
220
    protected function toArray()
221
    {
222
        return [
223
            'sensor_class' => $this->type,
224
            'device_id' => $this->device_id,
225
            'sensor_oids' => json_encode($this->oids),
226
            'sensor_index' => $this->index,
227
            'sensor_type' => $this->subtype,
228
            'sensor_descr' => $this->description,
229
            'sensor_divisor' => $this->divisor,
230
            'sensor_multiplier' => $this->multiplier,
231
            'sensor_aggregator' => $this->aggregator,
232
            'sensor_limit' => $this->high_limit,
233
            'sensor_limit_warn' => $this->high_warn,
234
            'sensor_limit_low' => $this->low_limit,
235
            'sensor_limit_low_warn' => $this->low_warn,
236
            'sensor_current' => $this->current,
237
            'entPhysicalIndex' => $this->entPhysicalIndex,
238
            'entPhysicalIndex_measured' => $this->entPhysicalMeasured,
239
        ];
240
    }
241
242
    /**
243
     * Escape null values so dbFacile doesn't mess them up
244
     * honestly, this should be the default, but could break shit
245
     *
246
     * @param  array  $array
247
     * @return array
248
     */
249
    private function escapeNull($array)
250
    {
251
        return array_map(function ($value) {
252
            return is_null($value) ? ['NULL'] : $value;
253
        }, $array);
254
    }
255
256
    /**
257
     * Run Sensors discovery for the supplied OS (device)
258
     *
259
     * @param  OS  $os
260
     */
261
    public static function runDiscovery(OS $os)
262
    {
263
        // Add discovery types here
264
    }
265
266
    /**
267
     * Poll sensors for the supplied OS (device)
268
     *
269
     * @param  OS  $os
270
     */
271
    public static function poll(OS $os)
272
    {
273
        $table = static::$table;
274
275
        $query = "SELECT * FROM `$table` WHERE `device_id` = ?";
276
        $params = [$os->getDeviceId()];
277
278
        $submodules = Config::get('poller_submodules.wireless', []);
279
        if (! empty($submodules)) {
280
            $query .= ' AND `sensor_class` IN ' . dbGenPlaceholders(count($submodules));
281
            $params = array_merge($params, $submodules);
282
        }
283
284
        // fetch and group sensors, decode oids
285
        $sensors = array_reduce(
286
            dbFetchRows($query, $params),
287
            function ($carry, $sensor) {
288
                $sensor['sensor_oids'] = json_decode($sensor['sensor_oids']);
289
                $carry[$sensor['sensor_class']][] = $sensor;
290
291
                return $carry;
292
            },
293
            []
294
        );
295
296
        foreach ($sensors as $type => $type_sensors) {
297
            // check for custom polling
298
            $typeInterface = static::getPollingInterface($type);
299
            if (! interface_exists($typeInterface)) {
300
                echo "ERROR: Polling Interface doesn't exist! $typeInterface\n";
301
            }
302
303
            // fetch custom data
304
            if ($os instanceof $typeInterface) {
305
                unset($sensors[$type]);  // remove from sensors array to prevent double polling
306
                static::pollSensorType($os, $type, $type_sensors);
307
            }
308
        }
309
310
        // pre-fetch all standard sensors
311
        $standard_sensors = collect($sensors)->flatten(1)->all();
312
        $pre_fetch = self::fetchSnmpData($os->getDeviceArray(), $standard_sensors);
313
314
        // poll standard sensors
315
        foreach ($sensors as $type => $type_sensors) {
316
            static::pollSensorType($os, $type, $type_sensors, $pre_fetch);
317
        }
318
    }
319
320
    /**
321
     * Poll all sensors of a specific class
322
     *
323
     * @param  OS  $os
324
     * @param  string  $type
325
     * @param  array  $sensors
326
     * @param  array  $prefetch
327
     */
328
    protected static function pollSensorType($os, $type, $sensors, $prefetch = [])
329
    {
330
        echo "$type:\n";
331
332
        // process data or run custom polling
333
        $typeInterface = static::getPollingInterface($type);
334
        if ($os instanceof $typeInterface) {
335
            d_echo("Using OS polling for $type\n");
336
            $function = static::getPollingMethod($type);
337
            $data = $os->$function($sensors);
338
        } else {
339
            $data = static::processSensorData($sensors, $prefetch);
340
        }
341
342
        d_echo($data);
343
344
        self::recordSensorData($os, $sensors, $data);
345
    }
346
347
    /**
348
     * Fetch snmp data from the device
349
     * Return an array keyed by oid
350
     *
351
     * @param  array  $device
352
     * @param  array  $sensors
353
     * @return array
354
     */
355
    private static function fetchSnmpData($device, $sensors)
356
    {
357
        $oids = self::getOidsFromSensors($sensors, get_device_oid_limit($device));
358
359
        $snmp_data = [];
360
        foreach ($oids as $oid_chunk) {
361
            $multi_data = snmp_get_multi_oid($device, $oid_chunk, '-OUQnt');
362
            $snmp_data = array_merge($snmp_data, $multi_data);
363
        }
364
365
        // deal with string values that may be surrounded by quotes, scientific number format and remove non numerical characters
366
        array_walk($snmp_data, function (&$oid) {
367
            preg_match('/-?\d+(\.\d+)?(e-?\d+)?/i', $oid, $matches);
368
            if (isset($matches[0])) {
369
                $oid = cast_number($matches[0]);
370
            } else {
371
                $oid = trim('"', $oid); // allow string only values
372
            }
373
        });
374
375
        return $snmp_data;
376
    }
377
378
    /**
379
     * Process the snmp data for the specified sensors
380
     * Returns an array sensor_id => value
381
     *
382
     * @param  array  $sensors
383
     * @param  array  $prefetch
384
     * @return array
385
     *
386
     * @internal param $device
387
     */
388
    protected static function processSensorData($sensors, $prefetch)
389
    {
390
        $sensor_data = [];
391
        foreach ($sensors as $sensor) {
392
            // pull out the data for this sensor
393
            $requested_oids = array_flip($sensor['sensor_oids']);
394
            $data = array_intersect_key($prefetch, $requested_oids);
395
396
            // if no data set null and continue to the next sensor
397
            if (empty($data)) {
398
                $data[$sensor['sensor_id']] = null;
399
                continue;
400
            }
401
402
            if (count($data) > 1) {
403
                // aggregate data
404
                if ($sensor['sensor_aggregator'] == 'avg') {
405
                    $sensor_value = array_sum($data) / count($data);
406
                } else {
407
                    // sum
408
                    $sensor_value = array_sum($data);
409
                }
410
            } else {
411
                $sensor_value = current($data);
412
            }
413
414
            if ($sensor['sensor_divisor'] && $sensor_value !== 0) {
415
                $sensor_value = ($sensor_value / $sensor['sensor_divisor']);
416
            }
417
418
            if ($sensor['sensor_multiplier']) {
419
                $sensor_value = ($sensor_value * $sensor['sensor_multiplier']);
420
            }
421
422
            $sensor_data[$sensor['sensor_id']] = $sensor_value;
423
        }
424
425
        return $sensor_data;
426
    }
427
428
    /**
429
     * Get a list of unique oids from an array of sensors and break it into chunks.
430
     *
431
     * @param  array  $sensors
432
     * @param  int  $chunk  How many oids per chunk.  Default 10.
433
     * @return array
434
     */
435
    private static function getOidsFromSensors($sensors, $chunk = 10)
436
    {
437
        // Sort the incoming oids and sensors
438
        $oids = array_reduce($sensors, function ($carry, $sensor) {
439
            return array_merge($carry, $sensor['sensor_oids']);
440
        }, []);
441
442
        // only unique oids and chunk
443
        $oids = array_chunk(array_keys(array_flip($oids)), $chunk);
444
445
        return $oids;
446
    }
447
448
    protected static function discoverType(OS $os, $type)
449
    {
450
        $typeInterface = static::getDiscoveryInterface($type);
451
        if (! interface_exists($typeInterface)) {
452
            echo "ERROR: Discovery Interface doesn't exist! $typeInterface\n";
453
        }
454
455
        $have_discovery = $os instanceof $typeInterface;
456
        if ($have_discovery) {
457
            echo "$type: ";
458
            $function = static::getDiscoveryMethod($type);
459
            $sensors = $os->$function();
460
            if (! is_array($sensors)) {
461
                c_echo("%RERROR:%n $function did not return an array! Skipping discovery.");
462
                $sensors = [];
463
            }
464
        } else {
465
            $sensors = [];  // delete non existent sensors
466
        }
467
468
        self::checkForDuplicateSensors($sensors);
469
470
        self::sync($os->getDeviceId(), $type, $sensors);
471
472
        if ($have_discovery) {
473
            echo PHP_EOL;
474
        }
475
    }
476
477
    private static function checkForDuplicateSensors($sensors)
478
    {
479
        $duplicate_check = [];
480
        $dup = false;
481
482
        foreach ($sensors as $sensor) {
483
            /** @var Sensor $sensor */
484
            $key = $sensor->getUniqueId();
485
            if (isset($duplicate_check[$key])) {
486
                c_echo("%R ERROR:%n A sensor already exists at this index $key ");
487
                $dup = true;
488
            }
489
            $duplicate_check[$key] = 1;
490
        }
491
492
        return $dup;
493
    }
494
495
    /**
496
     * Returns a string that must be unique for each sensor
497
     * type (class), subtype (type), index (index)
498
     *
499
     * @return string
500
     */
501
    private function getUniqueId()
502
    {
503
        return $this->type . '-' . $this->subtype . '-' . $this->index;
504
    }
505
506
    protected static function getDiscoveryInterface($type)
507
    {
508
        return str_to_class($type, 'LibreNMS\\Interfaces\\Discovery\\Sensors\\') . 'Discovery';
509
    }
510
511
    protected static function getDiscoveryMethod($type)
512
    {
513
        return 'discover' . str_to_class($type);
514
    }
515
516
    protected static function getPollingInterface($type)
517
    {
518
        return str_to_class($type, 'LibreNMS\\Interfaces\\Polling\\Sensors\\') . 'Polling';
519
    }
520
521
    protected static function getPollingMethod($type)
522
    {
523
        return 'poll' . str_to_class($type);
524
    }
525
526
    /**
527
     * Is this sensor valid?
528
     * If not, it should not be added to or in the database
529
     *
530
     * @return bool
531
     */
532
    public function isValid()
533
    {
534
        return $this->valid;
535
    }
536
537
    /**
538
     * Save sensors and remove invalid sensors
539
     * This the sensors array should contain all the sensors of a specific class
540
     * It may contain sensors from multiple tables and devices, but that isn't the primary use
541
     *
542
     * @param  int  $device_id
543
     * @param  string  $type
544
     * @param  array  $sensors
545
     */
546
    final public static function sync($device_id, $type, array $sensors)
547
    {
548
        // save and collect valid ids
549
        $valid_sensor_ids = [];
550
        foreach ($sensors as $sensor) {
551
            /** @var $this $sensor */
552
            if ($sensor->isValid()) {
553
                $valid_sensor_ids[] = $sensor->save();
554
            }
555
        }
556
557
        // delete invalid sensors
558
        self::clean($device_id, $type, $valid_sensor_ids);
559
    }
560
561
    /**
562
     * Remove invalid sensors.  Passing an empty array will remove all sensors of that class
563
     *
564
     * @param  int  $device_id
565
     * @param  string  $type
566
     * @param  array  $sensor_ids  valid sensor ids
567
     */
568
    private static function clean($device_id, $type, $sensor_ids)
569
    {
570
        $table = static::$table;
571
        $params = [$device_id, $type];
572
        $where = '`device_id`=? AND `sensor_class`=?';
573
574
        if (! empty($sensor_ids)) {
575
            $where .= ' AND `sensor_id` NOT IN ' . dbGenPlaceholders(count($sensor_ids));
576
            $params = array_merge($params, $sensor_ids);
577
        }
578
579
        $delete = dbFetchRows("SELECT * FROM `$table` WHERE $where", $params);
580
        foreach ($delete as $sensor) {
581
            echo '-';
582
583
            $message = static::$name;
584
            $message .= " Deleted: $type {$sensor['sensor_type']} {$sensor['sensor_index']} {$sensor['sensor_descr']}";
585
            log_event($message, $device_id, static::$table, 3, $sensor['sensor_id']);
586
        }
587
        if (! empty($delete)) {
588
            dbDelete($table, $where, $params);
589
        }
590
    }
591
592
    /**
593
     * Return a list of valid types with metadata about each type
594
     * $class => array(
595
     *  'short' - short text for this class
596
     *  'long'  - long text for this class
597
     *  'unit'  - units used by this class 'dBm' for example
598
     *  'icon'  - font awesome icon used by this class
599
     * )
600
     *
601
     * @param  bool  $valid  filter this list by valid types in the database
602
     * @param  int  $device_id  when filtering, only return types valid for this device_id
603
     * @return array
604
     */
605
    public static function getTypes($valid = false, $device_id = null)
606
    {
607
        return [];
608
    }
609
610
    /**
611
     * Record sensor data in the database and data stores
612
     *
613
     * @param  OS  $os
614
     * @param  array  $sensors
615
     * @param  array  $data
616
     */
617
    protected static function recordSensorData(OS $os, $sensors, $data)
618
    {
619
        $types = static::getTypes();
620
621
        foreach ($sensors as $sensor) {
622
            $sensor_value = $data[$sensor['sensor_id']];
623
624
            echo "  {$sensor['sensor_descr']}: $sensor_value " . __(static::$translation_prefix . '.' . $sensor['sensor_class'] . '.unit') . PHP_EOL;
0 ignored issues
show
Bug introduced by
Are you sure __(static::translation_p...nsor_class'] . '.unit') of type array|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

624
            echo "  {$sensor['sensor_descr']}: $sensor_value " . /** @scrutinizer ignore-type */ __(static::$translation_prefix . '.' . $sensor['sensor_class'] . '.unit') . PHP_EOL;
Loading history...
625
626
            // update rrd and database
627
            $rrd_name = [
628
                static::$data_name,
629
                $sensor['sensor_class'],
630
                $sensor['sensor_type'],
631
                $sensor['sensor_index'],
632
            ];
633
            $rrd_type = isset($types[$sensor['sensor_class']]['type']) ? strtoupper($types[$sensor['sensor_class']]['type']) : 'GAUGE';
634
            $rrd_def = RrdDefinition::make()->addDataset('sensor', $rrd_type);
635
636
            $fields = [
637
                'sensor' => isset($sensor_value) ? $sensor_value : 'U',
638
            ];
639
640
            $tags = [
641
                'sensor_class' => $sensor['sensor_class'],
642
                'sensor_type' => $sensor['sensor_type'],
643
                'sensor_descr' => $sensor['sensor_descr'],
644
                'sensor_index' => $sensor['sensor_index'],
645
                'rrd_name' => $rrd_name,
646
                'rrd_def' => $rrd_def,
647
            ];
648
            data_update($os->getDeviceArray(), static::$data_name, $tags, $fields);
649
650
            $update = [
651
                'sensor_prev' => $sensor['sensor_current'],
652
                'sensor_current' => $sensor_value,
653
                'lastupdate' => ['NOW()'],
654
            ];
655
            dbUpdate($update, static::$table, '`sensor_id` = ?', [$sensor['sensor_id']]);
656
        }
657
    }
658
}
659