Skip to content

power.routeros: power_on must promote saved 'off' to 'forced-on'#104

Merged
widgetii merged 1 commit into
masterfrom
fix-routeros-power-on-from-off
May 15, 2026
Merged

power.routeros: power_on must promote saved 'off' to 'forced-on'#104
widgetii merged 1 commit into
masterfrom
fix-routeros-power-on-from-off

Conversation

@widgetii
Copy link
Copy Markdown
Member

Why

`RouterOSController` saves the port's previous `poe-out` mode on `power_off` so `power_on` can put it back where it was — that preserves "auto-on" vs "forced-on" distinctions correctly. But if the port was already "off" when `power_off` ran (recovering a parked camera; bench is dark on a fresh shell), the saved "previous" mode is literally "off", and `power_on` then "restored" the port to off. Cycling a powered-down port left it powered down forever, which silently broke every recovery flow that started from off.

Surfaced while testing #103 — every fresh script that started with `power_off → power_on` left the camera unpowered, until I switched to `_set_poe(port, 'forced-on')` directly as a workaround.

What

`power_on` must always result in a powered port. If the saved mode is "off", promote to "forced-on" and log the promotion (visible at `-v`):

```python
async def power_on(self, port: str) -> None:
restore_mode = self._saved_poe_out.pop(port, "forced-on")
if restore_mode == "off":
logger.info("PoE ON: %s on %s (saved state was 'off' — promoting to 'forced-on')", port, self._host)
restore_mode = "forced-on"
else:
logger.info("PoE ON: %s on %s (restoring %s)", port, self._host, restore_mode)
await self._set_poe(port, restore_mode)
```

Test plan

  • `uv run pytest tests/ -x --ignore=tests/fuzz` — 527 passed, 2 skipped (5 new tests)
  • `uv run ruff check` + `mypy` on changed files — clean
  • Verified on real hardware (MikroTik 10.216.128.2 / ether8, currently powered down):
    ```
    before: ether8 poe-out='off'
    power_off: saved='off'
    power_on: saved=None, port now 'forced-on'
    ```

5 new tests in `tests/test_power.py::TestRouterOSPowerOnOff` covering forced-on round-trip, auto-on round-trip (preserved — not blindly clobbered to forced-on), off→on promotion (the bug), no-prior-off default, and double-power_off-doesn't-clobber-saved-state edge cases. They use a `_PoeStateRouterOS` subclass that stubs the two network primitives so we can exercise the save/restore state machine without a real switch.

🤖 Generated with Claude Code

RouterOSController saves the port's previous poe-out mode on power_off
so power_on can put it back where it was — preserves "auto-on" vs
"forced-on" distinctions, etc. But if the port was already "off" when
power_off ran (recovering a parked camera; bench is dark), the saved
"previous" mode is literally "off", and power_on then "restored" the
port to off. Cycling a powered-down port left it powered down forever,
which silently broke every recovery flow that started from off.

Fix: power_on always results in a powered port. If the saved mode is
"off", promote to "forced-on" and log the promotion (so it's visible
in `--verbose` output that this happened).

Verified on real hardware (MikroTik 10.216.128.2):
  before:        ether8 poe-out='off'
  power_off:     saved='off'
  power_on:      saved=None, port now 'forced-on'

5 new tests in tests/test_power.py covering: forced-on round-trip,
auto-on round-trip (preserved, not blindly clobbered), off→on
promotion (the bug), no-prior-off default, double-power_off doesn't
clobber the saved state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@widgetii widgetii merged commit 557b259 into master May 15, 2026
13 checks passed
@widgetii widgetii deleted the fix-routeros-power-on-from-off branch May 15, 2026 14:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant