diff --git a/sysutils/nut/Makefile b/sysutils/nut/Makefile index ed35203b97..559cd57818 100644 --- a/sysutils/nut/Makefile +++ b/sysutils/nut/Makefile @@ -1,6 +1,5 @@ PLUGIN_NAME= nut -PLUGIN_VERSION= 1.9 -PLUGIN_REVISION= 1 +PLUGIN_VERSION= 1.10 PLUGIN_COMMENT= Network UPS Tools PLUGIN_DEPENDS= nut PLUGIN_MAINTAINER= m.muenz@gmail.com diff --git a/sysutils/nut/src/opnsense/mvc/app/controllers/OPNsense/Nut/Api/DiagnosticsController.php b/sysutils/nut/src/opnsense/mvc/app/controllers/OPNsense/Nut/Api/DiagnosticsController.php index fe1faf74ac..aac5c15566 100644 --- a/sysutils/nut/src/opnsense/mvc/app/controllers/OPNsense/Nut/Api/DiagnosticsController.php +++ b/sysutils/nut/src/opnsense/mvc/app/controllers/OPNsense/Nut/Api/DiagnosticsController.php @@ -37,16 +37,30 @@ class DiagnosticsController extends ApiControllerBase public function upsstatusAction() { $mdl = new Nut(); - $host = '127.0.0.1'; - if (!empty((string)$mdl->netclient->address)) { + $targets = array(); + foreach ($mdl->upses->ups->iterateItems() as $ups) { + if ((string)$ups->enabled == '1') { + $targets[] = (string)$ups->name; + } + } + if ((string)$mdl->netclient->enable == '1' && !empty((string)$mdl->netclient->address)) { $host = (string)$mdl->netclient->address; + if (strpos($host, ':') !== false) { + $host = '[' . $host . ']'; + } + if (!empty((string)$mdl->netclient->port)) { + $host .= ':' . (string)$mdl->netclient->port; + } + $targets[] = (string)$mdl->netclient->name . '@' . $host; } - $upsname = 'UPSName'; - if (!empty((string)$mdl->general->name)) { - $upsname = (string)$mdl->general->name; + $items = array(); + if (!empty($targets)) { + $backend = new Backend(); + $items = json_decode((string)$backend->configdpRun('nut upsstatus', array(implode(',', $targets))), true); + if (!is_array($items)) { + $items = array(); + } } - $backend = new Backend(); - $response = $backend->configdpRun('nut upsstatus', array("{$upsname}@{$host}")); - return array("response" => $response); + return array("items" => $items); } } diff --git a/sysutils/nut/src/opnsense/mvc/app/controllers/OPNsense/Nut/Api/SettingsController.php b/sysutils/nut/src/opnsense/mvc/app/controllers/OPNsense/Nut/Api/SettingsController.php index b49e767c82..406fff7642 100644 --- a/sysutils/nut/src/opnsense/mvc/app/controllers/OPNsense/Nut/Api/SettingsController.php +++ b/sysutils/nut/src/opnsense/mvc/app/controllers/OPNsense/Nut/Api/SettingsController.php @@ -36,4 +36,34 @@ class SettingsController extends ApiMutableModelControllerBase { protected static $internalModelClass = '\OPNsense\Nut\Nut'; protected static $internalModelName = 'nut'; + + public function searchUpsAction() + { + return $this->searchBase('upses.ups', ['enabled', 'name', 'driver', 'args']); + } + + public function getUpsAction($uuid = null) + { + return $this->getBase('ups', 'upses.ups', $uuid); + } + + public function addUpsAction() + { + return $this->addBase('ups', 'upses.ups'); + } + + public function setUpsAction($uuid) + { + return $this->setBase('ups', 'upses.ups', $uuid); + } + + public function delUpsAction($uuid) + { + return $this->delBase('upses.ups', $uuid); + } + + public function toggleUpsAction($uuid) + { + return $this->toggleBase('upses.ups', $uuid); + } } diff --git a/sysutils/nut/src/opnsense/mvc/app/controllers/OPNsense/Nut/IndexController.php b/sysutils/nut/src/opnsense/mvc/app/controllers/OPNsense/Nut/IndexController.php index 8e5358bf08..45361a1c73 100644 --- a/sysutils/nut/src/opnsense/mvc/app/controllers/OPNsense/Nut/IndexController.php +++ b/sysutils/nut/src/opnsense/mvc/app/controllers/OPNsense/Nut/IndexController.php @@ -45,6 +45,8 @@ public function indexAction() { $backend = new Backend(); $this->view->settings = $this->getForm("settings"); + $this->view->formDialogUps = $this->getForm("dialogUps"); + $this->view->formGridUps = $this->getFormGrid("dialogUps"); $this->view->pick('OPNsense/Nut/index'); } } diff --git a/sysutils/nut/src/opnsense/mvc/app/controllers/OPNsense/Nut/forms/dialogUps.xml b/sysutils/nut/src/opnsense/mvc/app/controllers/OPNsense/Nut/forms/dialogUps.xml new file mode 100644 index 0000000000..ae38d6c447 --- /dev/null +++ b/sysutils/nut/src/opnsense/mvc/app/controllers/OPNsense/Nut/forms/dialogUps.xml @@ -0,0 +1,42 @@ +
+ + ups.enabled + + checkbox + Enable or disable this UPS. + + 6em + boolean + rowtoggle + + + + ups.name + + text + Set a name for your UPS. + + + ups.driver + + dropdown + Set the NUT driver used for this UPS. See the NUT hardware compatibility list for the driver matching your device. + + + ups.powervalue + + text + Set the number of power supplies this UPS feeds on this system. Use 0 to monitor the UPS without shutting down this system. + + false + + + + ups.args + + + select_multiple + true + Set arguments for this UPS, e.g. "port=auto". With multiple USB devices of the same driver, pin each UPS with "serial=...", or "vendorid=..." and "productid=...". + +
diff --git a/sysutils/nut/src/opnsense/mvc/app/controllers/OPNsense/Nut/forms/settings.xml b/sysutils/nut/src/opnsense/mvc/app/controllers/OPNsense/Nut/forms/settings.xml index 48b3fdf6ad..fe04ca8cee 100644 --- a/sysutils/nut/src/opnsense/mvc/app/controllers/OPNsense/Nut/forms/settings.xml +++ b/sysutils/nut/src/opnsense/mvc/app/controllers/OPNsense/Nut/forms/settings.xml @@ -13,12 +13,6 @@ dropdown Set the service mode. Currently only standalone and netclient are available. - - nut.general.name - - text - Set a name for your UPS. - nut.general.listen @@ -43,187 +37,43 @@ - - - - nut.usbhid.enable - - checkbox - Enable the USBHID driver. - - - nut.usbhid.args - - - select_multiple - true - Set extra arguments for this UPS, e.g. "port=auto". - - - - - nut.apcsmart.enable - - checkbox - Enable the APCSMART driver. - - - nut.apcsmart.args - - - select_multiple - true - Set extra arguments for this UPS, e.g. "port=auto". - - - - - nut.apcupsd.enable - - checkbox - Enable the APCUPSD controlled devices driver. - - - nut.apcupsd.hostname - - text - Set the hostname or ip of the remote apcupsd server. - - - nut.apcupsd.port - - text - Set the port of the remote apcupsd server (optional). - - - - - nut.bcmxcpusb.enable - - checkbox - Enable the PowerWare BCMXCPUSB driver. - - - nut.bcmxcpusb.args - - - select_multiple - true - Set extra arguments for this UPS, e.g. "port=auto". - - - - - nut.blazerusb.enable - - checkbox - Enable the BlazerUSB driver. - - - nut.blazerusb.args - - - select_multiple - true - Set extra arguments for this UPS, e.g. "port=auto". - - - - - nut.blazerser.enable - - checkbox - Enable the BlazerSerial driver. Please be aware that this driver needs to run nut-tools as root. - - - nut.blazerser.args - - - select_multiple - true - Set extra arguments for this UPS, e.g. "port=auto". - - - - - nut.netclient.enable - - checkbox - Enable the Netclient driver. - - - nut.netclient.address - - text - Set the IP address of the remote NUT server. - - - nut.netclient.port - - text - Set the TCP port of the remote NUT server. - - - nut.netclient.user - - text - Set the username of the remote NUT server. - - - nut.netclient.password - - password - Set the password of the remote NUT server. - - - - - nut.qx.enable - - checkbox - Enable the QX driver. - - - nut.qx.args - - - select_multiple - true - Set extra arguments for this UPS, e.g. "port=auto". - - - - - nut.riello.enable - - checkbox - Enable the Riello driver. - - - nut.riello.args - - - select_multiple - true - Set extra arguments for this UPS, e.g. "port=auto". - - - - - nut.snmp.enable - - checkbox - Enable the SNMP driver. - - - nut.snmp.args - - - select_multiple - true - Set extra arguments for this UPS, e.g. "community=public". - - + + + nut.netclient.enable + + checkbox + Enable the Netclient driver. + + + nut.netclient.name + + text + Set the name of the UPS on the remote NUT server. + + + nut.netclient.address + + text + Set the IP address of the remote NUT server. + + + nut.netclient.port + + text + Set the TCP port of the remote NUT server. + + + nut.netclient.user + + text + Set the username of the remote NUT server. + + + nut.netclient.password + + password + Set the password of the remote NUT server. + nut-general-settings diff --git a/sysutils/nut/src/opnsense/mvc/app/models/OPNsense/Nut/Migrations/M1_1_0.php b/sysutils/nut/src/opnsense/mvc/app/models/OPNsense/Nut/Migrations/M1_1_0.php new file mode 100644 index 0000000000..b73034955a --- /dev/null +++ b/sysutils/nut/src/opnsense/mvc/app/models/OPNsense/Nut/Migrations/M1_1_0.php @@ -0,0 +1,90 @@ + + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +namespace OPNsense\Nut\Migrations; + +use OPNsense\Base\BaseModelMigration; +use OPNsense\Core\Config; + +class M1_1_0 extends BaseModelMigration +{ + /** + * Convert the old single UPS driver sections into a list of UPS devices. + * All driver sections shared the same UPS name before, so rows after the + * first get the section name appended to keep names unique. + */ + public function run($model) + { + $cfgObj = Config::getInstance()->object(); + if (empty($cfgObj->OPNsense) || empty($cfgObj->OPNsense->Nut)) { + return; + } + $nut = $cfgObj->OPNsense->Nut; + $name = 'UPSName'; + if (!empty($nut->general) && !empty((string)$nut->general->name)) { + $name = (string)$nut->general->name; + } + + /* the netclient section keeps its own UPS name now */ + $model->netclient->name = $name; + + $drivers = array( + 'usbhid' => 'usbhid-ups', + 'apcsmart' => 'apcsmart', + 'apcupsd' => 'apcupsd-ups', + 'bcmxcpusb' => 'bcmxcp_usb', + 'blazerusb' => 'blazer_usb', + 'blazerser' => 'blazer_ser', + 'qx' => 'nutdrv_qx', + 'riello' => 'riello_usb', + 'snmp' => 'snmp-ups', + ); + $first = true; + foreach ($drivers as $section => $driver) { + if (empty($nut->$section) || (string)$nut->$section->enable != '1') { + continue; + } + if ($section == 'apcupsd') { + $args = 'port=' . (string)$nut->$section->hostname; + if (!empty((string)$nut->$section->port)) { + $args .= ':' . (string)$nut->$section->port; + } + } else { + $args = (string)$nut->$section->args; + } + $ups = $model->upses->ups->Add(); + $ups->setNodes(array( + 'enabled' => 1, + 'name' => $first ? $name : $name . '-' . $section, + 'driver' => $driver, + 'args' => $args, + )); + $first = false; + } + } +} diff --git a/sysutils/nut/src/opnsense/mvc/app/models/OPNsense/Nut/Nut.xml b/sysutils/nut/src/opnsense/mvc/app/models/OPNsense/Nut/Nut.xml index 8ff52d8237..035696dd19 100644 --- a/sysutils/nut/src/opnsense/mvc/app/models/OPNsense/Nut/Nut.xml +++ b/sysutils/nut/src/opnsense/mvc/app/models/OPNsense/Nut/Nut.xml @@ -1,7 +1,7 @@ //OPNsense/Nut Network UPS Tools - 1.0.4 + 1.1.0 @@ -16,12 +16,6 @@ netclient - - UPSName - Y - /^([0-9a-zA-Z._\-]){1,128}$/u - The name should only contain alphanumeric characters, dashes, underscores or a dot. - 127.0.0.1,::1 Y @@ -37,74 +31,65 @@ Password - - - Y - 0 - - - port=auto - N - - - - - Y - 0 - - - port=auto - N - - - - - Y - 0 - - - Y - localhost - - - N - - - - - Y - 0 - - - port=auto - N - - - - - Y - 0 - - - port=auto - N - - - - - Y - 0 - - - port=auto - N - - + + + + 1 + Y + + + Y + /^([0-9a-zA-Z._\-]){1,128}$/u + The name should only contain alphanumeric characters, dashes, underscores or a dot. + + + A UPS with this name already exists. + UniqueConstraint + + + + + usbhid-ups + Y + + usbhid-ups (USB HID devices) + apcsmart (APC smart protocol, serial) + apcupsd-ups (apcupsd network server) + bcmxcp_usb (PowerWare BCMXCP, USB) + blazer_usb (Megatec/Q1 protocol, USB) + blazer_ser (Megatec/Q1 protocol, serial) + nutdrv_qx (Megatec/Voltronic Q* protocol) + riello_usb (Riello, USB) + snmp-ups (SNMP) + + + + 1 + Y + 0 + 16 + The power value needs to be an integer value between 0 and 16. + + + port=auto + Y + /^[0-9a-zA-Z._\-]+(=[^,]*)?$/u + Y + Arguments should be a list of key=value pairs or driver flags. + + + Y 0 + + UPSName + Y + /^([0-9a-zA-Z._\-]){1,128}$/u + The name should only contain alphanumeric characters, dashes, underscores or a dot. +
3493 @@ -117,35 +102,5 @@ N - - - Y - 0 - - - port=auto - N - - - - - Y - 0 - - - port=auto - N - - - - - Y - 0 - - - community=public - N - - diff --git a/sysutils/nut/src/opnsense/mvc/app/views/OPNsense/Nut/diagnostics.volt b/sysutils/nut/src/opnsense/mvc/app/views/OPNsense/Nut/diagnostics.volt index 4cfd8aadf6..fe3a5e61e2 100644 --- a/sysutils/nut/src/opnsense/mvc/app/views/OPNsense/Nut/diagnostics.volt +++ b/sysutils/nut/src/opnsense/mvc/app/views/OPNsense/Nut/diagnostics.volt @@ -39,7 +39,9 @@ // Put API call into a function, needed for auto-refresh function update_diagnostics() { ajaxCall(url="/api/nut/diagnostics/upsstatus", sendData={}, callback=function(data,status) { - $("#listdiagnostics").text(data['response']); + $("#listdiagnostics").text((data['items'] || []).map(function(item) { + return '[' + item['name'] + ']\n' + item['status'] + '\n'; + }).join('\n')); }); } diff --git a/sysutils/nut/src/opnsense/mvc/app/views/OPNsense/Nut/index.volt b/sysutils/nut/src/opnsense/mvc/app/views/OPNsense/Nut/index.volt index f91b000c6b..44172d7842 100644 --- a/sysutils/nut/src/opnsense/mvc/app/views/OPNsense/Nut/index.volt +++ b/sysutils/nut/src/opnsense/mvc/app/views/OPNsense/Nut/index.volt @@ -39,6 +39,17 @@ updateServiceControlUI('nut'); + $("#{{formGridUps['table_id']}}").UIBootgrid({ + 'search': '/api/nut/settings/search_ups', + 'get': '/api/nut/settings/get_ups/', + 'set': '/api/nut/settings/set_ups/', + 'add': '/api/nut/settings/add_ups/', + 'del': '/api/nut/settings/del_ups/', + 'toggle': '/api/nut/settings/toggle_ups/' + }); + + $("#reconfigureAct").SimpleActionButton(); + // update history on tab state and implement navigation if(window.location.hash != "") { $('a[href="' + window.location.hash + '"]').click() @@ -72,8 +83,15 @@
{{ partial("layout_partials/base_tabs_content",['formData':settings]) }} +
+ {{ partial('layout_partials/base_bootgrid_table', formGridUps) }} + {{ partial('layout_partials/base_apply_button', {'data_endpoint': '/api/nut/service/reconfigure', 'data_service_widget': 'nut'}) }} +
+ +{{ partial("layout_partials/base_dialog",['fields':formDialogUps,'id':formGridUps['edit_dialog_id'],'label':lang._('Edit UPS')]) }} diff --git a/sysutils/nut/src/opnsense/scripts/OPNsense/Nut/upsstatus.py b/sysutils/nut/src/opnsense/scripts/OPNsense/Nut/upsstatus.py new file mode 100755 index 0000000000..08a828e07e --- /dev/null +++ b/sysutils/nut/src/opnsense/scripts/OPNsense/Nut/upsstatus.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 + +""" + Copyright (c) 2026 Tore Amundsen + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + + -------------------------------------------------------------------------- + list the variables of one or more UPS devices as json, e.g. + upsstatus.py myups,otherups@host:3493 + + upsd is queried directly because upsc from NUT 2.8.5 crashes on + FreeBSD 14 (https://github.com/networkupstools/nut/issues/3454). +""" + +import json +import socket +import sys + + +def parse_target(target): + """ split 'upsname[@host[:port]]' into its parts, allowing [ipv6] hosts """ + upsname, _, host = target.partition('@') + port = 3493 + if host.startswith('['): + host, _, rest = host[1:].partition(']') + if rest.startswith(':') and rest[1:].isdigit(): + port = int(rest[1:]) + elif host.count(':') == 1 and host.split(':')[1].isdigit(): + host, port = host.split(':')[0], int(host.split(':')[1]) + return upsname, host if host else '127.0.0.1', port + + +def unquote(value): + if value.startswith('"') and value.endswith('"'): + value = value[1:-1].replace('\\"', '"').replace('\\\\', '\\') + return value + + +def list_vars(conn, reader, upsname): + """ return the variables of a UPS in upsc output format """ + conn.sendall(('LIST VAR %s\n' % upsname).encode()) + result = [] + while True: + line = reader.readline() + if not line: + return 'Error: connection closed' + line = line.decode(errors='replace').rstrip('\n') + if line.startswith('ERR '): + return 'Error: %s' % line[4:] + if line.startswith('END LIST VAR'): + return '\n'.join(result) + parts = line.split(' ', 3) + if len(parts) == 4 and parts[0] == 'VAR' and parts[1] == upsname: + result.append('%s: %s' % (parts[2], unquote(parts[3]))) + + +if __name__ == '__main__': + if len(sys.argv) < 2: + print('usage: %s ' % sys.argv[0], file=sys.stderr) + sys.exit(1) + hosts = {} + for target in sys.argv[1].split(','): + upsname, host, port = parse_target(target) + hosts.setdefault((host, port), []).append(upsname) + items = [] + for (host, port), upslist in hosts.items(): + statuses = {} + try: + with socket.create_connection((host, port), timeout=5) as conn: + reader = conn.makefile('rb') + for upsname in upslist: + statuses[upsname] = list_vars(conn, reader, upsname) + conn.sendall(b'LOGOUT\n') + except OSError as error: + for upsname in upslist: + statuses.setdefault(upsname, 'Error: %s' % error) + for upsname in upslist: + items.append({'name': upsname, 'status': statuses[upsname]}) + print(json.dumps(items)) diff --git a/sysutils/nut/src/opnsense/service/conf/actions.d/actions_nut.conf b/sysutils/nut/src/opnsense/service/conf/actions.d/actions_nut.conf index 65816a99db..01d923e76a 100644 --- a/sysutils/nut/src/opnsense/service/conf/actions.d/actions_nut.conf +++ b/sysutils/nut/src/opnsense/service/conf/actions.d/actions_nut.conf @@ -23,7 +23,7 @@ type:script_output message:request nut status [upsstatus] -command:/usr/local/bin/upsc +command:/usr/local/opnsense/scripts/OPNsense/Nut/upsstatus.py parameters: %s type:script_output message:retrieve ups status diff --git a/sysutils/nut/src/opnsense/service/templates/OPNsense/Nut/ups.conf b/sysutils/nut/src/opnsense/service/templates/OPNsense/Nut/ups.conf index 846345cf7e..68a0940d06 100644 --- a/sysutils/nut/src/opnsense/service/templates/OPNsense/Nut/ups.conf +++ b/sysutils/nut/src/opnsense/service/templates/OPNsense/Nut/ups.conf @@ -2,86 +2,18 @@ # the next update. # {% if helpers.exists('OPNsense.Nut.general.enable') and OPNsense.Nut.general.enable == '1' %} -{% if helpers.exists('OPNsense.Nut.usbhid.enable') and OPNsense.Nut.usbhid.enable == '1' %} -[{{ OPNsense.Nut.general.name }}] -driver=usbhid-ups -{% if helpers.exists('OPNsense.Nut.usbhid.args') and OPNsense.Nut.usbhid.args != '' %} -{% for args in OPNsense.Nut.usbhid.args.split(',') %} -{{ args }} -{% endfor %} -{% endif %} -{% endif %} -{% if helpers.exists('OPNsense.Nut.apcsmart.enable') and OPNsense.Nut.apcsmart.enable == '1' %} -[{{ OPNsense.Nut.general.name }}] -driver=apcsmart -{% if helpers.exists('OPNsense.Nut.apcsmart.args') and OPNsense.Nut.apcsmart.args != '' %} -{% for args in OPNsense.Nut.apcsmart.args.split(',') %} -{{ args }} -{% endfor %} -{% endif %} -{% endif %} -{% if helpers.exists('OPNsense.Nut.apcupsd.enable') and OPNsense.Nut.apcupsd.enable == '1' %} -[{{ OPNsense.Nut.general.name }}] -driver=apcupsd-ups -{% if helpers.exists('OPNsense.Nut.apcupsd.port') and OPNsense.Nut.apcupsd.port != '' %} -port={{ OPNsense.Nut.apcupsd.hostname }}:{{ OPNsense.Nut.apcupsd.port }} -{% else %} -port={{ OPNsense.Nut.apcupsd.hostname }} -{% endif %} -{% endif %} -{% if helpers.exists('OPNsense.Nut.bcmxcpusb.enable') and OPNsense.Nut.bcmxcpusb.enable == '1' %} -[{{ OPNsense.Nut.general.name }}] -driver=bcmxcp_usb -{% if helpers.exists('OPNsense.Nut.bcmxcpusb.args') and OPNsense.Nut.bcmxcpusb.args != '' %} -{% for args in OPNsense.Nut.bcmxcpusb.args.split(',') %} -{{ args }} -{% endfor %} -{% endif %} -{% endif %} -{% if helpers.exists('OPNsense.Nut.blazerusb.enable') and OPNsense.Nut.blazerusb.enable == '1' %} -[{{ OPNsense.Nut.general.name }}] -driver=blazer_usb -{% if helpers.exists('OPNsense.Nut.blazerusb.args') and OPNsense.Nut.blazerusb.args != '' %} -{% for args in OPNsense.Nut.blazerusb.args.split(',') %} -{{ args }} -{% endfor %} -{% endif %} -{% endif %} -{% if helpers.exists('OPNsense.Nut.blazerser.enable') and OPNsense.Nut.blazerser.enable == '1' %} +{% for ups in helpers.toList('OPNsense.Nut.upses.ups') %} +{% if ups.enabled == '1' %} +[{{ ups.name }}] +driver={{ ups.driver }} +{% if ups.driver == 'blazer_ser' %} user=root -[{{ OPNsense.Nut.general.name }}] -driver=blazer_ser -{% if helpers.exists('OPNsense.Nut.blazerser.args') and OPNsense.Nut.blazerser.args != '' %} -{% for args in OPNsense.Nut.blazerser.args.split(',') %} -{{ args }} -{% endfor %} -{% endif %} -{% endif %} -{% if helpers.exists('OPNsense.Nut.qx.enable') and OPNsense.Nut.qx.enable == '1' %} -[{{ OPNsense.Nut.general.name }}] -driver=nutdrv_qx -{% if helpers.exists('OPNsense.Nut.qx.args') and OPNsense.Nut.qx.args != '' %} -{% for args in OPNsense.Nut.qx.args.split(',') %} -{{ args }} -{% endfor %} -{% endif %} -{% endif %} -{% if helpers.exists('OPNsense.Nut.riello.enable') and OPNsense.Nut.riello.enable == '1' %} -[{{ OPNsense.Nut.general.name }}] -driver=riello_usb -{% if helpers.exists('OPNsense.Nut.riello.args') and OPNsense.Nut.riello.args != '' %} -{% for args in OPNsense.Nut.riello.args.split(',') %} -{{ args }} -{% endfor %} -{% endif %} -{% endif %} -{% if helpers.exists('OPNsense.Nut.snmp.enable') and OPNsense.Nut.snmp.enable == '1' %} -[{{ OPNsense.Nut.general.name }}] -driver=snmp-ups -{% if helpers.exists('OPNsense.Nut.snmp.args') and OPNsense.Nut.snmp.args != '' %} -{% for args in OPNsense.Nut.snmp.args.split(',') %} +{% endif %} +{% if ups.args != '' %} +{% for args in ups.args.split(',') %} {{ args }} -{% endfor %} +{% endfor %} +{% endif %} {% endif %} -{% endif %} +{% endfor %} {% endif %} diff --git a/sysutils/nut/src/opnsense/service/templates/OPNsense/Nut/upsmon.conf b/sysutils/nut/src/opnsense/service/templates/OPNsense/Nut/upsmon.conf index e4d3ab405a..39a8f972ba 100644 --- a/sysutils/nut/src/opnsense/service/templates/OPNsense/Nut/upsmon.conf +++ b/sysutils/nut/src/opnsense/service/templates/OPNsense/Nut/upsmon.conf @@ -1,53 +1,15 @@ # Please don't modify this file as your changes might be overwritten with # the next update. # -{% if helpers.exists('OPNsense.Nut.usbhid.enable') and OPNsense.Nut.usbhid.enable == '1' %} -MONITOR {{ OPNsense.Nut.general.name }} 1 monuser {{ OPNsense.Nut.account.mon_password }} master -SHUTDOWNCMD "/usr/local/etc/rc.halt" -POWERDOWNFLAG /etc/killpower -{% endif %} -{% if helpers.exists('OPNsense.Nut.netclient.enable') and OPNsense.Nut.netclient.enable == '1' %} -MONITOR {{ OPNsense.Nut.general.name }}@{{ helpers.host_with_port('OPNsense.Nut.netclient.address', 'OPNsense.Nut.netclient.port') }} 1 {{ OPNsense.Nut.netclient.user }} {{ OPNsense.Nut.netclient.password }} slave -SHUTDOWNCMD "/usr/local/etc/rc.halt" -POWERDOWNFLAG /etc/killpower -{% endif %} -{% if helpers.exists('OPNsense.Nut.apcsmart.enable') and OPNsense.Nut.apcsmart.enable == '1' %} -MONITOR {{ OPNsense.Nut.general.name }} 1 monuser {{ OPNsense.Nut.account.mon_password }} master -SHUTDOWNCMD "/usr/local/etc/rc.halt" -POWERDOWNFLAG /etc/killpower -{% endif %} -{% if helpers.exists('OPNsense.Nut.apcupsd.enable') and OPNsense.Nut.apcupsd.enable == '1' %} -MONITOR {{ OPNsense.Nut.general.name }} 1 monuser {{ OPNsense.Nut.account.mon_password }} master -SHUTDOWNCMD "/usr/local/etc/rc.halt" -POWERDOWNFLAG /etc/killpower -{% endif %} -{% if helpers.exists('OPNsense.Nut.bcmxcpusb.enable') and OPNsense.Nut.bcmxcpusb.enable == '1' %} -MONITOR {{ OPNsense.Nut.general.name }} 1 monuser {{ OPNsense.Nut.account.mon_password }} master -SHUTDOWNCMD "/usr/local/etc/rc.halt" -POWERDOWNFLAG /etc/killpower -{% endif %} -{% if helpers.exists('OPNsense.Nut.blazerusb.enable') and OPNsense.Nut.blazerusb.enable == '1' %} -MONITOR {{ OPNsense.Nut.general.name }} 1 monuser {{ OPNsense.Nut.account.mon_password }} master -SHUTDOWNCMD "/usr/local/etc/rc.halt" -POWERDOWNFLAG /etc/killpower -{% endif %} -{% if helpers.exists('OPNsense.Nut.blazerser.enable') and OPNsense.Nut.blazerser.enable == '1' %} -MONITOR {{ OPNsense.Nut.general.name }} 1 monuser {{ OPNsense.Nut.account.mon_password }} master -SHUTDOWNCMD "/usr/local/etc/rc.halt" -POWERDOWNFLAG /etc/killpower -{% endif %} -{% if helpers.exists('OPNsense.Nut.qx.enable') and OPNsense.Nut.qx.enable == '1' %} -MONITOR {{ OPNsense.Nut.general.name }} 1 monuser {{ OPNsense.Nut.account.mon_password }} master -SHUTDOWNCMD "/usr/local/etc/rc.halt" -POWERDOWNFLAG /etc/killpower -{% endif %} -{% if helpers.exists('OPNsense.Nut.riello.enable') and OPNsense.Nut.riello.enable == '1' %} -MONITOR {{ OPNsense.Nut.general.name }} 1 monuser {{ OPNsense.Nut.account.mon_password }} master -SHUTDOWNCMD "/usr/local/etc/rc.halt" -POWERDOWNFLAG /etc/killpower -{% endif %} -{% if helpers.exists('OPNsense.Nut.snmp.enable') and OPNsense.Nut.snmp.enable == '1' %} -MONITOR {{ OPNsense.Nut.general.name }} 1 monuser {{ OPNsense.Nut.account.mon_password }} master +{% if helpers.exists('OPNsense.Nut.general.enable') and OPNsense.Nut.general.enable == '1' %} +{% for ups in helpers.toList('OPNsense.Nut.upses.ups') %} +{% if ups.enabled == '1' %} +MONITOR {{ ups.name }} {{ ups.powervalue }} monuser {{ OPNsense.Nut.account.mon_password }} master +{% endif %} +{% endfor %} +{% if helpers.exists('OPNsense.Nut.netclient.enable') and OPNsense.Nut.netclient.enable == '1' %} +MONITOR {{ OPNsense.Nut.netclient.name }}@{{ helpers.host_with_port('OPNsense.Nut.netclient.address', 'OPNsense.Nut.netclient.port') }} 1 {{ OPNsense.Nut.netclient.user }} {{ OPNsense.Nut.netclient.password }} slave +{% endif %} SHUTDOWNCMD "/usr/local/etc/rc.halt" POWERDOWNFLAG /etc/killpower {% endif %} diff --git a/sysutils/nut/src/opnsense/www/js/widgets/Nut.js b/sysutils/nut/src/opnsense/www/js/widgets/Nut.js index 6d65644b33..c2d86fdaba 100644 --- a/sysutils/nut/src/opnsense/www/js/widgets/Nut.js +++ b/sysutils/nut/src/opnsense/www/js/widgets/Nut.js @@ -62,57 +62,63 @@ export default class NutNetclient extends BaseTableWidget { // Fetch the NUT settings from the server. const nut_settings = await this.ajaxCall(`/api/nut/${'settings/get'}`); - // // If netclient is not enabled, display a message and stop further processing. - // if (nut_settings.nut?.netclient?.enable !== "1") { - // $('#nut-table').html(`${this.translations.netclient_unconfigured}`); - // return; - // } - // Fetch the UPS status data from the server. - const { response: nut_ups_status_response } = await this.ajaxCall(`/api/nut/${'diagnostics/upsstatus'}`); + const { items: nut_ups_status_items } = await this.ajaxCall(`/api/nut/${'diagnostics/upsstatus'}`); - if (!nut_ups_status_response) { + if (!nut_ups_status_items || !nut_ups_status_items.length) { $('#nut-table').html(`${this.translations.misconfigured}`); return; } - // Parse the UPS status data into a key-value object. - const nut_ups_status = nut_ups_status_response.split('\n').reduce((acc, line) => { - const [key, value] = line.split(': '); - if (key) acc[key] = value; // Only add non-empty keys. - return acc; - }, {}); - // Use the dataChanged method to check if the data has changed since the last tick - if (!this.dataChanged('ups_status', nut_ups_status)) { + if (!this.dataChanged('ups_status', nut_ups_status_items)) { return; } - // Prepare the rows for the table based on the fetched data. - const rows = [ - // Display the remote server address if available. - nut_settings.nut?.netclient?.address && nut_settings.nut?.netclient?.address && nut_settings.nut?.netclient?.user && this.makeTextRow("netclient_remote_server", `${nut_settings.nut?.netclient?.user}@${nut_settings.nut?.netclient?.address}:${nut_settings.nut?.netclient?.port}`), - // Display the manufacturer and model if available. - nut_ups_status['device.mfr'] && nut_ups_status['device.model'] && this.makeTextRow("status_model", `${nut_ups_status['device.mfr']} - ${nut_ups_status['device.model']}`), - // Display the UPS Status if available. - nut_ups_status['ups.status'] && this.makeColoredTextRow('status_status', this.nutMapStatus(nut_ups_status['ups.status']), /OL/, /OB|LB|RB|DISCHRG/, nut_ups_status['ups.status']), - // Display the UPS load with percentage and optional nominal power. - nut_ups_status['ups.load'] && nut_ups_status['ups.realpower'] && this.makeUpsLoadRow('status_load', parseFloat(nut_ups_status['ups.load']), parseFloat(nut_ups_status['ups.realpower'])), - // Display the battery charge as a progress bar if available. - nut_ups_status['battery.charge'] && this.makeProgressBarRow("status_bcharge", parseFloat(nut_ups_status['battery.charge'])), - // Display the battery status if available. - nut_ups_status['battery.charger.status'] && this.makeTextRow('status_battery', nut_ups_status['battery.charger.status']), - // Display the formatted battery runtime if available. - nut_ups_status['battery.runtime'] && this.makeTextRow('status_timeleft', this.formatRuntime(parseInt(nut_ups_status['battery.runtime'], 10))), - // Display the input voltage and frequency if available. - nut_ups_status['input.voltage'] && nut_ups_status['input.frequency'] && this.makeTextRow('status_input_power', `${nut_ups_status['input.voltage']} V | ${nut_ups_status['input.frequency']} Hz`), - // Display the output voltage and frequency if available. - nut_ups_status['output.voltage'] && nut_ups_status['output.frequency'] && this.makeTextRow('status_output_power', `${nut_ups_status['output.voltage']} V | ${nut_ups_status['output.frequency']} Hz`), - // Display the result of the UPS efficiency if available. - nut_ups_status['ups.efficiency'] && this.makeTextRow('status_efficiency', `${nut_ups_status['ups.efficiency']}%`), - // Display the result of the UPS self-test if available. - nut_ups_status['ups.test.result'] && this.makeTextRow('status_selftest', nut_ups_status['ups.test.result']), - ].filter(Boolean); // Remove any undefined or null rows. + const rows = []; + + // Display the remote server address if available. + if (nut_settings.nut?.netclient?.address && nut_settings.nut?.netclient?.user) { + rows.push(this.makeTextRow("netclient_remote_server", `${nut_settings.nut?.netclient?.user}@${nut_settings.nut?.netclient?.address}:${nut_settings.nut?.netclient?.port}`)); + } + + for (const item of nut_ups_status_items) { + // Parse the UPS status data into a key-value object. + const nut_ups_status = item.status.split('\n').reduce((acc, line) => { + const [key, value] = line.split(': '); + if (key) acc[key] = value; // Only add non-empty keys. + return acc; + }, {}); + + // Separate multiple UPS devices by their name. + if (nut_ups_status_items.length > 1) { + rows.push([$('').text(item.name).prop('outerHTML')]); + } + + // Prepare the rows for the table based on the fetched data. + rows.push(...[ + // Display the manufacturer and model if available. + nut_ups_status['device.mfr'] && nut_ups_status['device.model'] && this.makeTextRow("status_model", `${nut_ups_status['device.mfr']} - ${nut_ups_status['device.model']}`), + // Display the UPS Status if available. + nut_ups_status['ups.status'] && this.makeColoredTextRow('status_status', this.nutMapStatus(nut_ups_status['ups.status']), /OL/, /OB|LB|RB|DISCHRG/, nut_ups_status['ups.status']), + // Display the UPS load with percentage and optional nominal power. + nut_ups_status['ups.load'] && nut_ups_status['ups.realpower'] && this.makeUpsLoadRow('status_load', parseFloat(nut_ups_status['ups.load']), parseFloat(nut_ups_status['ups.realpower'])), + // Display the battery charge as a progress bar if available. + nut_ups_status['battery.charge'] && this.makeProgressBarRow("status_bcharge", parseFloat(nut_ups_status['battery.charge'])), + // Display the battery status if available. + nut_ups_status['battery.charger.status'] && this.makeTextRow('status_battery', nut_ups_status['battery.charger.status']), + // Display the formatted battery runtime if available. + nut_ups_status['battery.runtime'] && this.makeTextRow('status_timeleft', this.formatRuntime(parseInt(nut_ups_status['battery.runtime'], 10))), + // Display the input voltage and frequency if available. + nut_ups_status['input.voltage'] && nut_ups_status['input.frequency'] && this.makeTextRow('status_input_power', `${nut_ups_status['input.voltage']} V | ${nut_ups_status['input.frequency']} Hz`), + // Display the output voltage and frequency if available. + nut_ups_status['output.voltage'] && nut_ups_status['output.frequency'] && this.makeTextRow('status_output_power', `${nut_ups_status['output.voltage']} V | ${nut_ups_status['output.frequency']} Hz`), + // Display the result of the UPS efficiency if available. + nut_ups_status['ups.efficiency'] && this.makeTextRow('status_efficiency', `${nut_ups_status['ups.efficiency']}%`), + // Display the result of the UPS self-test if available. + nut_ups_status['ups.test.result'] && this.makeTextRow('status_selftest', nut_ups_status['ups.test.result']), + ].filter(Boolean)); // Remove any undefined or null rows. + } // Update the table with the prepared rows. this.updateTable('nut-table', rows);