Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 25 additions & 35 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,50 +9,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [5.2.2] - 2026-04-28

TUI usability fixes plus one safety-critical re-arm fix on the shutdown
state machine. Drop-in upgrade.
Bug-fix release. Drop-in upgrade.

### Fixed
- **Shutdown trigger never re-armed after `POWER_RESTORED`** when the
local-shutdown command failed to actually halt the host (custom
no-op command, sandboxed environment, dummy-UPS test rig — bug #4).
Subsequent on-battery transitions silently no-op'd at the flag-file
gate. The handler now clears the flag on the OB/FSD → OL transition,
and `MultiUPSCoordinator` resets its in-memory lock + global flag in
the same hook so multi-UPS deployments re-arm too. Gated re-triggers
also log a warning so the no-op is no longer silent.
- **`eneru tui --graph voltage` silently ignored in interactive mode.**
The CLI flag now seeds the initial graph metric, and `--time` seeds
the initial window; `<G>` / `<T>` cycle from there.
- **A single phantom 0 V sample squashed the voltage band into a
one-pixel strip at the top of the graph.** Writer drops on-line
`input.voltage <= 0` rows at sample time (real outages — `OB`/`FSD`
— still record the legitimate dip to 0 V); graph panel auto-scales
unbounded metrics from the 5th/95th percentile so any leftover
outliers don't dictate the band.
- Shutdown trigger never re-armed after `POWER_RESTORED` when the
local-shutdown command didn't actually halt the host (bug #4).
Single-UPS and multi-UPS coordinator paths both fixed. Gated
re-triggers now log a warning instead of returning silently.
- `eneru tui --graph voltage` silently ignored in interactive mode.
- Phantom 0 V samples squashed the voltage graph into a one-row strip
at the top. Writer drops on-line `input.voltage <= 0` (real
outages still record the dip); graph uses 5th/95th percentile bounds.
- Events panel showed daemon-lifecycle chatter instead of power
events. Priority filter is now tiered: power events always survive
the cap; daemon events fill remaining slots.
- `eneru tui --once --events-only` silently fell back to log parsing
because the default 1 h window was empty for sparse events. Events
no longer use a time window at all.

### Added
- **`--verbose` / `-v`** on `eneru tui` (live + `--once`): widens the
events filter to include low-priority chatter alongside the priority
defaults. Live TUI gains a `<V>` keybind to toggle in-session.
- **`--full-history`** for `eneru tui --once`: ignore the `--time`
window and query the events table from the beginning. Rejects with
`error: --full-history requires --once` (exit 2) when combined with
- `--verbose` / `-v`: include low-priority events. `<V>` toggles in
the live TUI.
- `--length N`: cap events output (default 30, `0` = no cap).

### Changed
- **Events panel defaults to priority-only** in both live TUI and
`eneru tui --once --events-only`. Daemon lifecycle, shutdown
triggers, and power transitions surface; per-condition chatter
is hidden. Scripts that grep `--once --events-only` for
low-priority event types must now pass `--verbose`.
- Live TUI events panel default cap raised from 8 to 20 rows so a few
voltage transitions don't push the daemon-level history off-screen.
- `--time` and `<T>` apply to the graph only. Use `--length` for events.
- Events panel defaults to priority-only.
- Live TUI events cap raised (now `min(30, visible_panel_rows)` so
power events stay inside the visible window on smaller terminals;
`<M>` still expands to 500 rows for full scrollable history).

### Migration notes
None for the package install. Scripts parsing `eneru tui --once
--events-only` output: add `--verbose` if you rely on seeing
low-priority event types (voltage flaps, etc.).
- Scripts grepping `--events-only` output for low-priority event
types: add `--verbose`.
- Scripts using `--time` to size events: switch to `--length`.

---

Expand Down
6 changes: 3 additions & 3 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -431,16 +431,16 @@ Common flags:
| `monitor` | `--once` | Print one status snapshot |
| `monitor` | `--interval` | TUI refresh interval |
| `monitor` | `--graph {charge,load,voltage,runtime}` | Initial graph metric (interactive: cycle with `<G>`) |
| `monitor` | `--time {1h,6h,24h,7d,30d}` | Initial graph / event window (interactive seeds the cycle, still toggle with `<T>`) |
| `monitor` | `--time {1h,6h,24h,7d,30d}` | **Graph** time range (interactive: cycle with `<T>`). Does NOT affect the events list — events have no time window |
| `monitor` | `--events-only` | Print recent events only |
| `monitor` | `--verbose`, `-v` | Show low-priority events too (default: priority only); `<V>` toggles in-session |
| `monitor` | `--full-history` | Ignore `--time`, query events from the beginning (`--once` only) |
| `monitor` | `--length N` | With `--once`: max events to print (default 30, 0 = no cap). Power events always preserved within the cap |

Example package commands:

```bash
sudo eneru validate --config /etc/ups-monitor/config.yaml
sudo eneru run --dry-run --config /etc/ups-monitor/config.yaml
sudo eneru monitor --once --events-only --config /etc/ups-monitor/config.yaml
sudo /opt/ups-monitor/eneru.py monitor --once --events-only --verbose --full-history --config /etc/ups-monitor/config.yaml
sudo eneru monitor --once --events-only --verbose --length 100 --config /etc/ups-monitor/config.yaml
```
Comment on lines 439 to 446
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Keep the package-user examples on one invocation style.

This block is headed as package-user examples, but it now mixes eneru ... and /opt/ups-monitor/eneru.py .... Please make the whole block use the packaged wrapper consistently.

Suggested change
-sudo eneru validate --config /etc/ups-monitor/config.yaml
-sudo eneru run --dry-run --config /etc/ups-monitor/config.yaml
-sudo eneru monitor --once --events-only --config /etc/ups-monitor/config.yaml
+sudo /opt/ups-monitor/eneru.py validate --config /etc/ups-monitor/config.yaml
+sudo /opt/ups-monitor/eneru.py run --dry-run --config /etc/ups-monitor/config.yaml
+sudo /opt/ups-monitor/eneru.py monitor --once --events-only --config /etc/ups-monitor/config.yaml
 sudo /opt/ups-monitor/eneru.py monitor --once --events-only --verbose --length 100 --config /etc/ups-monitor/config.yaml

As per coding guidelines "When writing documentation, use /opt/ups-monitor/eneru.py invocation style for package users (README, troubleshooting), python -m eneru or eneru for developers (CONTRIBUTING, testing), and eneru for PyPI users".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Example package commands:
```bash
sudo eneru validate --config /etc/ups-monitor/config.yaml
sudo eneru run --dry-run --config /etc/ups-monitor/config.yaml
sudo eneru monitor --once --events-only --config /etc/ups-monitor/config.yaml
sudo /opt/ups-monitor/eneru.py monitor --once --events-only --verbose --full-history --config /etc/ups-monitor/config.yaml
sudo /opt/ups-monitor/eneru.py monitor --once --events-only --verbose --length 100 --config /etc/ups-monitor/config.yaml
```
Example package commands:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/configuration.md` around lines 439 - 446, The examples block mixes
invocation styles (e.g., "sudo eneru validate ..." vs "sudo
/opt/ups-monitor/eneru.py monitor ..."); update every example so package-user
commands consistently use the packaged wrapper form "/opt/ups-monitor/eneru.py"
(e.g., replace "sudo eneru ..." and any "eneru ..." invocations with "sudo
/opt/ups-monitor/eneru.py ..."), keeping the same flags and arguments (validate,
run, monitor, --dry-run, --once, --events-only, --verbose, --length, --config)
so the block uniformly shows package-user usage.

58 changes: 29 additions & 29 deletions src/eneru/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,21 @@
apprise = None


def _non_negative_int(value: str) -> int:
"""argparse type for ``--length``: int >= 0 (0 = no cap)."""
try:
n = int(value)
except (TypeError, ValueError):
raise argparse.ArgumentTypeError(
f"--length must be a non-negative integer, got {value!r}"
)
if n < 0:
raise argparse.ArgumentTypeError(
f"--length must be >= 0 (0 = no cap), got {n}"
)
return n


def _load_config(args):
"""Load configuration from the --config path."""
return ConfigLoader.load(getattr(args, 'config', None))
Expand Down Expand Up @@ -368,31 +383,9 @@ def _cmd_deliver_stop(args):

def _cmd_monitor(args):
"""Launch the TUI dashboard."""
# Validate flag combinations BEFORE _load_config so the rejection
# isn't preceded by a "Configuration loaded from: ..." message --
# that ordering looked like the validate-then-fail had succeeded.
# argparse can't easily express "this flag requires --once" so we
# do it here, exit with the same code argparse would use, and
# mimic its ``error: ...`` stderr format so log scrapers keep
# working.
if getattr(args, "full_history", False) and not args.once:
# Compose the prefix from the actual invocation so users running
# ``eneru monitor`` don't get an "eneru tui: ..." message and
# vice versa. ``args.command`` is set by argparse via the
# subparsers' ``dest="command"``; fall back to ``argv[1]`` if
# that's somehow missing (defensive: monitor / tui are aliases).
sub = getattr(args, "command", None) or (
sys.argv[1] if len(sys.argv) > 1 else "monitor"
)
sys.stderr.write(
f"eneru {sub}: error: --full-history requires --once "
"(interactive TUI is real-time)\n"
)
sys.exit(2)

config = _load_config(args)

from eneru.tui import run_tui, run_once
from eneru.tui import run_tui, run_once, EVENTS_MAX_ROWS_NORMAL

if args.once:
run_once(
Expand All @@ -401,7 +394,7 @@ def _cmd_monitor(args):
time_range=getattr(args, "time", "1h"),
events_only=getattr(args, "events_only", False),
verbose=getattr(args, "verbose", False),
full_history=getattr(args, "full_history", False),
length=getattr(args, "length", EVENTS_MAX_ROWS_NORMAL),
)
else:
run_tui(
Expand Down Expand Up @@ -465,19 +458,26 @@ def _add_monitor_args(p):
choices=["charge", "load", "voltage", "runtime"],
help="Initial graph metric. With --once renders a Braille snapshot; "
"in interactive TUI pre-selects the metric (still cycle with <G>)")
# IMPORTANT: --time is GRAPH-ONLY in 5.2.2+. It must NOT be
# threaded into the events query. Events are sparse and a fixed
# window made the panel silently empty for normal homelab usage
# (the events panel then fell back to log parsing without the
# operator noticing). Use --length to size the events list.
p.add_argument("--time", default="1h",
help="Initial graph time range (1h/6h/24h/7d/30d). Applies to "
"--once snapshots and to the interactive TUI's initial view")
help="Graph time range (1h/6h/24h/7d/30d). Applies only to the "
"graph -- the events list is independent (use --length to size it)")
p.add_argument("--events-only", action="store_true",
help="With --once: print only the events list (SQLite, log-tail fallback)")
p.add_argument("--verbose", "-v", action="store_true",
help="Show low-priority events alongside the priority defaults "
"(daemon lifecycle, shutdown triggers, power transitions). "
"Applies to both --once and the interactive TUI; toggle "
"in-session with <V>")
p.add_argument("--full-history", action="store_true",
help="Ignore --time and query the events table from the beginning. "
"Only valid with --once")
p.add_argument("--length", type=_non_negative_int, default=30,
metavar="N",
help="With --once: max events to print (default: 30, 0 = no cap). "
"Power events are always preserved within the cap; daemon-"
"lifecycle events fill remaining slots")
p.set_defaults(func=_cmd_monitor)

mon_parser = subparsers.add_parser("monitor", help="Launch real-time TUI dashboard")
Expand Down
Loading
Loading