5 Commits
4 changed files with 107 additions and 15 deletions
+22 -3
View File
@@ -5,7 +5,7 @@ Ubiquiti UPS units seem to use a somewhat non-standard format for responses from
## Usage ## Usage
1. Enable the NUT server on your Ubiquiti UniFi-connected UPS unit. 1. Enable the NUT server on your Ubiquiti UniFi-connected UPS unit.
- Do **NOT** enable the "Login Credential" option! - The "Login Credential" setting is optional; if enabled, be sure to use the .Authenticate() method immediately after dialing!
2. This module can now be used to get variables from your UPS's NUT server as follows: 2. This module can now be used to get variables from your UPS's NUT server as follows:
```go ```go
package main package main
@@ -18,12 +18,29 @@ import (
func main() { func main() {
host := "IP:PORT" host := "IP:PORT"
upsID := "UPS-ID" username := "username"
password := "password"
// connect to NUT server
client, err := nut.Dial(host) client, err := nut.Dial(host)
if err != nil { if err != nil {
panic(err) panic(err)
} }
err = client.GetListVar(upsID)
// authenticate (if "Login Credential" is enabled on the NUT server)
err = client.Authenticate(username, password)
if err != nil {
panic(err)
}
// auto-detect UPS ID
err = client.AutomaticallySetID()
if err != nil {
panic(err)
}
// list all variables from your UPS
err = client.ListVar()
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -32,3 +49,5 @@ func main() {
} }
} }
``` ```
This module also features a .GetVar() method for retrieving the value of only one variable.
+1 -1
View File
@@ -1,3 +1,3 @@
module github.com/rwinkhart/uni-nut module github.com/rwinkhart/uni-nut
go 1.25.2 go 1.25.5
View File
+82 -9
View File
@@ -11,6 +11,8 @@ import (
type Client struct { type Client struct {
conn net.Conn conn net.Conn
br *bufio.Reader br *bufio.Reader
upsID string
upsIDLength int
} }
// Global map to be updated by GetListVar and read by the importing program. // Global map to be updated by GetListVar and read by the importing program.
@@ -36,9 +38,48 @@ func (c *Client) Close() error {
return c.conn.Close() return c.conn.Close()
} }
// GetListVar updates NutKeyValMap with the current status of all variables from <upsID>. // Authenticate logs into credential-protected sessions.
func (c *Client) GetListVar(upsID string) error { // If authentication is enabled on your NUT server, run
cmd := "LIST VAR \"" + upsID + "\"" // this immediately after dialing.
func (c *Client) Authenticate(username, password string) error {
var err error
if err = c.write("USERNAME " + username + "\nPASSWORD " + password); err != nil {
return err
}
_, err = c.clearBuffer("OK L", "")
if err != nil {
return err
}
return nil
}
// AutomaticallySetID detects the ID of the connected UPS.
// This only works when there is only one UPS per
// NUT server, which is the case with UniFi UPS units.
func (c *Client) AutomaticallySetID() error {
if err := c.write("LIST UPS"); err != nil {
return err
}
l, err := c.clearBuffer("END", "UPS")
if err != nil {
return err
}
lSplit := strings.Split(l, " ")
c.upsID = strings.Join(lSplit[1:len(lSplit)-2], " ")
c.upsIDLength = len(strings.Split(c.upsID, " "))
return nil
}
// ManuallySetID allows the user to specify the UPS ID
// manually if auto-detection is not desired.
func (c *Client) ManuallySetID(upsID string) {
c.upsID = upsID
c.upsIDLength = len(strings.Split(upsID, " "))
}
// ListVar updates NutKeyValMap with the current status of all variables from the target UPS.
func (c *Client) ListVar() error {
cmd := "LIST VAR \"" + c.upsID + "\""
if err := c.write(cmd); err != nil { if err := c.write(cmd); err != nil {
return err return err
} }
@@ -46,9 +87,9 @@ func (c *Client) GetListVar(upsID string) error {
if err != nil { if err != nil {
return err return err
} }
expected := "BEGIN " + cmd expectedPrefix := "BEGIN LIST VAR"
if l != expected { if !strings.HasPrefix(l, expectedPrefix) {
return fmt.Errorf("pre-loop error: expected %q, got %q", expected, l) return fmt.Errorf("pre-loop error: expected prefix %q, got line %q", expectedPrefix, l)
} }
outer: outer:
@@ -60,7 +101,7 @@ outer:
lSplit := strings.Split(l, " ") lSplit := strings.Split(l, " ")
switch lSplit[0] { switch lSplit[0] {
case "VAR": case "VAR":
NutKeyValMap[strings.Trim(lSplit[2], "\"")] = strings.Trim(strings.Join(lSplit[3:], " "), "\"") NutKeyValMap[lSplit[1+c.upsIDLength]] = strings.Trim(strings.Join(lSplit[2+c.upsIDLength:], " "), "\"")
default: default:
break outer break outer
} }
@@ -68,9 +109,25 @@ outer:
return nil return nil
} }
// newClient wraps an existing net.Conn. // GetVar returns the value of the specified variable for the target UPS.
func (c *Client) GetVar(varName string) (string, error) {
if err := c.write("GET VAR \"" + c.upsID + "\" \"" + varName + "\""); err != nil {
return "", err
}
l, err := c.read()
if err != nil {
return "", err
}
lSplit := strings.Split(l, " ")
if len(lSplit) < 4 {
return "", fmt.Errorf("invalid response to GET VAR; check your UPS ID")
}
value := strings.Trim(strings.Join(lSplit[2+c.upsIDLength:], " "), "\"")
return value, nil
}
func newClient(conn net.Conn) *Client { func newClient(conn net.Conn) *Client {
return &Client{conn, bufio.NewReader(conn)} return &Client{conn, bufio.NewReader(conn), "", 0}
} }
func (c *Client) write(s string) error { func (c *Client) write(s string) error {
@@ -88,3 +145,19 @@ func (c *Client) read() (string, error) {
} }
return l, nil return l, nil
} }
func (c *Client) clearBuffer(tillPrefix, storePrefix string) (string, error) {
var stored string
for {
l, err := c.read()
if err != nil {
return "", err
}
if storePrefix != "" && strings.HasPrefix(l, storePrefix) {
stored = l
} else if strings.HasPrefix(l, tillPrefix) {
break
}
}
return stored, nil
}