tescmd 0.1.2__py3-none-any.whl → 0.2.0__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 +8 -1
- tescmd/api/errors.py +8 -0
- tescmd/api/vehicle.py +19 -1
- tescmd/cache/response_cache.py +3 -2
- tescmd/cli/auth.py +30 -2
- tescmd/cli/key.py +149 -14
- tescmd/cli/main.py +44 -0
- tescmd/cli/setup.py +230 -22
- tescmd/cli/vehicle.py +464 -1
- tescmd/crypto/__init__.py +3 -1
- tescmd/crypto/ecdh.py +9 -0
- tescmd/crypto/schnorr.py +191 -0
- tescmd/deploy/tailscale_serve.py +154 -0
- tescmd/models/__init__.py +0 -2
- tescmd/models/auth.py +19 -0
- tescmd/models/config.py +1 -0
- tescmd/models/energy.py +0 -9
- tescmd/protocol/session.py +10 -3
- tescmd/telemetry/__init__.py +19 -0
- tescmd/telemetry/dashboard.py +227 -0
- tescmd/telemetry/decoder.py +284 -0
- tescmd/telemetry/fields.py +248 -0
- tescmd/telemetry/flatbuf.py +162 -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 +293 -0
- tescmd/telemetry/tailscale.py +300 -0
- {tescmd-0.1.2.dist-info → tescmd-0.2.0.dist-info}/METADATA +72 -35
- {tescmd-0.1.2.dist-info → tescmd-0.2.0.dist-info}/RECORD +47 -22
- {tescmd-0.1.2.dist-info → tescmd-0.2.0.dist-info}/WHEEL +0 -0
- {tescmd-0.1.2.dist-info → tescmd-0.2.0.dist-info}/entry_points.txt +0 -0
- {tescmd-0.1.2.dist-info → tescmd-0.2.0.dist-info}/licenses/LICENSE +0 -0
tescmd/cli/setup.py
CHANGED
|
@@ -197,7 +197,7 @@ def _developer_portal_setup(
|
|
|
197
197
|
|
|
198
198
|
|
|
199
199
|
def _domain_setup(formatter: OutputFormatter, settings: AppSettings) -> str:
|
|
200
|
-
"""Set up a domain via GitHub Pages or manual entry."""
|
|
200
|
+
"""Set up a domain via GitHub Pages, Tailscale Funnel, or manual entry."""
|
|
201
201
|
info = formatter.rich.info
|
|
202
202
|
|
|
203
203
|
if settings.domain:
|
|
@@ -207,22 +207,30 @@ def _domain_setup(formatter: OutputFormatter, settings: AppSettings) -> str:
|
|
|
207
207
|
|
|
208
208
|
info("[bold]Phase 1: Domain Setup[/bold]")
|
|
209
209
|
info("")
|
|
210
|
-
info(
|
|
211
|
-
"Tesla requires a registered domain for Fleet API access."
|
|
212
|
-
" The easiest approach is a free GitHub Pages site."
|
|
213
|
-
)
|
|
210
|
+
info("Tesla requires a registered domain for Fleet API access.")
|
|
214
211
|
info("")
|
|
215
212
|
|
|
216
|
-
#
|
|
213
|
+
# Priority 1: GitHub Pages (always-on hosting)
|
|
217
214
|
from tescmd.deploy.github_pages import is_gh_authenticated, is_gh_available
|
|
218
215
|
|
|
219
216
|
if is_gh_available() and is_gh_authenticated():
|
|
220
217
|
return _automated_domain_setup(formatter, settings)
|
|
221
218
|
|
|
222
|
-
#
|
|
219
|
+
# Priority 2: Tailscale Funnel (requires local machine running)
|
|
220
|
+
if run_async(_is_tailscale_ready()):
|
|
221
|
+
return _tailscale_domain_setup(formatter, settings)
|
|
222
|
+
|
|
223
|
+
# Priority 3: Manual
|
|
223
224
|
return _manual_domain_setup(formatter)
|
|
224
225
|
|
|
225
226
|
|
|
227
|
+
async def _is_tailscale_ready() -> bool:
|
|
228
|
+
"""Wrapper to check Tailscale readiness without raising."""
|
|
229
|
+
from tescmd.deploy.tailscale_serve import is_tailscale_serve_ready
|
|
230
|
+
|
|
231
|
+
return await is_tailscale_serve_ready()
|
|
232
|
+
|
|
233
|
+
|
|
226
234
|
def _automated_domain_setup(formatter: OutputFormatter, settings: AppSettings) -> str:
|
|
227
235
|
"""Offer to auto-create a GitHub Pages site."""
|
|
228
236
|
info = formatter.rich.info
|
|
@@ -239,6 +247,11 @@ def _automated_domain_setup(formatter: OutputFormatter, settings: AppSettings) -
|
|
|
239
247
|
info(f"GitHub CLI detected. Logged in as [cyan]{username}[/cyan].")
|
|
240
248
|
info(f"Suggested domain: [cyan]{suggested_domain}[/cyan]")
|
|
241
249
|
info("")
|
|
250
|
+
info("[dim]Note: GitHub Pages provides always-on key hosting but cannot")
|
|
251
|
+
info("serve as a Fleet Telemetry server. If you plan to use telemetry")
|
|
252
|
+
info("streaming, choose Tailscale instead (install Tailscale, then")
|
|
253
|
+
info("re-run setup).[/dim]")
|
|
254
|
+
info("")
|
|
242
255
|
|
|
243
256
|
try:
|
|
244
257
|
answer = input(f"Create/use {suggested_domain} as your domain? [Y/n] ").strip()
|
|
@@ -266,6 +279,56 @@ def _automated_domain_setup(formatter: OutputFormatter, settings: AppSettings) -
|
|
|
266
279
|
return domain
|
|
267
280
|
|
|
268
281
|
|
|
282
|
+
def _tailscale_domain_setup(formatter: OutputFormatter, settings: AppSettings) -> str:
|
|
283
|
+
"""Offer to use Tailscale Funnel for key hosting."""
|
|
284
|
+
info = formatter.rich.info
|
|
285
|
+
|
|
286
|
+
from tescmd.telemetry.tailscale import TailscaleManager
|
|
287
|
+
|
|
288
|
+
hostname: str = run_async(TailscaleManager().get_hostname())
|
|
289
|
+
|
|
290
|
+
info("Tailscale detected. Your key would be hosted at:")
|
|
291
|
+
info(f" [cyan]https://{hostname}/{_WELL_KNOWN_PATH}[/cyan]")
|
|
292
|
+
info("")
|
|
293
|
+
info("[yellow]Important:[/yellow] Tailscale Funnel requires your machine to be running.")
|
|
294
|
+
info("If your machine is off or Tailscale stops, Tesla cannot reach your")
|
|
295
|
+
info("public key. This is fine for development and testing.")
|
|
296
|
+
info("For always-on hosting, use GitHub Pages instead (install gh CLI).")
|
|
297
|
+
info("")
|
|
298
|
+
info("[green]Telemetry streaming:[/green] If you plan to use Fleet Telemetry")
|
|
299
|
+
info("streaming (tescmd vehicle telemetry stream), you should use your")
|
|
300
|
+
info("Tailscale hostname as your domain. Tesla requires the telemetry")
|
|
301
|
+
info("server hostname to match your registered domain.")
|
|
302
|
+
info("")
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
answer = input(f"Use {hostname} as your domain? [Y/n] ").strip()
|
|
306
|
+
except (EOFError, KeyboardInterrupt):
|
|
307
|
+
info("")
|
|
308
|
+
return ""
|
|
309
|
+
|
|
310
|
+
if answer.lower() == "n":
|
|
311
|
+
return _manual_domain_setup(formatter)
|
|
312
|
+
|
|
313
|
+
domain = hostname
|
|
314
|
+
|
|
315
|
+
# Persist domain and hosting method to .env
|
|
316
|
+
from tescmd.cli.auth import _write_env_value
|
|
317
|
+
|
|
318
|
+
_write_env_value("TESLA_DOMAIN", domain)
|
|
319
|
+
_write_env_value("TESLA_HOSTING_METHOD", "tailscale")
|
|
320
|
+
|
|
321
|
+
info(f"[green]Domain configured: {domain}[/green]")
|
|
322
|
+
info("[green]Hosting method: Tailscale Funnel[/green]")
|
|
323
|
+
info("")
|
|
324
|
+
|
|
325
|
+
return domain
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# Well-known path constant (shared with deploy modules)
|
|
329
|
+
_WELL_KNOWN_PATH = ".well-known/appspecific/com.tesla.3p.public-key.pem"
|
|
330
|
+
|
|
331
|
+
|
|
269
332
|
def _manual_domain_setup(formatter: OutputFormatter) -> str:
|
|
270
333
|
"""Prompt for a domain manually."""
|
|
271
334
|
from tescmd.cli.auth import _prompt_for_domain
|
|
@@ -279,7 +342,7 @@ def _manual_domain_setup(formatter: OutputFormatter) -> str:
|
|
|
279
342
|
|
|
280
343
|
|
|
281
344
|
def _key_setup(formatter: OutputFormatter, settings: AppSettings, domain: str) -> None:
|
|
282
|
-
"""Generate keys and deploy
|
|
345
|
+
"""Generate keys and deploy via the configured hosting method (full tier only)."""
|
|
283
346
|
info = formatter.rich.info
|
|
284
347
|
|
|
285
348
|
info("[bold]Phase 3: EC Key Generation & Deployment[/bold]")
|
|
@@ -291,7 +354,6 @@ def _key_setup(formatter: OutputFormatter, settings: AppSettings, domain: str) -
|
|
|
291
354
|
generate_ec_key_pair,
|
|
292
355
|
get_key_fingerprint,
|
|
293
356
|
has_key_pair,
|
|
294
|
-
load_public_key_pem,
|
|
295
357
|
)
|
|
296
358
|
|
|
297
359
|
# Generate keys if needed
|
|
@@ -305,7 +367,69 @@ def _key_setup(formatter: OutputFormatter, settings: AppSettings, domain: str) -
|
|
|
305
367
|
|
|
306
368
|
info("")
|
|
307
369
|
|
|
308
|
-
#
|
|
370
|
+
# Branch on hosting method
|
|
371
|
+
hosting = settings.hosting_method
|
|
372
|
+
|
|
373
|
+
if hosting == "tailscale":
|
|
374
|
+
_deploy_key_tailscale(formatter, settings, key_dir, domain)
|
|
375
|
+
else:
|
|
376
|
+
_deploy_key_github(formatter, settings, key_dir, domain)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _deploy_key_tailscale(
|
|
380
|
+
formatter: OutputFormatter,
|
|
381
|
+
settings: AppSettings,
|
|
382
|
+
key_dir: Path,
|
|
383
|
+
domain: str,
|
|
384
|
+
) -> None:
|
|
385
|
+
"""Deploy key via Tailscale Funnel."""
|
|
386
|
+
info = formatter.rich.info
|
|
387
|
+
|
|
388
|
+
from tescmd.crypto.keys import load_public_key_pem
|
|
389
|
+
from tescmd.deploy.tailscale_serve import (
|
|
390
|
+
deploy_public_key_tailscale,
|
|
391
|
+
get_key_url,
|
|
392
|
+
start_key_serving,
|
|
393
|
+
validate_tailscale_key_url,
|
|
394
|
+
wait_for_tailscale_deployment,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
# Check if key is already deployed
|
|
398
|
+
if run_async(validate_tailscale_key_url(domain)):
|
|
399
|
+
info(f"Public key: [green]already accessible[/green] at {get_key_url(domain)}")
|
|
400
|
+
info("")
|
|
401
|
+
return
|
|
402
|
+
|
|
403
|
+
info("Deploying public key via Tailscale Funnel...")
|
|
404
|
+
pem = load_public_key_pem(key_dir)
|
|
405
|
+
run_async(deploy_public_key_tailscale(pem))
|
|
406
|
+
run_async(start_key_serving())
|
|
407
|
+
|
|
408
|
+
info("[green]Tailscale serve + Funnel started.[/green]")
|
|
409
|
+
info("Waiting for key to become accessible...")
|
|
410
|
+
|
|
411
|
+
deployed = run_async(wait_for_tailscale_deployment(domain))
|
|
412
|
+
if deployed:
|
|
413
|
+
info(f"[green]Key is live at:[/green] {get_key_url(domain)}")
|
|
414
|
+
else:
|
|
415
|
+
info(
|
|
416
|
+
"[yellow]Key deployed but not yet accessible."
|
|
417
|
+
" Tailscale Funnel may still be propagating.[/yellow]"
|
|
418
|
+
)
|
|
419
|
+
info(" Run [cyan]tescmd key validate[/cyan] to check later.")
|
|
420
|
+
info("")
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _deploy_key_github(
|
|
424
|
+
formatter: OutputFormatter,
|
|
425
|
+
settings: AppSettings,
|
|
426
|
+
key_dir: Path,
|
|
427
|
+
domain: str,
|
|
428
|
+
) -> None:
|
|
429
|
+
"""Deploy key via GitHub Pages."""
|
|
430
|
+
info = formatter.rich.info
|
|
431
|
+
|
|
432
|
+
from tescmd.crypto.keys import load_public_key_pem
|
|
309
433
|
from tescmd.deploy.github_pages import (
|
|
310
434
|
deploy_public_key,
|
|
311
435
|
get_key_url,
|
|
@@ -367,7 +491,6 @@ async def _enrollment_step(
|
|
|
367
491
|
import webbrowser
|
|
368
492
|
|
|
369
493
|
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
494
|
|
|
372
495
|
info = formatter.rich.info
|
|
373
496
|
key_dir = Path(settings.config_dir).expanduser() / "keys"
|
|
@@ -388,9 +511,21 @@ async def _enrollment_step(
|
|
|
388
511
|
info(" must also be enrolled via the Tesla app.")
|
|
389
512
|
info("")
|
|
390
513
|
|
|
391
|
-
# Verify the public key is accessible
|
|
392
|
-
|
|
393
|
-
|
|
514
|
+
# Verify the public key is accessible (method-aware)
|
|
515
|
+
if settings.hosting_method == "tailscale":
|
|
516
|
+
from tescmd.deploy.tailscale_serve import get_key_url
|
|
517
|
+
from tescmd.deploy.tailscale_serve import (
|
|
518
|
+
validate_tailscale_key_url as _validate,
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
key_url = get_key_url(domain)
|
|
522
|
+
key_accessible = await _validate(domain)
|
|
523
|
+
else:
|
|
524
|
+
from tescmd.deploy.github_pages import get_key_url as gh_get_key_url
|
|
525
|
+
from tescmd.deploy.github_pages import validate_key_url
|
|
526
|
+
|
|
527
|
+
key_url = gh_get_key_url(domain)
|
|
528
|
+
key_accessible = validate_key_url(domain)
|
|
394
529
|
if not key_accessible:
|
|
395
530
|
info(f" [yellow]Public key not accessible at {key_url}[/yellow]")
|
|
396
531
|
info(" Enrollment requires the key to be live. Skipping for now.")
|
|
@@ -509,16 +644,30 @@ def _precheck_public_key(
|
|
|
509
644
|
"""
|
|
510
645
|
info = formatter.rich.info
|
|
511
646
|
|
|
512
|
-
from tescmd.deploy.github_pages import get_key_url, validate_key_url
|
|
513
|
-
|
|
514
647
|
info("Checking public key availability...")
|
|
515
648
|
|
|
516
|
-
|
|
517
|
-
|
|
649
|
+
# Check accessibility via the appropriate method
|
|
650
|
+
if settings.hosting_method == "tailscale":
|
|
651
|
+
from tescmd.deploy.tailscale_serve import (
|
|
652
|
+
get_key_url,
|
|
653
|
+
validate_tailscale_key_url,
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
key_url = get_key_url(domain)
|
|
657
|
+
accessible = run_async(validate_tailscale_key_url(domain))
|
|
658
|
+
else:
|
|
659
|
+
from tescmd.deploy.github_pages import get_key_url as gh_get_key_url
|
|
660
|
+
from tescmd.deploy.github_pages import validate_key_url
|
|
661
|
+
|
|
662
|
+
key_url = gh_get_key_url(domain)
|
|
663
|
+
accessible = validate_key_url(domain)
|
|
664
|
+
|
|
665
|
+
if accessible:
|
|
666
|
+
info(f" Public key: [green]accessible[/green] at {key_url}")
|
|
518
667
|
info("")
|
|
519
668
|
return True
|
|
520
669
|
|
|
521
|
-
info(f" Public key: [yellow]not found[/yellow] at {
|
|
670
|
+
info(f" Public key: [yellow]not found[/yellow] at {key_url}")
|
|
522
671
|
info("")
|
|
523
672
|
info(" Tesla requires your public key to be accessible before registration will succeed.")
|
|
524
673
|
info("")
|
|
@@ -543,7 +692,7 @@ def _auto_deploy_key(
|
|
|
543
692
|
settings: AppSettings,
|
|
544
693
|
domain: str,
|
|
545
694
|
) -> bool:
|
|
546
|
-
"""Generate a key pair (if needed), deploy
|
|
695
|
+
"""Generate a key pair (if needed), deploy via configured method, and wait.
|
|
547
696
|
|
|
548
697
|
Returns True when the key is confirmed accessible, False otherwise.
|
|
549
698
|
"""
|
|
@@ -554,7 +703,6 @@ def _auto_deploy_key(
|
|
|
554
703
|
generate_ec_key_pair,
|
|
555
704
|
get_key_fingerprint,
|
|
556
705
|
has_key_pair,
|
|
557
|
-
load_public_key_pem,
|
|
558
706
|
)
|
|
559
707
|
|
|
560
708
|
# 1. Generate keys if needed
|
|
@@ -566,7 +714,63 @@ def _auto_deploy_key(
|
|
|
566
714
|
info(f"[green]Key pair generated.[/green] Fingerprint: {get_key_fingerprint(key_dir)}")
|
|
567
715
|
info("")
|
|
568
716
|
|
|
569
|
-
# 2. Deploy
|
|
717
|
+
# 2. Deploy via the appropriate method
|
|
718
|
+
if settings.hosting_method == "tailscale":
|
|
719
|
+
return _auto_deploy_key_tailscale(info, key_dir, domain)
|
|
720
|
+
return _auto_deploy_key_github(info, settings, key_dir, domain)
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def _auto_deploy_key_tailscale(
|
|
724
|
+
info: _InfoFn,
|
|
725
|
+
key_dir: Path,
|
|
726
|
+
domain: str,
|
|
727
|
+
) -> bool:
|
|
728
|
+
"""Deploy key via Tailscale Funnel and wait."""
|
|
729
|
+
from tescmd.crypto.keys import load_public_key_pem
|
|
730
|
+
from tescmd.deploy.tailscale_serve import (
|
|
731
|
+
deploy_public_key_tailscale,
|
|
732
|
+
get_key_url,
|
|
733
|
+
start_key_serving,
|
|
734
|
+
validate_tailscale_key_url,
|
|
735
|
+
wait_for_tailscale_deployment,
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
# Already deployed?
|
|
739
|
+
if run_async(validate_tailscale_key_url(domain)):
|
|
740
|
+
info(f"Public key: [green]already accessible[/green] at {get_key_url(domain)}")
|
|
741
|
+
info("")
|
|
742
|
+
return True
|
|
743
|
+
|
|
744
|
+
info("Deploying public key via Tailscale Funnel...")
|
|
745
|
+
pem = load_public_key_pem(key_dir)
|
|
746
|
+
run_async(deploy_public_key_tailscale(pem))
|
|
747
|
+
run_async(start_key_serving())
|
|
748
|
+
|
|
749
|
+
info("[green]Tailscale serve + Funnel started.[/green]")
|
|
750
|
+
info("Waiting for key to become accessible...")
|
|
751
|
+
|
|
752
|
+
deployed = run_async(wait_for_tailscale_deployment(domain))
|
|
753
|
+
if deployed:
|
|
754
|
+
info(f"[green]Key is live at:[/green] {get_key_url(domain)}")
|
|
755
|
+
info("")
|
|
756
|
+
return True
|
|
757
|
+
|
|
758
|
+
info("[yellow]Key deployed but not yet accessible.[/yellow]")
|
|
759
|
+
info(
|
|
760
|
+
" Run [cyan]tescmd key validate[/cyan] to check, then [cyan]tescmd auth register[/cyan]."
|
|
761
|
+
)
|
|
762
|
+
info("")
|
|
763
|
+
return False
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def _auto_deploy_key_github(
|
|
767
|
+
info: _InfoFn,
|
|
768
|
+
settings: AppSettings,
|
|
769
|
+
key_dir: Path,
|
|
770
|
+
domain: str,
|
|
771
|
+
) -> bool:
|
|
772
|
+
"""Deploy key via GitHub Pages and wait."""
|
|
773
|
+
from tescmd.crypto.keys import load_public_key_pem
|
|
570
774
|
from tescmd.deploy.github_pages import (
|
|
571
775
|
deploy_public_key,
|
|
572
776
|
get_key_url,
|
|
@@ -632,6 +836,10 @@ def _remediate_412(info: _InfoFn, domain: str) -> None:
|
|
|
632
836
|
info(" 2. Open your application")
|
|
633
837
|
info(" 3. Set [cyan]Allowed Origin URL[/cyan] to:")
|
|
634
838
|
info(f" [bold]https://{domain}[/bold]")
|
|
839
|
+
info(
|
|
840
|
+
" [dim]For telemetry streaming, also add your Tailscale origin"
|
|
841
|
+
" (e.g. https://<machine>.tailnet.ts.net)[/dim]"
|
|
842
|
+
)
|
|
635
843
|
info(" 4. Save, then re-run [cyan]tescmd setup[/cyan]")
|
|
636
844
|
|
|
637
845
|
|