tescmd 0.1.2__py3-none-any.whl → 0.3.1__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 +1 -1
- tescmd/api/client.py +49 -5
- tescmd/api/command.py +1 -1
- tescmd/api/errors.py +13 -0
- tescmd/api/signed_command.py +19 -14
- tescmd/api/vehicle.py +19 -1
- tescmd/auth/oauth.py +5 -1
- tescmd/auth/server.py +6 -1
- tescmd/auth/token_store.py +8 -1
- tescmd/cache/response_cache.py +11 -3
- tescmd/cli/_client.py +142 -20
- tescmd/cli/_options.py +2 -4
- tescmd/cli/auth.py +121 -11
- tescmd/cli/energy.py +2 -0
- tescmd/cli/key.py +149 -14
- tescmd/cli/main.py +70 -7
- tescmd/cli/mcp_cmd.py +153 -0
- tescmd/cli/nav.py +3 -1
- tescmd/cli/openclaw.py +169 -0
- tescmd/cli/security.py +7 -1
- tescmd/cli/serve.py +923 -0
- tescmd/cli/setup.py +244 -25
- tescmd/cli/sharing.py +2 -0
- tescmd/cli/status.py +1 -1
- tescmd/cli/trunk.py +8 -17
- tescmd/cli/user.py +16 -1
- tescmd/cli/vehicle.py +156 -20
- tescmd/crypto/__init__.py +3 -1
- tescmd/crypto/ecdh.py +9 -0
- tescmd/crypto/schnorr.py +191 -0
- tescmd/deploy/github_pages.py +8 -0
- tescmd/deploy/tailscale_serve.py +154 -0
- tescmd/mcp/__init__.py +7 -0
- tescmd/mcp/server.py +648 -0
- tescmd/models/__init__.py +0 -2
- tescmd/models/auth.py +24 -2
- tescmd/models/config.py +1 -0
- tescmd/models/energy.py +0 -9
- tescmd/openclaw/__init__.py +23 -0
- tescmd/openclaw/bridge.py +330 -0
- tescmd/openclaw/config.py +167 -0
- tescmd/openclaw/dispatcher.py +522 -0
- tescmd/openclaw/emitter.py +175 -0
- tescmd/openclaw/filters.py +123 -0
- tescmd/openclaw/gateway.py +687 -0
- tescmd/openclaw/telemetry_store.py +53 -0
- tescmd/output/rich_output.py +46 -14
- tescmd/protocol/commands.py +2 -2
- tescmd/protocol/encoder.py +16 -13
- tescmd/protocol/payloads.py +132 -11
- tescmd/protocol/session.py +18 -8
- tescmd/protocol/signer.py +3 -17
- tescmd/telemetry/__init__.py +28 -0
- tescmd/telemetry/cache_sink.py +154 -0
- tescmd/telemetry/csv_sink.py +180 -0
- tescmd/telemetry/dashboard.py +227 -0
- tescmd/telemetry/decoder.py +284 -0
- tescmd/telemetry/fanout.py +49 -0
- tescmd/telemetry/fields.py +427 -0
- tescmd/telemetry/flatbuf.py +162 -0
- tescmd/telemetry/mapper.py +239 -0
- tescmd/telemetry/protos/__init__.py +4 -0
- tescmd/telemetry/protos/vehicle_alert.proto +31 -0
- tescmd/telemetry/protos/vehicle_alert_pb2.py +42 -0
- tescmd/telemetry/protos/vehicle_alert_pb2.pyi +44 -0
- tescmd/telemetry/protos/vehicle_connectivity.proto +23 -0
- tescmd/telemetry/protos/vehicle_connectivity_pb2.py +40 -0
- tescmd/telemetry/protos/vehicle_connectivity_pb2.pyi +33 -0
- tescmd/telemetry/protos/vehicle_data.proto +768 -0
- tescmd/telemetry/protos/vehicle_data_pb2.py +136 -0
- tescmd/telemetry/protos/vehicle_data_pb2.pyi +1336 -0
- tescmd/telemetry/protos/vehicle_error.proto +23 -0
- tescmd/telemetry/protos/vehicle_error_pb2.py +44 -0
- tescmd/telemetry/protos/vehicle_error_pb2.pyi +39 -0
- tescmd/telemetry/protos/vehicle_metric.proto +22 -0
- tescmd/telemetry/protos/vehicle_metric_pb2.py +44 -0
- tescmd/telemetry/protos/vehicle_metric_pb2.pyi +37 -0
- tescmd/telemetry/server.py +300 -0
- tescmd/telemetry/setup.py +468 -0
- tescmd/telemetry/tailscale.py +300 -0
- tescmd/telemetry/tui.py +1716 -0
- tescmd/triggers/__init__.py +18 -0
- tescmd/triggers/manager.py +264 -0
- tescmd/triggers/models.py +93 -0
- {tescmd-0.1.2.dist-info → tescmd-0.3.1.dist-info}/METADATA +125 -40
- tescmd-0.3.1.dist-info/RECORD +128 -0
- tescmd-0.1.2.dist-info/RECORD +0 -81
- {tescmd-0.1.2.dist-info → tescmd-0.3.1.dist-info}/WHEEL +0 -0
- {tescmd-0.1.2.dist-info → tescmd-0.3.1.dist-info}/entry_points.txt +0 -0
- {tescmd-0.1.2.dist-info → tescmd-0.3.1.dist-info}/licenses/LICENSE +0 -0
tescmd/cli/setup.py
CHANGED
|
@@ -182,13 +182,22 @@ def _developer_portal_setup(
|
|
|
182
182
|
info("[bold]Phase 2: Tesla Developer Portal Setup[/bold]")
|
|
183
183
|
info("")
|
|
184
184
|
|
|
185
|
+
# Detect Tailscale hostname if domain was set to Tailscale in Phase 1
|
|
186
|
+
ts_hostname = ""
|
|
187
|
+
if settings.hosting_method == "tailscale" and settings.domain:
|
|
188
|
+
ts_hostname = settings.domain
|
|
189
|
+
|
|
185
190
|
# Delegate to the existing interactive setup wizard, passing the domain
|
|
186
191
|
# so the portal instructions show the correct Allowed Origin URL
|
|
187
|
-
|
|
192
|
+
from tescmd.models.auth import DEFAULT_PORT
|
|
193
|
+
|
|
194
|
+
port = DEFAULT_PORT
|
|
188
195
|
redirect_uri = f"http://localhost:{port}/callback"
|
|
189
196
|
from tescmd.cli.auth import _interactive_setup
|
|
190
197
|
|
|
191
|
-
return _interactive_setup(
|
|
198
|
+
return _interactive_setup(
|
|
199
|
+
formatter, port, redirect_uri, domain=domain, tailscale_hostname=ts_hostname
|
|
200
|
+
)
|
|
192
201
|
|
|
193
202
|
|
|
194
203
|
# ---------------------------------------------------------------------------
|
|
@@ -197,7 +206,7 @@ def _developer_portal_setup(
|
|
|
197
206
|
|
|
198
207
|
|
|
199
208
|
def _domain_setup(formatter: OutputFormatter, settings: AppSettings) -> str:
|
|
200
|
-
"""Set up a domain via GitHub Pages or manual entry."""
|
|
209
|
+
"""Set up a domain via GitHub Pages, Tailscale Funnel, or manual entry."""
|
|
201
210
|
info = formatter.rich.info
|
|
202
211
|
|
|
203
212
|
if settings.domain:
|
|
@@ -207,22 +216,30 @@ def _domain_setup(formatter: OutputFormatter, settings: AppSettings) -> str:
|
|
|
207
216
|
|
|
208
217
|
info("[bold]Phase 1: Domain Setup[/bold]")
|
|
209
218
|
info("")
|
|
210
|
-
info(
|
|
211
|
-
"Tesla requires a registered domain for Fleet API access."
|
|
212
|
-
" The easiest approach is a free GitHub Pages site."
|
|
213
|
-
)
|
|
219
|
+
info("Tesla requires a registered domain for Fleet API access.")
|
|
214
220
|
info("")
|
|
215
221
|
|
|
216
|
-
#
|
|
222
|
+
# Priority 1: GitHub Pages (always-on hosting)
|
|
217
223
|
from tescmd.deploy.github_pages import is_gh_authenticated, is_gh_available
|
|
218
224
|
|
|
219
225
|
if is_gh_available() and is_gh_authenticated():
|
|
220
226
|
return _automated_domain_setup(formatter, settings)
|
|
221
227
|
|
|
222
|
-
#
|
|
228
|
+
# Priority 2: Tailscale Funnel (requires local machine running)
|
|
229
|
+
if run_async(_is_tailscale_ready()):
|
|
230
|
+
return _tailscale_domain_setup(formatter, settings)
|
|
231
|
+
|
|
232
|
+
# Priority 3: Manual
|
|
223
233
|
return _manual_domain_setup(formatter)
|
|
224
234
|
|
|
225
235
|
|
|
236
|
+
async def _is_tailscale_ready() -> bool:
|
|
237
|
+
"""Wrapper to check Tailscale readiness without raising."""
|
|
238
|
+
from tescmd.deploy.tailscale_serve import is_tailscale_serve_ready
|
|
239
|
+
|
|
240
|
+
return await is_tailscale_serve_ready()
|
|
241
|
+
|
|
242
|
+
|
|
226
243
|
def _automated_domain_setup(formatter: OutputFormatter, settings: AppSettings) -> str:
|
|
227
244
|
"""Offer to auto-create a GitHub Pages site."""
|
|
228
245
|
info = formatter.rich.info
|
|
@@ -239,6 +256,11 @@ def _automated_domain_setup(formatter: OutputFormatter, settings: AppSettings) -
|
|
|
239
256
|
info(f"GitHub CLI detected. Logged in as [cyan]{username}[/cyan].")
|
|
240
257
|
info(f"Suggested domain: [cyan]{suggested_domain}[/cyan]")
|
|
241
258
|
info("")
|
|
259
|
+
info("[dim]Note: GitHub Pages provides always-on key hosting but cannot[/dim]")
|
|
260
|
+
info("[dim]serve as a Fleet Telemetry server. If you plan to use telemetry[/dim]")
|
|
261
|
+
info("[dim]streaming, choose Tailscale instead (install Tailscale, then[/dim]")
|
|
262
|
+
info("[dim]re-run setup).[/dim]")
|
|
263
|
+
info("")
|
|
242
264
|
|
|
243
265
|
try:
|
|
244
266
|
answer = input(f"Create/use {suggested_domain} as your domain? [Y/n] ").strip()
|
|
@@ -266,6 +288,56 @@ def _automated_domain_setup(formatter: OutputFormatter, settings: AppSettings) -
|
|
|
266
288
|
return domain
|
|
267
289
|
|
|
268
290
|
|
|
291
|
+
def _tailscale_domain_setup(formatter: OutputFormatter, settings: AppSettings) -> str:
|
|
292
|
+
"""Offer to use Tailscale Funnel for key hosting."""
|
|
293
|
+
info = formatter.rich.info
|
|
294
|
+
|
|
295
|
+
from tescmd.telemetry.tailscale import TailscaleManager
|
|
296
|
+
|
|
297
|
+
hostname: str = run_async(TailscaleManager().get_hostname())
|
|
298
|
+
|
|
299
|
+
info("Tailscale detected. Your key would be hosted at:")
|
|
300
|
+
info(f" [cyan]https://{hostname}/{_WELL_KNOWN_PATH}[/cyan]")
|
|
301
|
+
info("")
|
|
302
|
+
info("[yellow]Important:[/yellow] Tailscale Funnel requires your machine to be running.")
|
|
303
|
+
info("If your machine is off or Tailscale stops, Tesla cannot reach your")
|
|
304
|
+
info("public key. This is fine for development and testing.")
|
|
305
|
+
info("For always-on hosting, use GitHub Pages instead (install gh CLI).")
|
|
306
|
+
info("")
|
|
307
|
+
info("[green]Telemetry streaming:[/green] If you plan to use Fleet Telemetry")
|
|
308
|
+
info("streaming (tescmd vehicle telemetry stream), you should use your")
|
|
309
|
+
info("Tailscale hostname as your domain. Tesla requires the telemetry")
|
|
310
|
+
info("server hostname to match your registered domain.")
|
|
311
|
+
info("")
|
|
312
|
+
|
|
313
|
+
try:
|
|
314
|
+
answer = input(f"Use {hostname} as your domain? [Y/n] ").strip()
|
|
315
|
+
except (EOFError, KeyboardInterrupt):
|
|
316
|
+
info("")
|
|
317
|
+
return ""
|
|
318
|
+
|
|
319
|
+
if answer.lower() == "n":
|
|
320
|
+
return _manual_domain_setup(formatter)
|
|
321
|
+
|
|
322
|
+
domain = hostname
|
|
323
|
+
|
|
324
|
+
# Persist domain and hosting method to .env
|
|
325
|
+
from tescmd.cli.auth import _write_env_value
|
|
326
|
+
|
|
327
|
+
_write_env_value("TESLA_DOMAIN", domain)
|
|
328
|
+
_write_env_value("TESLA_HOSTING_METHOD", "tailscale")
|
|
329
|
+
|
|
330
|
+
info(f"[green]Domain configured: {domain}[/green]")
|
|
331
|
+
info("[green]Hosting method: Tailscale Funnel[/green]")
|
|
332
|
+
info("")
|
|
333
|
+
|
|
334
|
+
return domain
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# Well-known path constant (shared with deploy modules)
|
|
338
|
+
_WELL_KNOWN_PATH = ".well-known/appspecific/com.tesla.3p.public-key.pem"
|
|
339
|
+
|
|
340
|
+
|
|
269
341
|
def _manual_domain_setup(formatter: OutputFormatter) -> str:
|
|
270
342
|
"""Prompt for a domain manually."""
|
|
271
343
|
from tescmd.cli.auth import _prompt_for_domain
|
|
@@ -279,7 +351,7 @@ def _manual_domain_setup(formatter: OutputFormatter) -> str:
|
|
|
279
351
|
|
|
280
352
|
|
|
281
353
|
def _key_setup(formatter: OutputFormatter, settings: AppSettings, domain: str) -> None:
|
|
282
|
-
"""Generate keys and deploy
|
|
354
|
+
"""Generate keys and deploy via the configured hosting method (full tier only)."""
|
|
283
355
|
info = formatter.rich.info
|
|
284
356
|
|
|
285
357
|
info("[bold]Phase 3: EC Key Generation & Deployment[/bold]")
|
|
@@ -291,7 +363,6 @@ def _key_setup(formatter: OutputFormatter, settings: AppSettings, domain: str) -
|
|
|
291
363
|
generate_ec_key_pair,
|
|
292
364
|
get_key_fingerprint,
|
|
293
365
|
has_key_pair,
|
|
294
|
-
load_public_key_pem,
|
|
295
366
|
)
|
|
296
367
|
|
|
297
368
|
# Generate keys if needed
|
|
@@ -305,7 +376,69 @@ def _key_setup(formatter: OutputFormatter, settings: AppSettings, domain: str) -
|
|
|
305
376
|
|
|
306
377
|
info("")
|
|
307
378
|
|
|
308
|
-
#
|
|
379
|
+
# Branch on hosting method
|
|
380
|
+
hosting = settings.hosting_method
|
|
381
|
+
|
|
382
|
+
if hosting == "tailscale":
|
|
383
|
+
_deploy_key_tailscale(formatter, settings, key_dir, domain)
|
|
384
|
+
else:
|
|
385
|
+
_deploy_key_github(formatter, settings, key_dir, domain)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _deploy_key_tailscale(
|
|
389
|
+
formatter: OutputFormatter,
|
|
390
|
+
settings: AppSettings,
|
|
391
|
+
key_dir: Path,
|
|
392
|
+
domain: str,
|
|
393
|
+
) -> None:
|
|
394
|
+
"""Deploy key via Tailscale Funnel."""
|
|
395
|
+
info = formatter.rich.info
|
|
396
|
+
|
|
397
|
+
from tescmd.crypto.keys import load_public_key_pem
|
|
398
|
+
from tescmd.deploy.tailscale_serve import (
|
|
399
|
+
deploy_public_key_tailscale,
|
|
400
|
+
get_key_url,
|
|
401
|
+
start_key_serving,
|
|
402
|
+
validate_tailscale_key_url,
|
|
403
|
+
wait_for_tailscale_deployment,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
# Check if key is already deployed
|
|
407
|
+
if run_async(validate_tailscale_key_url(domain)):
|
|
408
|
+
info(f"Public key: [green]already accessible[/green] at {get_key_url(domain)}")
|
|
409
|
+
info("")
|
|
410
|
+
return
|
|
411
|
+
|
|
412
|
+
info("Deploying public key via Tailscale Funnel...")
|
|
413
|
+
pem = load_public_key_pem(key_dir)
|
|
414
|
+
run_async(deploy_public_key_tailscale(pem))
|
|
415
|
+
run_async(start_key_serving())
|
|
416
|
+
|
|
417
|
+
info("[green]Tailscale serve + Funnel started.[/green]")
|
|
418
|
+
info("Waiting for key to become accessible...")
|
|
419
|
+
|
|
420
|
+
deployed = run_async(wait_for_tailscale_deployment(domain))
|
|
421
|
+
if deployed:
|
|
422
|
+
info(f"[green]Key is live at:[/green] {get_key_url(domain)}")
|
|
423
|
+
else:
|
|
424
|
+
info(
|
|
425
|
+
"[yellow]Key deployed but not yet accessible."
|
|
426
|
+
" Tailscale Funnel may still be propagating.[/yellow]"
|
|
427
|
+
)
|
|
428
|
+
info(" Run [cyan]tescmd key validate[/cyan] to check later.")
|
|
429
|
+
info("")
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _deploy_key_github(
|
|
433
|
+
formatter: OutputFormatter,
|
|
434
|
+
settings: AppSettings,
|
|
435
|
+
key_dir: Path,
|
|
436
|
+
domain: str,
|
|
437
|
+
) -> None:
|
|
438
|
+
"""Deploy key via GitHub Pages."""
|
|
439
|
+
info = formatter.rich.info
|
|
440
|
+
|
|
441
|
+
from tescmd.crypto.keys import load_public_key_pem
|
|
309
442
|
from tescmd.deploy.github_pages import (
|
|
310
443
|
deploy_public_key,
|
|
311
444
|
get_key_url,
|
|
@@ -367,7 +500,6 @@ async def _enrollment_step(
|
|
|
367
500
|
import webbrowser
|
|
368
501
|
|
|
369
502
|
from tescmd.crypto.keys import get_key_fingerprint, has_key_pair
|
|
370
|
-
from tescmd.deploy.github_pages import get_key_url, validate_key_url
|
|
371
503
|
|
|
372
504
|
info = formatter.rich.info
|
|
373
505
|
key_dir = Path(settings.config_dir).expanduser() / "keys"
|
|
@@ -388,9 +520,21 @@ async def _enrollment_step(
|
|
|
388
520
|
info(" must also be enrolled via the Tesla app.")
|
|
389
521
|
info("")
|
|
390
522
|
|
|
391
|
-
# Verify the public key is accessible
|
|
392
|
-
|
|
393
|
-
|
|
523
|
+
# Verify the public key is accessible (method-aware)
|
|
524
|
+
if settings.hosting_method == "tailscale":
|
|
525
|
+
from tescmd.deploy.tailscale_serve import get_key_url
|
|
526
|
+
from tescmd.deploy.tailscale_serve import (
|
|
527
|
+
validate_tailscale_key_url as _validate,
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
key_url = get_key_url(domain)
|
|
531
|
+
key_accessible = await _validate(domain)
|
|
532
|
+
else:
|
|
533
|
+
from tescmd.deploy.github_pages import get_key_url as gh_get_key_url
|
|
534
|
+
from tescmd.deploy.github_pages import validate_key_url
|
|
535
|
+
|
|
536
|
+
key_url = gh_get_key_url(domain)
|
|
537
|
+
key_accessible = validate_key_url(domain)
|
|
394
538
|
if not key_accessible:
|
|
395
539
|
info(f" [yellow]Public key not accessible at {key_url}[/yellow]")
|
|
396
540
|
info(" Enrollment requires the key to be live. Skipping for now.")
|
|
@@ -509,16 +653,30 @@ def _precheck_public_key(
|
|
|
509
653
|
"""
|
|
510
654
|
info = formatter.rich.info
|
|
511
655
|
|
|
512
|
-
from tescmd.deploy.github_pages import get_key_url, validate_key_url
|
|
513
|
-
|
|
514
656
|
info("Checking public key availability...")
|
|
515
657
|
|
|
516
|
-
|
|
517
|
-
|
|
658
|
+
# Check accessibility via the appropriate method
|
|
659
|
+
if settings.hosting_method == "tailscale":
|
|
660
|
+
from tescmd.deploy.tailscale_serve import (
|
|
661
|
+
get_key_url,
|
|
662
|
+
validate_tailscale_key_url,
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
key_url = get_key_url(domain)
|
|
666
|
+
accessible = run_async(validate_tailscale_key_url(domain))
|
|
667
|
+
else:
|
|
668
|
+
from tescmd.deploy.github_pages import get_key_url as gh_get_key_url
|
|
669
|
+
from tescmd.deploy.github_pages import validate_key_url
|
|
670
|
+
|
|
671
|
+
key_url = gh_get_key_url(domain)
|
|
672
|
+
accessible = validate_key_url(domain)
|
|
673
|
+
|
|
674
|
+
if accessible:
|
|
675
|
+
info(f" Public key: [green]accessible[/green] at {key_url}")
|
|
518
676
|
info("")
|
|
519
677
|
return True
|
|
520
678
|
|
|
521
|
-
info(f" Public key: [yellow]not found[/yellow] at {
|
|
679
|
+
info(f" Public key: [yellow]not found[/yellow] at {key_url}")
|
|
522
680
|
info("")
|
|
523
681
|
info(" Tesla requires your public key to be accessible before registration will succeed.")
|
|
524
682
|
info("")
|
|
@@ -543,7 +701,7 @@ def _auto_deploy_key(
|
|
|
543
701
|
settings: AppSettings,
|
|
544
702
|
domain: str,
|
|
545
703
|
) -> bool:
|
|
546
|
-
"""Generate a key pair (if needed), deploy
|
|
704
|
+
"""Generate a key pair (if needed), deploy via configured method, and wait.
|
|
547
705
|
|
|
548
706
|
Returns True when the key is confirmed accessible, False otherwise.
|
|
549
707
|
"""
|
|
@@ -554,7 +712,6 @@ def _auto_deploy_key(
|
|
|
554
712
|
generate_ec_key_pair,
|
|
555
713
|
get_key_fingerprint,
|
|
556
714
|
has_key_pair,
|
|
557
|
-
load_public_key_pem,
|
|
558
715
|
)
|
|
559
716
|
|
|
560
717
|
# 1. Generate keys if needed
|
|
@@ -566,7 +723,63 @@ def _auto_deploy_key(
|
|
|
566
723
|
info(f"[green]Key pair generated.[/green] Fingerprint: {get_key_fingerprint(key_dir)}")
|
|
567
724
|
info("")
|
|
568
725
|
|
|
569
|
-
# 2. Deploy
|
|
726
|
+
# 2. Deploy via the appropriate method
|
|
727
|
+
if settings.hosting_method == "tailscale":
|
|
728
|
+
return _auto_deploy_key_tailscale(info, key_dir, domain)
|
|
729
|
+
return _auto_deploy_key_github(info, settings, key_dir, domain)
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def _auto_deploy_key_tailscale(
|
|
733
|
+
info: _InfoFn,
|
|
734
|
+
key_dir: Path,
|
|
735
|
+
domain: str,
|
|
736
|
+
) -> bool:
|
|
737
|
+
"""Deploy key via Tailscale Funnel and wait."""
|
|
738
|
+
from tescmd.crypto.keys import load_public_key_pem
|
|
739
|
+
from tescmd.deploy.tailscale_serve import (
|
|
740
|
+
deploy_public_key_tailscale,
|
|
741
|
+
get_key_url,
|
|
742
|
+
start_key_serving,
|
|
743
|
+
validate_tailscale_key_url,
|
|
744
|
+
wait_for_tailscale_deployment,
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
# Already deployed?
|
|
748
|
+
if run_async(validate_tailscale_key_url(domain)):
|
|
749
|
+
info(f"Public key: [green]already accessible[/green] at {get_key_url(domain)}")
|
|
750
|
+
info("")
|
|
751
|
+
return True
|
|
752
|
+
|
|
753
|
+
info("Deploying public key via Tailscale Funnel...")
|
|
754
|
+
pem = load_public_key_pem(key_dir)
|
|
755
|
+
run_async(deploy_public_key_tailscale(pem))
|
|
756
|
+
run_async(start_key_serving())
|
|
757
|
+
|
|
758
|
+
info("[green]Tailscale serve + Funnel started.[/green]")
|
|
759
|
+
info("Waiting for key to become accessible...")
|
|
760
|
+
|
|
761
|
+
deployed = run_async(wait_for_tailscale_deployment(domain))
|
|
762
|
+
if deployed:
|
|
763
|
+
info(f"[green]Key is live at:[/green] {get_key_url(domain)}")
|
|
764
|
+
info("")
|
|
765
|
+
return True
|
|
766
|
+
|
|
767
|
+
info("[yellow]Key deployed but not yet accessible.[/yellow]")
|
|
768
|
+
info(
|
|
769
|
+
" Run [cyan]tescmd key validate[/cyan] to check, then [cyan]tescmd auth register[/cyan]."
|
|
770
|
+
)
|
|
771
|
+
info("")
|
|
772
|
+
return False
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def _auto_deploy_key_github(
|
|
776
|
+
info: _InfoFn,
|
|
777
|
+
settings: AppSettings,
|
|
778
|
+
key_dir: Path,
|
|
779
|
+
domain: str,
|
|
780
|
+
) -> bool:
|
|
781
|
+
"""Deploy key via GitHub Pages and wait."""
|
|
782
|
+
from tescmd.crypto.keys import load_public_key_pem
|
|
570
783
|
from tescmd.deploy.github_pages import (
|
|
571
784
|
deploy_public_key,
|
|
572
785
|
get_key_url,
|
|
@@ -632,6 +845,10 @@ def _remediate_412(info: _InfoFn, domain: str) -> None:
|
|
|
632
845
|
info(" 2. Open your application")
|
|
633
846
|
info(" 3. Set [cyan]Allowed Origin URL[/cyan] to:")
|
|
634
847
|
info(f" [bold]https://{domain}[/bold]")
|
|
848
|
+
info(
|
|
849
|
+
" [dim]For telemetry streaming, also add your Tailscale origin"
|
|
850
|
+
" (e.g. https://<machine>.tailnet.ts.net)[/dim]"
|
|
851
|
+
)
|
|
635
852
|
info(" 4. Save, then re-run [cyan]tescmd setup[/cyan]")
|
|
636
853
|
|
|
637
854
|
|
|
@@ -716,7 +933,9 @@ async def _oauth_login_step(
|
|
|
716
933
|
info("[bold]Phase 5: OAuth Login[/bold]")
|
|
717
934
|
info("")
|
|
718
935
|
|
|
719
|
-
|
|
936
|
+
from tescmd.models.auth import DEFAULT_PORT as _DEFAULT_PORT
|
|
937
|
+
|
|
938
|
+
port = _DEFAULT_PORT
|
|
720
939
|
redirect_uri = f"http://localhost:{port}/callback"
|
|
721
940
|
|
|
722
941
|
info("Opening your browser to sign in to Tesla...")
|
tescmd/cli/sharing.py
CHANGED
|
@@ -15,6 +15,7 @@ from tescmd.cli._client import (
|
|
|
15
15
|
require_vin,
|
|
16
16
|
)
|
|
17
17
|
from tescmd.cli._options import global_options
|
|
18
|
+
from tescmd.models.sharing import ShareInvite
|
|
18
19
|
|
|
19
20
|
if TYPE_CHECKING:
|
|
20
21
|
from tescmd.cli.main import AppContext
|
|
@@ -172,6 +173,7 @@ async def _cmd_list_invites(app_ctx: AppContext, vin_positional: str | None) ->
|
|
|
172
173
|
endpoint="sharing.list-invites",
|
|
173
174
|
fetch=lambda: api.list_invites(vin),
|
|
174
175
|
ttl=TTL_SLOW,
|
|
176
|
+
model_class=ShareInvite,
|
|
175
177
|
)
|
|
176
178
|
finally:
|
|
177
179
|
await client.close()
|
tescmd/cli/status.py
CHANGED
|
@@ -36,7 +36,7 @@ def status_cmd(app_ctx: AppContext) -> None:
|
|
|
36
36
|
meta = store.metadata or {}
|
|
37
37
|
expires_at = meta.get("expires_at", 0.0)
|
|
38
38
|
expires_in = max(0, int(expires_at - time.time())) if has_token else 0
|
|
39
|
-
has_refresh = store.refresh_token
|
|
39
|
+
has_refresh = bool(store.refresh_token)
|
|
40
40
|
|
|
41
41
|
# Key info
|
|
42
42
|
key_dir = Path(settings.config_dir).expanduser() / "keys"
|
tescmd/cli/trunk.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from typing import TYPE_CHECKING
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
6
|
|
|
7
7
|
import click
|
|
8
8
|
|
|
@@ -203,22 +203,13 @@ async def _cmd_window(
|
|
|
203
203
|
try:
|
|
204
204
|
|
|
205
205
|
async def _execute_window() -> CommandResponse:
|
|
206
|
-
if vent
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
use_lat, use_lon = lat, lon
|
|
214
|
-
else:
|
|
215
|
-
vdata = await vehicle_api.get_vehicle_data(vin, endpoints=["drive_state"])
|
|
216
|
-
ds = vdata.drive_state
|
|
217
|
-
if ds and ds.latitude is not None and ds.longitude is not None:
|
|
218
|
-
use_lat, use_lon = ds.latitude, ds.longitude
|
|
219
|
-
else:
|
|
220
|
-
use_lat, use_lon = 0.0, 0.0
|
|
221
|
-
return await cmd_api.window_control(vin, command=cmd_str, lat=use_lat, lon=use_lon)
|
|
206
|
+
cmd_str = "vent" if vent else "close"
|
|
207
|
+
kwargs: dict[str, Any] = {"command": cmd_str}
|
|
208
|
+
if lat is not None:
|
|
209
|
+
kwargs["lat"] = lat
|
|
210
|
+
if lon is not None:
|
|
211
|
+
kwargs["lon"] = lon
|
|
212
|
+
return await cmd_api.window_control(vin, **kwargs)
|
|
222
213
|
|
|
223
214
|
result = await auto_wake(
|
|
224
215
|
formatter,
|
tescmd/cli/user.py
CHANGED
|
@@ -9,6 +9,7 @@ import click
|
|
|
9
9
|
from tescmd._internal.async_utils import run_async
|
|
10
10
|
from tescmd.cli._client import TTL_DEFAULT, TTL_STATIC, cached_api_call, get_user_api
|
|
11
11
|
from tescmd.cli._options import global_options
|
|
12
|
+
from tescmd.models.user import FeatureConfig, UserInfo, UserRegion, VehicleOrder
|
|
12
13
|
|
|
13
14
|
if TYPE_CHECKING:
|
|
14
15
|
from tescmd.cli.main import AppContext
|
|
@@ -34,6 +35,7 @@ async def _cmd_me(app_ctx: AppContext) -> None:
|
|
|
34
35
|
endpoint="user.me",
|
|
35
36
|
fetch=lambda: api.me(),
|
|
36
37
|
ttl=TTL_STATIC,
|
|
38
|
+
model_class=UserInfo,
|
|
37
39
|
)
|
|
38
40
|
finally:
|
|
39
41
|
await client.close()
|
|
@@ -62,6 +64,7 @@ async def _cmd_region(app_ctx: AppContext) -> None:
|
|
|
62
64
|
endpoint="user.region",
|
|
63
65
|
fetch=lambda: api.region(),
|
|
64
66
|
ttl=TTL_STATIC,
|
|
67
|
+
model_class=UserRegion,
|
|
65
68
|
)
|
|
66
69
|
finally:
|
|
67
70
|
await client.close()
|
|
@@ -90,6 +93,7 @@ async def _cmd_orders(app_ctx: AppContext) -> None:
|
|
|
90
93
|
endpoint="user.orders",
|
|
91
94
|
fetch=lambda: api.orders(),
|
|
92
95
|
ttl=TTL_DEFAULT,
|
|
96
|
+
model_class=VehicleOrder,
|
|
93
97
|
)
|
|
94
98
|
finally:
|
|
95
99
|
await client.close()
|
|
@@ -130,6 +134,7 @@ async def _cmd_features(app_ctx: AppContext) -> None:
|
|
|
130
134
|
endpoint="user.features",
|
|
131
135
|
fetch=lambda: api.feature_config(),
|
|
132
136
|
ttl=TTL_STATIC,
|
|
137
|
+
model_class=FeatureConfig,
|
|
133
138
|
)
|
|
134
139
|
finally:
|
|
135
140
|
await client.close()
|
|
@@ -139,7 +144,17 @@ async def _cmd_features(app_ctx: AppContext) -> None:
|
|
|
139
144
|
else:
|
|
140
145
|
dumped = data.model_dump(exclude_none=True) if hasattr(data, "model_dump") else data
|
|
141
146
|
if dumped:
|
|
147
|
+
from rich.table import Table
|
|
148
|
+
|
|
149
|
+
table = Table(title="Feature Flags")
|
|
150
|
+
table.add_column("Feature", style="bold")
|
|
151
|
+
table.add_column("Value")
|
|
142
152
|
for key, val in sorted(dumped.items()):
|
|
143
|
-
|
|
153
|
+
if isinstance(val, dict):
|
|
154
|
+
parts = [f"{k}={v}" for k, v in val.items()]
|
|
155
|
+
table.add_row(key, ", ".join(parts))
|
|
156
|
+
else:
|
|
157
|
+
table.add_row(key, str(val))
|
|
158
|
+
formatter.rich._con.print(table)
|
|
144
159
|
else:
|
|
145
160
|
formatter.rich.info("[dim]No feature flags available.[/dim]")
|