qapu-cli 0.7.3__tar.gz → 0.7.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qapu-cli
3
- Version: 0.7.3
3
+ Version: 0.7.4
4
4
  Summary: CLI client for the Qapu API - built for the Hermes agent, but usable by anyone talking to api.ovoo.com.tr from outside the Swarm.
5
5
  Author: OVOO Technology
6
6
  Classifier: Programming Language :: Python :: 3
@@ -56,7 +56,7 @@ Either way installs a `qapu` command (see `pyproject.toml`'s `[project.scripts]`
56
56
  |---|---|
57
57
  | `qapu ct-check` | Diagnose CT (current transformer) wiring per phase - reversed polarity vs. wrong phase order - from live PF_R/S/T/AE_R/S/T (GET /hermes/data/{device_id}). |
58
58
  | `qapu current` | Current synthesis parameters, grouped (RMS, imbalance, THD, crest factor, fundamental component) (GET /hermes/data/{device_id}). |
59
- | `qapu data` | Readings per variable for a device - latest value, last N buffered readings (--last), daily min/avg/max (--days), or an explicit --start/--end range (GET /hermes/data/{device_id}). |
59
+ | `qapu data` | Readings per variable for a device - latest value, last N buffered readings (--last), daily min/avg/max (--days), an explicit --start/--end range, or a live-refreshed view (--watch) (GET /hermes/data/{device_id}). |
60
60
  | `qapu energy` | One day's active/reactive energy breakdown per phase (GET /hermes/energy/{device_id}). |
61
61
  | `qapu health` | Check CLI/server version, connectivity, and server status (GET /health, no auth required). |
62
62
  | `qapu pipeline` | Fleet-wide pipeline throughput for a time window - ingest/data/rule/blockchain packet+event counts (GET /hermes/pipeline/stats). `--watch` live-refreshes in place. |
@@ -319,6 +319,14 @@ qapu data <device_id> --start 2026-08-30 --end 2026-08-31 --days 5 # daily mi
319
319
 
320
320
  **Refined again same day, per three more rounds of direct feedback**: (1) `--last`'s TIME column showed a coarse relative bucket ("1 gün önce") for every one of the N readings - useless once several readings from the same day all collapse to the same bucket. Added a new `_format_time()` helper (`tools/cli/qapu_cli/main.py`, next to the existing `_relative_time()`) that renders the actual data timestamp as `YYYY-MM-DD HH:MM:SS` instead - used only where each row needs its own real time (this per-reading table); `_relative_time()` itself (SIM last-connection, timeline, etc.) is untouched, still relative, since "how long ago" is the right framing there. (2) The per-variable group tables were initially borderless (`box=None`) for a lighter look - reverted to the same bordered `Table()` style every other CLI table already uses ("görsel kısmı güzelleştir tablolu yap"), since the borderless version read as less like a real table, not more. `--json` output is untouched either way ("--json diyince ai kendine göre okur onu" - raw JSON is for machine consumption, formatting choices don't need to accommodate it). (3) The `VARIABLE_ID — Description (Unit)` heading was originally a plain `console.print()` line sitting above each group's table, not visually part of it - moved onto the `Table` itself via Rich's `title=`/`title_justify="left"`/`title_style="bold"` params, so it now renders attached to the table's own border ("kısmını da tabloya dahil edelim tablo yapısı ile görünsün"). Live-tested against real prod data: `qapu data <id> --search VRMS --last 5` now shows 5 grouped, bordered, titled tables with real per-reading timestamps (e.g. `2026-09-01 03:12:14`).
321
321
 
322
+ **`qapu data --watch`/`-w` (added 2026-09-04)** - the same live-refresh idea `qapu pipeline --watch` uses (see that command's own entry further down for the underlying pattern), applied to a single device's latest readings instead of the fleet-wide pipeline summary, per a same-day follow-up ("aynı şekilde data izlerken de bir canlı uç açabilirmiyiz"):
323
+ ```bash
324
+ qapu data <device_id> --watch # every variable, live
325
+ qapu data <device_id> --watch --search VRMS # combine with family/search filters, same as normal mode
326
+ qapu data <device_id> --watch --interval 2 # poll every 2s instead of the default 5s
327
+ ```
328
+ Only valid for the default latest-value mode - refuses clearly if combined with `--last`/`--days`/`--start`/`--end`/`--json`. Filtering logic (`--energy`/`--gsm`/`--voltage`/`--current`/`--battery`/`--search`) was extracted into a shared `_apply_data_filters()` so the watch loop and the normal path can't drift apart. Each poll after the first shows a `DEĞİŞİM` column - green `▲ <diff>` if a variable rose since the last poll, red `▼ <diff>` if it fell, `·` if unchanged - and `ZAMAN` shows the absolute reading timestamp (`_format_time()`, not `_relative_time()` - a 5-second poll loop would show "0 dk önce" for every row regardless of whether it just changed, same reasoning already applied to `--last`'s per-reading table above). Verified the same way as `pipeline --watch`'s delta logic: `_build_data_watch_table()` called directly with two synthetic consecutive polls (one variable rising, one falling, one flat), confirmed correct `▲`/`▼`/`·` rendering - `rich.Live`'s actual screen-redraw behavior wasn't separately re-verified end to end here either, for the same non-TTY sandbox reason noted under `pipeline --watch`.
329
+
322
330
  ```bash
323
331
  qapu trend <device_id> <variable_id> # GET /hermes/trend/{device_id}/{variable_id} - last 20 raw readings + trend
324
332
  qapu trend <device_id> <variable_id> --last 50 # last N raw readings instead of the default 20
@@ -1,15 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: qapu-cli
3
- Version: 0.7.3
4
- Summary: CLI client for the Qapu API - built for the Hermes agent, but usable by anyone talking to api.ovoo.com.tr from outside the Swarm.
5
- Author: OVOO Technology
6
- Classifier: Programming Language :: Python :: 3
7
- Requires-Python: >=3.10
8
- Description-Content-Type: text/markdown
9
- Requires-Dist: typer<1.0,>=0.12
10
- Requires-Dist: httpx<1.0,>=0.27
11
- Requires-Dist: rich<16.0,>=13.0
12
-
13
1
  # Qapu CLI
14
2
 
15
3
  A thin command-line client for the Qapu API (`api.ovoo.com.tr`), built for the Hermes agent (runs outside this Swarm, in a separate datacenter, and only ever talks to Qapu through this public API) - but usable by anything else that needs to script against Qapu from outside the private network.
@@ -56,7 +44,7 @@ Either way installs a `qapu` command (see `pyproject.toml`'s `[project.scripts]`
56
44
  |---|---|
57
45
  | `qapu ct-check` | Diagnose CT (current transformer) wiring per phase - reversed polarity vs. wrong phase order - from live PF_R/S/T/AE_R/S/T (GET /hermes/data/{device_id}). |
58
46
  | `qapu current` | Current synthesis parameters, grouped (RMS, imbalance, THD, crest factor, fundamental component) (GET /hermes/data/{device_id}). |
59
- | `qapu data` | Readings per variable for a device - latest value, last N buffered readings (--last), daily min/avg/max (--days), or an explicit --start/--end range (GET /hermes/data/{device_id}). |
47
+ | `qapu data` | Readings per variable for a device - latest value, last N buffered readings (--last), daily min/avg/max (--days), an explicit --start/--end range, or a live-refreshed view (--watch) (GET /hermes/data/{device_id}). |
60
48
  | `qapu energy` | One day's active/reactive energy breakdown per phase (GET /hermes/energy/{device_id}). |
61
49
  | `qapu health` | Check CLI/server version, connectivity, and server status (GET /health, no auth required). |
62
50
  | `qapu pipeline` | Fleet-wide pipeline throughput for a time window - ingest/data/rule/blockchain packet+event counts (GET /hermes/pipeline/stats). `--watch` live-refreshes in place. |
@@ -319,6 +307,14 @@ qapu data <device_id> --start 2026-08-30 --end 2026-08-31 --days 5 # daily mi
319
307
 
320
308
  **Refined again same day, per three more rounds of direct feedback**: (1) `--last`'s TIME column showed a coarse relative bucket ("1 gün önce") for every one of the N readings - useless once several readings from the same day all collapse to the same bucket. Added a new `_format_time()` helper (`tools/cli/qapu_cli/main.py`, next to the existing `_relative_time()`) that renders the actual data timestamp as `YYYY-MM-DD HH:MM:SS` instead - used only where each row needs its own real time (this per-reading table); `_relative_time()` itself (SIM last-connection, timeline, etc.) is untouched, still relative, since "how long ago" is the right framing there. (2) The per-variable group tables were initially borderless (`box=None`) for a lighter look - reverted to the same bordered `Table()` style every other CLI table already uses ("görsel kısmı güzelleştir tablolu yap"), since the borderless version read as less like a real table, not more. `--json` output is untouched either way ("--json diyince ai kendine göre okur onu" - raw JSON is for machine consumption, formatting choices don't need to accommodate it). (3) The `VARIABLE_ID — Description (Unit)` heading was originally a plain `console.print()` line sitting above each group's table, not visually part of it - moved onto the `Table` itself via Rich's `title=`/`title_justify="left"`/`title_style="bold"` params, so it now renders attached to the table's own border ("kısmını da tabloya dahil edelim tablo yapısı ile görünsün"). Live-tested against real prod data: `qapu data <id> --search VRMS --last 5` now shows 5 grouped, bordered, titled tables with real per-reading timestamps (e.g. `2026-09-01 03:12:14`).
321
309
 
310
+ **`qapu data --watch`/`-w` (added 2026-09-04)** - the same live-refresh idea `qapu pipeline --watch` uses (see that command's own entry further down for the underlying pattern), applied to a single device's latest readings instead of the fleet-wide pipeline summary, per a same-day follow-up ("aynı şekilde data izlerken de bir canlı uç açabilirmiyiz"):
311
+ ```bash
312
+ qapu data <device_id> --watch # every variable, live
313
+ qapu data <device_id> --watch --search VRMS # combine with family/search filters, same as normal mode
314
+ qapu data <device_id> --watch --interval 2 # poll every 2s instead of the default 5s
315
+ ```
316
+ Only valid for the default latest-value mode - refuses clearly if combined with `--last`/`--days`/`--start`/`--end`/`--json`. Filtering logic (`--energy`/`--gsm`/`--voltage`/`--current`/`--battery`/`--search`) was extracted into a shared `_apply_data_filters()` so the watch loop and the normal path can't drift apart. Each poll after the first shows a `DEĞİŞİM` column - green `▲ <diff>` if a variable rose since the last poll, red `▼ <diff>` if it fell, `·` if unchanged - and `ZAMAN` shows the absolute reading timestamp (`_format_time()`, not `_relative_time()` - a 5-second poll loop would show "0 dk önce" for every row regardless of whether it just changed, same reasoning already applied to `--last`'s per-reading table above). Verified the same way as `pipeline --watch`'s delta logic: `_build_data_watch_table()` called directly with two synthetic consecutive polls (one variable rising, one falling, one flat), confirmed correct `▲`/`▼`/`·` rendering - `rich.Live`'s actual screen-redraw behavior wasn't separately re-verified end to end here either, for the same non-TTY sandbox reason noted under `pipeline --watch`.
317
+
322
318
  ```bash
323
319
  qapu trend <device_id> <variable_id> # GET /hermes/trend/{device_id}/{variable_id} - last 20 raw readings + trend
324
320
  qapu trend <device_id> <variable_id> --last 50 # last N raw readings instead of the default 20
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "qapu-cli"
3
- version = "0.7.3"
3
+ version = "0.7.4"
4
4
  description = "CLI client for the Qapu API - built for the Hermes agent, but usable by anyone talking to api.ovoo.com.tr from outside the Swarm."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -804,42 +804,16 @@ def _print_data_daily_grouped(readings: list[dict[str, Any]]) -> None:
804
804
  console.print(table)
805
805
 
806
806
 
807
- # Get a Device's Readings per Variable, Optionally Filtered by Family/Time Window
808
- @app.command("data")
809
- def device_data(
810
- device_id: str = typer.Argument(..., help="Device ID."),
811
- energy: bool = typer.Option(False, "--energy", help="Only Energy segment variables."),
812
- gsm: bool = typer.Option(False, "--gsm", help="Only GSM segment variables (signal, network, etc)."),
813
- voltage: bool = typer.Option(False, "--voltage", help="Only voltage variables (Unit == V - includes battery voltage)."),
814
- current: bool = typer.Option(False, "--current", help="Only current variables (Unit == A)."),
815
- battery: bool = typer.Option(False, "--battery", help="Only battery variables (Variable ID starting with B_)."),
816
- search: Optional[str] = typer.Option(None, "--search", help="Case-insensitive substring match on the variable ID (e.g. --search vrms for VRMS_R/S/T, or --search AE for AE_R/AE_S/AE_T/AE_L/AE_G - drop the phase/direction suffix to pull the whole family)."),
817
- last: Optional[int] = typer.Option(None, "--last", help="Show the last N buffered readings per variable instead of just the latest. Bounded by the buffer's own depth (~50 most recent packets) - not a time window."),
818
- days: Optional[int] = typer.Option(None, "--days", help="Show daily min/avg/max over the last N days per variable instead of the latest value. Ignored if --start/--end is also given (the range wins)."),
819
- start: Optional[str] = typer.Option(None, "--start", help="ISO date/datetime lower bound, inclusive (e.g. 2026-08-30 or 2026-08-30T14:00:00). Combine with --end for an explicit range - takes priority over --last/--days. Daily min/avg/max narrowed to the range if --days was also given, raw readings otherwise."),
820
- end: Optional[str] = typer.Option(None, "--end", help="ISO date/datetime upper bound, inclusive."),
821
- as_json: bool = typer.Option(False, "--json", help="Print raw JSON instead of a table."),
822
- ):
823
- """Readings per variable for a device - latest value, last N buffered readings (--last), daily min/avg/max (--days), or an explicit --start/--end range (GET /hermes/data/{device_id})."""
824
-
825
- # Fetch the Readings - Mode Depends on --last/--days/--start/--end,
826
- # Already Enriched with Description/Unit/Segment Regardless of Mode
827
- params: dict[str, Any] = {}
828
- if days is not None:
829
- params["days"] = days
830
- elif last is not None:
831
- params["last"] = last
832
- if start is not None:
833
- params["start"] = start
834
- if end is not None:
835
- params["end"] = end
836
- readings: list[dict[str, Any]] = client.get(f"/hermes/data/{device_id}", params=params or None)
807
+ # Apply --energy/--gsm/--voltage/--current/--battery/--search Filters to a
808
+ # Readings List - Extracted 2026-09-04 So `data --watch`'s Poll Loop Can
809
+ # Reuse the Exact Same Filter Logic as the One-Shot Path, Rather Than
810
+ # Drifting into a Second Copy.
811
+ def _apply_data_filters(readings: list[dict[str, Any]], energy: bool, gsm: bool, voltage: bool, current: bool, battery: bool, search: Optional[str]) -> list[dict[str, Any]]:
837
812
 
