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
|
@@ -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.
|
|
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
|
-
|
|
51
|
-
|
|
53
|
+
[](https://pypi.org/project/tescmd/)
|
|
54
|
+
[](https://pypi.org/project/tescmd/)
|
|
55
|
+
[](https://github.com/oceanswave/tescmd/actions/workflows/test.yml)
|
|
52
56
|
[](LICENSE)
|
|
57
|
+
[](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
|
|
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
|
-
|
|
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
|
-
|
|
113
|
-
tescmd key enroll 5YJ3E1EA1NF000000
|
|
96
|
+
After setup completes, you can start using commands:
|
|
114
97
|
|
|
115
|
-
|
|
116
|
-
tescmd
|
|
117
|
-
tescmd
|
|
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 |
|
|
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
|
-
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
37
|
-
tescmd/cli/main.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
52
|
-
tescmd/crypto/ecdh.py,sha256=
|
|
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/
|
|
57
|
-
tescmd/models/
|
|
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=
|
|
60
|
-
tescmd/models/energy.py,sha256=
|
|
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=
|
|
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
|
|
78
|
-
tescmd
|
|
79
|
-
tescmd
|
|
80
|
-
tescmd
|
|
81
|
-
tescmd
|
|
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
|
|
File without changes
|
|
File without changes
|