Make more robust to SNMP failures

This commit is contained in:
Sam W 2023-01-31 17:17:22 +00:00
parent fd2bd06fed
commit 075ba1614b
4 changed files with 147 additions and 57 deletions

2
.gitignore vendored
View File

@ -1 +1,3 @@
config.toml config.toml
apc2mqtt
.direnv

View File

@ -1,5 +1,26 @@
{ {
"nodes": { "nodes": {
"devshell": {
"inputs": {
"flake-utils": [
"flake-utils"
],
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1649691969,
"narHash": "sha256-nY1aUWIyh3TcGVo3sn+3vyCh+tOiEZL4JtMX3aOZSeY=",
"owner": "numtide",
"repo": "devshell",
"rev": "e22633b05fec2fe196888c593d4d9b3f4f648a25",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "devshell",
"type": "github"
}
},
"flake-utils": { "flake-utils": {
"locked": { "locked": {
"lastModified": 1637014545, "lastModified": 1637014545,
@ -17,11 +38,27 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1637156900, "lastModified": 1643381941,
"narHash": "sha256-nusyaSsL1RLyUEWufUUywDrGKMXw+4ugSSZ3ss8TSuw=", "narHash": "sha256-pHTwvnN4tTsEKkWlXQ8JMY423epos8wUOhthpwJjtpc=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "12fc0f19fefa9dff68bc3e0938b815ab8d89df90", "rev": "5efc8ca954272c4376ac929f4c5ffefcc20551d5",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1675153841,
"narHash": "sha256-EWvU3DLq+4dbJiukfhS7r6sWZyJikgXn6kNl7eHljW8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ea692c2ad1afd6384e171eabef4f0887d2b882d3",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -31,8 +68,9 @@
}, },
"root": { "root": {
"inputs": { "inputs": {
"devshell": "devshell",
"flake-utils": "flake-utils", "flake-utils": "flake-utils",
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs_2"
} }
} }
}, },

View File

@ -2,10 +2,18 @@
description = "Samw's APC PDU MQTT bridge"; description = "Samw's APC PDU MQTT bridge";
inputs.flake-utils.url = "github:numtide/flake-utils"; inputs.flake-utils.url = "github:numtide/flake-utils";
inputs.devshell = {
url = "github:numtide/devshell";
inputs.flake-utils.follows = "flake-utils";
};
outputs = { self, nixpkgs, flake-utils }: outputs = { self, nixpkgs, flake-utils, devshell }:
flake-utils.lib.eachDefaultSystem (system: flake-utils.lib.eachDefaultSystem (system:
let pkgs = import nixpkgs { inherit system; }; let
pkgs = import nixpkgs {
inherit system;
overlays = [ devshell.overlay ];
};
in rec { in rec {
packages.apc2mqtt = pkgs.buildGoModule { packages.apc2mqtt = pkgs.buildGoModule {
name = "apc2mqtt"; name = "apc2mqtt";
@ -16,5 +24,7 @@
''; '';
}; };
defaultPackage = packages.apc2mqtt; defaultPackage = packages.apc2mqtt;
devShell =
pkgs.devshell.mkShell { packages = with pkgs; [ go gopls mosquitto ]; };
}); });
} }

94
main.go
View File

@ -35,6 +35,8 @@ type config struct {
MQTT struct { MQTT struct {
Host string Host string
Port uint16 Port uint16
User string
Pass string
} }
Targets []targetConfig Targets []targetConfig
} }
@ -76,6 +78,38 @@ type PDUCommand struct {
State bool State bool
} }
func getPDUState(snmp *gosnmp.GoSNMP) (*PDUState, error) {
res, err := snmp.Get([]string{sPDUMasterConfigPDUName, sPDUIdentSerialNumber, sPDUIdentModelNumber})
if err != nil {
return nil, fmt.Errorf("get PDU info: %w", err)
}
state := PDUState{
Name: string(res.Variables[0].Value.([]byte)),
Serial: string(res.Variables[1].Value.([]byte)),
Model: string(res.Variables[2].Value.([]byte)),
}
outletNames, err := snmp.WalkAll(sPDUOutletName)
if err != nil {
return nil, fmt.Errorf("walk PDU outlet names: %w", err)
}
for _, val := range outletNames {
state.Outlets = append(state.Outlets, Outlet{
Name: string(val.Value.([]byte)),
})
}
outletStates, err := snmp.WalkAll(sPDUOutletCtl)
if err != nil {
return nil, fmt.Errorf("walk PDU outlet states: %w", err)
}
for i, val := range outletStates {
state.Outlets[i].State = val.Value.(int) == 1
}
return &state, nil
}
func runSNMP(host string, port uint16, stateCh chan PDUState, commandCh chan PDUCommand) { func runSNMP(host string, port uint16, stateCh chan PDUState, commandCh chan PDUCommand) {
snmp := &gosnmp.GoSNMP{ snmp := &gosnmp.GoSNMP{
Target: host, Target: host,
@ -86,36 +120,27 @@ func runSNMP(host string, port uint16, stateCh chan PDUState, commandCh chan PDU
Version: gosnmp.Version1, Version: gosnmp.Version1,
Retries: 3, Retries: 3,
} }
err := snmp.Connect() // Connect loop
check(err) connect_loop:
for {
log.Info("Opening SNMP connection")
if err := snmp.Connect(); err != nil {
log.Warnf("Error connecting SNMP target %s: `%s`. Sleeping...", host, err.Error())
time.Sleep(time.Second * 5)
continue // Try again
}
defer snmp.Conn.Close() defer snmp.Conn.Close()
poll := time.NewTicker(1 * time.Second) poll := time.NewTicker(1 * time.Second)
for { for {
select { select {
case <-poll.C: case <-poll.C:
res, err := snmp.Get([]string{sPDUMasterConfigPDUName, sPDUIdentSerialNumber, sPDUIdentModelNumber}) state, err := getPDUState(snmp)
check(err) if err != nil {
state := PDUState{ log.Errorf("getting pdu state: %s", err.Error())
Name: string(res.Variables[0].Value.([]byte)), continue connect_loop // Reconnect
Serial: string(res.Variables[1].Value.([]byte)),
Model: string(res.Variables[2].Value.([]byte)),
} }
log.Debugf("Got state: %s", state)
outletNames, err := snmp.WalkAll(sPDUOutletName) stateCh <- *state
check(err)
for _, val := range outletNames {
state.Outlets = append(state.Outlets, Outlet{
Name: string(val.Value.([]byte)),
})
}
outletStates, err := snmp.WalkAll(sPDUOutletCtl)
check(err)
for i, val := range outletStates {
state.Outlets[i].State = val.Value.(int) == 1
}
stateCh <- state
case cmd := <-commandCh: case cmd := <-commandCh:
var value int var value int
if cmd.State { if cmd.State {
@ -128,12 +153,15 @@ func runSNMP(host string, port uint16, stateCh chan PDUState, commandCh chan PDU
Name: fmt.Sprintf("%s.%d", sPDUOutletCtl, cmd.Outlet), Name: fmt.Sprintf("%s.%d", sPDUOutletCtl, cmd.Outlet),
Type: gosnmp.Integer, Type: gosnmp.Integer,
}}) }})
check(err) if err != nil {
log.Errorf("setting pdu state: %s", err.Error())
}
if res.Error != gosnmp.NoError { if res.Error != gosnmp.NoError {
log.Errorf("error in snmp set: %s", res.Error) log.Errorf("error in snmp set: %s", res.Error)
} }
} }
} }
}
} }
type HassDeviceConfig struct { type HassDeviceConfig struct {
@ -202,21 +230,33 @@ func spawnTarget(target targetConfig, mqttClient mqtt.Client) {
} }
func main() { func main() {
var configpath = flag.String("conf", "config.toml", "Path to toml config file") var configpath = flag.String("conf", "config.toml", "Path to toml config file")
var verbose = flag.Bool("v", false, "Enable debug logging")
flag.Parse() flag.Parse()
if *verbose {
log.SetLevel(log.DebugLevel)
}
log.Infof("Starting version %s", version) log.Infof("Starting version %s", version)
conf, err := parseConfig(*configpath) conf, err := parseConfig(*configpath)
check(err) check(err)
mqttOpts := mqtt.NewClientOptions().AddBroker( mqttOpts := mqtt.NewClientOptions().AddBroker(
fmt.Sprintf("tcp://%s:%d", conf.MQTT.Host, conf.MQTT.Port), fmt.Sprintf("tcp://%s:%d", conf.MQTT.Host, conf.MQTT.Port),
).SetClientID("apc2mqtt") ).SetClientID("apc2mqtt").SetConnectTimeout(time.Second * 5)
if conf.MQTT.User != "" {
mqttOpts.SetUsername(conf.MQTT.User)
}
if conf.MQTT.Pass != "" {
mqttOpts.SetPassword(conf.MQTT.Pass)
}
mqttClient := mqtt.NewClient(mqttOpts) mqttClient := mqtt.NewClient(mqttOpts)
for {
if token := mqttClient.Connect(); token.Wait() && token.Error() != nil { if token := mqttClient.Connect(); token.Wait() && token.Error() != nil {
panic(token.Error()) log.Fatalf("connect mqtt: %s", token.Error())
} }
for _, target := range conf.Targets { for _, target := range conf.Targets {
go spawnTarget(target, mqttClient) go spawnTarget(target, mqttClient)
} }
select {} select {}
}
} }