Skip to content

Instantly share code, notes, and snippets.

@cardil
Created April 4, 2026 12:21
Show Gist options
  • Select an option

  • Save cardil/e76fbf025698fefac9c1df332f1565fd to your computer and use it in GitHub Desktop.

Select an option

Save cardil/e76fbf025698fefac9c1df332f1565fd to your computer and use it in GitHub Desktop.
APC Modbus OutletCommand_BF single-command test tool
#!/usr/bin/env python3
"""PR #3381 — Single-command outlet test.
Usage: python3 .ai/bug4-outlet-toggle-test.py <test> [port]
Tests:
og1.off.le SOG_0 (OG1) OFF, LE word order
og1.on.le SOG_0 (OG1) ON, LE word order
og1.off.be SOG_0 (OG1) OFF, BE word order
og1.on.be SOG_0 (OG1) ON, BE word order
og0.off.le MOG (OG0) OFF, LE word order
og0.on.le MOG (OG0) ON, LE word order
og0.off.be MOG (OG0) OFF, BE word order
og0.on.be MOG (OG0) ON, BE word order
load.off.le load.off (ALL groups), LE
load.on.le load.on (ALL groups), LE
load.off.be load.off (ALL groups), BE
load.on.be load.on (ALL groups), BE
sd.le shutdown.return (ALL+OFF_DELAY), LE
sd.be shutdown.return (ALL+OFF_DELAY), BE
cancel.le Cancel shutdown (ALL), LE
cancel.be Cancel shutdown (ALL), BE
probe.le Quirk probe (invalid combo), LE
probe.be Quirk probe (invalid combo), BE
Port default: /dev/ttyACM0
"""
import sys
import time
from pymodbus.client import ModbusSerialClient
# Command bits (from apc_modbus.h)
CMD_CANCEL = 1 << 0 # 0x0001
CMD_OUTPUT_ON = 1 << 1 # 0x0002
CMD_OUTPUT_OFF = 1 << 2 # 0x0004
CMD_OUTPUT_SHUTDOWN = 1 << 3 # 0x0008
# Modifier bits
MOD_USE_OFF_DELAY = 1 << 7 # 0x0080
# Target bits
TARGET_MOG = 1 << 8 # 0x0100 - Main Outlet Group (OG0)
TARGET_SOG_0 = 1 << 9 # 0x0200 - Switched Outlet Group 0 (OG1)
TARGET_ALL = TARGET_MOG | TARGET_SOG_0 # 0x0300 - All groups on SMT1500
REG_OUTLET_CMD = 1538
BAUDRATE = 9600
SLAVE_ID = 1
TESTS = {
"og1.off.le": ("SOG_0 (OG1) OFF, LE", CMD_OUTPUT_OFF | TARGET_SOG_0, "LE"),
"og1.on.le": ("SOG_0 (OG1) ON, LE", CMD_OUTPUT_ON | TARGET_SOG_0, "LE"),
"og1.off.be": ("SOG_0 (OG1) OFF, BE", CMD_OUTPUT_OFF | TARGET_SOG_0, "BE"),
"og1.on.be": ("SOG_0 (OG1) ON, BE", CMD_OUTPUT_ON | TARGET_SOG_0, "BE"),
"og0.off.le": ("MOG (OG0) OFF, LE", CMD_OUTPUT_OFF | TARGET_MOG, "LE"),
"og0.on.le": ("MOG (OG0) ON, LE", CMD_OUTPUT_ON | TARGET_MOG, "LE"),
"og0.off.be": ("MOG (OG0) OFF, BE", CMD_OUTPUT_OFF | TARGET_MOG, "BE"),
"og0.on.be": ("MOG (OG0) ON, BE", CMD_OUTPUT_ON | TARGET_MOG, "BE"),
"load.off.le": ("load.off ALL, LE", CMD_OUTPUT_OFF | TARGET_ALL, "LE"),
"load.on.le": ("load.on ALL, LE", CMD_OUTPUT_ON | TARGET_ALL, "LE"),
"load.off.be": ("load.off ALL, BE", CMD_OUTPUT_OFF | TARGET_ALL, "BE"),
"load.on.be": ("load.on ALL, BE", CMD_OUTPUT_ON | TARGET_ALL, "BE"),
"sd.le": ("shutdown.return ALL, LE", CMD_OUTPUT_SHUTDOWN | TARGET_ALL | MOD_USE_OFF_DELAY, "LE"),
"sd.be": ("shutdown.return ALL, BE", CMD_OUTPUT_SHUTDOWN | TARGET_ALL | MOD_USE_OFF_DELAY, "BE"),
"sd.mog.le": ("shutdown.return MOG, LE (NUT native)", CMD_OUTPUT_SHUTDOWN | TARGET_MOG | MOD_USE_OFF_DELAY, "LE"),
"sd.mog.be": ("shutdown.return MOG, BE (NUT native)", CMD_OUTPUT_SHUTDOWN | TARGET_MOG | MOD_USE_OFF_DELAY, "BE"),
"cancel.le": ("Cancel ALL, LE", CMD_CANCEL | TARGET_ALL, "LE"),
"cancel.be": ("Cancel ALL, BE", CMD_CANCEL | TARGET_ALL, "BE"),
"probe.le": ("Quirk probe (invalid combo), LE", CMD_CANCEL | CMD_OUTPUT_ON | CMD_OUTPUT_OFF, "LE"),
"probe.be": ("Quirk probe (invalid combo), BE", CMD_CANCEL | CMD_OUTPUT_ON | CMD_OUTPUT_OFF, "BE"),
}
def usage():
print(__doc__)
sys.exit(1)
def main():
if len(sys.argv) < 2 or sys.argv[1] not in TESTS:
usage()
test_name = sys.argv[1]
port = sys.argv[2] if len(sys.argv) > 2 else "/dev/ttyACM0"
desc, cmd_value, word_order = TESTS[test_name]
if word_order == "LE":
regs = [cmd_value & 0xFFFF, (cmd_value >> 16) & 0xFFFF]
else:
regs = [(cmd_value >> 16) & 0xFFFF, cmd_value & 0xFFFF]
print(f"TEST: {test_name} — {desc}")
print(f" Command: 0x{cmd_value:04x}")
print(f" Word order: {word_order}")
print(f" → reg 1538 = 0x{regs[0]:04x}, reg 1539 = 0x{regs[1]:04x}")
print(f" Port: {port}")
client = ModbusSerialClient(
port=port, baudrate=BAUDRATE,
parity='N', stopbits=1, bytesize=8, timeout=3,
)
if not client.connect():
print("ERROR: Cannot connect")
sys.exit(1)
result = client.write_registers(address=REG_OUTLET_CMD, values=regs, slave=SLAVE_ID)
if result.isError():
print(f" RESULT: ❌ REJECTED — {result}")
else:
print(f" RESULT: ✅ ACCEPTED")
time.sleep(1)
client.close()
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment