tescmd 0.1.2__py3-none-any.whl
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.
- tescmd/__init__.py +3 -0
- tescmd/__main__.py +5 -0
- tescmd/_internal/__init__.py +0 -0
- tescmd/_internal/async_utils.py +25 -0
- tescmd/_internal/permissions.py +43 -0
- tescmd/_internal/vin.py +44 -0
- tescmd/api/__init__.py +1 -0
- tescmd/api/charging.py +102 -0
- tescmd/api/client.py +189 -0
- tescmd/api/command.py +540 -0
- tescmd/api/energy.py +146 -0
- tescmd/api/errors.py +76 -0
- tescmd/api/partner.py +40 -0
- tescmd/api/sharing.py +65 -0
- tescmd/api/signed_command.py +277 -0
- tescmd/api/user.py +38 -0
- tescmd/api/vehicle.py +150 -0
- tescmd/auth/__init__.py +1 -0
- tescmd/auth/oauth.py +312 -0
- tescmd/auth/server.py +108 -0
- tescmd/auth/token_store.py +273 -0
- tescmd/ble/__init__.py +0 -0
- tescmd/cache/__init__.py +6 -0
- tescmd/cache/keys.py +51 -0
- tescmd/cache/response_cache.py +213 -0
- tescmd/cli/__init__.py +0 -0
- tescmd/cli/_client.py +603 -0
- tescmd/cli/_options.py +126 -0
- tescmd/cli/auth.py +682 -0
- tescmd/cli/billing.py +240 -0
- tescmd/cli/cache.py +85 -0
- tescmd/cli/charge.py +610 -0
- tescmd/cli/climate.py +501 -0
- tescmd/cli/energy.py +385 -0
- tescmd/cli/key.py +611 -0
- tescmd/cli/main.py +601 -0
- tescmd/cli/media.py +146 -0
- tescmd/cli/nav.py +242 -0
- tescmd/cli/partner.py +112 -0
- tescmd/cli/raw.py +75 -0
- tescmd/cli/security.py +495 -0
- tescmd/cli/setup.py +786 -0
- tescmd/cli/sharing.py +188 -0
- tescmd/cli/software.py +81 -0
- tescmd/cli/status.py +106 -0
- tescmd/cli/trunk.py +240 -0
- tescmd/cli/user.py +145 -0
- tescmd/cli/vehicle.py +837 -0
- tescmd/config/__init__.py +0 -0
- tescmd/crypto/__init__.py +19 -0
- tescmd/crypto/ecdh.py +46 -0
- tescmd/crypto/keys.py +122 -0
- tescmd/deploy/__init__.py +0 -0
- tescmd/deploy/github_pages.py +268 -0
- tescmd/models/__init__.py +85 -0
- tescmd/models/auth.py +108 -0
- tescmd/models/command.py +18 -0
- tescmd/models/config.py +63 -0
- tescmd/models/energy.py +56 -0
- tescmd/models/sharing.py +26 -0
- tescmd/models/user.py +37 -0
- tescmd/models/vehicle.py +185 -0
- tescmd/output/__init__.py +5 -0
- tescmd/output/formatter.py +132 -0
- tescmd/output/json_output.py +83 -0
- tescmd/output/rich_output.py +809 -0
- tescmd/protocol/__init__.py +23 -0
- tescmd/protocol/commands.py +175 -0
- tescmd/protocol/encoder.py +122 -0
- tescmd/protocol/metadata.py +116 -0
- tescmd/protocol/payloads.py +621 -0
- tescmd/protocol/protobuf/__init__.py +6 -0
- tescmd/protocol/protobuf/messages.py +564 -0
- tescmd/protocol/session.py +318 -0
- tescmd/protocol/signer.py +84 -0
- tescmd/py.typed +0 -0
- tescmd-0.1.2.dist-info/METADATA +458 -0
- tescmd-0.1.2.dist-info/RECORD +81 -0
- tescmd-0.1.2.dist-info/WHEEL +4 -0
- tescmd-0.1.2.dist-info/entry_points.txt +2 -0
- tescmd-0.1.2.dist-info/licenses/LICENSE +21 -0
tescmd/cli/charge.py
ADDED
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
"""CLI commands for charging operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from tescmd._internal.async_utils import run_async
|
|
11
|
+
from tescmd.cli._client import (
|
|
12
|
+
auto_wake,
|
|
13
|
+
cached_vehicle_data,
|
|
14
|
+
execute_command,
|
|
15
|
+
get_command_api,
|
|
16
|
+
get_vehicle_api,
|
|
17
|
+
invalidate_cache_for_vin,
|
|
18
|
+
require_vin,
|
|
19
|
+
)
|
|
20
|
+
from tescmd.cli._options import global_options
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from tescmd.cli.main import AppContext
|
|
24
|
+
|
|
25
|
+
charge_group = click.Group("charge", help="Charging commands")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Status (read via VehicleAPI)
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@charge_group.command("status")
|
|
34
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
35
|
+
@global_options
|
|
36
|
+
def status_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
37
|
+
"""Show current charging status."""
|
|
38
|
+
run_async(_cmd_status(app_ctx, vin_positional))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def _cmd_status(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
42
|
+
formatter = app_ctx.formatter
|
|
43
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
44
|
+
client, api = get_vehicle_api(app_ctx)
|
|
45
|
+
try:
|
|
46
|
+
vdata = await cached_vehicle_data(app_ctx, api, vin, endpoints=["charge_state"])
|
|
47
|
+
finally:
|
|
48
|
+
await client.close()
|
|
49
|
+
|
|
50
|
+
if formatter.format == "json":
|
|
51
|
+
cs = vdata.charge_state
|
|
52
|
+
formatter.output(
|
|
53
|
+
cs.model_dump(exclude_none=True) if cs else {},
|
|
54
|
+
command="charge.status",
|
|
55
|
+
)
|
|
56
|
+
else:
|
|
57
|
+
if vdata.charge_state:
|
|
58
|
+
formatter.rich.charge_status(vdata.charge_state)
|
|
59
|
+
else:
|
|
60
|
+
formatter.rich.info("No charge state data available.")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# Simple commands (write via CommandAPI)
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@charge_group.command("start")
|
|
69
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
70
|
+
@global_options
|
|
71
|
+
def start_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
72
|
+
"""Start charging."""
|
|
73
|
+
run_async(
|
|
74
|
+
execute_command(
|
|
75
|
+
app_ctx,
|
|
76
|
+
vin_positional,
|
|
77
|
+
"charge_start",
|
|
78
|
+
"charge.start",
|
|
79
|
+
success_message="Charging started.",
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@charge_group.command("stop")
|
|
85
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
86
|
+
@global_options
|
|
87
|
+
def stop_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
88
|
+
"""Stop charging."""
|
|
89
|
+
run_async(
|
|
90
|
+
execute_command(
|
|
91
|
+
app_ctx,
|
|
92
|
+
vin_positional,
|
|
93
|
+
"charge_stop",
|
|
94
|
+
"charge.stop",
|
|
95
|
+
success_message="Charging stopped.",
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@charge_group.command("limit-max")
|
|
101
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
102
|
+
@global_options
|
|
103
|
+
def limit_max_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
104
|
+
"""Set charge limit to maximum range."""
|
|
105
|
+
run_async(
|
|
106
|
+
execute_command(
|
|
107
|
+
app_ctx,
|
|
108
|
+
vin_positional,
|
|
109
|
+
"charge_max_range",
|
|
110
|
+
"charge.limit-max",
|
|
111
|
+
success_message="Charge limit set to maximum range.",
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@charge_group.command("limit-std")
|
|
117
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
118
|
+
@global_options
|
|
119
|
+
def limit_std_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
120
|
+
"""Set charge limit to standard range."""
|
|
121
|
+
run_async(
|
|
122
|
+
execute_command(
|
|
123
|
+
app_ctx,
|
|
124
|
+
vin_positional,
|
|
125
|
+
"charge_standard",
|
|
126
|
+
"charge.limit-std",
|
|
127
|
+
success_message="Charge limit set to standard range.",
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@charge_group.command("port-open")
|
|
133
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
134
|
+
@global_options
|
|
135
|
+
def port_open_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
136
|
+
"""Open the charge port door."""
|
|
137
|
+
run_async(
|
|
138
|
+
execute_command(
|
|
139
|
+
app_ctx,
|
|
140
|
+
vin_positional,
|
|
141
|
+
"charge_port_door_open",
|
|
142
|
+
"charge.port-open",
|
|
143
|
+
success_message="Charge port opened.",
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@charge_group.command("port-close")
|
|
149
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
150
|
+
@global_options
|
|
151
|
+
def port_close_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
152
|
+
"""Close the charge port door."""
|
|
153
|
+
run_async(
|
|
154
|
+
execute_command(
|
|
155
|
+
app_ctx,
|
|
156
|
+
vin_positional,
|
|
157
|
+
"charge_port_door_close",
|
|
158
|
+
"charge.port-close",
|
|
159
|
+
success_message="Charge port closed.",
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# ---------------------------------------------------------------------------
|
|
165
|
+
# Parameterised commands
|
|
166
|
+
# ---------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@charge_group.command("limit")
|
|
170
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
171
|
+
@click.argument("percent", type=click.IntRange(50, 100))
|
|
172
|
+
@global_options
|
|
173
|
+
def limit_cmd(app_ctx: AppContext, vin_positional: str | None, percent: int) -> None:
|
|
174
|
+
"""Set charge limit to PERCENT (50-100)."""
|
|
175
|
+
run_async(_cmd_limit(app_ctx, vin_positional, percent))
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
async def _cmd_limit(app_ctx: AppContext, vin_positional: str | None, percent: int) -> None:
|
|
179
|
+
formatter = app_ctx.formatter
|
|
180
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
181
|
+
client, vehicle_api, cmd_api = get_command_api(app_ctx)
|
|
182
|
+
try:
|
|
183
|
+
result = await auto_wake(
|
|
184
|
+
formatter,
|
|
185
|
+
vehicle_api,
|
|
186
|
+
vin,
|
|
187
|
+
lambda: cmd_api.set_charge_limit(vin, percent=percent),
|
|
188
|
+
auto=app_ctx.auto_wake,
|
|
189
|
+
)
|
|
190
|
+
finally:
|
|
191
|
+
await client.close()
|
|
192
|
+
|
|
193
|
+
invalidate_cache_for_vin(app_ctx, vin)
|
|
194
|
+
|
|
195
|
+
if formatter.format == "json":
|
|
196
|
+
formatter.output(result, command="charge.limit")
|
|
197
|
+
else:
|
|
198
|
+
msg = result.response.reason or f"Charge limit set to {percent}%."
|
|
199
|
+
formatter.rich.command_result(result.response.result, msg)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@charge_group.command("amps")
|
|
203
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
204
|
+
@click.argument("amps", type=click.IntRange(0, 48))
|
|
205
|
+
@global_options
|
|
206
|
+
def amps_cmd(app_ctx: AppContext, vin_positional: str | None, amps: int) -> None:
|
|
207
|
+
"""Set charging current to AMPS (0-48)."""
|
|
208
|
+
run_async(_cmd_amps(app_ctx, vin_positional, amps))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
async def _cmd_amps(app_ctx: AppContext, vin_positional: str | None, amps: int) -> None:
|
|
212
|
+
formatter = app_ctx.formatter
|
|
213
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
214
|
+
client, vehicle_api, cmd_api = get_command_api(app_ctx)
|
|
215
|
+
try:
|
|
216
|
+
result = await auto_wake(
|
|
217
|
+
formatter,
|
|
218
|
+
vehicle_api,
|
|
219
|
+
vin,
|
|
220
|
+
lambda: cmd_api.set_charging_amps(vin, charging_amps=amps),
|
|
221
|
+
auto=app_ctx.auto_wake,
|
|
222
|
+
)
|
|
223
|
+
finally:
|
|
224
|
+
await client.close()
|
|
225
|
+
|
|
226
|
+
invalidate_cache_for_vin(app_ctx, vin)
|
|
227
|
+
|
|
228
|
+
if formatter.format == "json":
|
|
229
|
+
formatter.output(result, command="charge.amps")
|
|
230
|
+
else:
|
|
231
|
+
msg = result.response.reason or f"Charging current set to {amps}A."
|
|
232
|
+
formatter.rich.command_result(result.response.result, msg)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@charge_group.command("schedule")
|
|
236
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
237
|
+
@click.option("--enable/--disable", default=True, help="Enable or disable scheduled charging")
|
|
238
|
+
@click.option(
|
|
239
|
+
"--time",
|
|
240
|
+
"time_minutes",
|
|
241
|
+
type=int,
|
|
242
|
+
default=0,
|
|
243
|
+
help="Scheduled start time in minutes past midnight",
|
|
244
|
+
)
|
|
245
|
+
@global_options
|
|
246
|
+
def schedule_cmd(
|
|
247
|
+
app_ctx: AppContext,
|
|
248
|
+
vin_positional: str | None,
|
|
249
|
+
enable: bool,
|
|
250
|
+
time_minutes: int,
|
|
251
|
+
) -> None:
|
|
252
|
+
"""Configure scheduled charging."""
|
|
253
|
+
run_async(_cmd_schedule(app_ctx, vin_positional, enable, time_minutes))
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
async def _cmd_schedule(
|
|
257
|
+
app_ctx: AppContext,
|
|
258
|
+
vin_positional: str | None,
|
|
259
|
+
enable: bool,
|
|
260
|
+
time_minutes: int,
|
|
261
|
+
) -> None:
|
|
262
|
+
formatter = app_ctx.formatter
|
|
263
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
264
|
+
client, vehicle_api, cmd_api = get_command_api(app_ctx)
|
|
265
|
+
try:
|
|
266
|
+
result = await auto_wake(
|
|
267
|
+
formatter,
|
|
268
|
+
vehicle_api,
|
|
269
|
+
vin,
|
|
270
|
+
lambda: cmd_api.set_scheduled_charging(vin, enable=enable, time=time_minutes),
|
|
271
|
+
auto=app_ctx.auto_wake,
|
|
272
|
+
)
|
|
273
|
+
finally:
|
|
274
|
+
await client.close()
|
|
275
|
+
|
|
276
|
+
invalidate_cache_for_vin(app_ctx, vin)
|
|
277
|
+
|
|
278
|
+
if formatter.format == "json":
|
|
279
|
+
formatter.output(result, command="charge.schedule")
|
|
280
|
+
else:
|
|
281
|
+
state = "enabled" if enable else "disabled"
|
|
282
|
+
msg = result.response.reason or f"Scheduled charging {state}."
|
|
283
|
+
formatter.rich.command_result(result.response.result, msg)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# ---------------------------------------------------------------------------
|
|
287
|
+
# Scheduled departure / precondition schedules
|
|
288
|
+
# ---------------------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@charge_group.command("departure")
|
|
292
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
293
|
+
@click.option(
|
|
294
|
+
"--time", "departure_time", type=int, required=True, help="Departure time (mins past midnight)"
|
|
295
|
+
)
|
|
296
|
+
@click.option("--on/--off", "enable", default=True, help="Enable or disable scheduled departure")
|
|
297
|
+
@click.option("--precondition", is_flag=True, default=False, help="Enable preconditioning")
|
|
298
|
+
@click.option(
|
|
299
|
+
"--precondition-weekdays", is_flag=True, default=False, help="Preconditioning weekdays only"
|
|
300
|
+
)
|
|
301
|
+
@click.option("--off-peak", is_flag=True, default=False, help="Enable off-peak charging")
|
|
302
|
+
@click.option("--off-peak-weekdays", is_flag=True, default=False, help="Off-peak weekdays only")
|
|
303
|
+
@click.option("--off-peak-end", type=int, default=0, help="End off-peak time (mins past midnight)")
|
|
304
|
+
@global_options
|
|
305
|
+
def departure_cmd(
|
|
306
|
+
app_ctx: AppContext,
|
|
307
|
+
vin_positional: str | None,
|
|
308
|
+
departure_time: int,
|
|
309
|
+
enable: bool,
|
|
310
|
+
precondition: bool,
|
|
311
|
+
precondition_weekdays: bool,
|
|
312
|
+
off_peak: bool,
|
|
313
|
+
off_peak_weekdays: bool,
|
|
314
|
+
off_peak_end: int,
|
|
315
|
+
) -> None:
|
|
316
|
+
"""Configure scheduled departure."""
|
|
317
|
+
run_async(
|
|
318
|
+
_cmd_departure(
|
|
319
|
+
app_ctx,
|
|
320
|
+
vin_positional,
|
|
321
|
+
enable,
|
|
322
|
+
departure_time,
|
|
323
|
+
precondition,
|
|
324
|
+
precondition_weekdays,
|
|
325
|
+
off_peak,
|
|
326
|
+
off_peak_weekdays,
|
|
327
|
+
off_peak_end,
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
async def _cmd_departure(
|
|
333
|
+
app_ctx: AppContext,
|
|
334
|
+
vin_positional: str | None,
|
|
335
|
+
enable: bool,
|
|
336
|
+
departure_time: int,
|
|
337
|
+
precondition: bool,
|
|
338
|
+
precondition_weekdays: bool,
|
|
339
|
+
off_peak: bool,
|
|
340
|
+
off_peak_weekdays: bool,
|
|
341
|
+
off_peak_end: int,
|
|
342
|
+
) -> None:
|
|
343
|
+
formatter = app_ctx.formatter
|
|
344
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
345
|
+
client, vehicle_api, cmd_api = get_command_api(app_ctx)
|
|
346
|
+
try:
|
|
347
|
+
result = await auto_wake(
|
|
348
|
+
formatter,
|
|
349
|
+
vehicle_api,
|
|
350
|
+
vin,
|
|
351
|
+
lambda: cmd_api.set_scheduled_departure(
|
|
352
|
+
vin,
|
|
353
|
+
enable=enable,
|
|
354
|
+
departure_time=departure_time,
|
|
355
|
+
preconditioning_enabled=precondition,
|
|
356
|
+
preconditioning_weekdays_only=precondition_weekdays,
|
|
357
|
+
off_peak_charging_enabled=off_peak,
|
|
358
|
+
off_peak_charging_weekdays_only=off_peak_weekdays,
|
|
359
|
+
end_off_peak_time=off_peak_end,
|
|
360
|
+
),
|
|
361
|
+
auto=app_ctx.auto_wake,
|
|
362
|
+
)
|
|
363
|
+
finally:
|
|
364
|
+
await client.close()
|
|
365
|
+
|
|
366
|
+
invalidate_cache_for_vin(app_ctx, vin)
|
|
367
|
+
|
|
368
|
+
if formatter.format == "json":
|
|
369
|
+
formatter.output(result, command="charge.departure")
|
|
370
|
+
else:
|
|
371
|
+
state = "enabled" if enable else "disabled"
|
|
372
|
+
msg = result.response.reason or f"Scheduled departure {state}."
|
|
373
|
+
formatter.rich.command_result(result.response.result, msg)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
@charge_group.command("precondition-add")
|
|
377
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
378
|
+
@click.argument("schedule_json")
|
|
379
|
+
@global_options
|
|
380
|
+
def precondition_add_cmd(
|
|
381
|
+
app_ctx: AppContext, vin_positional: str | None, schedule_json: str
|
|
382
|
+
) -> None:
|
|
383
|
+
"""Add a precondition schedule (SCHEDULE_JSON is a JSON string)."""
|
|
384
|
+
from tescmd.api.errors import ConfigError
|
|
385
|
+
|
|
386
|
+
try:
|
|
387
|
+
schedule = json.loads(schedule_json)
|
|
388
|
+
except json.JSONDecodeError as e:
|
|
389
|
+
raise ConfigError(f"Invalid JSON in SCHEDULE_JSON: {e}") from e
|
|
390
|
+
run_async(
|
|
391
|
+
execute_command(
|
|
392
|
+
app_ctx,
|
|
393
|
+
vin_positional,
|
|
394
|
+
"add_precondition_schedule",
|
|
395
|
+
"charge.precondition-add",
|
|
396
|
+
body=schedule,
|
|
397
|
+
success_message="Precondition schedule added.",
|
|
398
|
+
)
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
@charge_group.command("precondition-remove")
|
|
403
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
404
|
+
@click.argument("schedule_id", type=int)
|
|
405
|
+
@global_options
|
|
406
|
+
def precondition_remove_cmd(
|
|
407
|
+
app_ctx: AppContext, vin_positional: str | None, schedule_id: int
|
|
408
|
+
) -> None:
|
|
409
|
+
"""Remove a precondition schedule by ID."""
|
|
410
|
+
run_async(
|
|
411
|
+
execute_command(
|
|
412
|
+
app_ctx,
|
|
413
|
+
vin_positional,
|
|
414
|
+
"remove_precondition_schedule",
|
|
415
|
+
"charge.precondition-remove",
|
|
416
|
+
body={"id": schedule_id},
|
|
417
|
+
success_message="Precondition schedule removed.",
|
|
418
|
+
)
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
# ---------------------------------------------------------------------------
|
|
423
|
+
# Charge schedule management (firmware 2024.26+)
|
|
424
|
+
# ---------------------------------------------------------------------------
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
@charge_group.command("add-schedule")
|
|
428
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
429
|
+
@click.option("--id", "schedule_id", type=int, required=True, help="Schedule ID")
|
|
430
|
+
@click.option("--name", default=None, help="Schedule name")
|
|
431
|
+
@click.option(
|
|
432
|
+
"--days-of-week",
|
|
433
|
+
default=None,
|
|
434
|
+
help="Days of week bitmask (e.g. 127 for all days)",
|
|
435
|
+
)
|
|
436
|
+
@click.option("--start-time", type=int, default=None, help="Start time (minutes past midnight)")
|
|
437
|
+
@click.option("--end-time", type=int, default=None, help="End time (minutes past midnight)")
|
|
438
|
+
@click.option("--enabled/--disabled", default=True, help="Enable or disable the schedule")
|
|
439
|
+
@click.option("--one-time", is_flag=True, default=False, help="One-time schedule")
|
|
440
|
+
@global_options
|
|
441
|
+
def add_schedule_cmd(
|
|
442
|
+
app_ctx: AppContext,
|
|
443
|
+
vin_positional: str | None,
|
|
444
|
+
schedule_id: int,
|
|
445
|
+
name: str | None,
|
|
446
|
+
days_of_week: str | None,
|
|
447
|
+
start_time: int | None,
|
|
448
|
+
end_time: int | None,
|
|
449
|
+
enabled: bool,
|
|
450
|
+
one_time: bool,
|
|
451
|
+
) -> None:
|
|
452
|
+
"""Add or update a charge schedule (firmware 2024.26+)."""
|
|
453
|
+
schedule: dict[str, object] = {"id": schedule_id, "enabled": enabled}
|
|
454
|
+
if name is not None:
|
|
455
|
+
schedule["name"] = name
|
|
456
|
+
if days_of_week is not None:
|
|
457
|
+
schedule["days_of_week"] = int(days_of_week)
|
|
458
|
+
if start_time is not None:
|
|
459
|
+
schedule["start_time"] = start_time
|
|
460
|
+
if end_time is not None:
|
|
461
|
+
schedule["end_time"] = end_time
|
|
462
|
+
if one_time:
|
|
463
|
+
schedule["one_time"] = True
|
|
464
|
+
run_async(
|
|
465
|
+
execute_command(
|
|
466
|
+
app_ctx,
|
|
467
|
+
vin_positional,
|
|
468
|
+
"add_charge_schedule",
|
|
469
|
+
"charge.add-schedule",
|
|
470
|
+
body=schedule,
|
|
471
|
+
success_message="Charge schedule updated.",
|
|
472
|
+
)
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
@charge_group.command("remove-schedule")
|
|
477
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
478
|
+
@click.option("--id", "schedule_id", type=int, required=True, help="Schedule ID to remove")
|
|
479
|
+
@global_options
|
|
480
|
+
def remove_schedule_cmd(app_ctx: AppContext, vin_positional: str | None, schedule_id: int) -> None:
|
|
481
|
+
"""Remove a charge schedule (firmware 2024.26+)."""
|
|
482
|
+
run_async(
|
|
483
|
+
execute_command(
|
|
484
|
+
app_ctx,
|
|
485
|
+
vin_positional,
|
|
486
|
+
"remove_charge_schedule",
|
|
487
|
+
"charge.remove-schedule",
|
|
488
|
+
body={"id": schedule_id},
|
|
489
|
+
success_message="Charge schedule removed.",
|
|
490
|
+
)
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
@charge_group.command("clear-schedules")
|
|
495
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
496
|
+
@click.option("--home/--no-home", default=True, help="Remove home location schedules")
|
|
497
|
+
@click.option("--work/--no-work", default=True, help="Remove work location schedules")
|
|
498
|
+
@click.option("--other/--no-other", default=True, help="Remove other location schedules")
|
|
499
|
+
@global_options
|
|
500
|
+
def clear_schedules_cmd(
|
|
501
|
+
app_ctx: AppContext,
|
|
502
|
+
vin_positional: str | None,
|
|
503
|
+
home: bool,
|
|
504
|
+
work: bool,
|
|
505
|
+
other: bool,
|
|
506
|
+
) -> None:
|
|
507
|
+
"""Batch remove charge schedules by location type."""
|
|
508
|
+
run_async(
|
|
509
|
+
execute_command(
|
|
510
|
+
app_ctx,
|
|
511
|
+
vin_positional,
|
|
512
|
+
"batch_remove_charge_schedules",
|
|
513
|
+
"charge.clear-schedules",
|
|
514
|
+
body={"home": home, "work": work, "other": other},
|
|
515
|
+
success_message="Charge schedules removed.",
|
|
516
|
+
)
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
@charge_group.command("clear-preconditions")
|
|
521
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
522
|
+
@click.option("--home/--no-home", default=True, help="Remove home location schedules")
|
|
523
|
+
@click.option("--work/--no-work", default=True, help="Remove work location schedules")
|
|
524
|
+
@click.option("--other/--no-other", default=True, help="Remove other location schedules")
|
|
525
|
+
@global_options
|
|
526
|
+
def clear_preconditions_cmd(
|
|
527
|
+
app_ctx: AppContext,
|
|
528
|
+
vin_positional: str | None,
|
|
529
|
+
home: bool,
|
|
530
|
+
work: bool,
|
|
531
|
+
other: bool,
|
|
532
|
+
) -> None:
|
|
533
|
+
"""Batch remove precondition schedules by location type."""
|
|
534
|
+
run_async(
|
|
535
|
+
execute_command(
|
|
536
|
+
app_ctx,
|
|
537
|
+
vin_positional,
|
|
538
|
+
"batch_remove_precondition_schedules",
|
|
539
|
+
"charge.clear-preconditions",
|
|
540
|
+
body={"home": home, "work": work, "other": other},
|
|
541
|
+
success_message="Precondition schedules removed.",
|
|
542
|
+
)
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
# ---------------------------------------------------------------------------
|
|
547
|
+
# Managed charging (fleet)
|
|
548
|
+
# ---------------------------------------------------------------------------
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
@charge_group.command("managed-amps")
|
|
552
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
553
|
+
@click.argument("amps", type=int)
|
|
554
|
+
@global_options
|
|
555
|
+
def managed_amps_cmd(app_ctx: AppContext, vin_positional: str | None, amps: int) -> None:
|
|
556
|
+
"""Set managed charging current in amps (fleet management)."""
|
|
557
|
+
run_async(
|
|
558
|
+
execute_command(
|
|
559
|
+
app_ctx,
|
|
560
|
+
vin_positional,
|
|
561
|
+
"set_managed_charge_current_request",
|
|
562
|
+
"charge.managed-amps",
|
|
563
|
+
body={"charging_amps": amps},
|
|
564
|
+
success_message=f"Managed charging current set to {amps}A.",
|
|
565
|
+
)
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
@charge_group.command("managed-location")
|
|
570
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
571
|
+
@click.option("--lat", type=float, required=True, help="Latitude")
|
|
572
|
+
@click.option("--lon", type=float, required=True, help="Longitude")
|
|
573
|
+
@global_options
|
|
574
|
+
def managed_location_cmd(
|
|
575
|
+
app_ctx: AppContext, vin_positional: str | None, lat: float, lon: float
|
|
576
|
+
) -> None:
|
|
577
|
+
"""Set managed charger location (fleet management)."""
|
|
578
|
+
run_async(
|
|
579
|
+
execute_command(
|
|
580
|
+
app_ctx,
|
|
581
|
+
vin_positional,
|
|
582
|
+
"set_managed_charger_location",
|
|
583
|
+
"charge.managed-location",
|
|
584
|
+
body={"location": {"lat": lat, "lon": lon}},
|
|
585
|
+
success_message="Managed charger location set.",
|
|
586
|
+
)
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
@charge_group.command("managed-schedule")
|
|
591
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
592
|
+
@click.argument("time_minutes", type=click.IntRange(0, 1440))
|
|
593
|
+
@global_options
|
|
594
|
+
def managed_schedule_cmd(
|
|
595
|
+
app_ctx: AppContext, vin_positional: str | None, time_minutes: int
|
|
596
|
+
) -> None:
|
|
597
|
+
"""Set managed scheduled charging time (fleet management).
|
|
598
|
+
|
|
599
|
+
TIME_MINUTES is minutes past midnight (0-1440).
|
|
600
|
+
"""
|
|
601
|
+
run_async(
|
|
602
|
+
execute_command(
|
|
603
|
+
app_ctx,
|
|
604
|
+
vin_positional,
|
|
605
|
+
"set_managed_scheduled_charging_time",
|
|
606
|
+
"charge.managed-schedule",
|
|
607
|
+
body={"time": time_minutes},
|
|
608
|
+
success_message=f"Managed scheduled charging time set to {time_minutes} min.",
|
|
609
|
+
)
|
|
610
|
+
)
|