Skip to content

Commit 9327833

Browse files
authored
Merge pull request #76 from eman/copilot/organisational-dove
Add table output format for `nwp-cli reservations get`
2 parents eb821e4 + c8065d5 commit 9327833

5 files changed

Lines changed: 152 additions & 12 deletions

File tree

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Added
1414
- **CLI ``tou rates``**: Browse utilities and rate plans for a zip code (``nwp500 tou rates 94903``).
1515
- **CLI ``tou plan``**: View converted rate plan details with decoded pricing (``nwp500 tou plan 94903 "EV Rate A"``).
1616
- **CLI ``tou apply``**: Apply a rate plan to the water heater with optional ``--enable`` flag to activate TOU via MQTT.
17+
- **CLI Reservations Table Output**: ``nwp-cli reservations get`` now displays reservations as a formatted table by default with global status indicator (ENABLED/DISABLED). Use ``--json`` flag for JSON output.
1718

1819
Changed
1920
-------

docs/python_api/cli.rst

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -375,28 +375,53 @@ View and update reservation schedule.
375375

376376
.. code-block:: bash
377377
378-
# Get current reservations
378+
# Get current reservations (table format)
379379
python3 -m nwp500.cli reservations get
380380
381+
# Get current reservations (JSON format)
382+
python3 -m nwp500.cli reservations get --json
383+
381384
# Set reservations from JSON
382385
python3 -m nwp500.cli reservations set '[{"hour": 6, "min": 0, ...}]'
383386
384387
**Syntax:**
385388

386389
.. code-block:: bash
387390
388-
python3 -m nwp500.cli reservations get
391+
python3 -m nwp500.cli reservations get [--json]
389392
python3 -m nwp500.cli reservations set <json> [--disabled]
390393
391-
**Options:**
394+
**Options (get):**
395+
396+
.. option:: --json
397+
398+
Output raw JSON instead of formatted table.
399+
400+
**Options (set):**
392401

393402
.. option:: --disabled
394403

395404
Create reservation in disabled state.
396405

397-
**Output (get):** Current reservation schedule configuration.
406+
**Output (get):** Current reservation schedule displayed as a formatted table by default,
407+
showing the global reservation status (ENABLED/DISABLED) followed by individual reservations.
408+
Use ``--json`` flag for raw JSON output.
398409

399-
**Example Output:**
410+
**Example Table Output:**
411+
412+
.. code-block:: text
413+
414+
Reservations: ENABLED
415+
416+
RESERVATIONS
417+
================================================================================
418+
# Enabled Days Time Temp (°F)
419+
================================================================================
420+
1 Yes Mon-Fri 06:00 160
421+
2 No Sat-Sun 08:00 140
422+
================================================================================
423+
424+
**Example JSON Output (--json):**
400425

401426
.. code-block:: json
402427
@@ -407,10 +432,20 @@ View and update reservation schedule.
407432
{
408433
"number": 1,
409434
"enabled": true,
410-
"days": [1, 1, 1, 1, 1, 0, 0],
435+
"days": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
411436
"time": "06:00",
412437
"mode": 3,
413-
"temperatureF": 140
438+
"temperatureF": 160,
439+
"raw": {...}
440+
},
441+
{
442+
"number": 2,
443+
"enabled": false,
444+
"days": ["Saturday", "Sunday"],
445+
"time": "08:00",
446+
"mode": 3,
447+
"temperatureF": 140,
448+
"raw": {...}
414449
}
415450
]
416451
}
@@ -643,8 +678,13 @@ Example 8: Smart Scheduling with Reservations
643678
.. code-block:: bash
644679
645680
#!/bin/bash
646-
# Set reservation schedule: 6 AM - 10 PM at 140°F on weekdays
681+
# View current reservations (table format - default)
682+
python3 -m nwp500.cli reservations get
647683
684+
# View current reservations (JSON format)
685+
python3 -m nwp500.cli reservations get --json
686+
687+
# Set reservation schedule: 6 AM - 10 PM at 140°F on weekdays
648688
python3 -m nwp500.cli reservations set \
649689
'[{"hour": 6, "min": 0, "mode": 3, "temp": 140, "days": [1,1,1,1,1,0,0]}]'
650690

src/nwp500/cli/__main__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,10 +294,13 @@ def reservations() -> None:
294294

295295

296296
@reservations.command("get") # type: ignore[attr-defined]
297+
@click.option("--json", "output_json", is_flag=True, help="Output raw JSON")
297298
@async_command
298-
async def reservations_get(mqtt: NavienMqttClient, device: Any) -> None:
299+
async def reservations_get(
300+
mqtt: NavienMqttClient, device: Any, output_json: bool = False
301+
) -> None:
299302
"""Get current reservation schedule."""
300-
await handlers.handle_get_reservations_request(mqtt, device)
303+
await handlers.handle_get_reservations_request(mqtt, device, output_json)
301304

302305

303306
@reservations.command("set") # type: ignore[attr-defined]

