This commit is contained in:
Gabriel Smith 2026-05-25 09:39:46 +08:00 committed by GitHub
commit 66ac24f60c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 637 additions and 455 deletions

View file

@ -1,6 +1,5 @@
PLUGIN_NAME= nut
PLUGIN_VERSION= 1.9
PLUGIN_REVISION= 1
PLUGIN_VERSION= 2.0
PLUGIN_COMMENT= Network UPS Tools
PLUGIN_DEPENDS= nut
PLUGIN_MAINTAINER= m.muenz@gmail.com

View file

@ -9,6 +9,12 @@ and management interface.
Plugin Changelog
----------------
2.0
* Add support for any number UPS drivers
* Add support for any UPS driver
* Add support for arbitrary global UPS driver options
1.9
* Add dashboard widget

View file

@ -0,0 +1,69 @@
<?php
/**
* Copyright (C) 2026 Gabriel Smith <ga29smith@gmail.com>
*
* 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\Api;
use OPNsense\Base\ApiMutableModelControllerBase;
class DriversController extends ApiMutableModelControllerBase
{
protected static $internalModelClass = '\OPNsense\Nut\Nut';
protected static $internalModelName = 'nut';
public function searchDriverAction()
{
return $this->searchBase("drivers.ups", null, "name");
}
public function getDriverAction($uuid = null)
{
return $this->getBase("ups", "drivers.ups", $uuid);
}
public function addDriverAction()
{
return $this->addBase("ups", "drivers.ups");
}
public function setDriverAction($uuid)
{
return $this->setBase("ups", "drivers.ups", $uuid);
}
public function delDriverAction($uuid)
{
return $this->delBase("drivers.ups", $uuid);
}
public function toggleDriverAction($uuid, $enabled = null)
{
return $this->toggleBase("drivers.ups", $uuid, $enabled);
}
}

View file

@ -0,0 +1,46 @@
<?php
/*
* Copyright (C) 2026 Gabriel Smith <ga29smith@gmail.com>
*
* 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;
use OPNsense\Base\IndexController;
class DriversController extends IndexController
{
public function indexAction()
{
$this->view->pick('OPNsense/Nut/drivers');
$this->view->driversForm = $this->getForm("drivers");
$this->view->formDialogDriver = $this->getForm("dialogDriver");
$this->view->formGridDriver = $this->getFormGrid("dialogDriver");
$model = new \OPNsense\Nut\Nut();
$this->view->server_enabled = $model->general->mode == "standalone";
}
}

View file

@ -0,0 +1,43 @@
<form>
<field>
<id>ups.enabled</id>
<label>Enabled</label>
<type>checkbox</type>
<help>Enable or disable this UPS.</help>
<grid_view>
<width>20</width>
<type>boolean</type>
<formatter>rowtoggle</formatter>
</grid_view>
</field>
<field>
<id>ups.name</id>
<label>Name</label>
<type>text</type>
<help>Set a name for this UPS.</help>
</field>
<field>
<id>ups.driver</id>
<label>Driver</label>
<type>text</type>
<help>Set the driver for this UPS.</help>
</field>
<field>
<id>ups.port</id>
<label>Port</label>
<type>text</type>
<help>Set the port for this UPS.</help>
</field>
<field>
<id>ups.options</id>
<label>UPS Options</label>
<style>tokenize</style>
<separator>;</separator>
<type>select_multiple</type>
<allownew>true</allownew>
<help>Set options for for this UPS. Valid options depend on the driver.</help>
<grid_view>
<visible>false</visible>
</grid_view>
</field>
</form>

View file

@ -0,0 +1,16 @@
<form>
<field>
<type>header</type>
<label>Global Driver Settings</label>
<advanced>true</advanced>
</field>
<field>
<id>nut.drivers.extra_global_options</id>
<label>ups.conf Extra Global Options</label>
<style>tokenize</style>
<type>select_multiple</type>
<allownew>true</allownew>
<help>Set extra options for ups.conf not for a specific driver.</help>
<advanced>true</advanced>
</field>
</form>

View file

@ -1,230 +1,78 @@
<form>
<tab id="nut-general" description="General Settings">
<subtab id="nut-general-settings" description="General Nut Settings">
<field>
<id>nut.general.enable</id>
<label>Enable Nut</label>
<type>checkbox</type>
<help>Enable or disable the nut service. If enabled, the system will shutdown when the UPS emits a low battery warning.</help>
</field>
<field>
<id>nut.general.mode</id>
<label>Service Mode</label>
<type>dropdown</type>
<help>Set the service mode. Currently only standalone and netclient are available.</help>
</field>
<field>
<id>nut.general.name</id>
<label>Name</label>
<type>text</type>
<help>Set a name for your UPS.</help>
</field>
<field>
<id>nut.general.listen</id>
<label>Listen Address</label>
<style>tokenize</style>
<type>select_multiple</type>
<allownew>true</allownew>
<help>Set the addresses this service listen on.</help>
</field>
</subtab>
<subtab id="nut-general-account" description="Nut Account Settings">
<field>
<id>nut.account.admin_password</id>
<label>Admin Password</label>
<type>text</type>
<help>Set the password for admin user "admin".</help>
</field>
<field>
<id>nut.account.mon_password</id>
<label>Monitor Password</label>
<type>text</type>
<help>Set the password for monitoring user "monuser".</help>
</field>
</subtab>
<field>
<id>nut.general.enable</id>
<label>Enable Nut</label>
<type>checkbox</type>
<help>Enable or disable the nut service. If enabled, the system will shutdown when the UPS emits a low battery warning.</help>
</field>
<field>
<id>nut.general.mode</id>
<label>Service Mode</label>
<type>dropdown</type>
<help>Set the service mode. Currently only standalone and netclient are available.</help>
</field>
<field>
<id>nut.general.listen</id>
<label>Listen Address</label>
<style>tokenize</style>
<type>select_multiple</type>
<allownew>true</allownew>
<help>Set the addresses this service listen on.</help>
</field>
</tab>
<tab id="nut-ups-type" description="UPS Type">
<subtab id="nut-ups-usbhid" description="USBHID-Driver">
<field>
<id>nut.usbhid.enable</id>
<label>Enable</label>
<type>checkbox</type>
<help>Enable the USBHID driver.</help>
</field>
<field>
<id>nut.usbhid.args</id>
<label>Extra Arguments</label>
<style>tokenize</style>
<type>select_multiple</type>
<allownew>true</allownew>
<help>Set extra arguments for this UPS, e.g. "port=auto".</help>
</field>
</subtab>
<subtab id="nut-ups-apcsmart" description="APCSMART-Driver">
<field>
<id>nut.apcsmart.enable</id>
<label>Enable</label>
<type>checkbox</type>
<help>Enable the APCSMART driver.</help>
</field>
<field>
<id>nut.apcsmart.args</id>
<label>Extra Arguments</label>
<style>tokenize</style>
<type>select_multiple</type>
<allownew>true</allownew>
<help>Set extra arguments for this UPS, e.g. "port=auto".</help>
</field>
</subtab>
<subtab id="nut-ups-apcupsd" description="APCUPSD-Driver">
<field>
<id>nut.apcupsd.enable</id>
<label>Enable</label>
<type>checkbox</type>
<help>Enable the APCUPSD controlled devices driver.</help>
</field>
<field>
<id>nut.apcupsd.hostname</id>
<label>Hostname</label>
<type>text</type>
<help>Set the hostname or ip of the remote apcupsd server.</help>
</field>
<field>
<id>nut.apcupsd.port</id>
<label>Port</label>
<type>text</type>
<help>Set the port of the remote apcupsd server (optional).</help>
</field>
</subtab>
<subtab id="nut-ups-bcmxcpusb" description="BCMXCPUSB-Driver">
<field>
<id>nut.bcmxcpusb.enable</id>
<label>Enable</label>
<type>checkbox</type>
<help>Enable the PowerWare BCMXCPUSB driver.</help>
</field>
<field>
<id>nut.bcmxcpusb.args</id>
<label>Extra Arguments</label>
<style>tokenize</style>
<type>select_multiple</type>
<allownew>true</allownew>
<help>Set extra arguments for this UPS, e.g. "port=auto".</help>
</field>
</subtab>
<subtab id="nut-ups-blazerusb" description="BlazerUSB-Driver">
<field>
<id>nut.blazerusb.enable</id>
<label>Enable</label>
<type>checkbox</type>
<help>Enable the BlazerUSB driver.</help>
</field>
<field>
<id>nut.blazerusb.args</id>
<label>Extra Arguments</label>
<style>tokenize</style>
<type>select_multiple</type>
<allownew>true</allownew>
<help>Set extra arguments for this UPS, e.g. "port=auto".</help>
</field>
</subtab>
<subtab id="nut-ups-blazerser" description="BlazerSerial-Driver">
<field>
<id>nut.blazerser.enable</id>
<label>Enable</label>
<type>checkbox</type>
<help>Enable the BlazerSerial driver. Please be aware that this driver needs to run nut-tools as root.</help>
</field>
<field>
<id>nut.blazerser.args</id>
<label>Extra Arguments</label>
<style>tokenize</style>
<type>select_multiple</type>
<allownew>true</allownew>
<help>Set extra arguments for this UPS, e.g. "port=auto".</help>
</field>
</subtab>
<subtab id="nut-ups-netclient" description="Netclient">
<field>
<id>nut.netclient.enable</id>
<label>Enable</label>
<type>checkbox</type>
<help>Enable the Netclient driver.</help>
</field>
<field>
<id>nut.netclient.address</id>
<label>IP Address</label>
<type>text</type>
<help>Set the IP address of the remote NUT server.</help>
</field>
<field>
<id>nut.netclient.port</id>
<label>Port</label>
<type>text</type>
<help>Set the TCP port of the remote NUT server.</help>
</field>
<field>
<id>nut.netclient.user</id>
<label>Username</label>
<type>text</type>
<help>Set the username of the remote NUT server.</help>
</field>
<field>
<id>nut.netclient.password</id>
<label>Password</label>
<type>password</type>
<help>Set the password of the remote NUT server.</help>
</field>
</subtab>
<subtab id="nut-ups-qx" description="QX-Driver">
<field>
<id>nut.qx.enable</id>
<label>Enable</label>
<type>checkbox</type>
<help>Enable the QX driver.</help>
</field>
<field>
<id>nut.qx.args</id>
<label>Extra Arguments</label>
<style>tokenize</style>
<type>select_multiple</type>
<allownew>true</allownew>
<help>Set extra arguments for this UPS, e.g. "port=auto".</help>
</field>
</subtab>
<subtab id="nut-ups-riello" description="Riello-Driver">
<field>
<id>nut.riello.enable</id>
<label>Enable</label>
<type>checkbox</type>
<help>Enable the Riello driver.</help>
</field>
<field>
<id>nut.riello.args</id>
<label>Extra Arguments</label>
<style>tokenize</style>
<type>select_multiple</type>
<allownew>true</allownew>
<help>Set extra arguments for this UPS, e.g. "port=auto".</help>
</field>
</subtab>
<subtab id="nut-ups-snmp" description="SNMP-Driver">
<field>
<id>nut.snmp.enable</id>
<label>Enable</label>
<type>checkbox</type>
<help>Enable the SNMP driver.</help>
</field>
<field>
<id>nut.snmp.args</id>
<label>Extra Arguments</label>
<style>tokenize</style>
<type>select_multiple</type>
<allownew>true</allownew>
<help>Set extra arguments for this UPS, e.g. "community=public".</help>
</field>
</subtab>
<tab id="nut-account" description="Nut Account Settings">
<field>
<id>nut.account.admin_password</id>
<label>Admin Password</label>
<type>text</type>
<help>Set the password for admin user "admin".</help>
</field>
<field>
<id>nut.account.mon_password</id>
<label>Monitor Password</label>
<type>text</type>
<help>Set the password for monitoring user "monuser".</help>
</field>
</tab>
<tab id="nut-netclient" description="Netclient">
<field>
<id>nut.netclient.enable</id>
<label>Enable</label>
<type>checkbox</type>
<help>Enable the Netclient driver.</help>
</field>
<field>
<id>nut.netclient.name</id>
<label>Name</label>
<type>text</type>
<help>Set the name for the remote UPS.</help>
</field>
<field>
<id>nut.netclient.address</id>
<label>IP Address</label>
<type>text</type>
<help>Set the IP address of the remote NUT server.</help>
</field>
<field>
<id>nut.netclient.port</id>
<label>Port</label>
<type>text</type>
<help>Set the TCP port of the remote NUT server.</help>
</field>
<field>
<id>nut.netclient.user</id>
<label>Username</label>
<type>text</type>
<help>Set the username of the remote NUT server.</help>
</field>
<field>
<id>nut.netclient.password</id>
<label>Password</label>
<type>password</type>
<help>Set the password of the remote NUT server.</help>
</field>
</tab>
<activetab>nut-general-settings</activetab>
<activetab>nut-general</activetab>
</form>

View file

@ -2,7 +2,8 @@
<Services>
<Nut cssClass="fa fa-battery-full fa-fw">
<Configuration order="10" url="/ui/nut/index"/>
<Diagnostics order="20" url="/ui/nut/diagnostics"/>
<Drivers VisibleName="UPS Drivers" order="20" url="/ui/nut/drivers"/>
<Diagnostics order="30" url="/ui/nut/diagnostics"/>
</Nut>
</Services>
</menu>

View file

@ -0,0 +1,128 @@
<?php
/*
* Copyright (C) 2026 Gabriel Smith <ga29smith@gmail.com>
*
* 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;
use OPNsense\Firewall\Util;
class M2_0_0 extends BaseModelMigration
{
/**
* Migrate older keys into new model
* @param $model
*/
public function run($model)
{
$config = Config::getInstance()->object();
if (empty($config->OPNsense->Nut)) {
return;
}
$nutConfig = $config->OPNsense->Nut;
// netclient isn't a local UPS so it can't be migrated as such, but
// still needs a UPS name.
$model->netclient->name = $nutConfig->general->name;
// Migrate UPS/monitor definitions. Disabled UPSs' names are
// suffixed with the driver to not conflict with the enabled UPS.
// This should avoid breaking existing configurations. Technically
// this can still result in conflicts between enabled UPSs, but
// this would only happen if the configuration was broken before
// the migration.
$this->migrateGenericUps($model, $nutConfig, "usbhid");
$this->migrateGenericUps($model, $nutConfig, "apcsmart");
$this->migrateGenericUps($model, $nutConfig, "bcmxcpusb");
$this->migrateGenericUps($model, $nutConfig, "blazerusb");
$this->migrateGenericUps($model, $nutConfig, "blazerser");
$this->migrateGenericUps($model, $nutConfig, "qx");
$this->migrateGenericUps($model, $nutConfig, "riello");
$this->migrateGenericUps($model, $nutConfig, "snmp");
// apcupsd
$ups = $model->drivers->ups->add();
$ups->driver = "apcupsd";
$ups->enabled = $nutConfig->apcupsd->enable;
if (empty($nutConfig->apcupsd->port)) {
$ups->port = $nutConfig->apcupsd->hostname;
} else {
$ups->port = $nutConfig->apcupsd->hostname . ":"
. $nutConfig->apcupsd->port;
}
if ($ups->enabled == "1") {
$ups->name = $nutConfig->general->name;
} else {
$ups->name = $nutConfig->general->name . "_" . $ups->driver;
}
parent::run($model);
}
private function migrateGenericUps($model, $nutConfig, $driverName)
{
$upsName = $nutConfig->general->name;
$ups = $model->drivers->ups->add();
$ups->driver = $driverName;
$ups->enabled = $nutConfig->$driverName->enable;
if ($ups->enabled == "1") {
$ups->name = $upsName;
} else {
$ups->name = $upsName . "_" . $driverName;
}
$options = explode(",", $nutConfig->$driverName->args);
$ports = array_map(
function ($o) {
return str_replace("port=", "", $o);
},
array_filter(
$options,
function ($o) {
return str_starts_with($o, "port=");
}
)
);
if (empty($ports)) {
$ups->port = "auto";
} else {
$ups->port = $ports[array_key_last($ports)];
}
$ups->options = implode(
";",
array_filter(
$options,
function ($o) {
return !str_starts_with($o, "port=");
}
)
);
}
}

