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.
- tescmd/__init__.py +3 -0
- tescmd/__main__.py +5 -0
- tescmd/_internal/__init__.py +0 -0
- tescmd/_internal/async_utils.py +25 -0
- tescmd/_internal/permissions.py +43 -0
- tescmd/_internal/vin.py +44 -0
- tescmd/api/__init__.py +1 -0
- tescmd/api/charging.py +102 -0
- tescmd/api/client.py +189 -0
- tescmd/api/command.py +540 -0
- tescmd/api/energy.py +146 -0
- tescmd/api/errors.py +76 -0
- tescmd/api/partner.py +40 -0
- tescmd/api/sharing.py +65 -0
- tescmd/api/signed_command.py +277 -0
- tescmd/api/user.py +38 -0
- tescmd/api/vehicle.py +150 -0
- tescmd/auth/__init__.py +1 -0
- tescmd/auth/oauth.py +312 -0
- tescmd/auth/server.py +108 -0
- tescmd/auth/token_store.py +273 -0
- tescmd/ble/__init__.py +0 -0
- tescmd/cache/__init__.py +6 -0
- tescmd/cache/keys.py +51 -0
- tescmd/cache/response_cache.py +213 -0
- tescmd/cli/__init__.py +0 -0
- tescmd/cli/_client.py +603 -0
- tescmd/cli/_options.py +126 -0
- tescmd/cli/auth.py +682 -0
- tescmd/cli/billing.py +240 -0
- tescmd/cli/cache.py +85 -0
- tescmd/cli/charge.py +610 -0
- tescmd/cli/climate.py +501 -0
- tescmd/cli/energy.py +385 -0
- tescmd/cli/key.py +611 -0
- tescmd/cli/main.py +601 -0
- tescmd/cli/media.py +146 -0
- tescmd/cli/nav.py +242 -0
- tescmd/cli/partner.py +112 -0
- tescmd/cli/raw.py +75 -0
- tescmd/cli/security.py +495 -0
- tescmd/cli/setup.py +786 -0
- tescmd/cli/sharing.py +188 -0
- tescmd/cli/software.py +81 -0
- tescmd/cli/status.py +106 -0
- tescmd/cli/trunk.py +240 -0
- tescmd/cli/user.py +145 -0
- tescmd/cli/vehicle.py +837 -0
- tescmd/config/__init__.py +0 -0
- tescmd/crypto/__init__.py +19 -0
- tescmd/crypto/ecdh.py +46 -0
- tescmd/crypto/keys.py +122 -0
- tescmd/deploy/__init__.py +0 -0
- tescmd/deploy/github_pages.py +268 -0
- tescmd/models/__init__.py +85 -0
- tescmd/models/auth.py +108 -0
- tescmd/models/command.py +18 -0
- tescmd/models/config.py +63 -0
- tescmd/models/energy.py +56 -0
- tescmd/models/sharing.py +26 -0
- tescmd/models/user.py +37 -0
- tescmd/models/vehicle.py +185 -0
- tescmd/output/__init__.py +5 -0
- tescmd/output/formatter.py +132 -0
- tescmd/output/json_output.py +83 -0
- tescmd/output/rich_output.py +809 -0
- tescmd/protocol/__init__.py +23 -0
- tescmd/protocol/commands.py +175 -0
- tescmd/protocol/encoder.py +122 -0
- tescmd/protocol/metadata.py +116 -0
- tescmd/protocol/payloads.py +621 -0
- tescmd/protocol/protobuf/__init__.py +6 -0
- tescmd/protocol/protobuf/messages.py +564 -0
- tescmd/protocol/session.py +318 -0
- tescmd/protocol/signer.py +84 -0
- tescmd/py.typed +0 -0
- tescmd-0.1.2.dist-info/METADATA +458 -0
- tescmd-0.1.2.dist-info/RECORD +81 -0
- tescmd-0.1.2.dist-info/WHEEL +4 -0
- tescmd-0.1.2.dist-info/entry_points.txt +2 -0
- 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.")
|