tescmd 0.2.0__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.
Files changed (61) hide show
  1. tescmd/__init__.py +1 -1
  2. tescmd/api/client.py +41 -4
  3. tescmd/api/command.py +1 -1
  4. tescmd/api/errors.py +5 -0
  5. tescmd/api/signed_command.py +19 -14
  6. tescmd/auth/oauth.py +5 -1
  7. tescmd/auth/server.py +6 -1
  8. tescmd/auth/token_store.py +8 -1
  9. tescmd/cache/response_cache.py +8 -1
  10. tescmd/cli/_client.py +142 -20
  11. tescmd/cli/_options.py +2 -4
  12. tescmd/cli/auth.py +96 -14
  13. tescmd/cli/energy.py +2 -0
  14. tescmd/cli/main.py +27 -8
  15. tescmd/cli/mcp_cmd.py +153 -0
  16. tescmd/cli/nav.py +3 -1
  17. tescmd/cli/openclaw.py +169 -0
  18. tescmd/cli/security.py +7 -1
  19. tescmd/cli/serve.py +923 -0
  20. tescmd/cli/setup.py +18 -7
  21. tescmd/cli/sharing.py +2 -0
  22. tescmd/cli/status.py +1 -1
  23. tescmd/cli/trunk.py +8 -17
  24. tescmd/cli/user.py +16 -1
  25. tescmd/cli/vehicle.py +135 -462
  26. tescmd/deploy/github_pages.py +8 -0
  27. tescmd/mcp/__init__.py +7 -0
  28. tescmd/mcp/server.py +648 -0
  29. tescmd/models/auth.py +5 -2
  30. tescmd/openclaw/__init__.py +23 -0
  31. tescmd/openclaw/bridge.py +330 -0
  32. tescmd/openclaw/config.py +167 -0
  33. tescmd/openclaw/dispatcher.py +522 -0
  34. tescmd/openclaw/emitter.py +175 -0
  35. tescmd/openclaw/filters.py +123 -0
  36. tescmd/openclaw/gateway.py +687 -0
  37. tescmd/openclaw/telemetry_store.py +53 -0
  38. tescmd/output/rich_output.py +46 -14
  39. tescmd/protocol/commands.py +2 -2
  40. tescmd/protocol/encoder.py +16 -13
  41. tescmd/protocol/payloads.py +132 -11
  42. tescmd/protocol/session.py +8 -5
  43. tescmd/protocol/signer.py +3 -17
  44. tescmd/telemetry/__init__.py +9 -0
  45. tescmd/telemetry/cache_sink.py +154 -0
  46. tescmd/telemetry/csv_sink.py +180 -0
  47. tescmd/telemetry/dashboard.py +4 -4
  48. tescmd/telemetry/fanout.py +49 -0
  49. tescmd/telemetry/fields.py +308 -129
  50. tescmd/telemetry/mapper.py +239 -0
  51. tescmd/telemetry/server.py +26 -19
  52. tescmd/telemetry/setup.py +468 -0
  53. tescmd/telemetry/tui.py +1716 -0
  54. tescmd/triggers/__init__.py +18 -0
  55. tescmd/triggers/manager.py +264 -0
  56. tescmd/triggers/models.py +93 -0
  57. {tescmd-0.2.0.dist-info → tescmd-0.3.1.dist-info}/METADATA +80 -32
  58. {tescmd-0.2.0.dist-info → tescmd-0.3.1.dist-info}/RECORD +61 -39
  59. {tescmd-0.2.0.dist-info → tescmd-0.3.1.dist-info}/WHEEL +0 -0
  60. {tescmd-0.2.0.dist-info → tescmd-0.3.1.dist-info}/entry_points.txt +0 -0
  61. {tescmd-0.2.0.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
- port = 8085
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(formatter, port, redirect_uri, domain=domain)
198
+ return _interactive_setup(
199
+ formatter, port, redirect_uri, domain=domain, tailscale_hostname=ts_hostname
200
+ )
192
201
 
193
202
 
194
203
  # ---------------------------------------------------------------------------
@@ -247,10 +256,10 @@ def _automated_domain_setup(formatter: OutputFormatter, settings: AppSettings) -
247
256
  info(f"GitHub CLI detected. Logged in as [cyan]{username}[/cyan].")
248
257
  info(f"Suggested domain: [cyan]{suggested_domain}[/cyan]")
249
258
  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]")
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]")
254
263
  info("")
255
264
 
256
265
  try:
@@ -924,7 +933,9 @@ async def _oauth_login_step(
924
933
  info("[bold]Phase 5: OAuth Login[/bold]")
925
934
  info("")
926
935
 
927
- port = 8085
936
+ from tescmd.models.auth import DEFAULT_PORT as _DEFAULT_PORT
937
+
938
+ port = _DEFAULT_PORT
928
939
  redirect_uri = f"http://localhost:{port}/callback"
929
940
 
930
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 is not None
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
- cmd_str = "vent"
208
- use_lat = lat if lat is not None else 0.0
209
- use_lon = lon if lon is not None else 0.0
210
- else:
211
- cmd_str = "close"
212
- if lat is not None and lon is not None:
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
- formatter.rich.info(f" {key}: {val}")
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]")