View file

@ -2,6 +2,7 @@
/*
Copyright (C) 2017 Michael Muenz <m.muenz@gmail.com>
Copyright (C) 2026 Gabriel Smith <ga29smith@gmail.com>
All rights reserved.
Redistribution and use in source and binary forms, with or without
@ -29,7 +30,135 @@
namespace OPNsense\Nut;
use OPNsense\Base\BaseModel;
use OPNsense\Base\Messages\Message;
use OPNsense\Firewall\Util;
class Nut extends BaseModel
{
/**
* @var null|string the cached listen address for loopback connections if one exists
*/
private $cachedLoopbackListenAddress = null;
// Assumes that only 127.0.0.1 and ::1 are valid loopback addresses.
//
// Techinically all of 127.0.0.0/8 could be set up as loopback addresses,
// but by default opnSense/FreeBSD doesn't configure these.
public function getLoopbackListenAddress()
{
if ($this->cachedLoopbackListenAddress === null) {
$host = "";
foreach ($this->general->listen->getValues() as $address) {
if (
Util::isIpv4Address($address) &&
Util::isIPInCIDR($address, "127.0.0.1/32")
) {
$host = $address;
} elseif (
Util::isIpv6Address($address) &&
Util::isIPInCIDR($address, "::1/128")
) {
$host = $address;
}
}
$this->cachedLoopbackListenAddress = $host;
}
return $this->cachedLoopbackListenAddress;
}
// If any local monitors are defined, some sort of loopback must be defined
// in the listen field.
private function checkListenAddressForLocalMonitors($messages)
{
if (
$this->general->mode == "standalone" &&
!empty($this->drivers->ups->getNodes()) &&
empty($this->getLoopbackListenAddress())
) {
if ($this->general->listen->isFieldChanged()) {
$messages->appendMessage(new Message(
gettext(
"Loopback required: A loopback listen address is "
. "required when a local UPS is defined. Add a "
. "listen address using 127.0.0.1 or ::1."
),
"general.listen"
));
}
foreach ($this->drivers->ups->iterateItems() as $ups) {
if ($ups->isFieldChanged()) {
$messages->appendMessage(new Message(
gettext(
"Loopback required: A loopback listen address is "
. "required when local a UPS is defined. Add a "
. "listen address using 127.0.0.1 or ::1."
),
$ups->__reference . ".enabled"
));
}
}
}
}
private function checkForUniqueUpsDefinitions($messages)
{
// Ensure UPS names are unique.
$ups_names = [];
foreach ($this->drivers->ups->iterateItems() as $ups) {
$name = (string) $ups->name;
if (isset($ups_names[$name])) {
$messages->appendMessage(new Message(
sprintf(
gettext(
"Duplicate entry: The name %s is already used. "
. "Each name must be unique."
),
$name
),
$ups->__reference . ".name"
));
} else {
$ups_names[$name] = true;
}
}
// Ensure each UPS driver/port combination is unique.
$ups_ports = [];
foreach ($this->drivers->ups->iterateItems() as $ups) {
$driver = (string) $ups->driver;
$port = (string) $ups->port;
$key = $driver . "_" . $port;
if (isset($ups_ports[$key])) {
$messages->appendMessage(new Message(
sprintf(
gettext(
"Duplicate entry: A UPS with driver %s and port "
. "%s is already defined. Each driver and port "
. "combination must be unique."
),
$name
),
$ups->__reference . ".port"
));
} else {
$ups_ports[$key] = true;
}
}
}
public function performValidation($validateFullModel = false)
{
$messages = parent::performValidation($validateFullModel);
// Invalidate the cached loopback listen address if the listen addresses
// were changed.
if ($this->general->listen->isFieldChanged()) {
$this->cachedLoopbackListenAddress = null;
}
$this->checkForUniqueUpsDefinitions($messages);
$this->checkListenAddressForLocalMonitors($messages);
return $messages;
}
}

