Created
April 4, 2026 12:21
-
-
Save cardil/e76fbf025698fefac9c1df332f1565fd to your computer and use it in GitHub Desktop.
APC Modbus OutletCommand_BF single-command test tool
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 | |
| """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