tescmd 0.1.2__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 (81) hide show
  1. tescmd/__init__.py +3 -0
  2. tescmd/__main__.py +5 -0
  3. tescmd/_internal/__init__.py +0 -0
  4. tescmd/_internal/async_utils.py +25 -0
  5. tescmd/_internal/permissions.py +43 -0
  6. tescmd/_internal/vin.py +44 -0
  7. tescmd/api/__init__.py +1 -0
  8. tescmd/api/charging.py +102 -0
  9. tescmd/api/client.py +189 -0
  10. tescmd/api/command.py +540 -0
  11. tescmd/api/energy.py +146 -0
  12. tescmd/api/errors.py +76 -0
  13. tescmd/api/partner.py +40 -0
  14. tescmd/api/sharing.py +65 -0
  15. tescmd/api/signed_command.py +277 -0
  16. tescmd/api/user.py +38 -0
  17. tescmd/api/vehicle.py +150 -0
  18. tescmd/auth/__init__.py +1 -0
  19. tescmd/auth/oauth.py +312 -0
  20. tescmd/auth/server.py +108 -0
  21. tescmd/auth/token_store.py +273 -0
  22. tescmd/ble/__init__.py +0 -0
  23. tescmd/cache/__init__.py +6 -0
  24. tescmd/cache/keys.py +51 -0
  25. tescmd/cache/response_cache.py +213 -0
  26. tescmd/cli/__init__.py +0 -0
  27. tescmd/cli/_client.py +603 -0
  28. tescmd/cli/_options.py +126 -0
  29. tescmd/cli/auth.py +682 -0
  30. tescmd/cli/billing.py +240 -0
  31. tescmd/cli/cache.py +85 -0
  32. tescmd/cli/charge.py +610 -0
  33. tescmd/cli/climate.py +501 -0
  34. tescmd/cli/energy.py +385 -0
  35. tescmd/cli/key.py +611 -0
  36. tescmd/cli/main.py +601 -0
  37. tescmd/cli/media.py +146 -0
  38. tescmd/cli/nav.py +242 -0
  39. tescmd/cli/partner.py +112 -0
  40. tescmd/cli/raw.py +75 -0
  41. tescmd/cli/security.py +495 -0
  42. tescmd/cli/setup.py +786 -0
  43. tescmd/cli/sharing.py +188 -0
  44. tescmd/cli/software.py +81 -0
  45. tescmd/cli/status.py +106 -0
  46. tescmd/cli/trunk.py +240 -0
  47. tescmd/cli/user.py +145 -0
  48. tescmd/cli/vehicle.py +837 -0
  49. tescmd/config/__init__.py +0 -0
  50. tescmd/crypto/__init__.py +19 -0
  51. tescmd/crypto/ecdh.py +46 -0
  52. tescmd/crypto/keys.py +122 -0
  53. tescmd/deploy/__init__.py +0 -0
  54. tescmd/deploy/github_pages.py +268 -0
  55. tescmd/models/__init__.py +85 -0
  56. tescmd/models/auth.py +108 -0
  57. tescmd/models/command.py +18 -0
  58. tescmd/models/config.py +63 -0
  59. tescmd/models/energy.py +56 -0
  60. tescmd/models/sharing.py +26 -0
  61. tescmd/models/user.py +37 -0
  62. tescmd/models/vehicle.py +185 -0
  63. tescmd/output/__init__.py +5 -0
  64. tescmd/output/formatter.py +132 -0
  65. tescmd/output/json_output.py +83 -0
  66. tescmd/output/rich_output.py +809 -0
  67. tescmd/protocol/__init__.py +23 -0
  68. tescmd/protocol/commands.py +175 -0
  69. tescmd/protocol/encoder.py +122 -0
  70. tescmd/protocol/metadata.py +116 -0
  71. tescmd/protocol/payloads.py +621 -0
  72. tescmd/protocol/protobuf/__init__.py +6 -0
  73. tescmd/protocol/protobuf/messages.py +564 -0
  74. tescmd/protocol/session.py +318 -0
  75. tescmd/protocol/signer.py +84 -0
  76. tescmd/py.typed +0 -0
  77. tescmd-0.1.2.dist-info/METADATA +458 -0
  78. tescmd-0.1.2.dist-info/RECORD +81 -0
  79. tescmd-0.1.2.dist-info/WHEEL +4 -0
  80. tescmd-0.1.2.dist-info/entry_points.txt +2 -0
  81. tescmd-0.1.2.dist-info/licenses/LICENSE +21 -0
