Skip to content

Commit

Permalink
APC: Add support for Battery Recommended Days Remaining (#14653)
Browse files Browse the repository at this point in the history
* APC: Add support for Battery Recommended Days Remaining

* Extract number from string later during polling
Odd that this is different from the discovery process

* wip

* wip

* Apply fixes from StyleCI

* wip

* wip

* Apply fixes from StyleCI

---------

Co-authored-by: Tony Murray <murraytony@gmail.com>
  • Loading branch information
Jellyfrog and murrant committed Mar 10, 2023
1 parent 58acfe8 commit f111ac2
Show file tree
Hide file tree
Showing 14 changed files with 201 additions and 34 deletions.
27 changes: 27 additions & 0 deletions LibreNMS/Exceptions/UserFunctionExistException.php
@@ -0,0 +1,27 @@
<?php
/**
* UserFunctionExistException.php
*
* -Description-
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* @link https://www.librenms.org
*/

namespace LibreNMS\Exceptions;

class UserFunctionExistException extends \Exception
{
}
17 changes: 16 additions & 1 deletion LibreNMS/Util/Number.php
Expand Up @@ -93,7 +93,7 @@ public static function formatBi($value, $round = 2, $sf = 3, $suffix = 'B')
* @param mixed $number
* @return float|int
*/
public static function cast($number)
public static function cast(mixed $number): float|int
{
if (! is_numeric($number)) {
// match pre-PHP8 behavior
Expand All @@ -109,6 +109,21 @@ public static function cast($number)
return $float == $int ? $int : $float;
}

/**
* Extract the first number found from a string
*/
public static function extract(mixed $string): float|int
{
if (! is_numeric($string)) {
preg_match('/-?\d*\.?\d+/', $string, $matches);
if (! empty($matches[0])) {
$string = $matches[0];
}
}

return self::cast($string);
}

/**
* Calculate a percent, but make sure to not divide by zero. In that case, return 0.
*
Expand Down
11 changes: 11 additions & 0 deletions LibreNMS/Util/Time.php
Expand Up @@ -25,6 +25,7 @@

namespace LibreNMS\Util;

use Carbon\Carbon;
use Carbon\CarbonInterface;
use Carbon\CarbonInterval;

Expand Down Expand Up @@ -113,4 +114,14 @@ public static function parseAt(string|int $time): int

return (int) strtotime($time);
}

/**
* Take a date and return the number of days from now
*/
public static function dateToDays(string|int $date): int
{
$carbon = new Carbon();

return $carbon->diffInDays($date, false);
}
}
45 changes: 45 additions & 0 deletions LibreNMS/Util/UserFuncHelper.php
@@ -0,0 +1,45 @@
<?php
/**
* UserFuncHelper.php
*
* Helper class for "user_func"
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* @link https://www.librenms.org
*/

namespace LibreNMS\Util;

use LibreNMS\Exceptions\UserFunctionExistException;

class UserFuncHelper
{
public function __construct(
public string|int|float $value,
public string|int|float|null $value_raw = null,
public array $sensor = [],
) {
}

public function __call(string $name, array $arguments): mixed
{
throw new UserFunctionExistException("Invalid user function: $name");
}

public function dateToDays(): int
{
return \LibreNMS\Util\Time::dateToDays($this->value_raw);
}
}
8 changes: 8 additions & 0 deletions includes/definitions/discovery/apc.yaml
Expand Up @@ -1099,3 +1099,11 @@ modules:
warn_limit: 1
high_limit: 3

-
oid: upsAdvBatteryRecommendedReplaceDate
num_oid: '.1.3.6.1.4.1.318.1.1.1.2.2.21.{{ $index }}'
index: 'upsAdvBatteryRecommendedReplaceDate.{{ $index }}'
descr: 'Battery Recommended Days Remaining'
user_func: dateToDays
low_warn_limit: 30
low_limit: 0
23 changes: 11 additions & 12 deletions includes/definitions/discovery/loop-telecom.yaml
Expand Up @@ -36,7 +36,7 @@ modules:
- { value: 4, generic: 2, graph: 0, descr: dc-150w }
- { value: 5, generic: 0, graph: 0, descr: ac }
- { value: 6, generic: 3, graph: 0, descr: none }
- { value: 7, generic: 0, graph: 0, descr: dc-125v }
- { value: 7, generic: 0, graph: 0, descr: dc-125v }

- oid: ccConsoleStatus
num_oid: ".1.3.6.1.4.1.823.34441.1.4.21.{{ $index }}"
Expand All @@ -55,17 +55,17 @@ modules:
- { value: 15, generic: 0, graph: 0, descr: ssh1-active }
- { value: 16, generic: 0, graph: 0, descr: ssh2-active }
- { value: 17, generic: 0, graph: 0, descr: ssh3-active }
- { value: 18, generic: 0, graph: 0, descr: ssh4-active }
- { value: 19, generic: 0, graph: 0, descr: ssh5-active }
- { value: 18, generic: 0, graph: 0, descr: ssh4-active }
- { value: 19, generic: 0, graph: 0, descr: ssh5-active }

- oid: ccConsoleLockoutState
num_oid: ".1.3.6.1.4.1.823.34441.1.4.34.{{ $index }}"
index: 'ccConsoleLockoutState.{{ $index }}'
descr: Local console state
group: Remote access
states:
- { value: 1, generic: 2, graph: 0, descr: 'Locked' }
- { value: 2, generic: 0, graph: 0, descr: 'Open' }
- { value: 2, generic: 0, graph: 0, descr: 'Open' }

- oid: ccTelnetLockoutState
num_oid: ".1.3.6.1.4.1.823.34441.1.4.35.{{ $index }}"
Expand All @@ -74,8 +74,8 @@ modules:
group: Remote access
states:
- { value: 1, generic: 2, graph: 0, descr: 'Locked' }
- { value: 2, generic: 0, graph: 0, descr: 'Open' }
- { value: 2, generic: 0, graph: 0, descr: 'Open' }

power:
data:
- oid: dualPowerTable
Expand All @@ -89,15 +89,14 @@ modules:
num_oid: '.1.3.6.1.4.1.823.34441.1.10.11.{{ $index }}'
snmp_flags: '-OQUsb'
index: 'c1Temperature.{{ $index }}'
descr: Chassis 1
user_func: snmp_hexstring, Number::cast
descr: Chassis 1
high_limit: 90

- oid: dpwmTemperature
num_oid: '.1.3.6.1.4.1.823.34441.1.10.16.{{ $index }}'
snmp_flags: '-OQUsb'
index: ' dpwmTemperature.{{ $index }}'
descr: DPWM Temperature
user_func: snmp_hexstring, Number::cast
descr: DPWM Temperature
high_limit: 90

voltage:
Expand Down Expand Up @@ -146,4 +145,4 @@ modules:
- oid: dpwmCH3Current
num_oid: '.1.3.6.1.4.1.823.34441.1.10.24.{{ $index }}'
descr: DWMP Chanel 3 current
index: 'dpwmCH3Current.{{ $index }}'
index: 'dpwmCH3Current.{{ $index }}'
4 changes: 2 additions & 2 deletions includes/definitions/discovery/primekey.yaml
Expand Up @@ -288,12 +288,12 @@ modules:
descr: 'Int Battery'
group: 'HSM'
index: pkAHsmBatteryInt
user_func: Number::cast
user_func: \LibreNMS\Util\Number::cast
-
oid: PRIMEKEY-APPLIANCE-MIB::pkAHsmBatteryExt
value: PRIMEKEY-APPLIANCE-MIB::pkAHsmBatteryExt
num_oid: '.1.3.6.1.4.1.22408.1.1.2.2.4.104.115.109.55.1{{ $index }}'
descr: 'Ext Battery'
group: 'HSM'
index: pkAHsmBatteryExt
user_func: Number::cast
user_func: \LibreNMS\Util\Number::cast
17 changes: 13 additions & 4 deletions includes/discovery/functions.inc.php
Expand Up @@ -24,6 +24,7 @@
use LibreNMS\OS;
use LibreNMS\Util\IP;
use LibreNMS\Util\IPv6;
use LibreNMS\Util\UserFuncHelper;

function discover_new_device($hostname, $device = [], $method = '', $interface = '')
{
Expand Down Expand Up @@ -943,8 +944,12 @@ function discovery_process(&$valid, $os, $sensor_class, $pre_cache)
if (is_numeric($$limit)) {
$$limit = ($$limit / $divisor) * $multiplier;
}
if (is_numeric($$limit) && isset($user_function) && is_callable($user_function)) {
$$limit = $user_function($$limit);
if (is_numeric($$limit) && isset($user_function)) {
if (is_callable($user_function)) {
$$limit = $user_function($$limit);
} else {
$$limit = (new UserFuncHelper($$limit))->{$user_function}();
}
}
}
}
Expand All @@ -964,8 +969,12 @@ function discovery_process(&$valid, $os, $sensor_class, $pre_cache)
$entPhysicalIndex_measured = isset($data['entPhysicalIndex_measured']) ? $data['entPhysicalIndex_measured'] : null;

//user_func must be applied after divisor/multiplier
if (isset($user_function) && is_callable($user_function)) {
$value = $user_function($value);
if (isset($user_function)) {
if (is_callable($user_function)) {
$value = $user_function($value);
} else {
$value = (new UserFuncHelper($value, $snmp_data[$data['value']], $data))->{$user_function}();
}
}

$uindex = $index;
Expand Down
19 changes: 9 additions & 10 deletions includes/polling/functions.inc.php
Expand Up @@ -14,6 +14,8 @@
use LibreNMS\Exceptions\JsonAppWrongVersionException;
use LibreNMS\RRD\RrdDefinition;
use LibreNMS\Util\Debug;
use LibreNMS\Util\Number;
use LibreNMS\Util\UserFuncHelper;

function bulk_sensor_snmpget($device, $sensors)
{
Expand Down Expand Up @@ -83,13 +85,6 @@ function poll_sensor($device, $class)
require 'includes/polling/sensors/' . $class . '/' . $device['os_group'] . '.inc.php';
}

if (! is_numeric($sensor_value)) {
preg_match('/-?\d*\.?\d+/', $sensor_value, $temp_response);
if (! empty($temp_response[0])) {
$sensor_value = $temp_response[0];
}
}

if ($class == 'state') {
if (! is_numeric($sensor_value)) {
$state_value = dbFetchCell(
Expand Down Expand Up @@ -164,7 +159,7 @@ function record_sensor_data($device, $all_sensors)
foreach ($all_sensors as $sensor) {
$class = ucfirst($sensor['sensor_class']);
$unit = $supported_sensors[$sensor['sensor_class']];
$sensor_value = cast_number($sensor['new_value']);
$sensor_value = Number::extract($sensor['new_value']);
$prev_sensor_value = $sensor['sensor_current'];

if ($sensor_value == -32768 || is_nan($sensor_value)) {
Expand All @@ -180,8 +175,12 @@ function record_sensor_data($device, $all_sensors)
$sensor_value = ($sensor_value * $sensor['sensor_multiplier']);
}

if (isset($sensor['user_func']) && is_callable($sensor['user_func'])) {
$sensor_value = $sensor['user_func']($sensor_value);
if (isset($sensor['user_func'])) {
if (is_callable($sensor['user_func'])) {
$sensor_value = $sensor['user_func']($sensor_value);
} else {
$sensor_value = (new UserFuncHelper($sensor_value, $sensor['new_value'], $sensor))->{$sensor['user_func']}();
}
}

$rrd_name = get_sensor_rrd_name($device, $sensor);
Expand Down
3 changes: 3 additions & 0 deletions tests/OSModulesTest.php
Expand Up @@ -88,6 +88,8 @@ public function testDataIsValid($os, $variant, $modules)
*/
public function testOS($os, $variant, $modules)
{
// Lock testing time
$this->travelTo(new \DateTime('2022-01-01 00:00:00'));
$this->requireSnmpsim(); // require snmpsim for tests
// stub out Eventlog::log and Fping->ping, we don't need to store them for these tests
$this->stubClasses();
Expand Down Expand Up @@ -145,6 +147,7 @@ public function testOS($os, $variant, $modules)
}

DeviceCache::flush(); // clear cached devices
$this->travelBack();
}

public function dumpedDataProvider()
Expand Down
50 changes: 50 additions & 0 deletions tests/data/apc_sua750i.json
Expand Up @@ -478,6 +478,31 @@
"rrd_type": "GAUGE",
"state_name": null
},
{
"sensor_deleted": 0,
"sensor_class": "count",
"poller_type": "snmp",
"sensor_oid": ".1.3.6.1.4.1.318.1.1.1.2.2.21.0",
"sensor_index": "upsAdvBatteryRecommendedReplaceDate.0",
"sensor_type": "apc",
"sensor_descr": "Battery Recommended Days Remaining",
"group": null,
"sensor_divisor": 1,
"sensor_multiplier": 1,
"sensor_current": 9,
"sensor_limit": null,
"sensor_limit_warn": null,
"sensor_limit_low": 0,
"sensor_limit_low_warn": 30,
"sensor_alert": 1,
"sensor_custom": "No",
"entPhysicalIndex": null,
"entPhysicalIndex_measured": null,
"sensor_prev": null,
"user_func": "dateToDays",
"rrd_type": "GAUGE",
"state_name": null
},
{
"sensor_deleted": 0,
"sensor_class": "current",
Expand Down Expand Up @@ -952,6 +977,31 @@
"rrd_type": "GAUGE",
"state_name": null
},
{
"sensor_deleted": 0,
"sensor_class": "count",
"poller_type": "snmp",
"sensor_oid": ".1.3.6.1.4.1.318.1.1.1.2.2.21.0",
"sensor_index": "upsAdvBatteryRecommendedReplaceDate.0",
"sensor_type": "apc",
"sensor_descr": "Battery Recommended Days Remaining",
"group": null,
"sensor_divisor": 1,
"sensor_multiplier": 1,
"sensor_current": 9,
"sensor_limit": null,
"sensor_limit_warn": null,
"sensor_limit_low": 0,
"sensor_limit_low_warn": 30,
"sensor_alert": 1,
"sensor_custom": "No",
"entPhysicalIndex": null,
"entPhysicalIndex_measured": null,
"sensor_prev": null,
"user_func": "dateToDays",
"rrd_type": "GAUGE",
"state_name": null
},
{
"sensor_deleted": 0,
"sensor_class": "current",
Expand Down

2 comments on commit f111ac2

@librenms-bot
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commit has been mentioned on LibreNMS Community. There might be relevant details there:

https://community.librenms.org/t/apc-ups-warning-when-current-date-is-passed-recommended-replace-date/18982/3

@librenms-bot
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commit has been mentioned on LibreNMS Community. There might be relevant details there:

https://community.librenms.org/t/apc-ups-battery-last-replacement-date/14130/2

Please sign in to comment.