View file

@ -1,7 +1,7 @@
<model>
<mount>//OPNsense/Nut</mount>
<description>Network UPS Tools</description>
<version>1.0.4</version>
<version>2.0.0</version>
<items>
<general>
<enable type="BooleanField">
@ -16,12 +16,6 @@
<netclient>netclient</netclient>
</OptionValues>
</mode>
<name type="TextField">
<Default>UPSName</Default>
<Required>Y</Required>
<Mask>/^([0-9a-zA-Z._\-]){1,128}$/u</Mask>
<ValidationMessage>The name should only contain alphanumeric characters, dashes, underscores or a dot.</ValidationMessage>
</name>
<listen type="CSVListField">
<Default>127.0.0.1,::1</Default>
<Required>Y</Required>
@ -37,74 +31,16 @@
<Default>Password</Default>
</mon_password>
</account>
<usbhid>
<enable type="BooleanField">
<Required>Y</Required>
<Default>0</Default>
</enable>
<args type="CSVListField">
<Default>port=auto</Default>
<Required>N</Required>
</args>
</usbhid>
<apcsmart>
<enable type="BooleanField">
<Required>Y</Required>
<Default>0</Default>
</enable>
<args type="CSVListField">
<Default>port=auto</Default>
<Required>N</Required>
</args>
</apcsmart>
<apcupsd>
<enable type="BooleanField">
<Required>Y</Required>
<Default>0</Default>
</enable>
<hostname type="HostnameField">
<Required>Y</Required>
<Default>localhost</Default>
</hostname>
<port type="PortField">
<Required>N</Required>
</port>
</apcupsd>
<bcmxcpusb>
<enable type="BooleanField">
<Required>Y</Required>
<Default>0</Default>
</enable>
<args type="CSVListField">
<Default>port=auto</Default>
<Required>N</Required>
</args>
</bcmxcpusb>
<blazerusb>
<enable type="BooleanField">
<Required>Y</Required>
<Default>0</Default>
</enable>
<args type="CSVListField">
<Default>port=auto</Default>
<Required>N</Required>
</args>
</blazerusb>
<blazerser>
<enable type="BooleanField">
<Required>Y</Required>
<Default>0</Default>
</enable>
<args type="CSVListField">
<Default>port=auto</Default>
<Required>N</Required>
</args>
</blazerser>
<netclient>
<enable type="BooleanField">
<Required>Y</Required>
<Default>0</Default>
</enable>
<name type="TextField">
<Required>Y</Required>
<Mask>/^([0-9a-zA-Z._\-]){1,128}$/u</Mask>
<ValidationMessage>The name should only contain alphanumeric characters, dashes, underscores or a dot.</ValidationMessage>
</name>
<address type="HostnameField"/>
<port type="PortField">
<Default>3493</Default>
@ -117,35 +53,32 @@
<Required>N</Required>
</password>
</netclient>
<qx>
<enable type="BooleanField">
<Required>Y</Required>
<Default>0</Default>
</enable>
<args type="CSVListField">
<Default>port=auto</Default>
<Required>N</Required>
</args>
</qx>
<riello>
<enable type="BooleanField">
<Required>Y</Required>
<Default>0</Default>
</enable>
<args type="CSVListField">
<Default>port=auto</Default>
<Required>N</Required>
</args>
</riello>
<snmp>
<enable type="BooleanField">
<Required>Y</Required>
<Default>0</Default>
</enable>
<args type="CSVListField">
<Default>community=public</Default>
<Required>N</Required>
</args>
</snmp>
<drivers>
<extra_global_options type="CSVListField">
<FieldSeparator>;</FieldSeparator>
</extra_global_options>
<ups type="ArrayField">
<enabled type="BooleanField">
<Default>1</Default>
<Required>Y</Required>
</enabled>
<name type="TextField">
<Default>main</Default>
<Required>Y</Required>
<mask>/^([0-9a-zA-Z._\-]){1,128}$/u</mask>
<ValidationMessage>The name should only contain alphanumeric characters, dashes, underscores or a dot.</ValidationMessage>
</name>
<driver type="TextField">
<Required>Y</Required>
</driver>
<port type="TextField">
<Default>auto</Default>
<Required>Y</Required>
</port>
<options type="CSVListField">
<FieldSeparator>;</FieldSeparator>
</options>
</ups>
</drivers>
</items>
</model>