tescmd/cli/energy.py ADDED
@@ -0,0 +1,385 @@
1
+ """CLI commands for energy products (Powerwall, Solar)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ import click
8
+
9
+ from tescmd._internal.async_utils import run_async
10
+ from tescmd.cli._client import TTL_SLOW, cached_api_call, get_energy_api, invalidate_cache_for_site
11
+ from tescmd.cli._options import global_options
12
+
13
+ if TYPE_CHECKING:
14
+ from tescmd.cli.main import AppContext
15
+
16
+ energy_group = click.Group("energy", help="Energy product commands (Powerwall, Solar)")
17
+
18
+
19
+ @energy_group.command("list")
20
+ @global_options
21
+ def list_cmd(app_ctx: AppContext) -> None:
22
+ """List energy products (Powerwalls, Solar, etc.)."""
23
+ run_async(_cmd_list(app_ctx))
24
+
25
+
26
+ async def _cmd_list(app_ctx: AppContext) -> None:
27
+ formatter = app_ctx.formatter
28
+ client, api = get_energy_api(app_ctx)
29
+ try:
30
+ products = await cached_api_call(
31
+ app_ctx,
32
+ scope="account",
33
+ identifier="global",
34
+ endpoint="energy.list",
35
+ fetch=lambda: api.list_products(),
36
+ ttl=TTL_SLOW,
37
+ )
38
+ finally:
39
+ await client.close()
40
+
41
+ # Filter to energy products (have energy_site_id)
42
+ energy_products = [p for p in products if "energy_site_id" in p]
43
+
44
+ if formatter.format == "json":
45
+ formatter.output(energy_products, command="energy.list")
46
+ else:
47
+ formatter.rich.energy_site_list(energy_products)
48
+
49
+
50
+ @energy_group.command("status")
51
+ @click.argument("site_id", type=int)
52
+ @global_options
53
+ def status_cmd(app_ctx: AppContext, site_id: int) -> None:
54
+ """Show energy site info."""
55
+ run_async(_cmd_status(app_ctx, site_id))
56
+
57
+
58
+ async def _cmd_status(app_ctx: AppContext, site_id: int) -> None:
59
+ formatter = app_ctx.formatter
60
+ client, api = get_energy_api(app_ctx)
61
+ try:
62
+ data = await cached_api_call(
63
+ app_ctx,
64
+ scope="site",
65
+ identifier=str(site_id),
66
+ endpoint="energy.status",
67
+ fetch=lambda: api.site_info(site_id),
68
+ ttl=TTL_SLOW,
69
+ )
70
+ finally:
71
+ await client.close()
72
+
73
+ if formatter.format == "json":
74
+ formatter.output(data, command="energy.status")
75
+ else:
76
+ formatter.rich.energy_site_info(data)
77
+
78
+
79
+ @energy_group.command("live")
80
+ @click.argument("site_id", type=int)
81
+ @global_options
82
+ def live_cmd(app_ctx: AppContext, site_id: int) -> None:
83
+ """Show real-time power flow."""
84
+ run_async(_cmd_live(app_ctx, site_id))
85
+
86
+
87
+ async def _cmd_live(app_ctx: AppContext, site_id: int) -> None:
88
+ formatter = app_ctx.formatter
89
+ client, api = get_energy_api(app_ctx)
90
+ try:
91
+ data = await api.live_status(site_id)
92
+ finally:
93
+ await client.close()
94
+
95
+ if formatter.format == "json":
96
+ formatter.output(data, command="energy.live")
97
+ else:
98
+ formatter.rich.energy_live_status(data)
99
+
100
+
101
+ @energy_group.command("backup")
102
+ @click.argument("site_id", type=int)
103
+ @click.argument("percent", type=click.IntRange(0, 100))
104
+ @global_options
105
+ def backup_cmd(app_ctx: AppContext, site_id: int, percent: int) -> None:
106
+ """Set backup reserve percentage."""
107
+ run_async(_cmd_backup(app_ctx, site_id, percent))
108
+
109
+
110
+ async def _cmd_backup(app_ctx: AppContext, site_id: int, percent: int) -> None:
111
+ formatter = app_ctx.formatter
112
+ client, api = get_energy_api(app_ctx)
113
+ try:
114
+ result = await api.set_backup_reserve(site_id, percent=percent)
115
+ finally:
116
+ await client.close()
117
+
118
+ invalidate_cache_for_site(app_ctx, site_id)
119
+
120
+ if formatter.format == "json":
121
+ formatter.output(result, command="energy.backup")
122
+ else:
123
+ formatter.rich.info(f"Backup reserve set to {percent}%")
124
+
125
+
126
+ @energy_group.command("mode")
127
+ @click.argument("site_id", type=int)
128
+ @click.argument(
129
+ "mode",
130
+ type=click.Choice(["self_consumption", "backup", "autonomous"]),
131
+ )
132
+ @global_options
133
+ def mode_cmd(app_ctx: AppContext, site_id: int, mode: str) -> None:
134
+ """Set operation mode (self_consumption, backup, autonomous)."""
135
+ run_async(_cmd_mode(app_ctx, site_id, mode))
136
+
137
+
138
+ async def _cmd_mode(app_ctx: AppContext, site_id: int, mode: str) -> None:
139
+ formatter = app_ctx.formatter
140
+ client, api = get_energy_api(app_ctx)
141
+ try:
142
+ result = await api.set_operation_mode(site_id, mode=mode)
143
+ finally:
144
+ await client.close()
145
+
146
+ invalidate_cache_for_site(app_ctx, site_id)
147
+
148
+ if formatter.format == "json":
149
+ formatter.output(result, command="energy.mode")
150
+ else:
151
+ formatter.rich.info(f"Operation mode set to {mode}")
152
+
153
+
154
+ @energy_group.command("storm")
155
+ @click.argument("site_id", type=int)
156
+ @click.option("--on/--off", default=True, help="Enable or disable storm watch")
157
+ @global_options
158
+ def storm_cmd(app_ctx: AppContext, site_id: int, on: bool) -> None:
159
+ """Enable or disable storm watch."""
160
+ run_async(_cmd_storm(app_ctx, site_id, on))
161
+
162
+
163
+ async def _cmd_storm(app_ctx: AppContext, site_id: int, on: bool) -> None:
164
+ formatter = app_ctx.formatter
165
+ client, api = get_energy_api(app_ctx)
166
+ try:
167
+ result = await api.set_storm_mode(site_id, enabled=on)
168
+ finally:
169
+ await client.close()
170
+
171
+ invalidate_cache_for_site(app_ctx, site_id)
172
+
173
+ if formatter.format == "json":
174
+ formatter.output(result, command="energy.storm")
175
+ else:
176
+ label = "enabled" if on else "disabled"
177
+ formatter.rich.info(f"Storm watch {label}")
178
+
179
+
180
+ @energy_group.command("tou")
181
+ @click.argument("site_id", type=int)
182
+ @click.argument("settings_json")
183
+ @global_options
184
+ def tou_cmd(app_ctx: AppContext, site_id: int, settings_json: str) -> None:
185
+ """Set time-of-use schedule (SETTINGS_JSON is a JSON string)."""
186
+ import json
187
+
188
+ settings = json.loads(settings_json)
189
+ run_async(_cmd_tou(app_ctx, site_id, settings))
190
+
191
+
192
+ async def _cmd_tou(app_ctx: AppContext, site_id: int, settings: dict[str, object]) -> None:
193
+ formatter = app_ctx.formatter
194
+ client, api = get_energy_api(app_ctx)
195
+ try:
196
+ result = await api.time_of_use_settings(site_id, settings=settings)
197
+ finally:
198
+ await client.close()
199
+
200
+ invalidate_cache_for_site(app_ctx, site_id)
201
+
202
+ if formatter.format == "json":
203
+ formatter.output(result, command="energy.tou")
204
+ else:
205
+ formatter.rich.info("Time-of-use settings updated")
206
+
207
+
208
+ @energy_group.command("history")
209
+ @click.argument("site_id", type=int)
210
+ @global_options
211
+ def history_cmd(app_ctx: AppContext, site_id: int) -> None:
212
+ """Show charging history for an energy site."""
213
+ run_async(_cmd_history(app_ctx, site_id))
214
+
215
+
216
+ async def _cmd_history(app_ctx: AppContext, site_id: int) -> None:
217
+ formatter = app_ctx.formatter
218
+ client, api = get_energy_api(app_ctx)
219
+ try:
220
+ data = await api.charging_history(site_id)
221
+ finally:
222
+ await client.close()
223
+
224
+ if formatter.format == "json":
225
+ formatter.output(data, command="energy.history")
226
+ else:
227
+ if data.time_series:
228
+ formatter.rich.info(f"Charging history: {len(data.time_series)} entries")
229
+ else:
230
+ formatter.rich.info("No charging history available.")
231
+
232
+
233
+ @energy_group.command("off-grid")
234
+ @click.argument("site_id", type=int)
235
+ @click.argument("reserve", type=click.IntRange(0, 100))
236
+ @global_options
237
+ def off_grid_cmd(app_ctx: AppContext, site_id: int, reserve: int) -> None:
238
+ """Set off-grid EV charging reserve percentage."""
239
+ run_async(_cmd_off_grid(app_ctx, site_id, reserve))
240
+
241
+
242
+ async def _cmd_off_grid(app_ctx: AppContext, site_id: int, reserve: int) -> None:
243
+ formatter = app_ctx.formatter
244
+ client, api = get_energy_api(app_ctx)
245
+ try:
246
+ result = await api.off_grid_vehicle_charging_reserve(site_id, reserve=reserve)
247
+ finally:
248
+ await client.close()
249
+
250
+ invalidate_cache_for_site(app_ctx, site_id)
251
+
252
+ if formatter.format == "json":
253
+ formatter.output(result, command="energy.off-grid")
254
+ else:
255
+ formatter.rich.info(f"Off-grid EV charging reserve set to {reserve}%")
256
+
257
+
258
+ @energy_group.command("grid-config")
259
+ @click.argument("site_id", type=int)
260
+ @click.argument("config_json")
261
+ @global_options
262
+ def grid_config_cmd(app_ctx: AppContext, site_id: int, config_json: str) -> None:
263
+ """Set grid import/export config (CONFIG_JSON is a JSON string)."""
264
+ import json
265
+
266
+ config = json.loads(config_json)
267
+ run_async(_cmd_grid_config(app_ctx, site_id, config))
268
+
269
+
270
+ async def _cmd_grid_config(app_ctx: AppContext, site_id: int, config: dict[str, object]) -> None:
271
+ formatter = app_ctx.formatter
272
+ client, api = get_energy_api(app_ctx)
273
+ try:
274
+ result = await api.grid_import_export(site_id, config=config)
275
+ finally:
276
+ await client.close()
277
+
278
+ invalidate_cache_for_site(app_ctx, site_id)
279
+
280
+ if formatter.format == "json":
281
+ formatter.output(result, command="energy.grid-config")
282
+ else:
283
+ formatter.rich.info("Grid import/export config updated")
284
+
285
+
286
+ @energy_group.command("telemetry")
287
+ @click.argument("site_id", type=int)
288
+ @click.option(
289
+ "--kind", type=click.Choice(["charge", "power"]), default="charge", help="Telemetry data type"
290
+ )
291
+ @click.option("--start-date", default=None, help="Start date (YYYY-MM-DD)")
292
+ @click.option("--end-date", default=None, help="End date (YYYY-MM-DD)")
293
+ @click.option("--time-zone", default=None, help="Time zone (e.g. America/Los_Angeles)")
294
+ @global_options
295
+ def telemetry_cmd(
296
+ app_ctx: AppContext,
297
+ site_id: int,
298
+ kind: str,
299
+ start_date: str | None,
300
+ end_date: str | None,
301
+ time_zone: str | None,
302
+ ) -> None:
303
+ """Show telemetry history for an energy site (wall connector)."""
304
+ run_async(_cmd_telemetry(app_ctx, site_id, kind, start_date, end_date, time_zone))
305
+
306
+
307
+ async def _cmd_telemetry(
308
+ app_ctx: AppContext,
309
+ site_id: int,
310
+ kind: str,
311
+ start_date: str | None,
312
+ end_date: str | None,
313
+ time_zone: str | None,
314
+ ) -> None:
315
+ formatter = app_ctx.formatter
316
+ client, api = get_energy_api(app_ctx)
317
+ try:
318
+ data = await api.telemetry_history(
319
+ site_id,
320
+ kind=kind,
321
+ start_date=start_date,
322
+ end_date=end_date,
323
+ time_zone=time_zone,
324
+ )
325
+ finally:
326
+ await client.close()
327
+
328
+ if formatter.format == "json":
329
+ formatter.output(data, command="energy.telemetry")
330
+ else:
331
+ if data.time_series:
332
+ formatter.rich.info(f"Telemetry history: {len(data.time_series)} entries")
333
+ else:
334
+ formatter.rich.info("No telemetry history available.")
335
+
336
+
337
+ @energy_group.command("calendar")
338
+ @click.argument("site_id", type=int)
339
+ @click.option(
340
+ "--kind", type=click.Choice(["energy", "backup"]), default="energy", help="History type"
341
+ )
342
+ @click.option("--period", type=click.Choice(["day", "week", "month", "year"]), default="day")
343
+ @click.option("--start-date", default=None, help="Start date (YYYY-MM-DD)")
344
+ @click.option("--end-date", default=None, help="End date (YYYY-MM-DD)")
345
+ @global_options
346
+ def calendar_cmd(
347
+ app_ctx: AppContext,
348
+ site_id: int,
349
+ kind: str,
350
+ period: str,
351
+ start_date: str | None,
352
+ end_date: str | None,
353
+ ) -> None:
354
+ """Show calendar-based history for an energy site."""
355
+ run_async(_cmd_calendar(app_ctx, site_id, kind, period, start_date, end_date))
356
+
357
+
358
+ async def _cmd_calendar(
359
+ app_ctx: AppContext,
360
+ site_id: int,
361
+ kind: str,
362
+ period: str,
363
+ start_date: str | None,
364
+ end_date: str | None,
365
+ ) -> None:
366
+ formatter = app_ctx.formatter
367
+ client, api = get_energy_api(app_ctx)
368
+ try:
369
+ data = await api.calendar_history(
370
+ site_id,
371
+ kind=kind,
372
+ period=period,
373
+ start_date=start_date,
374
+ end_date=end_date,
375
+ )
376
+ finally:
377
+ await client.close()
378
+
379
+ if formatter.format == "json":
380
+ formatter.output(data, command="energy.calendar")
381
+ else:
382
+ if data.time_series:
383
+ formatter.rich.info(f"Calendar history: {len(data.time_series)} entries")
384
+ else:
385
+ formatter.rich.info("No calendar history available.")