power.routeros: power_on must promote saved 'off' to 'forced-on'#104
Merged
Conversation
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>
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
```
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