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.
Files changed (81) hide show
  1. tescmd/__init__.py +3 -0
  2. tescmd/__main__.py +5 -0
  3. tescmd/_internal/__init__.py +0 -0
  4. tescmd/_internal/async_utils.py +25 -0
  5. tescmd/_internal/permissions.py +43 -0
  6. tescmd/_internal/vin.py +44 -0
  7. tescmd/api/__init__.py +1 -0
  8. tescmd/api/charging.py +102 -0
  9. tescmd/api/client.py +189 -0
  10. tescmd/api/command.py +540 -0
  11. tescmd/api/energy.py +146 -0
  12. tescmd/api/errors.py +76 -0
  13. tescmd/api/partner.py +40 -0
  14. tescmd/api/sharing.py +65 -0
  15. tescmd/api/signed_command.py +277 -0
  16. tescmd/api/user.py +38 -0
  17. tescmd/api/vehicle.py +150 -0
  18. tescmd/auth/__init__.py +1 -0
  19. tescmd/auth/oauth.py +312 -0
  20. tescmd/auth/server.py +108 -0
  21. tescmd/auth/token_store.py +273 -0
  22. tescmd/ble/__init__.py +0 -0
  23. tescmd/cache/__init__.py +6 -0
  24. tescmd/cache/keys.py +51 -0
  25. tescmd/cache/response_cache.py +213 -0
  26. tescmd/cli/__init__.py +0 -0
  27. tescmd/cli/_client.py +603 -0
  28. tescmd/cli/_options.py +126 -0
  29. tescmd/cli/auth.py +682 -0
  30. tescmd/cli/billing.py +240 -0
  31. tescmd/cli/cache.py +85 -0
  32. tescmd/cli/charge.py +610 -0
  33. tescmd/cli/climate.py +501 -0
  34. tescmd/cli/energy.py +385 -0
  35. tescmd/cli/key.py +611 -0
  36. tescmd/cli/main.py +601 -0
  37. tescmd/cli/media.py +146 -0
  38. tescmd/cli/nav.py +242 -0
  39. tescmd/cli/partner.py +112 -0
  40. tescmd/cli/raw.py +75 -0
  41. tescmd/cli/security.py +495 -0
  42. tescmd/cli/setup.py +786 -0
  43. tescmd/cli/sharing.py +188 -0
  44. tescmd/cli/software.py +81 -0
  45. tescmd/cli/status.py +106 -0
  46. tescmd/cli/trunk.py +240 -0
  47. tescmd/cli/user.py +145 -0
  48. tescmd/cli/vehicle.py +837 -0
  49. tescmd/config/__init__.py +0 -0
  50. tescmd/crypto/__init__.py +19 -0
  51. tescmd/crypto/ecdh.py +46 -0
  52. tescmd/crypto/keys.py +122 -0
  53. tescmd/deploy/__init__.py +0 -0
  54. tescmd/deploy/github_pages.py +268 -0
  55. tescmd/models/__init__.py +85 -0
  56. tescmd/models/auth.py +108 -0
  57. tescmd/models/command.py +18 -0
  58. tescmd/models/config.py +63 -0
  59. tescmd/models/energy.py +56 -0
  60. tescmd/models/sharing.py +26 -0
  61. tescmd/models/user.py +37 -0
  62. tescmd/models/vehicle.py +185 -0
  63. tescmd/output/__init__.py +5 -0
  64. tescmd/output/formatter.py +132 -0
  65. tescmd/output/json_output.py +83 -0
  66. tescmd/output/rich_output.py +809 -0
  67. tescmd/protocol/__init__.py +23 -0
  68. tescmd/protocol/commands.py +175 -0
  69. tescmd/protocol/encoder.py +122 -0
  70. tescmd/protocol/metadata.py +116 -0
  71. tescmd/protocol/payloads.py +621 -0
  72. tescmd/protocol/protobuf/__init__.py +6 -0
  73. tescmd/protocol/protobuf/messages.py +564 -0
  74. tescmd/protocol/session.py +318 -0
  75. tescmd/protocol/signer.py +84 -0
  76. tescmd/py.typed +0 -0
  77. tescmd-0.1.2.dist-info/METADATA +458 -0
  78. tescmd-0.1.2.dist-info/RECORD +81 -0
  79. tescmd-0.1.2.dist-info/WHEEL +4 -0
  80. tescmd-0.1.2.dist-info/entry_points.txt +2 -0
  81. tescmd-0.1.2.dist-info/licenses/LICENSE +21 -0
