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.
Files changed (47) hide show
  1. tescmd/__init__.py +1 -1
  2. tescmd/api/client.py +8 -1
  3. tescmd/api/errors.py +8 -0
  4. tescmd/api/vehicle.py +19 -1
  5. tescmd/cache/response_cache.py +3 -2
  6. tescmd/cli/auth.py +30 -2
  7. tescmd/cli/key.py +149 -14
  8. tescmd/cli/main.py +44 -0
  9. tescmd/cli/setup.py +230 -22
  10. tescmd/cli/vehicle.py +464 -1
  11. tescmd/crypto/__init__.py +3 -1
  12. tescmd/crypto/ecdh.py +9 -0
  13. tescmd/crypto/schnorr.py +191 -0
  14. tescmd/deploy/tailscale_serve.py +154 -0
  15. tescmd/models/__init__.py +0 -2
  16. tescmd/models/auth.py +19 -0
  17. tescmd/models/config.py +1 -0
  18. tescmd/models/energy.py +0 -9
  19. tescmd/protocol/session.py +10 -3
  20. tescmd/telemetry/__init__.py +19 -0
  21. tescmd/telemetry/dashboard.py +227 -0
  22. tescmd/telemetry/decoder.py +284 -0
  23. tescmd/telemetry/fields.py +248 -0
  24. tescmd/telemetry/flatbuf.py +162 -0
  25. tescmd/telemetry/protos/__init__.py +4 -0
  26. tescmd/telemetry/protos/vehicle_alert.proto +31 -0
  27. tescmd/telemetry/protos/vehicle_alert_pb2.py +42 -0
  28. tescmd/telemetry/protos/vehicle_alert_pb2.pyi +44 -0
  29. tescmd/telemetry/protos/vehicle_connectivity.proto +23 -0
  30. tescmd/telemetry/protos/vehicle_connectivity_pb2.py +40 -0
  31. tescmd/telemetry/protos/vehicle_connectivity_pb2.pyi +33 -0
  32. tescmd/telemetry/protos/vehicle_data.proto +768 -0
  33. tescmd/telemetry/protos/vehicle_data_pb2.py +136 -0
  34. tescmd/telemetry/protos/vehicle_data_pb2.pyi +1336 -0
  35. tescmd/telemetry/protos/vehicle_error.proto +23 -0
  36. tescmd/telemetry/protos/vehicle_error_pb2.py +44 -0
  37. tescmd/telemetry/protos/vehicle_error_pb2.pyi +39 -0
  38. tescmd/telemetry/protos/vehicle_metric.proto +22 -0
  39. tescmd/telemetry/protos/vehicle_metric_pb2.py +44 -0
  40. tescmd/telemetry/protos/vehicle_metric_pb2.pyi +37 -0
  41. tescmd/telemetry/server.py +293 -0
  42. tescmd/telemetry/tailscale.py +300 -0
  43. {tescmd-0.1.2.dist-info → tescmd-0.2.0.dist-info}/METADATA +72 -35
  44. {tescmd-0.1.2.dist-info → tescmd-0.2.0.dist-info}/RECORD +47 -22
  45. {tescmd-0.1.2.dist-info → tescmd-0.2.0.dist-info}/WHEEL +0 -0
  46. {tescmd-0.1.2.dist-info → tescmd-0.2.0.dist-info}/entry_points.txt +0 -0
  47. {tescmd-0.1.2.dist-info → tescmd-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,300 @@
1
+ """Tailscale integration for Fleet Telemetry Funnel setup.
2
+
3
+ Manages Tailscale presence checking, Funnel start/stop, and TLS
4
+ certificate retrieval — all via CLI subprocess (Funnel has no API).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import json
11
+ import logging
12
+ import shutil
13
+ import sys
14
+ import tempfile
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ from tescmd.api.errors import TailscaleError
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ _SUBPROCESS_TIMEOUT = 15 # seconds
23
+
24
+
25
+ class TailscaleManager:
26
+ """Manages Tailscale for Fleet Telemetry Funnel setup."""
27
+
28
+ _port: int | None
29
+ _funnel_started: bool
30
+
31
+ def __init__(self) -> None:
32
+ self._port = None
33
+ self._funnel_started = False
34
+
35
+ # ------------------------------------------------------------------
36
+ # Checks
37
+ # ------------------------------------------------------------------
38
+
39
+ async def check_available(self) -> None:
40
+ """Verify ``tailscale`` CLI binary is on PATH.
41
+
42
+ Raises :class:`TailscaleError` with install guidance if not found.
43
+ """
44
+ if shutil.which("tailscale") is None:
45
+ platform = sys.platform
46
+ if platform == "darwin":
47
+ hint = "Install via: brew install tailscale"
48
+ elif platform == "linux":
49
+ hint = "Install via: curl -fsSL https://tailscale.com/install.sh | sh"
50
+ else:
51
+ hint = "Install from https://tailscale.com/download"
52
+ raise TailscaleError(f"Tailscale CLI not found on PATH. {hint}")
53
+
54
+ async def check_running(self) -> dict[str, Any]:
55
+ """Verify the Tailscale daemon is running and authenticated.
56
+
57
+ Returns the parsed ``tailscale status --json`` output.
58
+ Raises :class:`TailscaleError` if the daemon is not running.
59
+ """
60
+ returncode, stdout, stderr = await self._run("tailscale", "status", "--json")
61
+ if returncode != 0:
62
+ raise TailscaleError(
63
+ f"Tailscale is not running or not authenticated: {stderr.strip()}"
64
+ )
65
+ try:
66
+ status: dict[str, Any] = json.loads(stdout)
67
+ except json.JSONDecodeError as exc:
68
+ raise TailscaleError(f"Failed to parse tailscale status JSON: {exc}") from exc
69
+
70
+ backend_state = status.get("BackendState", "")
71
+ if backend_state != "Running":
72
+ raise TailscaleError(
73
+ f"Tailscale backend state is '{backend_state}', expected 'Running'. "
74
+ "Run 'tailscale up' to authenticate."
75
+ )
76
+ return status
77
+
78
+ async def get_hostname(self) -> str:
79
+ """Extract the machine DNS name from ``tailscale status --json``.
80
+
81
+ Returns e.g. ``'machine.tailnet.ts.net'`` (trailing dot stripped).
82
+ """
83
+ status = await self.check_running()
84
+
85
+ # Self node info
86
+ self_node = status.get("Self", {})
87
+ dns_name: str = self_node.get("DNSName", "")
88
+ if not dns_name:
89
+ raise TailscaleError(
90
+ "Could not determine Tailscale hostname. "
91
+ "Ensure MagicDNS is enabled in your tailnet."
92
+ )
93
+ return dns_name.rstrip(".")
94
+
95
+ async def check_funnel_available(self) -> bool:
96
+ """Check if Funnel is enabled in tailnet ACL.
97
+
98
+ Runs ``tailscale funnel status`` to probe availability.
99
+ Returns False (without raising) if Funnel is not available.
100
+ """
101
+ try:
102
+ returncode, _stdout, _stderr = await self._run(
103
+ "tailscale",
104
+ "funnel",
105
+ "status",
106
+ )
107
+ return returncode == 0
108
+ except TailscaleError:
109
+ return False
110
+
111
+ # ------------------------------------------------------------------
112
+ # Serve management (static file hosting)
113
+ # ------------------------------------------------------------------
114
+
115
+ async def start_serve(self, path: str, target: str | Path) -> None:
116
+ """Serve a local directory at a URL path prefix.
117
+
118
+ Runs: ``tailscale serve --bg --set-path <path> <target>``
119
+
120
+ Args:
121
+ path: URL path prefix (e.g. ``/.well-known/``).
122
+ target: Local directory to serve.
123
+ """
124
+ returncode, stdout, stderr = await self._run(
125
+ "tailscale",
126
+ "serve",
127
+ "--bg",
128
+ "--set-path",
129
+ path,
130
+ str(target),
131
+ )
132
+ if returncode != 0:
133
+ msg = stderr.strip() or stdout.strip()
134
+ raise TailscaleError(f"Failed to start Tailscale serve: {msg}")
135
+ logger.info("Tailscale serve started: %s -> %s", path, target)
136
+
137
+ async def stop_serve(self, path: str) -> None:
138
+ """Remove a serve handler for a path.
139
+
140
+ Runs: ``tailscale serve --bg --set-path <path> off``
141
+ """
142
+ returncode, stdout, stderr = await self._run(
143
+ "tailscale",
144
+ "serve",
145
+ "--bg",
146
+ "--set-path",
147
+ path,
148
+ "off",
149
+ )
150
+ if returncode != 0:
151
+ logger.warning(
152
+ "Failed to stop Tailscale serve for %s (may already be stopped): %s",
153
+ path,
154
+ stderr.strip() or stdout.strip(),
155
+ )
156
+ logger.info("Tailscale serve stopped for path: %s", path)
157
+
158
+ async def enable_funnel(self) -> None:
159
+ """Enable Funnel on port 443 (expose all serve handlers publicly).
160
+
161
+ Runs: ``tailscale funnel --bg 443``
162
+ """
163
+ returncode, stdout, stderr = await self._run(
164
+ "tailscale",
165
+ "funnel",
166
+ "--bg",
167
+ "443",
168
+ )
169
+ if returncode != 0:
170
+ msg = stderr.strip() or stdout.strip()
171
+ raise TailscaleError(f"Failed to enable Tailscale Funnel: {msg}")
172
+ logger.info("Tailscale Funnel enabled on port 443")
173
+
174
+ # ------------------------------------------------------------------
175
+ # Funnel management (port proxying for telemetry)
176
+ # ------------------------------------------------------------------
177
+
178
+ async def start_funnel(self, port: int) -> str:
179
+ """Start Tailscale Funnel proxying to a local port.
180
+
181
+ Args:
182
+ port: Local port the WebSocket server is listening on.
183
+
184
+ Returns:
185
+ The public HTTPS URL (e.g. ``https://machine.tailnet.ts.net``).
186
+
187
+ Raises:
188
+ TailscaleError: If Funnel cannot be started.
189
+ """
190
+ hostname = await self.get_hostname()
191
+
192
+ # tailscale funnel --bg <port> starts background proxy on 443
193
+ returncode, stdout, stderr = await self._run(
194
+ "tailscale",
195
+ "funnel",
196
+ "--bg",
197
+ str(port),
198
+ )
199
+ if returncode != 0:
200
+ msg = stderr.strip() or stdout.strip()
201
+ raise TailscaleError(f"Failed to start Tailscale Funnel: {msg}")
202
+
203
+ self._port = port
204
+ self._funnel_started = True
205
+ url = f"https://{hostname}"
206
+ logger.info("Tailscale Funnel started: %s -> localhost:%d", url, port)
207
+ return url
208
+
209
+ async def stop_funnel(self) -> None:
210
+ """Stop Tailscale Funnel. Idempotent — safe to call if not started."""
211
+ if not self._funnel_started:
212
+ return
213
+
214
+ returncode, stdout, stderr = await self._run(
215
+ "tailscale",
216
+ "funnel",
217
+ "--bg",
218
+ "off",
219
+ )
220
+ if returncode != 0:
221
+ logger.warning(
222
+ "Failed to stop Tailscale Funnel (may already be stopped): %s",
223
+ stderr.strip() or stdout.strip(),
224
+ )
225
+ self._funnel_started = False
226
+ self._port = None
227
+ logger.info("Tailscale Funnel stopped")
228
+
229
+ async def get_cert_pem(self) -> str:
230
+ """Retrieve the TLS certificate chain for the Funnel hostname.
231
+
232
+ Uses ``tailscale cert`` to fetch/renew the cert, then reads the
233
+ PEM file. Returns the PEM string for the Fleet Telemetry config
234
+ ``ca`` field.
235
+ """
236
+ hostname = await self.get_hostname()
237
+
238
+ with tempfile.TemporaryDirectory() as tmpdir:
239
+ cert_path = Path(tmpdir) / f"{hostname}.crt"
240
+ key_path = Path(tmpdir) / f"{hostname}.key"
241
+
242
+ returncode, stdout, stderr = await self._run(
243
+ "tailscale",
244
+ "cert",
245
+ "--cert-file",
246
+ str(cert_path),
247
+ "--key-file",
248
+ str(key_path),
249
+ hostname,
250
+ timeout=60, # cert provisioning involves ACME/Let's Encrypt
251
+ )
252
+ if returncode != 0:
253
+ msg = stderr.strip() or stdout.strip()
254
+ raise TailscaleError(f"Failed to get Tailscale cert: {msg}")
255
+
256
+ if not cert_path.exists():
257
+ raise TailscaleError(f"tailscale cert succeeded but {cert_path} not found")
258
+
259
+ return cert_path.read_text()
260
+
261
+ # ------------------------------------------------------------------
262
+ # Internal helpers
263
+ # ------------------------------------------------------------------
264
+
265
+ @staticmethod
266
+ async def _run(*args: str, timeout: int | None = None) -> tuple[int, str, str]:
267
+ """Run a subprocess with timeout.
268
+
269
+ Returns ``(returncode, stdout, stderr)``.
270
+
271
+ Uses ``asyncio.create_subprocess_exec`` which passes arguments
272
+ directly to the OS without shell interpretation (no injection risk).
273
+
274
+ *timeout* overrides the default ``_SUBPROCESS_TIMEOUT`` when a
275
+ command is known to be slow (e.g. certificate provisioning).
276
+ """
277
+ effective_timeout = timeout if timeout is not None else _SUBPROCESS_TIMEOUT
278
+ logger.debug("Running: %s", " ".join(args))
279
+ try:
280
+ proc = await asyncio.create_subprocess_exec(
281
+ *args,
282
+ stdout=asyncio.subprocess.PIPE,
283
+ stderr=asyncio.subprocess.PIPE,
284
+ )
285
+ stdout_bytes, stderr_bytes = await asyncio.wait_for(
286
+ proc.communicate(), timeout=effective_timeout
287
+ )
288
+ except FileNotFoundError as exc:
289
+ raise TailscaleError(f"Command not found: {args[0]}") from exc
290
+ except TimeoutError as exc:
291
+ raise TailscaleError(
292
+ f"Command timed out after {effective_timeout}s: {' '.join(args)}"
293
+ ) from exc
294
+
295
+ assert proc.returncode is not None
296
+ return (
297
+ proc.returncode,
298
+ stdout_bytes.decode(errors="replace"),
299
+ stderr_bytes.decode(errors="replace"),
300
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tescmd
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Summary: A Python CLI for querying and controlling Tesla vehicles via the Fleet API
5
5
  Project-URL: Homepage, https://github.com/oceanswave/tescmd
6
6
  Project-URL: Repository, https://github.com/oceanswave/tescmd
@@ -41,15 +41,20 @@ Requires-Dist: mypy>=1.13; extra == 'dev'
41
41
  Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
42
42
  Requires-Dist: pytest-cov>=5.0; extra == 'dev'
43
43
  Requires-Dist: pytest-httpx>=0.34; extra == 'dev'
44
+ Requires-Dist: pytest-xdist>=3.0; extra == 'dev'
44
45
  Requires-Dist: pytest>=8.0; extra == 'dev'
45
46
  Requires-Dist: ruff>=0.8; extra == 'dev'
47
+ Provides-Extra: telemetry
48
+ Requires-Dist: websockets>=14.0; extra == 'telemetry'
46
49
  Description-Content-Type: text/markdown
47
50
 
48
51
  # tescmd
49
52
 
50
- <!-- [![PyPI](https://img.shields.io/pypi/v/tescmd)](https://pypi.org/project/tescmd/) -->
51
- <!-- [![Python](https://img.shields.io/pypi/pyversions/tescmd)](https://pypi.org/project/tescmd/) -->
53
+ [![PyPI](https://img.shields.io/pypi/v/tescmd)](https://pypi.org/project/tescmd/)
54
+ [![Python](https://img.shields.io/pypi/pyversions/tescmd)](https://pypi.org/project/tescmd/)
55
+ [![Build](https://img.shields.io/github/actions/workflow/status/oceanswave/tescmd/test.yml?branch=main&label=build)](https://github.com/oceanswave/tescmd/actions/workflows/test.yml)
52
56
  [![License](https://img.shields.io/github/license/oceanswave/tescmd)](LICENSE)
57
+ [![GitHub Release](https://img.shields.io/github/v/release/oceanswave/tescmd)](https://github.com/oceanswave/tescmd/releases)
53
58
 
54
59
  A Python CLI for querying and controlling Tesla vehicles via the Fleet API — built for both human operators and AI agents.
55
60
 
@@ -68,7 +73,7 @@ tescmd is designed to work as a tool that AI agents can invoke directly. Platfor
68
73
  - **Tier enforcement** — readonly tier blocks write commands with clear guidance to upgrade
69
74
  - **Energy products** — Powerwall live status, site info, backup reserve, operation mode, storm mode, time-of-use settings, charging history, calendar history, grid import/export
70
75
  - **User & sharing** — account info, region, orders, feature flags, driver management, vehicle sharing invites
71
- - **Fleet Telemetry awareness** — setup wizard highlights Fleet Telemetry streaming for up to 97% API cost reduction
76
+ - **Fleet Telemetry streaming** — `tescmd vehicle telemetry stream` starts a real-time dashboard with push-based data from your vehicle via Tailscale Funnel — no polling, 99%+ cost reduction
72
77
  - **Universal response caching** — all read commands are cached with tiered TTLs (1h for specs/warranty, 5m for fleet lists, 1m standard, 30s for location-dependent); bots can call tescmd as often as needed — within the TTL window, responses are instant and free
73
78
  - **Cost-aware wake** — prompts before sending billable wake API calls; `--wake` flag for scripts that accept the cost
74
79
  - **Guided OAuth2 setup** — `tescmd auth login` walks you through browser-based authentication with PKCE
@@ -83,40 +88,23 @@ tescmd is designed to work as a tool that AI agents can invoke directly. Platfor
83
88
 
84
89
  ```bash
85
90
  pip install tescmd
86
-
87
- # First-time setup (interactive wizard)
88
91
  tescmd setup
92
+ ```
89
93
 
90
- # Authenticate (opens browser)
91
- tescmd auth login
92
-
93
- # List your vehicles
94
- tescmd vehicle list
95
-
96
- # Get full vehicle data snapshot
97
- tescmd vehicle info
98
-
99
- # Check charge status (uses cache — second call is instant)
100
- tescmd charge status
101
-
102
- # Start charging (auto-invalidates cache)
103
- tescmd charge start --wake
104
-
105
- # Climate control
106
- tescmd climate on --wake
107
- tescmd climate set 72
108
-
109
- # Lock the car
110
- tescmd security lock --wake
94
+ That's it. The interactive setup wizard walks you through everything: creating a Tesla Developer app, generating an EC key pair, hosting the public key (via GitHub Pages or Tailscale Funnel), registering with the Fleet API, authenticating via OAuth2, and enrolling your key on a vehicle. Each step checks prerequisites and offers remediation if something is missing.
111
95
 
112
- # Enroll your key on a vehicle (required for signed commands)
113
- tescmd key enroll 5YJ3E1EA1NF000000
96
+ After setup completes, you can start using commands:
114
97
 
115
- # Cache management
116
- tescmd cache status
117
- tescmd cache clear
98
+ ```bash
99
+ tescmd charge status # Check battery and charging state
100
+ tescmd vehicle info # Full vehicle data snapshot
101
+ tescmd climate on --wake # Turn on climate (wakes vehicle if asleep)
102
+ tescmd security lock --wake # Lock the car
103
+ tescmd vehicle telemetry stream # Real-time telemetry dashboard
118
104
  ```
119
105
 
106
+ Every read command is cached — repeat calls within the TTL window are instant and free.
107
+
120
108
  ## Prerequisites
121
109
 
122
110
  The following tools should be installed and authenticated before running `tescmd setup`:
@@ -125,9 +113,11 @@ The following tools should be installed and authenticated before running `tescmd
125
113
  |------|----------|---------|------|
126
114
  | **Git** | Yes | Version control, repo management | N/A |
127
115
  | **GitHub CLI** (`gh`) | Recommended | Auto-creates `*.github.io` domain for key hosting | `gh auth login` |
128
- | **Tailscale** | Optional | Secure remote access to vehicles via Fleet Telemetry | `tailscale login` |
116
+ | **Tailscale** | Optional | Key hosting via Funnel + Fleet Telemetry streaming | `tailscale login` |
117
+
118
+ Without the GitHub CLI, `tescmd setup` will try Tailscale Funnel for key hosting (requires Funnel enabled in your tailnet ACL). Without either, you'll need to manually host your public key at the Tesla-required `.well-known` path on your own domain.
129
119
 
130
- Without the GitHub CLI, you'll need to manually host your public key at the Tesla-required `.well-known` path on your own domain. Tailscale is only needed if you plan to use Fleet Telemetry streaming for reduced API costs.
120
+ For telemetry streaming, you need **Tailscale** with Funnel enabled.
131
121
 
132
122
  ## Installation
133
123
 
@@ -210,7 +200,7 @@ Check which backend is active with `tescmd status` — the output includes a `To
210
200
  |---|---|---|
211
201
  | `setup` | *(interactive wizard)* | First-run configuration: client ID, secret, region, domain, key enrollment |
212
202
  | `auth` | `login`, `logout`, `status`, `refresh`, `register`, `export`, `import` | OAuth2 authentication lifecycle |
213
- | `vehicle` | `list`, `get`, `info`, `data`, `location`, `wake`, `rename`, `mobile-access`, `nearby-chargers`, `alerts`, `release-notes`, `service`, `drivers`, `calendar`, `subscriptions`, `upgrades`, `options`, `specs`, `warranty`, `fleet-status`, `low-power`, `accessory-power`, `telemetry {config,create,delete,errors}` | Vehicle discovery, state queries, fleet telemetry, power management |
203
+ | `vehicle` | `list`, `get`, `info`, `data`, `location`, `wake`, `rename`, `mobile-access`, `nearby-chargers`, `alerts`, `release-notes`, `service`, `drivers`, `calendar`, `subscriptions`, `upgrades`, `options`, `specs`, `warranty`, `fleet-status`, `low-power`, `accessory-power`, `telemetry {config,create,delete,errors,stream}` | Vehicle discovery, state queries, fleet telemetry streaming, power management |
214
204
  | `charge` | `status`, `start`, `stop`, `limit`, `limit-max`, `limit-std`, `amps`, `port-open`, `port-close`, `schedule`, `departure`, `precondition-add`, `precondition-remove`, `add-schedule`, `remove-schedule`, `clear-schedules`, `clear-preconditions`, `managed-amps`, `managed-location`, `managed-schedule` | Charge queries, control, scheduling, and fleet management |
215
205
  | `billing` | `history`, `sessions`, `invoice` | Supercharger billing history and invoices |
216
206
  | `climate` | `status`, `on`, `off`, `set`, `precondition`, `seat`, `seat-cool`, `wheel-heater`, `overheat`, `bioweapon`, `keeper`, `cop-temp`, `auto-seat`, `auto-wheel`, `wheel-level` | Climate, seat, and steering wheel control |
@@ -355,6 +345,41 @@ Configure via environment variables:
355
345
  | `TESLA_CACHE_TTL` | `60` | Time-to-live in seconds |
356
346
  | `TESLA_CACHE_DIR` | `~/.cache/tescmd` | Cache directory path |
357
347
 
348
+ ## Fleet Telemetry Streaming
349
+
350
+ Tesla's Fleet Telemetry lets your vehicle push real-time data directly to your server — no polling, no per-request charges. tescmd handles all the setup:
351
+
352
+ ```bash
353
+ # Install telemetry dependencies
354
+ pip install tescmd[telemetry]
355
+
356
+ # Stream real-time data (Rich dashboard in TTY, JSONL when piped)
357
+ tescmd vehicle telemetry stream
358
+
359
+ # Select field presets
360
+ tescmd vehicle telemetry stream --fields driving # Speed, location, power
361
+ tescmd vehicle telemetry stream --fields charging # Battery, voltage, current
362
+ tescmd vehicle telemetry stream --fields climate # Temps, HVAC state
363
+ tescmd vehicle telemetry stream --fields all # Everything (120+ fields)
364
+
365
+ # Override polling interval
366
+ tescmd vehicle telemetry stream --interval 5 # Every 5 seconds
367
+
368
+ # JSONL output for scripting
369
+ tescmd vehicle telemetry stream --format json | jq .
370
+ ```
371
+
372
+ **Requires Tailscale** with Funnel enabled. The stream command starts a local WebSocket server, exposes it via Tailscale Funnel (handles TLS + NAT traversal), configures Tesla to push data to it, and renders an interactive dashboard with live uptime counter, unit conversion, and connection status. Press `q` to stop — cleanup messages show each step (removing telemetry config, restoring partner domain, stopping tunnel).
373
+
374
+ ### Telemetry vs Polling Costs
375
+
376
+ | Approach | 1 vehicle, 5-second interval, 24 hours | Monthly cost estimate |
377
+ |---|---|---|
378
+ | **Polling `vehicle_data`** | ~17,280 requests × $0.001 = **$17/day** | **$500+/month** |
379
+ | **Fleet Telemetry streaming** | 1 config create + 1 config delete = **2 requests** | **< $0.01/month** |
380
+
381
+ Fleet Telemetry streaming is a flat-cost alternative: you pay only for the initial config setup and teardown, regardless of how much data flows. The tradeoff is that you need Tailscale running on a machine to receive the push.
382
+
358
383
  ## Key Enrollment & Vehicle Command Protocol
359
384
 
360
385
  Newer Tesla vehicles require commands to be signed using the [Vehicle Command Protocol](https://github.com/teslamotors/vehicle-command). tescmd handles this transparently:
@@ -449,6 +474,18 @@ Run this periodically or after modifying API methods to catch drift.
449
474
 
450
475
  See [docs/development.md](docs/development.md) for detailed contribution guidelines.
451
476
 
477
+ ## Documentation
478
+
479
+ - [Setup Guide](docs/setup.md) — step-by-step walkthrough of `tescmd setup`
480
+ - [FAQ](docs/faq.md) — common questions about tescmd, costs, hosting, and configuration
481
+ - [Command Reference](docs/commands.md) — detailed usage for every command
482
+ - [API Costs](docs/api-costs.md) — detailed cost breakdown and savings calculations
483
+ - [Bot Integration](docs/bot-integration.md) — JSON schema, exit codes, telemetry streaming, headless auth
484
+ - [Architecture](docs/architecture.md) — layered design, module responsibilities, design decisions
485
+ - [Vehicle Command Protocol](docs/vehicle-command-protocol.md) — ECDH sessions and signed commands
486
+ - [Authentication](docs/authentication.md) — OAuth2 PKCE flow, token storage, scopes
487
+ - [Development](docs/development.md) — contribution guidelines, testing, linting
488
+
452
489
  ## Changelog
453
490
 
454
491
  See [CHANGELOG.md](CHANGELOG.md) for release history.
@@ -1,4 +1,4 @@
1
- tescmd/__init__.py,sha256=9pBEWtaf-0va5j9CIk56XKKnJG0W0INFsj2hROYYa-I,116
1
+ tescmd/__init__.py,sha256=o_SIbXUPkIxWQmUmtk7DOL4K90g-89A7oWcV4n0DO2Q,116
2
2
  tescmd/__main__.py,sha256=ecNCDo0sINhjJZiauhAcUMU67U6XUCU23ocf7vQG45E,83
3
3
  tescmd/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  tescmd/_internal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -7,15 +7,15 @@ tescmd/_internal/permissions.py,sha256=cawM9XIHJKNqi5cGYssF5iPNi_F4gONLhyLOJCbzZ
7
7
  tescmd/_internal/vin.py,sha256=VlliRJzsq0in7Sny-i-ng6rMkNjCD-rJ6sIHiq3uxT8,1133
8
8
  tescmd/api/__init__.py,sha256=OPKuH6YMSJU5PO4_0cuTXx5KYTmc0aR3SU8z695AZ7U,36
9
9
  tescmd/api/charging.py,sha256=Se58NcLTSxCNZvpBCMJswMoI-bYhWV_MM4LHmry9_Tw,3464
10
- tescmd/api/client.py,sha256=XKtx1ywi0CxdEUaOOnpOAh09kStF3de0iyQCLAmlN9M,6939
10
+ tescmd/api/client.py,sha256=fZbrGSwa8UqgPosFaZwRyCOxni7fCs7sGtEkZcIP6gg,7183
11
11
  tescmd/api/command.py,sha256=TpVb2raih3S_cYopJmmm4bV_O0WDB1NgQQkr_yuBRNA,22402
12
12
  tescmd/api/energy.py,sha256=_XOv-er9379Cza8IkE5Gjtc1FyzwsLfPdsPQR2rA1Us,5562
13
- tescmd/api/errors.py,sha256=oyfWc3xdycGeLbBEV7uu_DCh_gCqhf78Xt3_nIV9pgc,1963
13
+ tescmd/api/errors.py,sha256=B2Q9k5Z8caj7YzgkwN9nfeH0-inb-Tlngzg2fkpEJY4,2163
14
14
  tescmd/api/partner.py,sha256=f5VWMY8V5oeiDcYduq7zxihWacdzCd6wBtPIXXwwkzY,1405
15
15
  tescmd/api/sharing.py,sha256=qjlP3B8jo4hYqNOIiXiDYHxUwMHT3FAz87SLH8Y6fSM,2475
16
16
  tescmd/api/signed_command.py,sha256=hMegcCnoYub0eQ3rRq0_-990tgfaWeZSWTbnQDQ_wwY,10136
17
17
  tescmd/api/user.py,sha256=pz4sB9i-9pHukdIBOwGrgfRpWf4zd5gnqAqm3zAJpcA,1411
18
- tescmd/api/vehicle.py,sha256=JYgWnK4ffissI1aAkA2uY6Jcpc-w441A5xG8_yZ1BGw,6603
18
+ tescmd/api/vehicle.py,sha256=esCCnyWyUz7QNGfzmbrGx824YZkDmSI_LsemsA6faDU,7377
19
19
  tescmd/auth/__init__.py,sha256=PHsT1-mgm9b-4xft0-adq3i6fwyCSj2of4KTpMAYUhw,76
20
20
  tescmd/auth/oauth.py,sha256=kgp_-Zk7zJqLIpSB9yOZLIWjXdD7WnG7nvyfZC0lKFY,9820
21
21
  tescmd/auth/server.py,sha256=3h8NELWNMe9DfMu7NsfA00sc34D6mGoaFQTqPSOOdzE,3602
@@ -23,41 +23,43 @@ tescmd/auth/token_store.py,sha256=v2rx3oj3dwaMn8-yz_ge8RD22joUmfP_ZJ4rhJaUw0g,86
23
23
  tescmd/ble/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
24
  tescmd/cache/__init__.py,sha256=V-oZyDgU4Dr6cExNVdGcsEbF6hrnOYml7VV6ZoOiyJY,249
25
25
  tescmd/cache/keys.py,sha256=dcof4GM1gZEADNBx8xHtjCCIK_4ra3FxPeRVJYPoFow,1736
26
- tescmd/cache/response_cache.py,sha256=1X-zr_-QLR5i9epH4ZobZeDO4bp482QO51YsJTI6ImE,7375
26
+ tescmd/cache/response_cache.py,sha256=HYbAkT2uIUQf3rNUiEZdW6wW6iq45w8nPyo9oY9dlJM,7385
27
27
  tescmd/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
28
  tescmd/cli/_client.py,sha256=lr_ugunILZLLklPQ3syO3nGqzmyqxNh93eFQtxbVojs,21503
29
29
  tescmd/cli/_options.py,sha256=lM4-lvVDLv89PahshWIRRoNUXBki4DOabt_pYaPlK-U,4175
30
- tescmd/cli/auth.py,sha256=WqmJh1DdQnuoS7YHtSZ5s-8RlMxa1nEXtHvgOjMjN-U,22163
30
+ tescmd/cli/auth.py,sha256=GK0_8n4fCQtFgOYoFN9YpHKlmbMCcHsNonlQcEAvqjk,23168
31
31
  tescmd/cli/billing.py,sha256=BcuAI10Q2Tf6fobejoKsaO4qVN7TcOy95HSdylBn23M,7946
32
32
  tescmd/cli/cache.py,sha256=LaOOfHS1ycF8LoCFdcUQfw11PYor2S__-SdWtRpA_pA,2729
33
33
  tescmd/cli/charge.py,sha256=TOXRn7YGuFYj5GimBhh2bTOB_5vzU5sc86PODifHXGk,20088
34
34
  tescmd/cli/climate.py,sha256=npekNHOcJ63oG1-2o-fZiTRWFmemdZ1Tl_pASWwWBlI,16890
35
35
  tescmd/cli/energy.py,sha256=L7I1o3BucT71MGSsdYENN40iQKANXioNcUK19mj9wz8,11855
36
- tescmd/cli/key.py,sha256=kMOfP4R-71T5mfj_C8q3JQV5jqvrJ3BCYDsgluk_nFQ,21666
37
- tescmd/cli/main.py,sha256=PXMEJCnWtBlTnKON8K4I8LVkd8zmeF5kW9L6O7Qa050,20259
36
+ tescmd/cli/key.py,sha256=ehxbxtTQ7pj6vAoqR3P8ak0FE0-nmQQm4TDAX878n1M,26356
37
+ tescmd/cli/main.py,sha256=OmSso0JRCPFnt5bFYMbBlVcK1vbQxA3lETmC0OicTmk,21750
38
38
  tescmd/cli/media.py,sha256=PQ-QJkjkIiM8IJ5CXtDOY0N1yC3DlaC5YLnebxT1nrI,4267
39
39
  tescmd/cli/nav.py,sha256=22UxJDltcE9S3SfrLwtBfh51HDEm8vxun0KrQzTvZz0,7519
40
40
  tescmd/cli/partner.py,sha256=0GfDCwM_Kfy5g95JwUhrrsjDFn9lSbkuNgfquIQqcSo,3488
41
41
  tescmd/cli/raw.py,sha256=hj5hZs8tHHHAeXtpf7kHXewciXDvl0RlMJkPK7i3ouY,2238
42
42
  tescmd/cli/security.py,sha256=laoRaF2mBda5q_jWCXyBjNCU98hsQ2Pr1a_MNWdQF3E,15713
43
- tescmd/cli/setup.py,sha256=L8lcP7bvOxXNNLkRR89ViIm7c4r5v9fj9sAa0yYTdJw,25723
43
+ tescmd/cli/setup.py,sha256=AVl3NLfM8yCHfqyfhu9d_Jyb3F53CkdP6e3VfnAOisU,32722
44
44
  tescmd/cli/sharing.py,sha256=IIhkPlLb8sKkPmgMKVDgWTndCd5ZRvgKnZbknxYCBFc,6211
45
45
  tescmd/cli/software.py,sha256=f3Jl5GRruE_wNHl3jfLbVQHEXQvQxe4y-UaNIY1j76Y,2711
46
46
  tescmd/cli/status.py,sha256=T6YoG_BE24fJSmc24Uhw0IdH5mUUDySgGjn9s1LY8yc,4003
47
47
  tescmd/cli/trunk.py,sha256=8ktL7_TpTXphQdrjHMUeDAP149ULB3naZvUFOoBh880,7577
48
48
  tescmd/cli/user.py,sha256=eit0pQhku1l53x3Gl4um6l6c_wgFl9dU7OhyFd1g4gQ,4100
49
- tescmd/cli/vehicle.py,sha256=q8767lw9u7XLnUpVwfaABCv3WnDtD0LI54OiGLpq_Io,27531
49
+ tescmd/cli/vehicle.py,sha256=JUGY4Ey9qJ4AJ-6P-5UUG_RJoyfYyONciPq8l8hM41I,46083
50
50
  tescmd/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
- tescmd/crypto/__init__.py,sha256=ctsVTIt0trOfAlq7SogJaGQRIyly1zxj_ImSHS7I6nM,406
52
- tescmd/crypto/ecdh.py,sha256=2DlJcK7qxiBYRcAiWlJuj3INogid83tQeMIJ32DAb84,1366
51
+ tescmd/crypto/__init__.py,sha256=ZcXLJ9OOATi7R8m9zD3K8R5Zj0QBPq6QUavpUPetn18,499
52
+ tescmd/crypto/ecdh.py,sha256=xFe4BR1G5CznwLUJnWvOWr_ZHsWUpdu9IJlL3-LaAk4,1759
53
53
  tescmd/crypto/keys.py,sha256=jzFZwZKMYFlHGPeTSrw5pVLBwjxvLV57LzTK9d4-caM,4037
54
+ tescmd/crypto/schnorr.py,sha256=qgISnRMDrH-LkinjLoapV8wsoVNdwBnCZvLPMgGIGyo,6626
54
55
  tescmd/deploy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
55
56
  tescmd/deploy/github_pages.py,sha256=8W0Xjn8XIuLAHhiEDo-Zn6KrlbgdkVAe_2wWxdDS4_Y,8150
56
- tescmd/models/__init__.py,sha256=bHDWLA8QPxfTe6RGlr-TcOST2fqUFBxjs3l4HKzayvw,1801
57
- tescmd/models/auth.py,sha256=5J4U8dEvSmSegkU8Dhhdo6Rr-SDssXccOL8uQDZKBsA,2815
57
+ tescmd/deploy/tailscale_serve.py,sha256=2wBHTmpF3pXtasKCI9uWCkGtHCUZL1e_keuy3H0E3Dg,4771
58
+ tescmd/models/__init__.py,sha256=mo6ngQp9m-244UGykbgY-DoqnunLD4DwEqHdJwP9GLc,1767
59
+ tescmd/models/auth.py,sha256=lVfJa6BgxbSbo7rAziGbh8tGy598yfRqm8I7jXpb4_M,3456
58
60
  tescmd/models/command.py,sha256=6gK9TjThXU5cKa_B3xOYpaLaGixxYp5iEhjQEMlh0Ws,388
59
- tescmd/models/config.py,sha256=BHq7Ui2OwkYokK3hGHR1R5wCKpWXor9Oqlm0lI4ieQY,1921
60
- tescmd/models/energy.py,sha256=u7NBa_ee3kd4C-lmPqqkRrYwZvb3g_z34EHCuGZuLHc,1429
61
+ tescmd/models/config.py,sha256=BjL_oJjyH7vsJY0JruDb0Z8nMRiDB1m34KYqmRBYD6E,2001
62
+ tescmd/models/energy.py,sha256=xj1nEdEHGkfrj4Wd-pDKZKZna40r7R58huowqoPMeZk,1237
61
63
  tescmd/models/sharing.py,sha256=sJv4fCRT5AUkcqmdK8bN2Y8hpuHITEnxNp_BlbX7L4Y,609
62
64
  tescmd/models/user.py,sha256=2hATFqUppheKtWDFg3axIRmxX0gmPTdwgMwZJGSQhtc,770
63
65
  tescmd/models/vehicle.py,sha256=GXkQOAB406JQI4vI7xhEbWB1hK5H0SLhzwtJEXPEsTw,5733
@@ -70,12 +72,35 @@ tescmd/protocol/commands.py,sha256=_pXDyXAUZYAJyIrgsmLlxnRrBXajnFWtDB6JDuR9fPU,8
70
72
  tescmd/protocol/encoder.py,sha256=L4_qtsn1cSi3BbkDvTj1JjFuviXKIvrKtuJzlBud8XA,3543
71
73
  tescmd/protocol/metadata.py,sha256=Hb9QaoGRz54z04bKIQigSD5vJKphQNLu3bcUHEW_tl8,4076
72
74
  tescmd/protocol/payloads.py,sha256=l1DiJjdkDsXurs-QSfe8s-mwyCxCOrk6syZhvjei4Io,24093
73
- tescmd/protocol/session.py,sha256=EbijNW174_ssLqt1rHmoWiU07TsYiY2xENssvWi0mI0,11405
75
+ tescmd/protocol/session.py,sha256=s8eiyxb2mdYnKN1w5dRU8DCpolrcpU3pbW7z8GKXm4w,11682
74
76
  tescmd/protocol/signer.py,sha256=L-OVqCk9XZBuFrLGeATzJSqzMm5JWfOde4hRgL8_xAs,2794
75
77
  tescmd/protocol/protobuf/__init__.py,sha256=hAvTsN4tkJMoKhLsjnrO_XepHbhrK7PfymBCpah9hUg,291
76
78
  tescmd/protocol/protobuf/messages.py,sha256=8vBH8i-1--FrR5ydSJFAn9UwNMIrSkm6henqWkOMXxU,21124
77
- tescmd-0.1.2.dist-info/METADATA,sha256=LKAwyRI-f8vO9mAwSuZCXRkz0vh-7FCxGBY2gIFPzpA,21608
78
- tescmd-0.1.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
79
- tescmd-0.1.2.dist-info/entry_points.txt,sha256=e-Uk81_gfLu4XzJl9bv6-bUIodJbnxAgfR5ugFyeD2E,48
80
- tescmd-0.1.2.dist-info/licenses/LICENSE,sha256=gFEbRZ5xHSPxkT3OgbLFhDWVUxZv80kFDnv0t3G1E7M,1070
81
- tescmd-0.1.2.dist-info/RECORD,,
79
+ tescmd/telemetry/__init__.py,sha256=DC0rddsgzFjwHhBstkDJXq_6By3l6fLeX_0AKZy5rMc,568
80
+ tescmd/telemetry/dashboard.py,sha256=A4Kqr-fulLqYgYH84zxDj1FXc1Mf9XtzvDuXjo6HJXA,8267
81
+ tescmd/telemetry/decoder.py,sha256=sk7EYFXjPQ1Qrq8Yqa0F_dYNg5-WA_w2mc6BI2aPuQg,9592
82
+ tescmd/telemetry/fields.py,sha256=oH82jKjRe8x_iy70J_dwFbxjVQSQaUZWnHsu2m8HT4I,8125
83
+ tescmd/telemetry/flatbuf.py,sha256=2q4LvlL6SAabTrQg7bS5qrgokO9GylenWQKna363th8,5508
84
+ tescmd/telemetry/server.py,sha256=p5qCdJKfQecMRMCtsKjopxXJHx8mBLBF6_EwQaF6fY8,10220
85
+ tescmd/telemetry/tailscale.py,sha256=OLbDdEKhIMkVAJRxGRdsez648NKImbzmSYFuWmz1os8,10497
86
+ tescmd/telemetry/protos/__init__.py,sha256=VAyp9-euADAEHyi533DRswRK5yEGdL08nXCTiWysgqM,150
87
+ tescmd/telemetry/protos/vehicle_alert.proto,sha256=NZnGi1-PGYBf6JpKD0ptynVOh2AOthRXfsAcvYsLLN4,708
88
+ tescmd/telemetry/protos/vehicle_alert_pb2.py,sha256=s0XzDPekU7V2w3j22EKMeOYNIEEv93PADGJhLufU-ag,2369
89
+ tescmd/telemetry/protos/vehicle_alert_pb2.pyi,sha256=dECQ-dyiVJpyYNlWcnmMixFM2iB4QOBELfv6N5S8Tok,2104
90
+ tescmd/telemetry/protos/vehicle_connectivity.proto,sha256=yWGjxSnWQMTM0XCWMMJke8KxPmProz_SB371Ve7Unw0,579
91
+ tescmd/telemetry/protos/vehicle_connectivity_pb2.py,sha256=-9O8lP85AfhKKshjpFH3fpcGKC53XVUaJIRQ35fcKA0,2137
92
+ tescmd/telemetry/protos/vehicle_connectivity_pb2.pyi,sha256=q1AIYCRDglO4-aKouyqLmOwTOGjOH9pgfwgcOHNZ5Ic,1517
93
+ tescmd/telemetry/protos/vehicle_data.proto,sha256=RZbrodJrcutpu_dHgxvw6y7sEGpUBI2bx9bedlRGaXw,21291
94
+ tescmd/telemetry/protos/vehicle_data_pb2.py,sha256=NQ6gGcCKDUYV978WYMNWHmAaqdVD4ZI9E6oLbpsc3uQ,36537
95
+ tescmd/telemetry/protos/vehicle_data_pb2.pyi,sha256=FJeKsJ2hUoEI9IE0No0MHAxyrTvspeVoKS2MZnwcemo,60073
96
+ tescmd/telemetry/protos/vehicle_error.proto,sha256=EXBKom55rZkmKx8r81GSTvtg3PQ-On-ko_Q5Ionj-EI,531
97
+ tescmd/telemetry/protos/vehicle_error_pb2.py,sha256=rrWfi9DyzN_NvHnqzJ3S_J_Ei7fdzqIs7EZX0NwO4aI,2451
98
+ tescmd/telemetry/protos/vehicle_error_pb2.pyi,sha256=RCYvv4jx4yF7kTarLEDoBz4Iayy8Um7J-r0txn57ZnY,1872
99
+ tescmd/telemetry/protos/vehicle_metric.proto,sha256=eFuBVRnhNfP6ef9KlxPMQKPH9xvsEPoF1n9q2Khbn4s,515
100
+ tescmd/telemetry/protos/vehicle_metric_pb2.py,sha256=uFg2RgxwMy-ZRnlq5bK-_MbiTowrJS2idbP4koiXyWQ,2340
101
+ tescmd/telemetry/protos/vehicle_metric_pb2.pyi,sha256=hFjmnO_D41FYO39NvCakOczCGLVVNm0WGavbnaHXkqI,1676
102
+ tescmd-0.2.0.dist-info/METADATA,sha256=NUeaLIWUmYjqETYZ7xV9D9UMS6eQYsGhAFHCNUJVInw,25156
103
+ tescmd-0.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
104
+ tescmd-0.2.0.dist-info/entry_points.txt,sha256=e-Uk81_gfLu4XzJl9bv6-bUIodJbnxAgfR5ugFyeD2E,48
105
+ tescmd-0.2.0.dist-info/licenses/LICENSE,sha256=gFEbRZ5xHSPxkT3OgbLFhDWVUxZv80kFDnv0t3G1E7M,1070
106
+ tescmd-0.2.0.dist-info/RECORD,,
File without changes