Skip to content

Instantly share code, notes, and snippets.

@imavroukakis
Last active March 30, 2025 19:26
Show Gist options
  • Save imavroukakis/164a17a79655c265aceb887978f4038f to your computer and use it in GitHub Desktop.
Save imavroukakis/164a17a79655c265aceb887978f4038f to your computer and use it in GitHub Desktop.
A script to expose a locally attached UPS via SNMP
#!/usr/bin/env python3
import sys
import subprocess
DEBUG_LOG_FILE = "/tmp/nut_ups_mib.log"
debug_logging = False # Set to True to enable debug logging
def debug_log(message):
if not debug_logging:
return
try:
with open(DEBUG_LOG_FILE, "a") as log_file:
log_file.write(message + "\n")
except Exception:
pass
# Overwrite the debug log at startup if logging is enabled.
#if debug_logging:
# with open(DEBUG_LOG_FILE, "w") as f:
# f.write("Debug log initialized.\n")
debug_log("Script started with arguments: " + " ".join(sys.argv))
# Mapping from UPS MIB OIDs (as defined in RFC 1628) to a tuple:
# (NUT key, conversion function, SNMP type)
# This is for an Eaton 5E 1200 G2
mapping = {
# Identification group
'.1.3.6.1.2.1.33.1.1.1.0': ('ups.mfr', lambda x: x, "string"), # Manufacturer
'.1.3.6.1.2.1.33.1.1.2.0': ('ups.model', lambda x: x, "string"), # Model
'.1.3.6.1.2.1.33.1.1.3.0': ('ups.serial', lambda x: x, "string"), # Serial number
'.1.3.6.1.2.1.33.1.1.4.0': ('ups.firmware', lambda x: x, "string"), # Firmware revision
# Battery group
'.1.3.6.1.2.1.33.1.2.1.0': ('battery.charge', lambda x: int(x), "integer"), # Battery status (here using charge %)
'.1.3.6.1.2.1.33.1.2.3.0': ('battery.runtime', lambda x: int(x)/60, "integer"), # Battery time remaining
'.1.3.6.1.2.1.33.1.2.4.0': ('battery.voltage', lambda x: float(x), "gauge"), # Battery voltage
'.1.3.6.1.2.1.33.1.2.5.0': ('battery.current', lambda x: float(x), "gauge"), # Battery current
'.1.3.6.1.2.1.33.1.2.6.0': ('battery.temperature', lambda x: float(x), "gauge"), # Battery temperature
'.1.3.6.1.2.1.33.1.2.7.0': ('battery.type', lambda x: x, "string"), # Battery chemistry/type
# Input group
'.1.3.6.1.2.1.33.1.3.2.0': ('input.frequency', lambda x: int(float(x)), "integer"), # Input line frequency
'.1.3.6.1.2.1.33.1.4.1.0': ('input.voltage', lambda x: int(float(x)), "integer"), # Input line voltage
# Output group
'.1.3.6.1.2.1.33.1.4.2.0': ('output.voltage', lambda x: int(float(x)), "integer"), # Output voltage
'.1.3.6.1.2.1.33.1.4.3.0': ('output.frequency', lambda x: int(float(x)), "integer"), # Output frequency
# Load group
'.1.3.6.1.2.1.33.1.5.2.0': ('ups.load', lambda x: int(x), "integer"), # Load (%)
# Bypass group
'.1.3.6.1.2.1.33.1.6.3.8': ('bypass.voltage', lambda x: int(float(x)), "integer"), # Bypass voltage
# Alarm group – these mappings use placeholder NUT keys.
'.1.3.6.1.2.1.33.1.7.1.0': ('alarm.battery', lambda x: x, "string"), # Alarm: Battery Bad
'.1.3.6.1.2.1.33.1.7.3.0': ('outlet.1.status', lambda x: x, "string"), # Alarm: Outlet status (e.g. off)
'.1.3.6.1.2.1.33.1.8.2.0': ('alarm.output.overload', lambda x: x, "string"), # Alarm: Output Overload
'.1.3.6.1.2.1.33.1.8.3.0': ('alarm.bypass.active', lambda x: x, "string"), # Alarm: Bypass Active
'.1.3.6.1.2.1.33.1.8.4.0': ('alarm.shutdown', lambda x: x, "string"), # Alarm: Shutdown
'.1.3.6.1.2.1.33.1.8.5.0': ('alarm.replace.battery', lambda x: x, "string"), # Alarm: Replace Battery
'.1.3.6.1.2.1.33.1.9.1.0': ('alarm.test.result', lambda x: x, "string"), # Alarm: Test Result
'.1.3.6.1.2.1.33.1.9.2.0': ('alarm.low.battery', lambda x: x, "string"), # Alarm: Low Battery
'.1.3.6.1.2.1.33.1.9.3.0': ('alarm.overload', lambda x: x, "string"), # Alarm: Overload
'.1.3.6.1.2.1.33.1.9.4.0': ('alarm.overtemp', lambda x: x, "string"), # Alarm: Over Temperature
'.1.3.6.1.2.1.33.1.9.5.0': ('alarm.shutdown.delay', lambda x: x, "string"), # Alarm: Shutdown Delay
'.1.3.6.1.2.1.33.1.9.6.0': ('alarm.start.delay', lambda x: x, "string"), # Alarm: Start Delay
'.1.3.6.1.2.1.33.1.9.7.0': ('alarm.comm.lost', lambda x: x, "string"), # Alarm: Communication Lost
'.1.3.6.1.2.1.33.1.9.8.0': ('alarm.fuse', lambda x: x, "string"), # Alarm: Fuse
'.1.3.6.1.2.1.33.1.9.9.0': ('alarm.charger.fault', lambda x: x, "string"), # Alarm: Charger Fault
'.1.3.6.1.2.1.33.1.9.10.0': ('alarm.charger.overload', lambda x: x, "string") # Alarm: Charger Overload
}
def get_nut_values():
try:
debug_log("Calling upsc to get UPS values.")
output = subprocess.check_output(['/usr/bin/upsc', 'eaton'],
universal_newlines=True,
stderr=subprocess.STDOUT)
debug_log("upsc output:\n" + output)
except Exception as e:
debug_log("upsc call failed: " + str(e))
return {}
values = {}
for line in output.splitlines():
if "Init SSL without certificate database" in line:
debug_log("Skipping SSL init line: " + line)
continue
if ':' in line:
key, value = line.split(':', 1)
values[key.strip()] = value.strip()
debug_log("Parsed UPS values: " + str(values))
return values
def main():
# Always ignore the first argument (-g) and use the second as the requested OID.
if len(sys.argv) < 3:
debug_log("Insufficient arguments provided.")
sys.exit(1)
requested_oid = sys.argv[2]
debug_log("Requested OID: " + requested_oid)
if requested_oid in mapping:
nut_key, conv, snmp_type = mapping[requested_oid]
debug_log("Mapping found: " + requested_oid + " -> " + nut_key)
nut_values = get_nut_values()
if nut_key in nut_values:
try:
value = conv(nut_values[nut_key])
debug_log("Converted value: " + str(value))
# Output exactly three lines: OID, SNMP type, and the value.
print(requested_oid)
print(snmp_type)
print(value)
except Exception as e:
debug_log("Conversion error for " + nut_key + ": " + str(e))
print("NONE")
else:
debug_log("NUT key not found in UPS output: " + nut_key)
print("NONE")
else:
debug_log("Requested OID not in mapping: " + requested_oid)
print("NONE")
if __name__ == '__main__':
main()
@imavroukakis
Copy link
Author