838
813
  # Apply Family Filters, if Any - Union (OR) Across Whichever Were Given -
839
814
  # Same Filter Logic Regardless of Mode, Since Variable_ID/Unit/Segment
840
815
  # are Present on Every Row in Every Mode
841
- filters_selected = energy or gsm or voltage or current or battery
842
- if filters_selected:
816
+ if energy or gsm or voltage or current or battery:
843
817
 
844
818
  # Filter In Place
845
819
  filtered: list[dict[str, Any]] = []
@@ -874,6 +848,112 @@ def device_data(
874
848
  needle = search.strip().lower()
875
849
  readings = [r for r in readings if needle in (r.get("Variable_ID") or "").lower()]
876
850
 
851
+ # Return the Filtered List, Sorted for Stable Output
852
+ return sorted(readings, key=lambda r: r.get("Variable_ID") or "")
853
+
854
+
855
+ # Build a Live-Watch Table for `data --watch` - VARIABLE/VALUE/UNIT/
856
+ # DEĞİŞİM (Changed-Since-Last-Poll Indicator)/TIME, Grouped Neither by
857
+ # Variable (There's Only Ever One Reading per Variable in "Latest" Mode,
858
+ # Unlike --last's Multi-Reading Groups) - Absolute Time (_format_time),
859
+ # not Relative, Since a 5-Second Poll Loop Would Show "0 dk önce" for
860
+ # Every Row Regardless of Whether it Actually Just Changed.
861
+ def _build_data_watch_table(device_id: str, readings: list[dict[str, Any]], previous: Optional[dict[str, Any]], as_of: str) -> Group:
862
+
863
+ header = f"[bold]{device_id}[/bold] - Canlı Veri [dim](güncellendi: {as_of})[/dim]"
864
+
865
+ table = Table(show_header=True, header_style="bold")
866
+ table.add_column("DEĞİŞKEN")
867
+ table.add_column("DEĞER", justify="right")
868
+ table.add_column("BİRİM")
869
+ if previous is not None:
870
+ table.add_column("DEĞİŞİM")
871
+ table.add_column("ZAMAN")
872
+
873
+ for r in readings:
874
+ variable_id = r.get("Variable_ID") or "-"
875
+ value = r.get("Value")
876
+ value_str = f"{value:g}" if isinstance(value, (int, float)) else "-"
877
+ unit = r.get("Unit") or "-"
878
+ reading_time = _format_time(r.get("Time"))
879
+
880
+ row = [variable_id, value_str, unit]
881
+ if previous is not None:
882
+ prev_value = previous.get(variable_id, {}).get("Value") if isinstance(previous.get(variable_id), dict) else None
883
+ if prev_value is None or not isinstance(value, (int, float)):
884
+ row.append("")
885
+ elif value > prev_value:
886
+ row.append(f"[green]▲ {value - prev_value:g}[/green]")
887
+ elif value < prev_value:
888
+ row.append(f"[red]▼ {prev_value - value:g}[/red]")
889
+ else:
890
+ row.append("·")
891
+ row.append(reading_time)
892
+ table.add_row(*row)
893
+
894
+ return Group(header, table)
895
+
896
+
897
+ # Get a Device's Readings per Variable, Optionally Filtered by Family/Time Window
898
+ @app.command("data")
899
+ def device_data(
900
+ device_id: str = typer.Argument(..., help="Device ID."),
901
+ energy: bool = typer.Option(False, "--energy", help="Only Energy segment variables."),
902
+ gsm: bool = typer.Option(False, "--gsm", help="Only GSM segment variables (signal, network, etc)."),
903
+ voltage: bool = typer.Option(False, "--voltage", help="Only voltage variables (Unit == V - includes battery voltage)."),
904
+ current: bool = typer.Option(False, "--current", help="Only current variables (Unit == A)."),
905
+ battery: bool = typer.Option(False, "--battery", help="Only battery variables (Variable ID starting with B_)."),
906
+ search: Optional[str] = typer.Option(None, "--search", help="Case-insensitive substring match on the variable ID (e.g. --search vrms for VRMS_R/S/T, or --search AE for AE_R/AE_S/AE_T/AE_L/AE_G - drop the phase/direction suffix to pull the whole family)."),
907
+ last: Optional[int] = typer.Option(None, "--last", help="Show the last N buffered readings per variable instead of just the latest. Bounded by the buffer's own depth (~50 most recent packets) - not a time window."),
908
+ days: Optional[int] = typer.Option(None, "--days", help="Show daily min/avg/max over the last N days per variable instead of the latest value. Ignored if --start/--end is also given (the range wins)."),
909
+ start: Optional[str] = typer.Option(None, "--start", help="ISO date/datetime lower bound, inclusive (e.g. 2026-08-30 or 2026-08-30T14:00:00). Combine with --end for an explicit range - takes priority over --last/--days. Daily min/avg/max narrowed to the range if --days was also given, raw readings otherwise."),
910
+ end: Optional[str] = typer.Option(None, "--end", help="ISO date/datetime upper bound, inclusive."),
911
+ watch: bool = typer.Option(False, "--watch", "-w", help="Live-refresh the latest-value table in place every --interval seconds (Ctrl+C to stop). Only compatible with the default latest-value mode - not combinable with --last/--days/--start/--end/--json."),
912
+ interval: float = typer.Option(5.0, "--interval", help="Seconds between refreshes in --watch mode."),
913
+ as_json: bool = typer.Option(False, "--json", help="Print raw JSON instead of a table."),
914
+ ):
915
+ """Readings per variable for a device - latest value, last N buffered readings (--last), daily min/avg/max (--days), an explicit --start/--end range, or a live-refreshed view (--watch) (GET /hermes/data/{device_id})."""
916
+
917
+ # --watch: Only Meaningful for the Latest-Value Mode - Refuse Clearly
918
+ # Rather than Silently Ignoring --last/--days/--start/--end/--json
919
+ if watch:
920
+ if last is not None or days is not None or start is not None or end is not None or as_json:
921
+ typer.echo("Error: --watch sadece varsayılan (son değer) modla kullanılabilir, --last/--days/--start/--end/--json ile birlikte kullanılamaz.", err=True)
922
+ raise typer.Exit(code=1)
923
+
924
+ previous: Optional[dict[str, dict[str, Any]]] = None
925
+ try:
926
+ with Live(console=console, refresh_per_second=4) as live:
927
+ while True:
928
+ readings = client.get(f"/hermes/data/{device_id}")
929
+ readings = _apply_data_filters(readings, energy, gsm, voltage, current, battery, search)
930
+ as_of = datetime.now().strftime("%H:%M:%S")
931
+ if not readings:
932
+ live.update(Group(f"[bold]{device_id}[/bold] - Canlı Veri [dim](güncellendi: {as_of})[/dim]", "\n[yellow]Filtreye uyan veri yok.[/yellow]"))
933
+ else:
934
+ live.update(_build_data_watch_table(device_id, readings, previous, as_of))
935
+ previous = {r.get("Variable_ID"): r for r in readings if r.get("Variable_ID")}
936
+ time.sleep(interval)
937
+ except KeyboardInterrupt:
938
+ return
939
+
940
+ # Fetch the Readings - Mode Depends on --last/--days/--start/--end,
941
+ # Already Enriched with Description/Unit/Segment Regardless of Mode
942
+ params: dict[str, Any] = {}
943
+ if days is not None:
944
+ params["days"] = days
945
+ elif last is not None:
946
+ params["last"] = last
947
+ if start is not None:
948
+ params["start"] = start
949
+ if end is not None:
950
+ params["end"] = end
951
+ readings: list[dict[str, Any]] = client.get(f"/hermes/data/{device_id}", params=params or None)
952
+
953
+ # Apply Family/Search Filters, if Any
954
+ filters_selected = energy or gsm or voltage or current or battery
955
+ readings = _apply_data_filters(readings, energy, gsm, voltage, current, battery, search)
956
+
877
957
  # JSON Output - Everything at Once, Never Paginated
878
958
  if as_json:
879
959
 
@@ -886,11 +966,10 @@ def device_data(
886
966
  typer.echo("Bu cihaz için henüz veri yok." if not (filters_selected or search) else "Filtreye uyan veri bulunamadı.")
887
967
  return
888
968
 
889
- # Sort for Stable Output, then Print with the Table Shape the Mode Needs -
890
- # the Response Shape (Daily/History/Value Keys) is What Actually Decides
891
- # This, not the Request Params Directly, Since --start/--end Alone (no
892
- # --days) Comes Back in the Same "History" Shape --last Uses
893
- readings = sorted(readings, key=lambda r: r.get("Variable_ID") or "")
969
+ # Print with the Table Shape the Mode Needs - the Response Shape
970
+ # (Daily/History/Value Keys) is What Actually Decides This, not the
971
+ # Request Params Directly, Since --start/--end Alone (no --days) Comes
972
+ # Back in the Same "History" Shape --last Uses
894
973
  if days is not None:
895
974
  _print_data_daily_grouped(readings)
896
975
  elif last is not None or start is not None or end is not None:
@@ -1,3 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: qapu-cli
3
+ Version: 0.7.4
4
+ Summary: CLI client for the Qapu API - built for the Hermes agent, but usable by anyone talking to api.ovoo.com.tr from outside the Swarm.
5
+ Author: OVOO Technology
6
+ Classifier: Programming Language :: Python :: 3
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: typer<1.0,>=0.12
10
+ Requires-Dist: httpx<1.0,>=0.27
11
+ Requires-Dist: rich<16.0,>=13.0
12
+
1
13
  # Qapu CLI
2
14
 
3
15
  A thin command-line client for the Qapu API (`api.ovoo.com.tr`), built for the Hermes agent (runs outside this Swarm, in a separate datacenter, and only ever talks to Qapu through this public API) - but usable by anything else that needs to script against Qapu from outside the private network.
@@ -44,7 +56,7 @@ Either way installs a `qapu` command (see `pyproject.toml`'s `[project.scripts]`
44
56
  |---|---|
45
57
  | `qapu ct-check` | Diagnose CT (current transformer) wiring per phase - reversed polarity vs. wrong phase order - from live PF_R/S/T/AE_R/S/T (GET /hermes/data/{device_id}). |
46
58
  | `qapu current` | Current synthesis parameters, grouped (RMS, imbalance, THD, crest factor, fundamental component) (GET /hermes/data/{device_id}). |
47
- | `qapu data` | Readings per variable for a device - latest value, last N buffered readings (--last), daily min/avg/max (--days), or an explicit --start/--end range (GET /hermes/data/{device_id}). |
59
+ | `qapu data` | Readings per variable for a device - latest value, last N buffered readings (--last), daily min/avg/max (--days), an explicit --start/--end range, or a live-refreshed view (--watch) (GET /hermes/data/{device_id}). |
48
60
  | `qapu energy` | One day's active/reactive energy breakdown per phase (GET /hermes/energy/{device_id}). |
49
61
  | `qapu health` | Check CLI/server version, connectivity, and server status (GET /health, no auth required). |
50
62
  | `qapu pipeline` | Fleet-wide pipeline throughput for a time window - ingest/data/rule/blockchain packet+event counts (GET /hermes/pipeline/stats). `--watch` live-refreshes in place. |
@@ -307,6 +319,14 @@ qapu data <device_id> --start 2026-08-30 --end 2026-08-31 --days 5 # daily mi
307
319
 
308
320
  **Refined again same day, per three more rounds of direct feedback**: (1) `--last`'s TIME column showed a coarse relative bucket ("1 gün önce") for every one of the N readings - useless once several readings from the same day all collapse to the same bucket. Added a new `_format_time()` helper (`tools/cli/qapu_cli/main.py`, next to the existing `_relative_time()`) that renders the actual data timestamp as `YYYY-MM-DD HH:MM:SS` instead - used only where each row needs its own real time (this per-reading table); `_relative_time()` itself (SIM last-connection, timeline, etc.) is untouched, still relative, since "how long ago" is the right framing there. (2) The per-variable group tables were initially borderless (`box=None`) for a lighter look - reverted to the same bordered `Table()` style every other CLI table already uses ("görsel kısmı güzelleştir tablolu yap"), since the borderless version read as less like a real table, not more. `--json` output is untouched either way ("--json diyince ai kendine göre okur onu" - raw JSON is for machine consumption, formatting choices don't need to accommodate it). (3) The `VARIABLE_ID — Description (Unit)` heading was originally a plain `console.print()` line sitting above each group's table, not visually part of it - moved onto the `Table` itself via Rich's `title=`/`title_justify="left"`/`title_style="bold"` params, so it now renders attached to the table's own border ("kısmını da tabloya dahil edelim tablo yapısı ile görünsün"). Live-tested against real prod data: `qapu data <id> --search VRMS --last 5` now shows 5 grouped, bordered, titled tables with real per-reading timestamps (e.g. `2026-09-01 03:12:14`).
309
321
 
322
+ **`qapu data --watch`/`-w` (added 2026-09-04)** - the same live-refresh idea `qapu pipeline --watch` uses (see that command's own entry further down for the underlying pattern), applied to a single device's latest readings instead of the fleet-wide pipeline summary, per a same-day follow-up ("aynı şekilde data izlerken de bir canlı uç açabilirmiyiz"):
323
+ ```bash
324
+ qapu data <device_id> --watch # every variable, live
325
+ qapu data <device_id> --watch --search VRMS # combine with family/search filters, same as normal mode
326
+ qapu data <device_id> --watch --interval 2 # poll every 2s instead of the default 5s
327
+ ```
328
+ Only valid for the default latest-value mode - refuses clearly if combined with `--last`/`--days`/`--start`/`--end`/`--json`. Filtering logic (`--energy`/`--gsm`/`--voltage`/`--current`/`--battery`/`--search`) was extracted into a shared `_apply_data_filters()` so the watch loop and the normal path can't drift apart. Each poll after the first shows a `DEĞİŞİM` column - green `▲ <diff>` if a variable rose since the last poll, red `▼ <diff>` if it fell, `·` if unchanged - and `ZAMAN` shows the absolute reading timestamp (`_format_time()`, not `_relative_time()` - a 5-second poll loop would show "0 dk önce" for every row regardless of whether it just changed, same reasoning already applied to `--last`'s per-reading table above). Verified the same way as `pipeline --watch`'s delta logic: `_build_data_watch_table()` called directly with two synthetic consecutive polls (one variable rising, one falling, one flat), confirmed correct `▲`/`▼`/`·` rendering - `rich.Live`'s actual screen-redraw behavior wasn't separately re-verified end to end here either, for the same non-TTY sandbox reason noted under `pipeline --watch`.
329
+
310
330
  ```bash
311
331
  qapu trend <device_id> <variable_id> # GET /hermes/trend/{device_id}/{variable_id} - last 20 raw readings + trend
312
332
  qapu trend <device_id> <variable_id> --last 50 # last N raw readings instead of the default 20
File without changes
File without changes
File without changes