-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcli.py
More file actions
293 lines (229 loc) · 8.65 KB
/
cli.py
File metadata and controls
293 lines (229 loc) · 8.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
#!/usr/bin/env python3
"""
Balboa Spa CLI - Command-line interface for controlling your spa.
Usage:
python cli.py status
python cli.py set-temp 102
python cli.py pump 0 toggle
python cli.py light toggle
"""
import asyncio
import os
import sys
import click
from dotenv import load_dotenv
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from spa_control import BalboaSpa
# Load environment variables
load_dotenv()
console = Console()
def get_spa_host() -> str:
"""Get spa host from environment or fail."""
host = os.getenv("SPA_HOST")
if not host:
console.print(
"[red]Error:[/red] SPA_HOST not set. "
"Set it in .env file or as environment variable."
)
sys.exit(1)
return host
async def async_status(host: str):
"""Get and display spa status."""
async with BalboaSpa(host) as spa:
status = await spa.get_status()
return status
async def async_set_temp(host: str, temp: float):
"""Set spa temperature."""
async with BalboaSpa(host) as spa:
await spa.set_temperature(temp)
return await spa.get_status()
async def async_toggle_pump(host: str, pump_index: int):
"""Toggle pump."""
async with BalboaSpa(host) as spa:
await spa.toggle_pump(pump_index)
return await spa.get_status()
async def async_set_pump(host: str, pump_index: int, state: int):
"""Set pump state."""
async with BalboaSpa(host) as spa:
await spa.set_pump(pump_index, state)
return await spa.get_status()
async def async_toggle_light(host: str, light_index: int):
"""Toggle light."""
async with BalboaSpa(host) as spa:
await spa.toggle_light(light_index)
return await spa.get_status()
async def async_toggle_blower(host: str):
"""Toggle blower."""
async with BalboaSpa(host) as spa:
await spa.toggle_blower()
return await spa.get_status()
async def async_set_heat_mode(host: str, mode: str):
"""Set heat mode."""
async with BalboaSpa(host) as spa:
await spa.set_heat_mode(mode)
return await spa.get_status()
async def async_set_temp_range(host: str, temp_range: str):
"""Set temperature range."""
async with BalboaSpa(host) as spa:
await spa.set_temp_range(temp_range)
return await spa.get_status()
def display_status(status):
"""Display spa status in a nice table format."""
# Main status panel
temp_display = f"{status.current_temp}°{status.temp_unit}" if status.current_temp else "---"
target_display = f"{status.target_temp}°{status.temp_unit}"
heating_indicator = "🔥 " if status.heating else ""
main_info = f"""
[bold]Temperature:[/bold] {temp_display} → {target_display} {heating_indicator}
[bold]Heat Mode:[/bold] {status.heat_mode}
[bold]Temp Range:[/bold] {status.temp_range}
[bold]Heat State:[/bold] {status.heat_state}
[bold]Connected:[/bold] {"✓ Yes" if status.connected else "✗ No"}
"""
console.print(Panel(main_info.strip(), title="🛁 Spa Status", border_style="blue"))
# Pumps table
if status.pumps:
pump_table = Table(title="Pumps")
pump_table.add_column("Name", style="cyan")
pump_table.add_column("State", style="green")
pump_table.add_column("Speeds", style="yellow")
for pump in status.pumps:
state_names = ["Off", "Low", "High"]
state_str = state_names[pump["state"]] if pump["state"] < len(state_names) else str(pump["state"])
pump_table.add_row(
pump["name"],
state_str,
str(pump["speeds"])
)
console.print(pump_table)
# Lights table
if status.lights:
light_table = Table(title="Lights")
light_table.add_column("Name", style="cyan")
light_table.add_column("State", style="green")
for light in status.lights:
light_table.add_row(
light["name"],
"💡 On" if light["on"] else "Off"
)
console.print(light_table)
# Blower
if status.blower:
state_names = ["Off", "Low", "Medium", "High"]
blower_state = status.blower["state"]
state_str = state_names[blower_state] if blower_state < len(state_names) else str(blower_state)
console.print(f"[bold]Blower:[/bold] {state_str}")
# Circ pump
if status.circ_pump:
console.print(f"[bold]Circulation Pump:[/bold] {'Running' if status.circ_pump['running'] else 'Off'}")
@click.group()
@click.option("--host", envvar="SPA_HOST", help="Spa IP address (or set SPA_HOST env var)")
@click.pass_context
def cli(ctx, host):
"""Balboa Spa Control CLI - Control your hot tub from the command line."""
ctx.ensure_object(dict)
ctx.obj["host"] = host or get_spa_host()
@cli.command()
@click.pass_context
def status(ctx):
"""Get current spa status."""
host = ctx.obj["host"]
console.print(f"[dim]Connecting to spa at {host}...[/dim]")
try:
spa_status = asyncio.run(async_status(host))
display_status(spa_status)
except Exception as e:
console.print(f"[red]Error:[/red] {e}")
sys.exit(1)
@cli.command("set-temp")
@click.argument("temperature", type=float)
@click.pass_context
def set_temp(ctx, temperature):
"""Set target temperature."""
host = ctx.obj["host"]
console.print(f"[dim]Setting temperature to {temperature}...[/dim]")
try:
spa_status = asyncio.run(async_set_temp(host, temperature))
console.print(f"[green]✓[/green] Temperature set to {temperature}")
display_status(spa_status)
except Exception as e:
console.print(f"[red]Error:[/red] {e}")
sys.exit(1)
@cli.command()
@click.argument("pump_index", type=int)
@click.argument("action", type=click.Choice(["toggle", "off", "low", "high"]))
@click.pass_context
def pump(ctx, pump_index, action):
"""Control pumps. PUMP_INDEX is 0-based (0 = Pump 1)."""
host = ctx.obj["host"]
console.print(f"[dim]Controlling pump {pump_index + 1}...[/dim]")
try:
if action == "toggle":
spa_status = asyncio.run(async_toggle_pump(host, pump_index))
else:
state_map = {"off": 0, "low": 1, "high": 2}
spa_status = asyncio.run(async_set_pump(host, pump_index, state_map[action]))
console.print(f"[green]✓[/green] Pump {pump_index + 1} {action}")
display_status(spa_status)
except Exception as e:
console.print(f"[red]Error:[/red] {e}")
sys.exit(1)
@cli.command()
@click.option("--index", "-i", default=0, help="Light index (0-based)")
@click.pass_context
def light(ctx, index):
"""Toggle lights."""
host = ctx.obj["host"]
console.print(f"[dim]Toggling light {index + 1}...[/dim]")
try:
spa_status = asyncio.run(async_toggle_light(host, index))
console.print(f"[green]✓[/green] Light {index + 1} toggled")
display_status(spa_status)
except Exception as e:
console.print(f"[red]Error:[/red] {e}")
sys.exit(1)
@cli.command()
@click.pass_context
def blower(ctx):
"""Toggle blower."""
host = ctx.obj["host"]
console.print("[dim]Toggling blower...[/dim]")
try:
spa_status = asyncio.run(async_toggle_blower(host))
console.print("[green]✓[/green] Blower toggled")
display_status(spa_status)
except Exception as e:
console.print(f"[red]Error:[/red] {e}")
sys.exit(1)
@cli.command("heat-mode")
@click.argument("mode", type=click.Choice(["ready", "rest"]))
@click.pass_context
def heat_mode(ctx, mode):
"""Set heat mode (ready or rest)."""
host = ctx.obj["host"]
console.print(f"[dim]Setting heat mode to {mode}...[/dim]")
try:
spa_status = asyncio.run(async_set_heat_mode(host, mode))
console.print(f"[green]✓[/green] Heat mode set to {mode}")
display_status(spa_status)
except Exception as e:
console.print(f"[red]Error:[/red] {e}")
sys.exit(1)
@cli.command("temp-range")
@click.argument("range_setting", type=click.Choice(["high", "low"]))
@click.pass_context
def temp_range(ctx, range_setting):
"""Set temperature range (high or low)."""
host = ctx.obj["host"]
console.print(f"[dim]Setting temp range to {range_setting}...[/dim]")
try:
spa_status = asyncio.run(async_set_temp_range(host, range_setting))
console.print(f"[green]✓[/green] Temp range set to {range_setting}")
display_status(spa_status)
except Exception as e:
console.print(f"[red]Error:[/red] {e}")
sys.exit(1)
if __name__ == "__main__":
cli()