View file

@ -0,0 +1,68 @@
{#
# Copyright (C) 2026 Gabriel Smith <ga29smith@gmail.com>
#
# 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.
#}
<script>
$(document).ready(function() {
var data_get_map = {'frm_nut':'/api/nut/settings/get'};
mapDataToFormUI(data_get_map).done(function(){
formatTokenizersUI();
$('.selectpicker').selectpicker('refresh');
});
$("#{{formGridDriver['table_id']}}").UIBootgrid(
{
search:'/api/nut/drivers/search_driver/',
get:'/api/nut/drivers/get_driver/',
set:'/api/nut/drivers/set_driver/',
add:'/api/nut/drivers/add_driver/',
del:'/api/nut/drivers/del_driver/',
toggle:'/api/nut/drivers/toggle_driver/'
}
);
$("#reconfigureAct").SimpleActionButton();
updateServiceControlUI('nut');
});
</script>
{% if !server_enabled %}
<div class="alert alert-warning" role="alert" id="nut_server_disabled" style="min-height:65px;">
<div style="margin-top: 8px;">
{{ lang._('NUT is in netclient mode. All local driver definitions will be ignored.') }}
</div>
</div>
{% endif %}
<div class="content-box">
{{ partial("layout_partials/base_form", ['fields':driversForm, 'id':'frm_nut-drivers']) }}
{{ partial('layout_partials/base_bootgrid_table', formGridDriver) }}
</div>
{{ partial('layout_partials/base_apply_button', {'data_endpoint': '/api/nut/service/reconfigure'}) }}
{{ partial("layout_partials/base_dialog",['fields':formDialogDriver,'id':formGridDriver['edit_dialog_id'],'label':lang._('Edit UPS')])}}

View file

@ -1,87 +1,20 @@
# Please don't modify this file as your changes might be overwritten with
# 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' %}
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(',') %}
{{ args }}
{% endfor %}
{% endif %}
{% endif %}
{% if helpers.exists('OPNsense.Nut.drivers.extra_global_options') %}
{% for option in OPNsense.Nut.drivers.extra_global_options.split(';') %}
{{ option }}
{% endfor %}
{% endif %}
{% for ups in helpers.toList('OPNsense.Nut.drivers.ups') %}
{% if ups.enabled|default("0") == "1" %}
[{{ ups.name }}]
driver={{ ups.driver }}
port="{{ ups.port }}"
{% for option in (ups.options|default("")).split(";") if option %}
{{ option }}
{% endfor %}
{% endif %}
{% endfor %}

View file

@ -1,53 +1,16 @@
# 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
{% set generalSettings = helpers.getNodeByTag('OPNsense.Nut.general') %}
{% if generalSettings.mode|default("none") == "standalone" %}
{% for ups in helpers.toList("OPNsense.Nut.drivers.ups") %}
{% if ups.enabled|default("0") == "1" %}
MONITOR {{ ups.name }} 1 monuser {{ OPNsense.Nut.account.mon_password }} primary
{% endif %}
{% endfor %}
{% 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
SHUTDOWNCMD "/usr/local/etc/rc.halt"
POWERDOWNFLAG /etc/killpower
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 }} secondary
{% endif %}