diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9534529..4299280 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,12 @@
* feature: Add a per-UPS Timeout (seconds) setting, 5 by default and limited to 1-120, for `apcupsd` and NUT UPS'. NUT connects and conversations are now bounded by twice the timeout in total, and `apcaccess` is run through Cacti's `exec_with_timeout()` so a hung `apcaccess` can no longer stall the poller; a timed out `apcaccess` now records an error on the UPS
* feature: Make the UPSes table on the Fleet Dashboard sort like the HMIB dashboards: click (or press Enter/Space on) a column header to sort and click again to reverse, in the card and in its maximize dialog
* issue: Right-align the Load, Charge, Time Left, Est. Power and Last Updated columns of the Fleet Dashboard UPSes table
+* feature: Add a "Run Graph Automation" action to the UPSes list bulk-action dropdown. It runs Cacti's graph automation (the same as the Devices page's "Apply Automation Rules") for the Cacti Device linked to each selected UPS, creating the graphs, data sources and tree items its templates and automation rules call for, so a device that never got its graphs can be given them without leaving the plugin. Graphs that already exist are left alone, a UPS with no Cacti Device yet is skipped and reported, and the action needs the same realm as Cacti's own device action (Sites/Devices/Data)
+* issue: Centre the Status column of the Fleet Dashboard UPSes table
+* issue: Make the UPS Dashboard bullets and gauges find the Device Package's data sources. They looked for names the package does not use (`batt_volts`, `line_volts`, `output_volts`, `batt_charge`), so Battery Voltage, Line Voltage and Output Voltage never found an RRD even when the device had graphs. The RRD profile's consolidation functions are now checked first, and when an RRD has no MIN or MAX archive (for example one created from an older or customised profile) the window is bounded by its averages instead of leaving the bound blank. A device with no graphs has no RRD files, so it still has no min/avg/max history; the readings simply show the current value
+* issue: The bullet charts now label the ends of the track with the axis (0 and the full-scale value) and list the current, min, avg and max readings above it, leaving out any that are unknown instead of printing N/A; gauge captions do the same
+* feature: The Fleet Power gauge now shows min and max ticks and a min / avg / max caption like the other fleet gauges, derived from each UPS's load RRD scaled by its (constant) nominal power
+* issue: Changing the Refresh interval on the dashboards now applies it straight away, restarting or stopping the refresh timer, instead of forcing a full page reload; saves are sent one at a time so only a value that was actually saved takes effect, a failed save puts the dropdown (including Cacti's Select2 picker) back to the interval in force, and turning Refresh off puts Cacti's browser auto-logout timer back. The dashboard script is also now loaded before the script that starts the refresh, so the in-place refresh applies the first time the page is opened through Cacti's ajax navigation, not just on later visits
* feature: Share the UPS Dashboard card arrangement across every UPS (graph cards stay per UPS since they belong to each device), hide N/A rows in the UPS Information card, combine Cable and Driver on one row, and add a per-UPS Estimated Power gauge card that reuses the shared nominal power x load % estimate
* issue: Show N/A for a slave UPS's (e.g. ONLINE SLAVE) estimated power on the Fleet Dashboard and leave it out of the fleet power total, since its draw is already counted in its master's
* feature: Estimate each UPS's power draw as nominal power x load % for UPSes that do not report real wattage, and show it on the Fleet Dashboard (an "Est. Power" tile, a new "Fleet Power" gauge card of total estimated watts against rated capacity, and an "Est. Power" column with a fleet total in the UPSes card) and as a sortable "Est. Power" column in the UPSes list
diff --git a/README.md b/README.md
index ed6c6c5..b7e63a2 100644
--- a/README.md
+++ b/README.md
@@ -57,6 +57,15 @@ After you have installed the plugin, install the Device Package in the
templates folder from Console > Import/Export > Import Package. This
Device Template will be used for showing available graphs.
+The dashboards read each reading's min, average and max from the RRD files of
+the UPS's Cacti Device, so the device needs its graphs. They are created when
+the device is first added, but if a device has none (for example the Device
+Package was imported afterwards), tick the UPS on the UPSes page and choose
+`Run Graph Automation` from the action dropdown. It runs Cacti's graph
+automation for the linked device, the same as `Apply Automation Rules` on the
+Devices page, and leaves graphs that already exist alone. It needs the
+`Sites/Devices/Data` permission.
+
## Monitoring UPS' with NUT
Support for Network UPS Tools (NUT) is new in version 1.4.0. The plugin talks
diff --git a/css/apcupsd.css b/css/apcupsd.css
index d5794e3..ab44a4d 100644
--- a/css/apcupsd.css
+++ b/css/apcupsd.css
@@ -530,6 +530,13 @@
white-space: nowrap;
}
+/* Centred columns, such as the status pill. Must outrank
+ ".apcupsdDashTable th, .apcupsdDashTable td { text-align: left }". */
+.apcupsdDashTable td.apcupsdDashCenter,
+.apcupsdDashTable th.apcupsdDashCenter {
+ text-align: center;
+}
+
/* Sortable column headers (same behaviour as the HMIB dashboards): click or
press Enter/Space to sort; the arrow shows the active direction. */
.apcupsdDashTable th.apcupsdDashSortable {
diff --git a/includes/dashboard.php b/includes/dashboard.php
index 1ffc20d..c145131 100644
--- a/includes/dashboard.php
+++ b/includes/dashboard.php
@@ -214,7 +214,7 @@ function apcupsd_dashboard_metrics(): array {
'col' => 'ups_line_voltage',
'unit' => ' V',
'group' => 'power',
- 'aliases' => ['linev', 'line_voltage', 'voltage', 'inputv'],
+ 'aliases' => ['line_volts', 'linev', 'line_voltage', 'voltage', 'inputv'],
'scale' => 150.0,
'nominal' => 'ups_nominal_voltage',
'pct' => false,
@@ -224,7 +224,7 @@ function apcupsd_dashboard_metrics(): array {
'col' => 'ups_output_voltage',
'unit' => ' V',
'group' => 'power',
- 'aliases' => ['outputv', 'output_voltage', 'outv'],
+ 'aliases' => ['output_volts', 'outputv', 'output_voltage', 'outv'],
'scale' => 150.0,
'nominal' => 'ups_nominal_output_voltage',
'pct' => false,
@@ -254,7 +254,7 @@ function apcupsd_dashboard_metrics(): array {
'col' => 'ups_battery_charge',
'unit' => '%',
'group' => 'battery',
- 'aliases' => ['bcharge', 'battery_charge', 'charge'],
+ 'aliases' => ['batt_charge', 'bcharge', 'battery_charge', 'charge'],
'scale' => 100.0,
'nominal' => '',
'pct' => true,
@@ -264,7 +264,7 @@ function apcupsd_dashboard_metrics(): array {
'col' => 'ups_battery_voltage',
'unit' => ' V',
'group' => 'battery',
- 'aliases' => ['battv', 'battery_voltage', 'batteryv'],
+ 'aliases' => ['batt_volts', 'battv', 'battery_voltage', 'batteryv'],
'scale' => 30.0,
'nominal' => 'ups_nominal_batt_voltage',
'pct' => false,
@@ -379,23 +379,61 @@ function apcupsd_dashboard_find_data_source($host_id, array $aliases): array {
}
}
-if (!function_exists('apcupsd_dashboard_rrd_value')) {
+if (!function_exists('apcupsd_dashboard_rrd_consolidations')) {
/**
- * Fetch a single consolidated value for a data source over a window from its
- * RRD file. The requested consolidation function is applied across the whole
- * series (min of the MINs, max of the MAXs, etc.).
+ * The consolidation functions a data source's RRD keeps, from its data source
+ * profile. Cacti's current default profiles keep AVERAGE, MIN, MAX and LAST,
+ * but RRDs created from older or customised profiles often have no MIN
+ * archive, and fetching a consolidation the file lacks only returns an error.
+ *
+ * @param int $local_data_id The data source to look up.
+ *
+ * @return string[] Any of 'AVERAGE', 'MIN', 'MAX', 'LAST'; AVERAGE and MAX when the profile cannot be read.
+ */
+ function apcupsd_dashboard_rrd_consolidations($local_data_id): array {
+ static $cache = [];
+
+ $local_data_id = (int) $local_data_id;
+
+ if (!array_key_exists($local_data_id, $cache)) {
+ $rows = db_fetch_assoc_prepared('SELECT dspc.consolidation_function_id AS cf
+ FROM data_template_data AS dtd
+ INNER JOIN data_source_profiles_cf AS dspc ON dspc.data_source_profile_id = dtd.data_source_profile_id
+ WHERE dtd.local_data_id = ?',
+ [$local_data_id]);
+
+ $names = [1 => 'AVERAGE', 2 => 'MIN', 3 => 'MAX', 4 => 'LAST'];
+ $cfs = [];
+
+ foreach (is_array($rows) ? $rows : [] as $row) {
+ if (isset($names[(int) $row['cf']])) {
+ $cfs[] = $names[(int) $row['cf']];
+ }
+ }
+
+ $cache[$local_data_id] = cacti_sizeof($cfs) ? $cfs : ['AVERAGE', 'MAX'];
+ }
+
+ return $cache[$local_data_id];
+ }
+}
+
+if (!function_exists('apcupsd_dashboard_rrd_series')) {
+ /**
+ * Fetch the finite values of one data source over a window from its RRD file,
+ * oldest first, for one consolidation function.
*
* @param int $local_data_id The data source to read.
* @param string $ds_name The RRD data-source (column) name.
- * @param int $start The window start (relative seconds, negative).
- * @param int $end The window end (relative seconds, <= 0).
- * @param string $cf 'MIN', 'MAX', 'AVERAGE' or 'LAST'.
+ * @param int $start The window start (absolute, or relative seconds when negative).
+ * @param int $end The window end (absolute, or relative seconds when <= 0).
+ * @param string $cf 'AVERAGE', 'MIN', 'MAX' or 'LAST'.
*
- * @return float|null The reduced value, or null when no finite data exists.
+ * @return float[] The values, or [] when the RRD is missing, empty or has no such column.
*/
- function apcupsd_dashboard_rrd_value($local_data_id, string $ds_name, $start, $end, string $cf): ?float {
+ function apcupsd_dashboard_rrd_series($local_data_id, string $ds_name, $start, $end, string $cf): array {
if ((int) $local_data_id <= 0) {
- return null;
+ return [];
}
// Resolve the RRD path explicitly: on Cacti develop the sixth parameter of
@@ -404,19 +442,19 @@ function apcupsd_dashboard_rrd_value($local_data_id, string $ds_name, $start, $e
$rrd_path = get_data_source_path((int) $local_data_id, true);
if (empty($rrd_path)) {
- return null;
+ return [];
}
$fetch = rrdtool_function_fetch((int) $local_data_id, $start, $end, 0, false, $rrd_path, $cf);
if (!is_array($fetch) || empty($fetch['data_source_names']) || empty($fetch['values'])) {
- return null;
+ return [];
}
$index = array_search($ds_name, $fetch['data_source_names'], true);
if ($index === false || !isset($fetch['values'][$index])) {
- return null;
+ return [];
}
$numbers = [];
@@ -427,28 +465,81 @@ function apcupsd_dashboard_rrd_value($local_data_id, string $ds_name, $start, $e
}
}
- if (!cacti_sizeof($numbers)) {
- return null;
+ return $numbers;
+ }
+}
+
+if (!function_exists('apcupsd_dashboard_rrd_stats')) {
+ /**
+ * The min, average, max and most recent value of a data source over a window.
+ * The averages are always read. MIN and MAX archives are used when the RRD
+ * keeps them; without one (RRDs often have no MIN archive) the window is
+ * bounded by its averages instead, so the lower and upper bounds are not lost.
+ * Memoized per request.
+ *
+ * @param int $local_data_id The data source to read.
+ * @param string $ds_name The RRD data-source (column) name.
+ * @param int $start The window start.
+ * @param int $end The window end.
+ *
+ * @return array{min: float|null, avg: float|null, max: float|null, last: float|null} All null when the RRD has no data in the window.
+ */
+ function apcupsd_dashboard_rrd_stats($local_data_id, string $ds_name, $start, $end): array {
+ static $cache = [];
+
+ $key = (int) $local_data_id . ':' . $ds_name . ':' . $start . ':' . $end;
+
+ if (array_key_exists($key, $cache)) {
+ return $cache[$key];
}
- switch ($cf) {
- case 'MIN':
- return min($numbers);
- case 'MAX':
- return max($numbers);
- case 'LAST':
- return (float) end($numbers);
- default:
- return array_sum($numbers) / count($numbers);
+ $average = apcupsd_dashboard_rrd_series($local_data_id, $ds_name, $start, $end, 'AVERAGE');
+ $cfs = apcupsd_dashboard_rrd_consolidations($local_data_id);
+ $mins = in_array('MIN', $cfs, true) ? apcupsd_dashboard_rrd_series($local_data_id, $ds_name, $start, $end, 'MIN') : [];
+ $maxs = in_array('MAX', $cfs, true) ? apcupsd_dashboard_rrd_series($local_data_id, $ds_name, $start, $end, 'MAX') : [];
+
+ if (!cacti_sizeof($average) && !cacti_sizeof($mins) && !cacti_sizeof($maxs)) {
+ apcupsd_dashboard_debug(sprintf('no RRD data for data source %d (%s) between %s and %s', (int) $local_data_id, $ds_name, $start, $end));
+
+ return $cache[$key] = ['min' => null, 'avg' => null, 'max' => null, 'last' => null];
}
+
+ // The average and latest value only exist when the AVERAGE archive has data;
+ // the bounds fall back to the averages when a MIN or MAX archive is missing.
+ $lows = cacti_sizeof($mins) ? $mins : $average;
+ $highs = cacti_sizeof($maxs) ? $maxs : $average;
+
+ return $cache[$key] = [
+ 'min' => cacti_sizeof($lows) ? min($lows) : null,
+ 'avg' => cacti_sizeof($average) ? array_sum($average) / count($average) : null,
+ 'max' => cacti_sizeof($highs) ? max($highs) : null,
+ 'last' => cacti_sizeof($average) ? (float) end($average) : null,
+ ];
+ }
+}
+
+if (!function_exists('apcupsd_dashboard_debug')) {
+ /**
+ * Write a line to the Cacti log at debug verbosity, to help work out why a
+ * dashboard reading has no RRD history (no matching data source, no data in
+ * the window, ...).
+ *
+ * @param string $message The message.
+ *
+ * @return void
+ */
+ function apcupsd_dashboard_debug(string $message): void {
+ cacti_log('DEBUG: ' . $message, false, 'APCUPSD', defined('POLLER_VERBOSITY_DEBUG') ? POLLER_VERBOSITY_DEBUG : 5);
}
}
if (!function_exists('apcupsd_dashboard_bullet')) {
/**
* Render a min/avg/max bullet chart: a 0..scale track, a translucent band
- * from min to max, and a strong marker at the current value. The average and
- * min/max endpoints are labelled.
+ * from min to max, and a strong marker at the current value. The track ends
+ * are labelled with the axis (0 and the full-scale value); the current, min,
+ * average and max readings are written above it, leaving out any that are
+ * unknown rather than printing N/A.
*
* @param string $label The reading label.
* @param float|null $current The current value (marker), or null.
@@ -462,26 +553,24 @@ function apcupsd_dashboard_rrd_value($local_data_id, string $ds_name, $start, $e
* @return string The bullet HTML.
*/
function apcupsd_dashboard_bullet(string $label, ?float $current, ?float $min, ?float $avg, ?float $max, float $scale, string $unit, string $severity): string {
- $fmt = function(?float $value) use ($unit): string {
- if ($value === null) {
- return __('N/A', 'apcupsd');
- }
-
- $decimals = (abs($value) >= 100 || floor($value) == $value) ? 0 : 1;
-
- return number_format_i18n($value, $decimals) . $unit;
- };
-
$marker_value = $current !== null ? $current : $avg;
$summary = [];
if ($current !== null) {
- $summary[] = __('now %s', $fmt($current), 'apcupsd');
+ $summary[] = sprintf(__('now %s', 'apcupsd'), apcupsd_dashboard_format_value($current, $unit));
+ }
+
+ if ($min !== null) {
+ $summary[] = sprintf(__('min %s', 'apcupsd'), apcupsd_dashboard_format_value($min, $unit));
}
if ($avg !== null) {
- $summary[] = __('avg %s', $fmt($avg), 'apcupsd');
+ $summary[] = sprintf(__('avg %s', 'apcupsd'), apcupsd_dashboard_format_value($avg, $unit));
+ }
+
+ if ($max !== null) {
+ $summary[] = sprintf(__('max %s', 'apcupsd'), apcupsd_dashboard_format_value($max, $unit));
}
$head = '
'
. '';
}
}
@@ -617,12 +706,14 @@ function apcupsd_dashboard_render_bullets(string $group, $ups_id): string {
$min = $avg = $max = null;
if ($local_data_id > 0) {
- $min = apcupsd_dashboard_rrd_value($local_data_id, $ds_name, $start, $end, 'MIN');
- $avg = apcupsd_dashboard_rrd_value($local_data_id, $ds_name, $start, $end, 'AVERAGE');
- $max = apcupsd_dashboard_rrd_value($local_data_id, $ds_name, $start, $end, 'MAX');
+ $stats = apcupsd_dashboard_rrd_stats($local_data_id, $ds_name, $start, $end);
+
+ $min = $stats['min'];
+ $avg = $stats['avg'];
+ $max = $stats['max'];
if ($current === null) {
- $current = apcupsd_dashboard_rrd_value($local_data_id, $ds_name, $start, $end, 'LAST');
+ $current = $stats['last'];
}
}
@@ -742,13 +833,17 @@ function apcupsd_dashboard_metric_reading(string $key, $ups_id): ?array {
$min = $avg = $max = null;
if ($local_data_id > 0) {
- $min = apcupsd_dashboard_rrd_value($local_data_id, $ds_name, $start, $end, 'MIN');
- $avg = apcupsd_dashboard_rrd_value($local_data_id, $ds_name, $start, $end, 'AVERAGE');
- $max = apcupsd_dashboard_rrd_value($local_data_id, $ds_name, $start, $end, 'MAX');
+ $stats = apcupsd_dashboard_rrd_stats($local_data_id, $ds_name, $start, $end);
+
+ $min = $stats['min'];
+ $avg = $stats['avg'];
+ $max = $stats['max'];
if ($current === null) {
- $current = apcupsd_dashboard_rrd_value($local_data_id, $ds_name, $start, $end, 'LAST');
+ $current = $stats['last'];
}
+ } else {
+ apcupsd_dashboard_debug(sprintf('no data source on device %d matches %s (looked for %s)', $host_id, $key, implode(', ', $def['aliases'])));
}
$scale = (float) $def['scale'];
@@ -782,6 +877,37 @@ function apcupsd_dashboard_metric_reading(string $key, $ups_id): ?array {
}
}
+if (!function_exists('apcupsd_dashboard_stats_caption')) {
+ /**
+ * The "min x · avg y · max z" caption under a gauge. Only the readings that
+ * are known are listed; an unknown one is left out rather than printed as N/A.
+ *
+ * @param float|null $min The window minimum, or null.
+ * @param float|null $avg The window average, or null (also null to leave it out, e.g. when it is the centre value).
+ * @param float|null $max The window maximum, or null.
+ * @param string $unit The value unit suffix.
+ *
+ * @return string The caption, or '' when nothing is known.
+ */
+ function apcupsd_dashboard_stats_caption(?float $min, ?float $avg, ?float $max, string $unit): string {
+ $parts = [];
+
+ if ($min !== null) {
+ $parts[] = sprintf(__('min %s', 'apcupsd'), apcupsd_dashboard_format_value($min, $unit));
+ }
+
+ if ($avg !== null) {
+ $parts[] = sprintf(__('avg %s', 'apcupsd'), apcupsd_dashboard_format_value($avg, $unit));
+ }
+
+ if ($max !== null) {
+ $parts[] = sprintf(__('max %s', 'apcupsd'), apcupsd_dashboard_format_value($max, $unit));
+ }
+
+ return implode(' · ', $parts);
+ }
+}
+
if (!function_exists('apcupsd_dashboard_gauge_from_reading')) {
/**
* Render a gauge from a resolved reading: the current value swept on the dial
@@ -805,14 +931,7 @@ function apcupsd_dashboard_gauge_from_reading(?array $reading): string {
$unit = $reading['unit'];
$center = apcupsd_dashboard_format_value($value, $unit);
-
- $sub = '';
-
- if ($reading['avg'] !== null) {
- $sub = __('min %s · avg %s · max %s', apcupsd_dashboard_format_value($reading['min'], $unit), apcupsd_dashboard_format_value($reading['avg'], $unit), apcupsd_dashboard_format_value($reading['max'], $unit), 'apcupsd');
- } elseif ($reading['min'] !== null || $reading['max'] !== null) {
- $sub = __('min %s · max %s', apcupsd_dashboard_format_value($reading['min'], $unit), apcupsd_dashboard_format_value($reading['max'], $unit), 'apcupsd');
- }
+ $sub = apcupsd_dashboard_stats_caption($reading['min'], $reading['avg'], $reading['max'], $unit);
return apcupsd_dashboard_gauge($value, $reading['min'], $reading['max'], $reading['scale'], $reading['severity'], $center, $sub);
}
@@ -1809,11 +1928,14 @@ function apcupsd_dashboard(): void {
apcupsd_dashboard_init_timespan();
$current_refresh = (int) read_user_setting('apcupsd_dashboard_refresh', 0);
+ $refresh_page = $config['url_path'] . 'plugins/apcupsd/upses.php?action=view&id=' . $ups_id . '&header=false';
- apcupsd_dashboard_print_refresh($current_refresh, $config['url_path'] . 'plugins/apcupsd/upses.php?action=view&id=' . $ups_id . '&header=false');
-
+ // The dashboard script goes first: when Cacti loads this page through ajax the
+ // scripts run in order, and the refresh script below needs its functions.
print '';
+ apcupsd_dashboard_print_refresh($current_refresh, $refresh_page);
+
$ups = apcupsd_dashboard_ups($ups_id);
$meta = apcupsd_dashboard_card_meta($ups_id);
$layout = apcupsd_dashboard_layout($ups_id);
@@ -1885,6 +2007,7 @@ function apcupsd_dashboard(): void {
print '
';
foreach ($layout['order'] as $key) {
@@ -1946,6 +2069,79 @@ function apcupsd_dashboard_tabs(string $current): void {
}
}
+if (!function_exists('apcupsd_fleet_estimable_sql')) {
+ /**
+ * The SQL condition (over the `ups` and `stats` aliases) for the UPSes whose
+ * power is estimated and counted in the fleet totals: enabled, not a slave
+ * (its draw is already part of its master's) and with a nominal power rating
+ * and a load reading.
+ *
+ * @return string The condition; constant text, safe to embed in a query.
+ */
+ function apcupsd_fleet_estimable_sql(): string {
+ return "ups.enabled = 'on' AND COALESCE(stats.ups_status, '') NOT LIKE '%SLAVE%' AND stats.ups_nominal_power > 0 AND stats.ups_load_percent >= 0";
+ }
+}
+
+if (!function_exists('apcupsd_fleet_power_stats')) {
+ /**
+ * The min, average and max of the fleet's estimated power draw over a window.
+ * Each counted UPS has a load RRD and a nominal power rating that is a
+ * constant, so its power history is the load history scaled by the rating; the
+ * fleet figure is the sum across UPSes. A UPS with no load history counts at
+ * its latest estimate, so the range stays consistent with the total shown. The
+ * minimum and maximum are the sums of each UPS's own minimum and maximum, so
+ * they bound the fleet's draw rather than being the exact extremes of the sum.
+ *
+ * @param int $start The window start.
+ * @param int $end The window end.
+ *
+ * @return array{min: float|null, avg: float|null, max: float|null} All null when no UPS has load history.
+ */
+ function apcupsd_fleet_power_stats($start, $end): array {
+ static $cache = [];
+
+ $key = $start . ':' . $end;
+
+ if (array_key_exists($key, $cache)) {
+ return $cache[$key];
+ }
+
+ $rows = db_fetch_assoc('SELECT ups.host_id, stats.ups_nominal_power, stats.ups_load_percent
+ FROM apcupsd_ups AS ups
+ INNER JOIN apcupsd_ups_stats AS stats ON stats.ups_id = ups.id
+ WHERE ' . apcupsd_fleet_estimable_sql());
+
+ $aliases = apcupsd_dashboard_metrics()['load_percent']['aliases'];
+ $totals = ['min' => 0.0, 'avg' => 0.0, 'max' => 0.0];
+ $with = 0;
+
+ foreach (is_array($rows) ? $rows : [] as $row) {
+ $nominal = (float) $row['ups_nominal_power'];
+ $now = (float) apcupsd_estimate_watts($nominal, $row['ups_load_percent']);
+ $stats = ['min' => null, 'avg' => null, 'max' => null];
+
+ list($local_data_id, $ds_name) = apcupsd_dashboard_find_data_source((int) $row['host_id'], $aliases);
+
+ if ($local_data_id > 0) {
+ $stats = apcupsd_dashboard_rrd_stats($local_data_id, $ds_name, $start, $end);
+ }
+
+ if ($stats['avg'] !== null || $stats['min'] !== null || $stats['max'] !== null) {
+ $with++;
+ }
+
+ foreach ($totals as $name => $total) {
+ $watts = $stats[$name] !== null ? apcupsd_estimate_watts($nominal, $stats[$name]) : null;
+
+ $totals[$name] += $watts !== null ? $watts : $now;
+ }
+ }
+
+ return $cache[$key] = $with > 0 ? $totals : ['min' => null, 'avg' => null, 'max' => null];
+ }
+}
+
if (!function_exists('apcupsd_fleet_stats')) {
/**
* Memoized fleet aggregates: UPS counts by status (online/on-battery/other/
@@ -1960,7 +2156,7 @@ function apcupsd_fleet_stats(): array {
static $cache = null;
if ($cache === null) {
- $estimable = "ups.enabled = 'on' AND COALESCE(stats.ups_status, '') NOT LIKE '%SLAVE%' AND stats.ups_nominal_power > 0 AND stats.ups_load_percent >= 0";
+ $estimable = apcupsd_fleet_estimable_sql();
$row = db_fetch_row("SELECT
COUNT(*) AS total,
@@ -2102,20 +2298,18 @@ function apcupsd_fleet_rrd_stats(string $metric_key): array {
continue;
}
- $min = apcupsd_dashboard_rrd_value($local_data_id, $ds_name, $start, $end, 'MIN');
- $avg = apcupsd_dashboard_rrd_value($local_data_id, $ds_name, $start, $end, 'AVERAGE');
- $max = apcupsd_dashboard_rrd_value($local_data_id, $ds_name, $start, $end, 'MAX');
+ $stats = apcupsd_dashboard_rrd_stats($local_data_id, $ds_name, $start, $end);
- if ($min !== null) {
- $mins[] = $min;
+ if ($stats['min'] !== null) {
+ $mins[] = $stats['min'];
}
- if ($avg !== null) {
- $avgs[] = $avg;
+ if ($stats['avg'] !== null) {
+ $avgs[] = $stats['avg'];
}
- if ($max !== null) {
- $maxs[] = $max;
+ if ($stats['max'] !== null) {
+ $maxs[] = $stats['max'];
}
}
}
@@ -2292,14 +2486,21 @@ function apcupsd_fleet_card_body(string $key): string {
}
$est_load = $est_watts / $est_capacity * 100;
- $est_sub = __('%s of %s rated', apcupsd_dashboard_format_value($est_load, '%'), apcupsd_dashboard_format_value($est_capacity, ' W'), 'apcupsd');
+ $rated = sprintf(__('%s of %s rated', 'apcupsd'), apcupsd_dashboard_format_value($est_load, '%'), apcupsd_dashboard_format_value($est_capacity, ' W'));
+
+ // The range comes from each UPS's load RRD scaled by its (constant) nominal power,
+ // like the Load gauge; without load history the rating is the caption instead.
+ apcupsd_dashboard_init_timespan();
+
+ $range = apcupsd_fleet_power_stats(get_current_graph_start(), get_current_graph_end());
+ $sub = apcupsd_dashboard_stats_caption($range['min'], $range['avg'], $range['max'], ' W');
print '
" . __n('Click \'Continue\' to run Graph Automation for the following UPS. Graphs, data sources and tree items are created from the device\'s templates and the matching automation rules; graphs that already exist are left as they are. Only a UPS that already has a Cacti Device is affected.', 'Click \'Continue\' to run Graph Automation for the following UPSes. Graphs, data sources and tree items are created from each device\'s templates and the matching automation rules; graphs that already exist are left as they are. Only a UPS that already has a Cacti Device is affected.', cacti_sizeof($ups_array), 'apcupsd') . "