Last active
March 30, 2025 19:26
-
-
Save imavroukakis/164a17a79655c265aceb887978f4038f to your computer and use it in GitHub Desktop.
A script to expose a locally attached UPS via SNMP
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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() |
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
couple this with
supports only
snmpget
notsnmpwalk
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"