imavroukakis commented Mar 12, 2025

couple this with

rocommunity ups
pass .1.3.6.1.2.1.33 /etc/snmp/nut_ups_mib.py

supports only snmpget not snmpwalk e.g.

snmpget -v2c -c ups localhost .1.3.6.1.2.1.33.1.1.2.0
SNMPv2-SMI::mib-2.33.1.1.2.0 = STRING: "Eaton 5E 1200 G2"

@imavroukakis
Copy link
Author

The Synology SNMP UPS implementation asks for these OIDs

.1.3.6.1.2.1.33.1.1.2.0
.1.3.6.1.2.1.33.1.7.1.0
.1.3.6.1.2.1.33.1.8.2.0
.1.3.6.1.2.1.33.1.8.3.0
.1.3.6.1.2.1.33.1.9.8.0
.1.3.6.1.2.1.33.1.1.1.0
.1.3.6.1.2.1.33.1.1.2.0
.1.3.6.1.2.1.33.1.1.3.0
.1.3.6.1.2.1.33.1.1.4.0
.1.3.6.1.2.1.33.1.2.1.0
.1.3.6.1.2.1.33.1.2.3.0
.1.3.6.1.2.1.33.1.2.4.0
.1.3.6.1.2.1.33.1.2.5.0
.1.3.6.1.2.1.33.1.2.6.0
.1.3.6.1.2.1.33.1.2.7.0
.1.3.6.1.2.1.33.1.3.2.0
.1.3.6.1.2.1.33.1.4.1.0
.1.3.6.1.2.1.33.1.4.2.0
.1.3.6.1.2.1.33.1.4.3.0
.1.3.6.1.2.1.33.1.5.2.0
.1.3.6.1.2.1.33.1.6.3.8
.1.3.6.1.2.1.33.1.7.1.0
.1.3.6.1.2.1.33.1.7.3.0
.1.3.6.1.2.1.33.1.8.2.0
.1.3.6.1.2.1.33.1.8.3.0
.1.3.6.1.2.1.33.1.8.4.0
.1.3.6.1.2.1.33.1.8.5.0
.1.3.6.1.2.1.33.1.9.1.0
.1.3.6.1.2.1.33.1.9.2.0
.1.3.6.1.2.1.33.1.9.3.0
.1.3.6.1.2.1.33.1.9.4.0
.1.3.6.1.2.1.33.1.9.5.0
.1.3.6.1.2.1.33.1.9.6.0
.1.3.6.1.2.1.33.1.9.7.0
.1.3.6.1.2.1.33.1.9.8.0
.1.3.6.1.2.1.33.1.9.9.0
.1.3.6.1.2.1.33.1.9.10.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment