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
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Added
- **CLI ``tou rates``**: Browse utilities and rate plans for a zip code (``nwp500 tou rates 94903``).
- **CLI ``tou plan``**: View converted rate plan details with decoded pricing (``nwp500 tou plan 94903 "EV Rate A"``).
- **CLI ``tou apply``**: Apply a rate plan to the water heater with optional ``--enable`` flag to activate TOU via MQTT.
- **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.

Changed
-------
Expand Down
56 changes: 48 additions & 8 deletions docs/python_api/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -375,28 +375,53 @@ View and update reservation schedule.

.. code-block:: bash

# Get current reservations
# Get current reservations (table format)
python3 -m nwp500.cli reservations get

# Get current reservations (JSON format)
python3 -m nwp500.cli reservations get --json

# Set reservations from JSON
python3 -m nwp500.cli reservations set '[{"hour": 6, "min": 0, ...}]'

**Syntax:**

.. code-block:: bash

python3 -m nwp500.cli reservations get
python3 -m nwp500.cli reservations get [--json]
python3 -m nwp500.cli reservations set <json> [--disabled]

**Options:**
**Options (get):**

.. option:: --json

Output raw JSON instead of formatted table.

**Options (set):**

.. option:: --disabled

Create reservation in disabled state.

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

**Example Output:**
**Example Table Output:**

.. code-block:: text

Reservations: ENABLED

RESERVATIONS
================================================================================
# Enabled Days Time Temp (°F)
================================================================================
1 Yes Mon-Fri 06:00 160
2 No Sat-Sun 08:00 140
================================================================================

**Example JSON Output (--json):**

.. code-block:: json

Expand All @@ -407,10 +432,20 @@ View and update reservation schedule.
{
"number": 1,
"enabled": true,
"days": [1, 1, 1, 1, 1, 0, 0],
"days": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
"time": "06:00",
"mode": 3,
"temperatureF": 140
"temperatureF": 160,
"raw": {...}
},
{
"number": 2,
"enabled": false,
"days": ["Saturday", "Sunday"],
"time": "08:00",
"mode": 3,
"temperatureF": 140,
"raw": {...}
}
]
}
Expand Down Expand Up @@ -643,8 +678,13 @@ Example 8: Smart Scheduling with Reservations
.. code-block:: bash

#!/bin/bash
# Set reservation schedule: 6 AM - 10 PM at 140°F on weekdays
# View current reservations (table format - default)
python3 -m nwp500.cli reservations get

# View current reservations (JSON format)
python3 -m nwp500.cli reservations get --json

# Set reservation schedule: 6 AM - 10 PM at 140°F on weekdays
python3 -m nwp500.cli reservations set \
'[{"hour": 6, "min": 0, "mode": 3, "temp": 140, "days": [1,1,1,1,1,0,0]}]'

Expand Down
7 changes: 5 additions & 2 deletions src/nwp500/cli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,10 +294,13 @@ def reservations() -> None:


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


@reservations.command("set") # type: ignore[attr-defined]
Expand Down
12 changes: 10 additions & 2 deletions src/nwp500/cli/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ async def handle_power_request(


async def handle_get_reservations_request(
mqtt: NavienMqttClient, device: Device
mqtt: NavienMqttClient, device: Device, output_json: bool = False
) -> None:
"""Request current reservation schedule."""
future = asyncio.get_running_loop().create_future()
Expand Down Expand Up @@ -301,7 +301,15 @@ def raw_callback(topic: str, message: dict[str, Any]) -> None:
for i, e in enumerate(reservations)
],
}
print_json(output)

if output_json:
print_json(output)
else:
reservation_list = output["reservations"]
is_enabled = bool(output["reservationEnabled"])
_formatter.print_reservations_table(
reservation_list, is_enabled
)
future.set_result(None)

device_type = str(device.device_info.device_type)
Expand Down
88 changes: 88 additions & 0 deletions src/nwp500/cli/rich_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,20 @@ def print_tou_schedule(
decode_price_fn,
)

def print_reservations_table(
self, reservations: list[dict[str, Any]], enabled: bool = False
) -> None:
"""Print reservations as a formatted table.

Args:
reservations: List of reservation dictionaries
enabled: Whether reservations are enabled globally
"""
if not self.use_rich:
self._print_reservations_plain(reservations, enabled)
else:
self._print_reservations_rich(reservations, enabled)

# Plain text implementations (fallback)

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

def _print_reservations_plain(
self, reservations: list[dict[str, Any]], enabled: bool = False
) -> None:
"""Plain text reservations output (fallback)."""
status_str = "ENABLED" if enabled else "DISABLED"
print(f"Reservations: {status_str}")
print()

if not reservations:
print("No reservations configured")
return

print("RESERVATIONS")
print("=" * 80)
print(
f" {'#':<3} {'Enabled':<10} {'Days':<25} "
f"{'Time':<8} {'Temp (°F)':<10}"
)
print("=" * 80)

for res in reservations:
num = res.get("number", "?")
is_enabled = res.get("enabled", False)
enabled_str = "Yes" if is_enabled else "No"
days_str = _abbreviate_days(res.get("days", []))
time_str = res.get("time", "??:??")
temp = res.get("temperatureF", "?")
print(
f" {num:<3} {enabled_str:<10} {days_str:<25} "
f"{time_str:<8} {temp:<10}"
)
print("=" * 80)

def _print_error_plain(
self,
message: str,
Expand Down Expand Up @@ -549,6 +596,47 @@ def _print_tou_rich(

self.console.print(table)

def _print_reservations_rich(
self, reservations: list[dict[str, Any]], enabled: bool = False
) -> None:
"""Rich-enhanced reservations output."""
assert self.console is not None
assert _rich_available

status_color = "green" if enabled else "red"
status_text = "ENABLED" if enabled else "DISABLED"
panel = cast(Any, Panel)(
f"[{status_color}]{status_text}[/{status_color}]",
title="📋 Reservations Status",
border_style=status_color,
)
self.console.print(panel)

if not reservations:
panel = cast(Any, Panel)("No reservations configured")
self.console.print(panel)
return

table = cast(Any, Table)(
title="💧 Reservations", show_header=True, highlight=True
)
table.add_column("#", style="cyan", width=3, justify="center")
table.add_column("Status", style="magenta", width=10)
table.add_column("Days", style="white", width=25)
table.add_column("Time", style="yellow", width=8, justify="center")
table.add_column("Temp (°F)", style="green", width=10, justify="center")

for res in reservations:
num = str(res.get("number", "?"))
enabled = res.get("enabled", False)
status = "[green]✓[/green]" if enabled else "[dim]✗[/dim]"
days_str = _abbreviate_days(res.get("days", []))
time_str = res.get("time", "??:??")
temp = str(res.get("temperatureF", "?"))
table.add_row(num, status, days_str, time_str, temp)

self.console.print(table)

# Rich implementations

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