src/nwp500/cli/handlers.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ async def handle_power_request(
265265

266266

267267
async def handle_get_reservations_request(
268-
mqtt: NavienMqttClient, device: Device
268+
mqtt: NavienMqttClient, device: Device, output_json: bool = False
269269
) -> None:
270270
"""Request current reservation schedule."""
271271
future = asyncio.get_running_loop().create_future()
@@ -301,7 +301,15 @@ def raw_callback(topic: str, message: dict[str, Any]) -> None:
301301
for i, e in enumerate(reservations)
302302
],
303303
}
304-
print_json(output)
304+
305+
if output_json:
306+
print_json(output)
307+
else:
308+
reservation_list = output["reservations"]
309+
is_enabled = bool(output["reservationEnabled"])
310+
_formatter.print_reservations_table(
311+
reservation_list, is_enabled
312+
)
305313
future.set_result(None)
306314

307315
device_type = str(device.device_info.device_type)

src/nwp500/cli/rich_output.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,20 @@ def print_tou_schedule(
311311
decode_price_fn,
312312
)
313313

314+
def print_reservations_table(
315+
self, reservations: list[dict[str, Any]], enabled: bool = False
316+
) -> None:
317+
"""Print reservations as a formatted table.
318+
319+
Args:
320+
reservations: List of reservation dictionaries
321+
enabled: Whether reservations are enabled globally
322+
"""
323+
if not self.use_rich:
324+
self._print_reservations_plain(reservations, enabled)
325+
else:
326+
self._print_reservations_rich(reservations, enabled)
327+
314328
# Plain text implementations (fallback)
315329

316330
def _print_status_plain(self, items: list[tuple[str, str, str]]) -> None:
@@ -407,6 +421,39 @@ def _print_tou_plain(
407421
f" {p_min:>10.5f} {p_max:>10.5f}"
408422
)
409423

424+
def _print_reservations_plain(
425+
self, reservations: list[dict[str, Any]], enabled: bool = False
426+
) -> None:
427+
"""Plain text reservations output (fallback)."""
428+
status_str = "ENABLED" if enabled else "DISABLED"
429+
print(f"Reservations: {status_str}")
430+
print()
431+
432+
if not reservations:
433+
print("No reservations configured")
434+
return
435+
436+
print("RESERVATIONS")
437+
print("=" * 80)
438+
print(
439+
f" {'#':<3} {'Enabled':<10} {'Days':<25} "
440+
f"{'Time':<8} {'Temp (°F)':<10}"
441+
)
442+
print("=" * 80)
443+
444+
for res in reservations:
445+
num = res.get("number", "?")
446+
is_enabled = res.get("enabled", False)
447+
enabled_str = "Yes" if is_enabled else "No"
448+
days_str = _abbreviate_days(res.get("days", []))
449+
time_str = res.get("time", "??:??")
450+
temp = res.get("temperatureF", "?")
451+
print(
452+
f" {num:<3} {enabled_str:<10} {days_str:<25} "
453+
f"{time_str:<8} {temp:<10}"
454+
)
455+
print("=" * 80)
456+
410457
def _print_error_plain(
411458
self,
412459
message: str,
@@ -549,6 +596,47 @@ def _print_tou_rich(
549596

550597
self.console.print(table)
551598

599+
def _print_reservations_rich(
600+
self, reservations: list[dict[str, Any]], enabled: bool = False
601+
) -> None:
602+
"""Rich-enhanced reservations output."""
603+
assert self.console is not None
604+
assert _rich_available
605+
606+
status_color = "green" if enabled else "red"
607+
status_text = "ENABLED" if enabled else "DISABLED"
608+
panel = cast(Any, Panel)(
609+
f"[{status_color}]{status_text}[/{status_color}]",
610+
title="📋 Reservations Status",
611+
border_style=status_color,
612+
)
613+
self.console.print(panel)
614+
615+
if not reservations:
616+
panel = cast(Any, Panel)("No reservations configured")
617+
self.console.print(panel)
618+
return
619+
620+
table = cast(Any, Table)(
621+
title="💧 Reservations", show_header=True, highlight=True
622+
)
623+
table.add_column("#", style="cyan", width=3, justify="center")
624+
table.add_column("Status", style="magenta", width=10)
625+
table.add_column("Days", style="white", width=25)
626+
table.add_column("Time", style="yellow", width=8, justify="center")
627+
table.add_column("Temp (°F)", style="green", width=10, justify="center")
628+
629+
for res in reservations:
630+
num = str(res.get("number", "?"))
631+
enabled = res.get("enabled", False)
632+
status = "[green]✓[/green]" if enabled else "[dim]✗[/dim]"
633+
days_str = _abbreviate_days(res.get("days", []))
634+
time_str = res.get("time", "??:??")
635+
temp = str(res.get("temperatureF", "?"))
636+
table.add_row(num, status, days_str, time_str, temp)
637+
638+
self.console.print(table)
639+
552640
# Rich implementations
553641

554642
def _print_status_rich(self, items: list[tuple[str, str, str]]) -> None:

0 commit comments

Comments
 (0)