tescmd/cli/climate.py ADDED
@@ -0,0 +1,501 @@
1
+ """CLI commands for climate operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ import click
8
+
9
+ from tescmd._internal.async_utils import run_async
10
+ from tescmd.cli._client import (
11
+ auto_wake,
12
+ cached_vehicle_data,
13
+ execute_command,
14
+ get_command_api,
15
+ get_vehicle_api,
16
+ invalidate_cache_for_vin,
17
+ require_vin,
18
+ )
19
+ from tescmd.cli._options import global_options
20
+
21
+ if TYPE_CHECKING:
22
+ from tescmd.cli.main import AppContext
23
+
24
+ climate_group = click.Group("climate", help="Climate and comfort commands")
25
+
26
+ # Tesla's internal seat position indices
27
+ SEAT_MAP: dict[str, int] = {
28
+ "driver": 0,
29
+ "passenger": 1,
30
+ "rear-left": 2,
31
+ "rear-center": 4,
32
+ "rear-right": 5,
33
+ }
34
+
35
+ # Climate keeper mode string → integer
36
+ KEEPER_MODE_MAP: dict[str, int] = {
37
+ "off": 0,
38
+ "on": 1,
39
+ "dog": 2,
40
+ "camp": 3,
41
+ }
42
+
43
+
44
+ def _f_to_c(fahrenheit: float) -> float:
45
+ """Convert Fahrenheit to Celsius."""
46
+ return (fahrenheit - 32.0) * 5.0 / 9.0
47
+
48
+
49
+ # ---------------------------------------------------------------------------
50
+ # Status (read via VehicleAPI)
51
+ # ---------------------------------------------------------------------------
52
+
53
+
54
+ @climate_group.command("status")
55
+ @click.argument("vin_positional", required=False, default=None, metavar="VIN")
56
+ @global_options
57
+ def status_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
58
+ """Show current climate status."""
59
+ run_async(_cmd_status(app_ctx, vin_positional))
60
+
61
+
62
+ async def _cmd_status(app_ctx: AppContext, vin_positional: str | None) -> None:
63
+ formatter = app_ctx.formatter
64
+ vin = require_vin(vin_positional, app_ctx.vin)
65
+ client, api = get_vehicle_api(app_ctx)
66
+ try:
67
+ vdata = await cached_vehicle_data(app_ctx, api, vin, endpoints=["climate_state"])
68
+ finally:
69
+ await client.close()
70
+
71
+ if formatter.format == "json":
72
+ cs = vdata.climate_state
73
+ formatter.output(
74
+ cs.model_dump(exclude_none=True) if cs else {},
75
+ command="climate.status",
76
+ )
77
+ else:
78
+ if vdata.climate_state:
79
+ formatter.rich.climate_status(vdata.climate_state)
80
+ else:
81
+ formatter.rich.info("No climate state data available.")
82
+
83
+
84
+ # ---------------------------------------------------------------------------
85
+ # Simple on/off commands
86
+ # ---------------------------------------------------------------------------
87
+
88
+
89
+ @climate_group.command("on")
90
+ @click.argument("vin_positional", required=False, default=None, metavar="VIN")
91
+ @global_options
92
+ def on_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
93
+ """Turn on climate control (auto conditioning)."""
94
+ run_async(
95
+ execute_command(
96
+ app_ctx,
97
+ vin_positional,
98
+ "auto_conditioning_start",
99
+ "climate.on",
100
+ success_message="Climate control turned on.",
101
+ )
102
+ )
103
+
104
+
105
+ @climate_group.command("off")
106
+ @click.argument("vin_positional", required=False, default=None, metavar="VIN")
107
+ @global_options
108
+ def off_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
109
+ """Turn off climate control (auto conditioning)."""
110
+ run_async(
111
+ execute_command(
112
+ app_ctx,
113
+ vin_positional,
114
+ "auto_conditioning_stop",
115
+ "climate.off",
116
+ success_message="Climate control turned off.",
117
+ )
118
+ )
119
+
120
+
121
+ # ---------------------------------------------------------------------------
122
+ # Parameterised commands
123
+ # ---------------------------------------------------------------------------
124
+
125
+
126
+ @climate_group.command("set")
127
+ @click.argument("vin_positional", required=False, default=None, metavar="VIN")
128
+ @click.argument("temp", type=float)
129
+ @click.option("--passenger", type=float, default=None, help="Separate passenger temperature")
130
+ @click.option("--celsius", is_flag=True, default=False, help="Input temperature is in Celsius")
131
+ @global_options
132
+ def set_cmd(
133
+ app_ctx: AppContext,
134
+ vin_positional: str | None,
135
+ temp: float,
136
+ passenger: float | None,
137
+ celsius: bool,
138
+ ) -> None:
139
+ """Set cabin temperature to TEMP (default: Fahrenheit)."""
140
+ run_async(_cmd_set(app_ctx, vin_positional, temp, passenger, celsius))
141
+
142
+
143
+ async def _cmd_set(
144
+ app_ctx: AppContext,
145
+ vin_positional: str | None,
146
+ temp: float,
147
+ passenger: float | None,
148
+ celsius: bool,
149
+ ) -> None:
150
+ formatter = app_ctx.formatter
151
+ vin = require_vin(vin_positional, app_ctx.vin)
152
+
153
+ driver_c = temp if celsius else _f_to_c(temp)
154
+ passenger_c = driver_c
155
+ if passenger is not None:
156
+ passenger_c = passenger if celsius else _f_to_c(passenger)
157
+
158
+ client, vehicle_api, cmd_api = get_command_api(app_ctx)
159
+ try:
160
+ result = await auto_wake(
161
+ formatter,
162
+ vehicle_api,
163
+ vin,
164
+ lambda: cmd_api.set_temps(vin, driver_temp=driver_c, passenger_temp=passenger_c),
165
+ auto=app_ctx.auto_wake,
166
+ )
167
+ finally:
168
+ await client.close()
169
+
170
+ invalidate_cache_for_vin(app_ctx, vin)
171
+
172
+ if formatter.format == "json":
173
+ formatter.output(result, command="climate.set")
174
+ else:
175
+ msg = result.response.reason or "Temperature set."
176
+ formatter.rich.command_result(result.response.result, msg)
177
+
178
+
179
+ @climate_group.command("precondition")
180
+ @click.argument("vin_positional", required=False, default=None, metavar="VIN")
181
+ @click.option("--on/--off", default=True, help="Enable or disable max preconditioning")
182
+ @global_options
183
+ def precondition_cmd(app_ctx: AppContext, vin_positional: str | None, on: bool) -> None:
184
+ """Enable or disable max preconditioning."""
185
+ run_async(_cmd_precondition(app_ctx, vin_positional, on))
186
+
187
+
188
+ async def _cmd_precondition(app_ctx: AppContext, vin_positional: str | None, on: bool) -> None:
189
+ formatter = app_ctx.formatter
190
+ vin = require_vin(vin_positional, app_ctx.vin)
191
+ client, vehicle_api, cmd_api = get_command_api(app_ctx)
192
+ try:
193
+ result = await auto_wake(
194
+ formatter,
195
+ vehicle_api,
196
+ vin,
197
+ lambda: cmd_api.set_preconditioning_max(vin, on=on),
198
+ auto=app_ctx.auto_wake,
199
+ )
200
+ finally:
201
+ await client.close()
202
+
203
+ invalidate_cache_for_vin(app_ctx, vin)
204
+
205
+ if formatter.format == "json":
206
+ formatter.output(result, command="climate.precondition")
207
+ else:
208
+ state = "enabled" if on else "disabled"
209
+ msg = result.response.reason or f"Max preconditioning {state}."
210
+ formatter.rich.command_result(result.response.result, msg)
211
+
212
+
213
+ @climate_group.command("seat")
214
+ @click.argument("vin_positional", required=False, default=None, metavar="VIN")
215
+ @click.argument("seat", type=click.Choice(list(SEAT_MAP)))
216
+ @click.argument("level", type=click.IntRange(0, 3))
217
+ @global_options
218
+ def seat_cmd(app_ctx: AppContext, vin_positional: str | None, seat: str, level: int) -> None:
219
+ """Set seat heater for SEAT to LEVEL (0-3)."""
220
+ run_async(_cmd_seat(app_ctx, vin_positional, seat, level))
221
+
222
+
223
+ async def _cmd_seat(
224
+ app_ctx: AppContext,
225
+ vin_positional: str | None,
226
+ seat: str,
227
+ level: int,
228
+ ) -> None:
229
+ formatter = app_ctx.formatter
230
+ vin = require_vin(vin_positional, app_ctx.vin)
231
+ client, vehicle_api, cmd_api = get_command_api(app_ctx)
232
+ try:
233
+ result = await auto_wake(
234
+ formatter,
235
+ vehicle_api,
236
+ vin,
237
+ lambda: cmd_api.remote_seat_heater_request(
238
+ vin, seat_position=SEAT_MAP[seat], level=level
239
+ ),
240
+ auto=app_ctx.auto_wake,
241
+ )
242
+ finally:
243
+ await client.close()
244
+
245
+ invalidate_cache_for_vin(app_ctx, vin)
246
+
247
+ if formatter.format == "json":
248
+ formatter.output(result, command="climate.seat")
249
+ else:
250
+ msg = result.response.reason or f"Seat heater set for {seat} (level {level})."
251
+ formatter.rich.command_result(result.response.result, msg)
252
+
253
+
254
+ @climate_group.command("seat-cool")
255
+ @click.argument("vin_positional", required=False, default=None, metavar="VIN")
256
+ @click.argument("seat", type=click.Choice(list(SEAT_MAP)))
257
+ @click.argument("level", type=click.IntRange(0, 3))
258
+ @global_options
259
+ def seat_cool_cmd(app_ctx: AppContext, vin_positional: str | None, seat: str, level: int) -> None:
260
+ """Set seat cooler for SEAT to LEVEL (0-3)."""
261
+ run_async(_cmd_seat_cool(app_ctx, vin_positional, seat, level))
262
+
263
+
264
+ async def _cmd_seat_cool(
265
+ app_ctx: AppContext,
266
+ vin_positional: str | None,
267
+ seat: str,
268
+ level: int,
269
+ ) -> None:
270
+ formatter = app_ctx.formatter
271
+ vin = require_vin(vin_positional, app_ctx.vin)
272
+ client, vehicle_api, cmd_api = get_command_api(app_ctx)
273
+ try:
274
+ result = await auto_wake(
275
+ formatter,
276
+ vehicle_api,
277
+ vin,
278
+ lambda: cmd_api.remote_seat_cooler_request(
279
+ vin, seat_position=SEAT_MAP[seat], seat_cooler_level=level
280
+ ),
281
+ auto=app_ctx.auto_wake,
282
+ )
283
+ finally:
284
+ await client.close()
285
+
286
+ invalidate_cache_for_vin(app_ctx, vin)
287
+
288
+ if formatter.format == "json":
289
+ formatter.output(result, command="climate.seat-cool")
290
+ else:
291
+ msg = result.response.reason or f"Seat cooler set for {seat} (level {level})."
292
+ formatter.rich.command_result(result.response.result, msg)
293
+
294
+
295
+ @climate_group.command("wheel-heater")
296
+ @click.argument("vin_positional", required=False, default=None, metavar="VIN")
297
+ @click.option("--on/--off", default=True, help="Enable or disable steering wheel heater")
298
+ @global_options
299
+ def wheel_heater_cmd(app_ctx: AppContext, vin_positional: str | None, on: bool) -> None:
300
+ """Enable or disable steering wheel heater."""
301
+ run_async(_cmd_wheel_heater(app_ctx, vin_positional, on))
302
+
303
+
304
+ async def _cmd_wheel_heater(app_ctx: AppContext, vin_positional: str | None, on: bool) -> None:
305
+ formatter = app_ctx.formatter
306
+ vin = require_vin(vin_positional, app_ctx.vin)
307
+ client, vehicle_api, cmd_api = get_command_api(app_ctx)
308
+ try:
309
+ result = await auto_wake(
310
+ formatter,
311
+ vehicle_api,
312
+ vin,
313
+ lambda: cmd_api.remote_steering_wheel_heater_request(vin, on=on),
314
+ auto=app_ctx.auto_wake,
315
+ )
316
+ finally:
317
+ await client.close()
318
+
319
+ invalidate_cache_for_vin(app_ctx, vin)
320
+
321
+ if formatter.format == "json":
322
+ formatter.output(result, command="climate.wheel-heater")
323
+ else:
324
+ state = "enabled" if on else "disabled"
325
+ msg = result.response.reason or f"Steering wheel heater {state}."
326
+ formatter.rich.command_result(result.response.result, msg)
327
+
328
+
329
+ @climate_group.command("overheat")
330
+ @click.argument("vin_positional", required=False, default=None, metavar="VIN")
331
+ @click.option("--on/--off", default=True, help="Enable or disable cabin overheat protection")
332
+ @click.option("--fan-only", is_flag=True, default=False, help="Use fan only (no AC)")
333
+ @global_options
334
+ def overheat_cmd(
335
+ app_ctx: AppContext, vin_positional: str | None, on: bool, fan_only: bool
336
+ ) -> None:
337
+ """Configure cabin overheat protection."""
338
+ run_async(_cmd_overheat(app_ctx, vin_positional, on, fan_only))
339
+
340
+
341
+ async def _cmd_overheat(
342
+ app_ctx: AppContext,
343
+ vin_positional: str | None,
344
+ on: bool,
345
+ fan_only: bool,
346
+ ) -> None:
347
+ formatter = app_ctx.formatter
348
+ vin = require_vin(vin_positional, app_ctx.vin)
349
+ client, vehicle_api, cmd_api = get_command_api(app_ctx)
350
+ try:
351
+ result = await auto_wake(
352
+ formatter,
353
+ vehicle_api,
354
+ vin,
355
+ lambda: cmd_api.set_cabin_overheat_protection(vin, on=on, fan_only=fan_only),
356
+ auto=app_ctx.auto_wake,
357
+ )
358
+ finally:
359
+ await client.close()
360
+
361
+ invalidate_cache_for_vin(app_ctx, vin)
362
+
363
+ if formatter.format == "json":
364
+ formatter.output(result, command="climate.overheat")
365
+ else:
366
+ state = "enabled" if on else "disabled"
367
+ msg = result.response.reason or f"Cabin overheat protection {state}."
368
+ formatter.rich.command_result(result.response.result, msg)
369
+
370
+
371
+ @climate_group.command("bioweapon")
372
+ @click.argument("vin_positional", required=False, default=None, metavar="VIN")
373
+ @click.option("--on/--off", default=True, help="Enable or disable bioweapon defense mode")
374
+ @click.option("--manual-override", is_flag=True, default=False, help="Force manual override")
375
+ @global_options
376
+ def bioweapon_cmd(
377
+ app_ctx: AppContext, vin_positional: str | None, on: bool, manual_override: bool
378
+ ) -> None:
379
+ """Enable or disable bioweapon defense mode."""
380
+ state = "enabled" if on else "disabled"
381
+ run_async(
382
+ execute_command(
383
+ app_ctx,
384
+ vin_positional,
385
+ "set_bioweapon_mode",
386
+ "climate.bioweapon",
387
+ body={"on": on, "manual_override": manual_override},
388
+ success_message=f"Bioweapon defense mode {state}.",
389
+ )
390
+ )
391
+
392
+
393
+ @climate_group.command("cop-temp")
394
+ @click.argument("vin_positional", required=False, default=None, metavar="VIN")
395
+ @click.argument("level", type=click.IntRange(0, 2))
396
+ @global_options
397
+ def cop_temp_cmd(app_ctx: AppContext, vin_positional: str | None, level: int) -> None:
398
+ """Set cabin overheat protection temperature (0=low, 1=medium, 2=high)."""
399
+ run_async(
400
+ execute_command(
401
+ app_ctx,
402
+ vin_positional,
403
+ "set_cop_temp",
404
+ "climate.cop-temp",
405
+ body={"cop_temp": level},
406
+ success_message="Overheat protection temperature set.",
407
+ )
408
+ )
409
+
410
+
411
+ @climate_group.command("auto-seat")
412
+ @click.argument("vin_positional", required=False, default=None, metavar="VIN")
413
+ @click.argument("seat", type=click.Choice(list(SEAT_MAP)))
414
+ @click.option("--on/--off", default=True, help="Enable or disable auto seat climate")
415
+ @global_options
416
+ def auto_seat_cmd(app_ctx: AppContext, vin_positional: str | None, seat: str, on: bool) -> None:
417
+ """Enable or disable auto seat climate for SEAT."""
418
+ state = "enabled" if on else "disabled"
419
+ run_async(
420
+ execute_command(
421
+ app_ctx,
422
+ vin_positional,
423
+ "remote_auto_seat_climate_request",
424
+ "climate.auto-seat",
425
+ body={"auto_seat_position": SEAT_MAP[seat], "auto_climate_on": on},
426
+ success_message=f"Auto seat climate {state} for {seat}.",
427
+ )
428
+ )
429
+
430
+
431
+ @climate_group.command("auto-wheel")
432
+ @click.argument("vin_positional", required=False, default=None, metavar="VIN")
433
+ @click.option("--on/--off", default=True, help="Enable or disable auto steering wheel heat")
434
+ @global_options
435
+ def auto_wheel_cmd(app_ctx: AppContext, vin_positional: str | None, on: bool) -> None:
436
+ """Enable or disable auto steering wheel heat."""
437
+ state = "enabled" if on else "disabled"
438
+ run_async(
439
+ execute_command(
440
+ app_ctx,
441
+ vin_positional,
442
+ "remote_auto_steering_wheel_heat_climate_request",
443
+ "climate.auto-wheel",
444
+ body={"on": on},
445
+ success_message=f"Auto steering wheel heat {state}.",
446
+ )
447
+ )
448
+
449
+
450
+ @climate_group.command("wheel-level")
451
+ @click.argument("vin_positional", required=False, default=None, metavar="VIN")
452
+ @click.argument("level", type=click.IntRange(0, 3))
453
+ @global_options
454
+ def wheel_level_cmd(app_ctx: AppContext, vin_positional: str | None, level: int) -> None:
455
+ """Set steering wheel heat level (0=off, 1=low, 2=med, 3=high)."""
456
+ level_names = {0: "off", 1: "low", 2: "medium", 3: "high"}
457
+ run_async(
458
+ execute_command(
459
+ app_ctx,
460
+ vin_positional,
461
+ "remote_steering_wheel_heat_level_request",
462
+ "climate.wheel-level",
463
+ body={"level": level},
464
+ success_message=f"Steering wheel heat set to {level_names.get(level, level)}.",
465
+ )
466
+ )
467
+
468
+
469
+ @climate_group.command("keeper")
470
+ @click.argument("vin_positional", required=False, default=None, metavar="VIN")
471
+ @click.argument("mode", type=click.Choice(list(KEEPER_MODE_MAP)))
472
+ @global_options
473
+ def keeper_cmd(app_ctx: AppContext, vin_positional: str | None, mode: str) -> None:
474
+ """Set climate keeper mode (off/on/dog/camp)."""
475
+ run_async(_cmd_keeper(app_ctx, vin_positional, mode))
476
+
477
+
478
+ async def _cmd_keeper(app_ctx: AppContext, vin_positional: str | None, mode: str) -> None:
479
+ formatter = app_ctx.formatter
480
+ vin = require_vin(vin_positional, app_ctx.vin)
481
+ client, vehicle_api, cmd_api = get_command_api(app_ctx)
482
+ try:
483
+ result = await auto_wake(
484
+ formatter,
485
+ vehicle_api,
486
+ vin,
487
+ lambda: cmd_api.set_climate_keeper_mode(
488
+ vin, climate_keeper_mode=KEEPER_MODE_MAP[mode]
489
+ ),
490
+ auto=app_ctx.auto_wake,
491
+ )
492
+ finally:
493
+ await client.close()
494
+
495
+ invalidate_cache_for_vin(app_ctx, vin)
496
+
497
+ if formatter.format == "json":
498
+ formatter.output(result, command="climate.keeper")
499
+ else:
500
+ msg = result.response.reason or f"Climate keeper mode set to {mode}."
501
+ formatter.rich.command_result(result.response.result, msg)