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/api/command.py
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
"""Vehicle command API — wraps POST /api/1/vehicles/{vin}/command/{name}."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from tescmd.models.command import CommandResponse
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from tescmd.api.client import TeslaFleetClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CommandAPI:
|
|
14
|
+
"""Vehicle command operations (composition over :class:`TeslaFleetClient`)."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, client: TeslaFleetClient) -> None:
|
|
17
|
+
self._client = client
|
|
18
|
+
|
|
19
|
+
async def _command(
|
|
20
|
+
self, vin: str, command: str, body: dict[str, Any] | None = None
|
|
21
|
+
) -> CommandResponse:
|
|
22
|
+
path = f"/api/1/vehicles/{vin}/command/{command}"
|
|
23
|
+
kwargs: dict[str, Any] = {}
|
|
24
|
+
if body is not None:
|
|
25
|
+
kwargs["json"] = body
|
|
26
|
+
data = await self._client.post(path, **kwargs)
|
|
27
|
+
return CommandResponse.model_validate(data)
|
|
28
|
+
|
|
29
|
+
# ------------------------------------------------------------------
|
|
30
|
+
# Key enrollment
|
|
31
|
+
# ------------------------------------------------------------------
|
|
32
|
+
# Note: Initial key enrollment is NOT available via REST or signed_command.
|
|
33
|
+
# The Tesla Go SDK explicitly blocks add_key_request for Fleet API with
|
|
34
|
+
# ErrRequiresBLE. For Fleet API apps, enrollment happens through the
|
|
35
|
+
# Tesla app portal flow: https://tesla.com/_ak/<domain>
|
|
36
|
+
# See cli/key.py for the enrollment flow implementation.
|
|
37
|
+
|
|
38
|
+
# ------------------------------------------------------------------
|
|
39
|
+
# Security / convenience commands
|
|
40
|
+
# ------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
async def auto_secure_vehicle(self, vin: str) -> CommandResponse:
|
|
43
|
+
"""Close falcon-wing doors and lock (Model X only)."""
|
|
44
|
+
return await self._command(vin, "auto_secure_vehicle")
|
|
45
|
+
|
|
46
|
+
# ------------------------------------------------------------------
|
|
47
|
+
# Charging commands
|
|
48
|
+
# ------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
async def charge_start(self, vin: str) -> CommandResponse:
|
|
51
|
+
return await self._command(vin, "charge_start")
|
|
52
|
+
|
|
53
|
+
async def charge_stop(self, vin: str) -> CommandResponse:
|
|
54
|
+
return await self._command(vin, "charge_stop")
|
|
55
|
+
|
|
56
|
+
async def charge_standard(self, vin: str) -> CommandResponse:
|
|
57
|
+
return await self._command(vin, "charge_standard")
|
|
58
|
+
|
|
59
|
+
async def charge_max_range(self, vin: str) -> CommandResponse:
|
|
60
|
+
return await self._command(vin, "charge_max_range")
|
|
61
|
+
|
|
62
|
+
async def charge_port_door_open(self, vin: str) -> CommandResponse:
|
|
63
|
+
return await self._command(vin, "charge_port_door_open")
|
|
64
|
+
|
|
65
|
+
async def charge_port_door_close(self, vin: str) -> CommandResponse:
|
|
66
|
+
return await self._command(vin, "charge_port_door_close")
|
|
67
|
+
|
|
68
|
+
async def set_charge_limit(self, vin: str, *, percent: int) -> CommandResponse:
|
|
69
|
+
return await self._command(vin, "set_charge_limit", {"percent": percent})
|
|
70
|
+
|
|
71
|
+
async def set_charging_amps(self, vin: str, *, charging_amps: int) -> CommandResponse:
|
|
72
|
+
return await self._command(vin, "set_charging_amps", {"charging_amps": charging_amps})
|
|
73
|
+
|
|
74
|
+
async def set_scheduled_charging(
|
|
75
|
+
self, vin: str, *, enable: bool, time: int
|
|
76
|
+
) -> CommandResponse:
|
|
77
|
+
return await self._command(vin, "set_scheduled_charging", {"enable": enable, "time": time})
|
|
78
|
+
|
|
79
|
+
async def set_scheduled_departure(
|
|
80
|
+
self,
|
|
81
|
+
vin: str,
|
|
82
|
+
*,
|
|
83
|
+
enable: bool,
|
|
84
|
+
departure_time: int,
|
|
85
|
+
preconditioning_enabled: bool = False,
|
|
86
|
+
preconditioning_weekdays_only: bool = False,
|
|
87
|
+
off_peak_charging_enabled: bool = False,
|
|
88
|
+
off_peak_charging_weekdays_only: bool = False,
|
|
89
|
+
end_off_peak_time: int = 0,
|
|
90
|
+
) -> CommandResponse:
|
|
91
|
+
return await self._command(
|
|
92
|
+
vin,
|
|
93
|
+
"set_scheduled_departure",
|
|
94
|
+
{
|
|
95
|
+
"enable": enable,
|
|
96
|
+
"departure_time": departure_time,
|
|
97
|
+
"preconditioning_enabled": preconditioning_enabled,
|
|
98
|
+
"preconditioning_weekdays_only": preconditioning_weekdays_only,
|
|
99
|
+
"off_peak_charging_enabled": off_peak_charging_enabled,
|
|
100
|
+
"off_peak_charging_weekdays_only": off_peak_charging_weekdays_only,
|
|
101
|
+
"end_off_peak_time": end_off_peak_time,
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
async def add_precondition_schedule(
|
|
106
|
+
self,
|
|
107
|
+
vin: str,
|
|
108
|
+
*,
|
|
109
|
+
lat: float | None = None,
|
|
110
|
+
lon: float | None = None,
|
|
111
|
+
precondition_time: int | None = None,
|
|
112
|
+
one_time: bool | None = None,
|
|
113
|
+
days_of_week: str | None = None,
|
|
114
|
+
id: int | None = None,
|
|
115
|
+
enabled: bool | None = None,
|
|
116
|
+
) -> CommandResponse:
|
|
117
|
+
body: dict[str, Any] = {}
|
|
118
|
+
if lat is not None:
|
|
119
|
+
body["lat"] = lat
|
|
120
|
+
if lon is not None:
|
|
121
|
+
body["lon"] = lon
|
|
122
|
+
if precondition_time is not None:
|
|
123
|
+
body["precondition_time"] = precondition_time
|
|
124
|
+
if one_time is not None:
|
|
125
|
+
body["one_time"] = one_time
|
|
126
|
+
if days_of_week is not None:
|
|
127
|
+
body["days_of_week"] = days_of_week
|
|
128
|
+
if id is not None:
|
|
129
|
+
body["id"] = id
|
|
130
|
+
if enabled is not None:
|
|
131
|
+
body["enabled"] = enabled
|
|
132
|
+
return await self._command(vin, "add_precondition_schedule", body)
|
|
133
|
+
|
|
134
|
+
async def remove_precondition_schedule(self, vin: str, *, id: int) -> CommandResponse:
|
|
135
|
+
return await self._command(vin, "remove_precondition_schedule", {"id": id})
|
|
136
|
+
|
|
137
|
+
async def batch_remove_precondition_schedules(
|
|
138
|
+
self, vin: str, *, home: bool, work: bool, other: bool
|
|
139
|
+
) -> CommandResponse:
|
|
140
|
+
"""Remove precondition schedules by location type (home/work/other)."""
|
|
141
|
+
return await self._command(
|
|
142
|
+
vin,
|
|
143
|
+
"batch_remove_precondition_schedules",
|
|
144
|
+
{"home": home, "work": work, "other": other},
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# ------------------------------------------------------------------
|
|
148
|
+
# Climate commands
|
|
149
|
+
# ------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
async def auto_conditioning_start(self, vin: str) -> CommandResponse:
|
|
152
|
+
return await self._command(vin, "auto_conditioning_start")
|
|
153
|
+
|
|
154
|
+
async def auto_conditioning_stop(self, vin: str) -> CommandResponse:
|
|
155
|
+
return await self._command(vin, "auto_conditioning_stop")
|
|
156
|
+
|
|
157
|
+
async def set_temps(
|
|
158
|
+
self, vin: str, *, driver_temp: float, passenger_temp: float
|
|
159
|
+
) -> CommandResponse:
|
|
160
|
+
return await self._command(
|
|
161
|
+
vin,
|
|
162
|
+
"set_temps",
|
|
163
|
+
{"driver_temp": driver_temp, "passenger_temp": passenger_temp},
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
async def set_preconditioning_max(
|
|
167
|
+
self, vin: str, *, on: bool, manual_override: bool = False
|
|
168
|
+
) -> CommandResponse:
|
|
169
|
+
return await self._command(
|
|
170
|
+
vin, "set_preconditioning_max", {"on": on, "manual_override": manual_override}
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
async def remote_seat_heater_request(
|
|
174
|
+
self, vin: str, *, seat_position: int, level: int
|
|
175
|
+
) -> CommandResponse:
|
|
176
|
+
return await self._command(
|
|
177
|
+
vin, "remote_seat_heater_request", {"seat_position": seat_position, "level": level}
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
async def remote_seat_cooler_request(
|
|
181
|
+
self, vin: str, *, seat_position: int, seat_cooler_level: int
|
|
182
|
+
) -> CommandResponse:
|
|
183
|
+
return await self._command(
|
|
184
|
+
vin,
|
|
185
|
+
"remote_seat_cooler_request",
|
|
186
|
+
{"seat_position": seat_position, "seat_cooler_level": seat_cooler_level},
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
async def remote_steering_wheel_heater_request(self, vin: str, *, on: bool) -> CommandResponse:
|
|
190
|
+
return await self._command(vin, "remote_steering_wheel_heater_request", {"on": on})
|
|
191
|
+
|
|
192
|
+
async def set_cabin_overheat_protection(
|
|
193
|
+
self, vin: str, *, on: bool, fan_only: bool = False
|
|
194
|
+
) -> CommandResponse:
|
|
195
|
+
return await self._command(
|
|
196
|
+
vin,
|
|
197
|
+
"set_cabin_overheat_protection",
|
|
198
|
+
{"on": on, "fan_only": fan_only},
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
async def set_climate_keeper_mode(
|
|
202
|
+
self, vin: str, *, climate_keeper_mode: int, manual_override: bool = False
|
|
203
|
+
) -> CommandResponse:
|
|
204
|
+
return await self._command(
|
|
205
|
+
vin,
|
|
206
|
+
"set_climate_keeper_mode",
|
|
207
|
+
{"climate_keeper_mode": climate_keeper_mode, "manual_override": manual_override},
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
async def set_cop_temp(self, vin: str, *, cop_temp: int) -> CommandResponse:
|
|
211
|
+
"""Set cabin overheat protection temperature (0=low, 1=medium, 2=high)."""
|
|
212
|
+
return await self._command(vin, "set_cop_temp", {"cop_temp": cop_temp})
|
|
213
|
+
|
|
214
|
+
async def remote_auto_seat_climate_request(
|
|
215
|
+
self, vin: str, *, auto_seat_position: int, auto_climate_on: bool
|
|
216
|
+
) -> CommandResponse:
|
|
217
|
+
return await self._command(
|
|
218
|
+
vin,
|
|
219
|
+
"remote_auto_seat_climate_request",
|
|
220
|
+
{"auto_seat_position": auto_seat_position, "auto_climate_on": auto_climate_on},
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
async def remote_auto_steering_wheel_heat_climate_request(
|
|
224
|
+
self, vin: str, *, on: bool
|
|
225
|
+
) -> CommandResponse:
|
|
226
|
+
return await self._command(
|
|
227
|
+
vin, "remote_auto_steering_wheel_heat_climate_request", {"on": on}
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
async def remote_steering_wheel_heat_level_request(
|
|
231
|
+
self, vin: str, *, level: int
|
|
232
|
+
) -> CommandResponse:
|
|
233
|
+
return await self._command(
|
|
234
|
+
vin, "remote_steering_wheel_heat_level_request", {"level": level}
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# ------------------------------------------------------------------
|
|
238
|
+
# Security commands
|
|
239
|
+
# ------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
async def door_lock(self, vin: str) -> CommandResponse:
|
|
242
|
+
return await self._command(vin, "door_lock")
|
|
243
|
+
|
|
244
|
+
async def door_unlock(self, vin: str) -> CommandResponse:
|
|
245
|
+
return await self._command(vin, "door_unlock")
|
|
246
|
+
|
|
247
|
+
async def set_sentry_mode(self, vin: str, *, on: bool) -> CommandResponse:
|
|
248
|
+
return await self._command(vin, "set_sentry_mode", {"on": on})
|
|
249
|
+
|
|
250
|
+
async def set_valet_mode(
|
|
251
|
+
self, vin: str, *, on: bool, password: str | None = None
|
|
252
|
+
) -> CommandResponse:
|
|
253
|
+
body: dict[str, Any] = {"on": on}
|
|
254
|
+
if password is not None:
|
|
255
|
+
body["password"] = password
|
|
256
|
+
return await self._command(vin, "set_valet_mode", body)
|
|
257
|
+
|
|
258
|
+
async def reset_valet_pin(self, vin: str) -> CommandResponse:
|
|
259
|
+
return await self._command(vin, "reset_valet_pin")
|
|
260
|
+
|
|
261
|
+
async def speed_limit_activate(self, vin: str, *, pin: str) -> CommandResponse:
|
|
262
|
+
return await self._command(vin, "speed_limit_activate", {"pin": pin})
|
|
263
|
+
|
|
264
|
+
async def speed_limit_deactivate(self, vin: str, *, pin: str) -> CommandResponse:
|
|
265
|
+
return await self._command(vin, "speed_limit_deactivate", {"pin": pin})
|
|
266
|
+
|
|
267
|
+
async def speed_limit_set_limit(self, vin: str, *, limit_mph: float) -> CommandResponse:
|
|
268
|
+
return await self._command(vin, "speed_limit_set_limit", {"limit_mph": limit_mph})
|
|
269
|
+
|
|
270
|
+
async def reset_pin_to_drive_pin(self, vin: str) -> CommandResponse:
|
|
271
|
+
return await self._command(vin, "reset_pin_to_drive_pin")
|
|
272
|
+
|
|
273
|
+
async def clear_pin_to_drive_admin(self, vin: str) -> CommandResponse:
|
|
274
|
+
return await self._command(vin, "clear_pin_to_drive_admin")
|
|
275
|
+
|
|
276
|
+
async def speed_limit_clear_pin(self, vin: str, *, pin: str) -> CommandResponse:
|
|
277
|
+
return await self._command(vin, "speed_limit_clear_pin", {"pin": pin})
|
|
278
|
+
|
|
279
|
+
async def speed_limit_clear_pin_admin(self, vin: str) -> CommandResponse:
|
|
280
|
+
return await self._command(vin, "speed_limit_clear_pin_admin")
|
|
281
|
+
|
|
282
|
+
async def remote_start_drive(self, vin: str) -> CommandResponse:
|
|
283
|
+
return await self._command(vin, "remote_start_drive")
|
|
284
|
+
|
|
285
|
+
async def flash_lights(self, vin: str) -> CommandResponse:
|
|
286
|
+
return await self._command(vin, "flash_lights")
|
|
287
|
+
|
|
288
|
+
async def honk_horn(self, vin: str) -> CommandResponse:
|
|
289
|
+
return await self._command(vin, "honk_horn")
|
|
290
|
+
|
|
291
|
+
# ------------------------------------------------------------------
|
|
292
|
+
# Media commands
|
|
293
|
+
# ------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
async def media_toggle_playback(self, vin: str) -> CommandResponse:
|
|
296
|
+
return await self._command(vin, "media_toggle_playback")
|
|
297
|
+
|
|
298
|
+
async def media_next_track(self, vin: str) -> CommandResponse:
|
|
299
|
+
return await self._command(vin, "media_next_track")
|
|
300
|
+
|
|
301
|
+
async def media_prev_track(self, vin: str) -> CommandResponse:
|
|
302
|
+
return await self._command(vin, "media_prev_track")
|
|
303
|
+
|
|
304
|
+
async def media_next_fav(self, vin: str) -> CommandResponse:
|
|
305
|
+
return await self._command(vin, "media_next_fav")
|
|
306
|
+
|
|
307
|
+
async def media_prev_fav(self, vin: str) -> CommandResponse:
|
|
308
|
+
return await self._command(vin, "media_prev_fav")
|
|
309
|
+
|
|
310
|
+
async def media_volume_up(self, vin: str) -> CommandResponse:
|
|
311
|
+
return await self._command(vin, "media_volume_up")
|
|
312
|
+
|
|
313
|
+
async def media_volume_down(self, vin: str) -> CommandResponse:
|
|
314
|
+
return await self._command(vin, "media_volume_down")
|
|
315
|
+
|
|
316
|
+
async def adjust_volume(self, vin: str, *, volume: float) -> CommandResponse:
|
|
317
|
+
return await self._command(vin, "adjust_volume", {"volume": volume})
|
|
318
|
+
|
|
319
|
+
# ------------------------------------------------------------------
|
|
320
|
+
# Navigation commands
|
|
321
|
+
# ------------------------------------------------------------------
|
|
322
|
+
|
|
323
|
+
async def share(self, vin: str, *, address: str) -> CommandResponse:
|
|
324
|
+
import time as _time
|
|
325
|
+
|
|
326
|
+
body = {
|
|
327
|
+
"type": "share_ext_content_raw",
|
|
328
|
+
"value": {"android.intent.extra.TEXT": address},
|
|
329
|
+
"locale": "en-US",
|
|
330
|
+
"timestamp_ms": str(int(_time.time() * 1000)),
|
|
331
|
+
}
|
|
332
|
+
path = f"/api/1/vehicles/{vin}/command/share"
|
|
333
|
+
data = await self._client.post(path, json=body)
|
|
334
|
+
return CommandResponse.model_validate(data)
|
|
335
|
+
|
|
336
|
+
async def navigation_gps_request(
|
|
337
|
+
self, vin: str, *, lat: float, lon: float, order: int | None = None
|
|
338
|
+
) -> CommandResponse:
|
|
339
|
+
body: dict[str, Any] = {"lat": lat, "lon": lon}
|
|
340
|
+
if order is not None:
|
|
341
|
+
body["order"] = order
|
|
342
|
+
return await self._command(vin, "navigation_gps_request", body)
|
|
343
|
+
|
|
344
|
+
async def navigation_sc_request(
|
|
345
|
+
self, vin: str, *, id: int = 0, order: int = 0
|
|
346
|
+
) -> CommandResponse:
|
|
347
|
+
return await self._command(vin, "navigation_sc_request", {"id": id, "order": order})
|
|
348
|
+
|
|
349
|
+
async def trigger_homelink(self, vin: str, *, lat: float, lon: float) -> CommandResponse:
|
|
350
|
+
return await self._command(vin, "trigger_homelink", {"lat": lat, "lon": lon})
|
|
351
|
+
|
|
352
|
+
async def navigation_request(
|
|
353
|
+
self,
|
|
354
|
+
vin: str,
|
|
355
|
+
*,
|
|
356
|
+
type: str = "share_ext_content_raw",
|
|
357
|
+
locale: str = "en-US",
|
|
358
|
+
timestamp_ms: str = "",
|
|
359
|
+
value: dict[str, Any] | None = None,
|
|
360
|
+
) -> CommandResponse:
|
|
361
|
+
"""Legacy navigation request (REST-only, deprecated in favour of 'share')."""
|
|
362
|
+
import time as _time
|
|
363
|
+
|
|
364
|
+
body: dict[str, Any] = {
|
|
365
|
+
"type": type,
|
|
366
|
+
"locale": locale,
|
|
367
|
+
"timestamp_ms": timestamp_ms or str(int(_time.time() * 1000)),
|
|
368
|
+
}
|
|
369
|
+
if value is not None:
|
|
370
|
+
body["value"] = value
|
|
371
|
+
path = f"/api/1/vehicles/{vin}/command/navigation_request"
|
|
372
|
+
data = await self._client.post(path, json=body)
|
|
373
|
+
return CommandResponse.model_validate(data)
|
|
374
|
+
|
|
375
|
+
async def navigation_waypoints_request(self, vin: str, *, waypoints: str) -> CommandResponse:
|
|
376
|
+
return await self._command(vin, "navigation_waypoints_request", {"waypoints": waypoints})
|
|
377
|
+
|
|
378
|
+
# ------------------------------------------------------------------
|
|
379
|
+
# Software commands
|
|
380
|
+
# ------------------------------------------------------------------
|
|
381
|
+
|
|
382
|
+
async def schedule_software_update(self, vin: str, *, offset_sec: int) -> CommandResponse:
|
|
383
|
+
return await self._command(vin, "schedule_software_update", {"offset_sec": offset_sec})
|
|
384
|
+
|
|
385
|
+
async def cancel_software_update(self, vin: str) -> CommandResponse:
|
|
386
|
+
return await self._command(vin, "cancel_software_update")
|
|
387
|
+
|
|
388
|
+
# ------------------------------------------------------------------
|
|
389
|
+
# Bioweapon / sunroof / PIN-to-drive / guest-mode / erase / boombox
|
|
390
|
+
# ------------------------------------------------------------------
|
|
391
|
+
|
|
392
|
+
async def set_bioweapon_mode(
|
|
393
|
+
self, vin: str, *, on: bool, manual_override: bool = False
|
|
394
|
+
) -> CommandResponse:
|
|
395
|
+
return await self._command(
|
|
396
|
+
vin, "set_bioweapon_mode", {"on": on, "manual_override": manual_override}
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
async def sun_roof_control(self, vin: str, *, state: str) -> CommandResponse:
|
|
400
|
+
return await self._command(vin, "sun_roof_control", {"state": state})
|
|
401
|
+
|
|
402
|
+
async def set_pin_to_drive(
|
|
403
|
+
self, vin: str, *, on: bool, password: str | None = None
|
|
404
|
+
) -> CommandResponse:
|
|
405
|
+
body: dict[str, Any] = {"on": on}
|
|
406
|
+
if password is not None:
|
|
407
|
+
body["password"] = password
|
|
408
|
+
return await self._command(vin, "set_pin_to_drive", body)
|
|
409
|
+
|
|
410
|
+
async def guest_mode(self, vin: str, *, enable: bool) -> CommandResponse:
|
|
411
|
+
return await self._command(vin, "guest_mode", {"enable": enable})
|
|
412
|
+
|
|
413
|
+
async def erase_user_data(self, vin: str) -> CommandResponse:
|
|
414
|
+
return await self._command(vin, "erase_user_data")
|
|
415
|
+
|
|
416
|
+
async def remote_boombox(self, vin: str, *, sound: int = 2000) -> CommandResponse:
|
|
417
|
+
return await self._command(vin, "remote_boombox", {"sound": sound})
|
|
418
|
+
|
|
419
|
+
# ------------------------------------------------------------------
|
|
420
|
+
# Charge schedule commands (firmware 2024.26+)
|
|
421
|
+
# ------------------------------------------------------------------
|
|
422
|
+
|
|
423
|
+
async def add_charge_schedule(
|
|
424
|
+
self,
|
|
425
|
+
vin: str,
|
|
426
|
+
*,
|
|
427
|
+
lat: float | None = None,
|
|
428
|
+
lon: float | None = None,
|
|
429
|
+
start_time: int | None = None,
|
|
430
|
+
start_enabled: bool | None = None,
|
|
431
|
+
end_time: int | None = None,
|
|
432
|
+
end_enabled: bool | None = None,
|
|
433
|
+
days_of_week: str | None = None,
|
|
434
|
+
id: int | None = None,
|
|
435
|
+
enabled: bool | None = None,
|
|
436
|
+
one_time: bool | None = None,
|
|
437
|
+
) -> CommandResponse:
|
|
438
|
+
body: dict[str, Any] = {}
|
|
439
|
+
if lat is not None:
|
|
440
|
+
body["lat"] = lat
|
|
441
|
+
if lon is not None:
|
|
442
|
+
body["lon"] = lon
|
|
443
|
+
if start_time is not None:
|
|
444
|
+
body["start_time"] = start_time
|
|
445
|
+
if start_enabled is not None:
|
|
446
|
+
body["start_enabled"] = start_enabled
|
|
447
|
+
if end_time is not None:
|
|
448
|
+
body["end_time"] = end_time
|
|
449
|
+
if end_enabled is not None:
|
|
450
|
+
body["end_enabled"] = end_enabled
|
|
451
|
+
if days_of_week is not None:
|
|
452
|
+
body["days_of_week"] = days_of_week
|
|
453
|
+
if id is not None:
|
|
454
|
+
body["id"] = id
|
|
455
|
+
if enabled is not None:
|
|
456
|
+
body["enabled"] = enabled
|
|
457
|
+
if one_time is not None:
|
|
458
|
+
body["one_time"] = one_time
|
|
459
|
+
return await self._command(vin, "add_charge_schedule", body)
|
|
460
|
+
|
|
461
|
+
async def remove_charge_schedule(self, vin: str, *, id: int) -> CommandResponse:
|
|
462
|
+
return await self._command(vin, "remove_charge_schedule", {"id": id})
|
|
463
|
+
|
|
464
|
+
async def batch_remove_charge_schedules(
|
|
465
|
+
self, vin: str, *, home: bool, work: bool, other: bool
|
|
466
|
+
) -> CommandResponse:
|
|
467
|
+
"""Remove charge schedules by location type (home/work/other)."""
|
|
468
|
+
return await self._command(
|
|
469
|
+
vin,
|
|
470
|
+
"batch_remove_charge_schedules",
|
|
471
|
+
{"home": home, "work": work, "other": other},
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
# ------------------------------------------------------------------
|
|
475
|
+
# Vehicle name / calendar
|
|
476
|
+
# ------------------------------------------------------------------
|
|
477
|
+
|
|
478
|
+
async def set_vehicle_name(self, vin: str, *, vehicle_name: str) -> CommandResponse:
|
|
479
|
+
return await self._command(vin, "set_vehicle_name", {"vehicle_name": vehicle_name})
|
|
480
|
+
|
|
481
|
+
async def upcoming_calendar_entries(self, vin: str, *, calendar_data: str) -> CommandResponse:
|
|
482
|
+
return await self._command(
|
|
483
|
+
vin, "upcoming_calendar_entries", {"calendar_data": calendar_data}
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
# ------------------------------------------------------------------
|
|
487
|
+
# Trunk / window commands
|
|
488
|
+
# ------------------------------------------------------------------
|
|
489
|
+
|
|
490
|
+
async def actuate_trunk(self, vin: str, *, which_trunk: str) -> CommandResponse:
|
|
491
|
+
return await self._command(vin, "actuate_trunk", {"which_trunk": which_trunk})
|
|
492
|
+
|
|
493
|
+
async def window_control(
|
|
494
|
+
self, vin: str, *, command: str, lat: float, lon: float
|
|
495
|
+
) -> CommandResponse:
|
|
496
|
+
return await self._command(
|
|
497
|
+
vin, "window_control", {"command": command, "lat": lat, "lon": lon}
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
# ------------------------------------------------------------------
|
|
501
|
+
# Tonneau cover commands (Cybertruck)
|
|
502
|
+
# ------------------------------------------------------------------
|
|
503
|
+
|
|
504
|
+
async def open_tonneau(self, vin: str) -> CommandResponse:
|
|
505
|
+
return await self._command(vin, "open_tonneau")
|
|
506
|
+
|
|
507
|
+
async def close_tonneau(self, vin: str) -> CommandResponse:
|
|
508
|
+
return await self._command(vin, "close_tonneau")
|
|
509
|
+
|
|
510
|
+
async def stop_tonneau(self, vin: str) -> CommandResponse:
|
|
511
|
+
return await self._command(vin, "stop_tonneau")
|
|
512
|
+
|
|
513
|
+
# ------------------------------------------------------------------
|
|
514
|
+
# Power management commands
|
|
515
|
+
# ------------------------------------------------------------------
|
|
516
|
+
|
|
517
|
+
async def set_low_power_mode(self, vin: str, *, enable: bool) -> CommandResponse:
|
|
518
|
+
return await self._command(vin, "set_low_power_mode", {"enable": enable})
|
|
519
|
+
|
|
520
|
+
async def keep_accessory_power_mode(self, vin: str, *, enable: bool) -> CommandResponse:
|
|
521
|
+
return await self._command(vin, "keep_accessory_power_mode", {"enable": enable})
|
|
522
|
+
|
|
523
|
+
# ------------------------------------------------------------------
|
|
524
|
+
# Managed charging (fleet)
|
|
525
|
+
# ------------------------------------------------------------------
|
|
526
|
+
|
|
527
|
+
async def set_managed_charge_current_request(
|
|
528
|
+
self, vin: str, *, charging_amps: int
|
|
529
|
+
) -> CommandResponse:
|
|
530
|
+
return await self._command(
|
|
531
|
+
vin, "set_managed_charge_current_request", {"charging_amps": charging_amps}
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
async def set_managed_charger_location(
|
|
535
|
+
self, vin: str, *, location: dict[str, Any]
|
|
536
|
+
) -> CommandResponse:
|
|
537
|
+
return await self._command(vin, "set_managed_charger_location", location)
|
|
538
|
+
|
|
539
|
+
async def set_managed_scheduled_charging_time(self, vin: str, *, time: int) -> CommandResponse:
|
|
540
|
+
return await self._command(vin, "set_managed_scheduled_charging_time", {"time": time})
|
tescmd/api/energy.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Energy site API — wraps /api/1/energy_sites/{site_id} endpoints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from tescmd.models.energy import CalendarHistory, LiveStatus, SiteInfo
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from tescmd.api.client import TeslaFleetClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EnergyAPI:
|
|
14
|
+
"""Energy site operations (Powerwall, Solar, etc.)."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, client: TeslaFleetClient) -> None:
|
|
17
|
+
self._client = client
|
|
18
|
+
|
|
19
|
+
async def list_products(self) -> list[dict[str, Any]]:
|
|
20
|
+
"""Return all energy products on the account."""
|
|
21
|
+
data = await self._client.get("/api/1/products")
|
|
22
|
+
result: list[dict[str, Any]] = data.get("response", [])
|
|
23
|
+
return result
|
|
24
|
+
|
|
25
|
+
async def live_status(self, site_id: int) -> LiveStatus:
|
|
26
|
+
"""Fetch real-time power flow for a site."""
|
|
27
|
+
data = await self._client.get(f"/api/1/energy_sites/{site_id}/live_status")
|
|
28
|
+
return LiveStatus.model_validate(data.get("response", {}))
|
|
29
|
+
|
|
30
|
+
async def site_info(self, site_id: int) -> SiteInfo:
|
|
31
|
+
"""Fetch site configuration and metadata."""
|
|
32
|
+
data = await self._client.get(f"/api/1/energy_sites/{site_id}/site_info")
|
|
33
|
+
return SiteInfo.model_validate(data.get("response", {}))
|
|
34
|
+
|
|
35
|
+
async def set_backup_reserve(self, site_id: int, *, percent: int) -> dict[str, Any]:
|
|
36
|
+
"""Set the backup reserve percentage."""
|
|
37
|
+
data = await self._client.post(
|
|
38
|
+
f"/api/1/energy_sites/{site_id}/backup",
|
|
39
|
+
json={"backup_reserve_percent": percent},
|
|
40
|
+
)
|
|
41
|
+
result: dict[str, Any] = data.get("response", {})
|
|
42
|
+
return result
|
|
43
|
+
|
|
44
|
+
async def set_operation_mode(self, site_id: int, *, mode: str) -> dict[str, Any]:
|
|
45
|
+
"""Set the site operation mode (self_consumption, backup, autonomous)."""
|
|
46
|
+
data = await self._client.post(
|
|
47
|
+
f"/api/1/energy_sites/{site_id}/operation",
|
|
48
|
+
json={"default_real_mode": mode},
|
|
49
|
+
)
|
|
50
|
+
result: dict[str, Any] = data.get("response", {})
|
|
51
|
+
return result
|
|
52
|
+
|
|
53
|
+
async def set_storm_mode(self, site_id: int, *, enabled: bool) -> dict[str, Any]:
|
|
54
|
+
"""Enable or disable storm watch."""
|
|
55
|
+
data = await self._client.post(
|
|
56
|
+
f"/api/1/energy_sites/{site_id}/storm_mode",
|
|
57
|
+
json={"enabled": enabled},
|
|
58
|
+
)
|
|
59
|
+
result: dict[str, Any] = data.get("response", {})
|
|
60
|
+
return result
|
|
61
|
+
|
|
62
|
+
async def time_of_use_settings(
|
|
63
|
+
self, site_id: int, *, settings: dict[str, Any]
|
|
64
|
+
) -> dict[str, Any]:
|
|
65
|
+
"""Set time-of-use schedule."""
|
|
66
|
+
data = await self._client.post(
|
|
67
|
+
f"/api/1/energy_sites/{site_id}/time_of_use_settings",
|
|
68
|
+
json={"tou_settings": settings},
|
|
69
|
+
)
|
|
70
|
+
result: dict[str, Any] = data.get("response", {})
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
async def charging_history(self, site_id: int) -> CalendarHistory:
|
|
74
|
+
"""Fetch charging history for a site (wall connector telemetry)."""
|
|
75
|
+
data = await self._client.get(
|
|
76
|
+
f"/api/1/energy_sites/{site_id}/telemetry_history",
|
|
77
|
+
params={"kind": "charge"},
|
|
78
|
+
)
|
|
79
|
+
return CalendarHistory.model_validate(data.get("response", {}))
|
|
80
|
+
|
|
81
|
+
async def off_grid_vehicle_charging_reserve(
|
|
82
|
+
self, site_id: int, *, reserve: int
|
|
83
|
+
) -> dict[str, Any]:
|
|
84
|
+
"""Set off-grid EV charging reserve percentage."""
|
|
85
|
+
data = await self._client.post(
|
|
86
|
+
f"/api/1/energy_sites/{site_id}/off_grid_vehicle_charging_reserve",
|
|
87
|
+
json={"off_grid_vehicle_charging_reserve_percent": reserve},
|
|
88
|
+
)
|
|
89
|
+
result: dict[str, Any] = data.get("response", {})
|
|
90
|
+
return result
|
|
91
|
+
|
|
92
|
+
async def grid_import_export(self, site_id: int, *, config: dict[str, Any]) -> dict[str, Any]:
|
|
93
|
+
"""Set grid import/export configuration."""
|
|
94
|
+
data = await self._client.post(
|
|
95
|
+
f"/api/1/energy_sites/{site_id}/grid_import_export",
|
|
96
|
+
json=config,
|
|
97
|
+
)
|
|
98
|
+
result: dict[str, Any] = data.get("response", {})
|
|
99
|
+
return result
|
|
100
|
+
|
|
101
|
+
async def telemetry_history(
|
|
102
|
+
self,
|
|
103
|
+
site_id: int,
|
|
104
|
+
*,
|
|
105
|
+
kind: str = "charge",
|
|
106
|
+
start_date: str | None = None,
|
|
107
|
+
end_date: str | None = None,
|
|
108
|
+
time_zone: str | None = None,
|
|
109
|
+
) -> CalendarHistory:
|
|
110
|
+
"""Fetch telemetry-based charge history for a site."""
|
|
111
|
+
params: dict[str, str] = {"kind": kind}
|
|
112
|
+
if start_date:
|
|
113
|
+
params["start_date"] = start_date
|
|
114
|
+
if end_date:
|
|
115
|
+
params["end_date"] = end_date
|
|
116
|
+
if time_zone:
|
|
117
|
+
params["time_zone"] = time_zone
|
|
118
|
+
data = await self._client.get(
|
|
119
|
+
f"/api/1/energy_sites/{site_id}/telemetry_history",
|
|
120
|
+
params=params,
|
|
121
|
+
)
|
|
122
|
+
return CalendarHistory.model_validate(data.get("response", {}))
|
|
123
|
+
|
|
124
|
+
async def calendar_history(
|
|
125
|
+
self,
|
|
126
|
+
site_id: int,
|
|
127
|
+
*,
|
|
128
|
+
kind: str = "energy",
|
|
129
|
+
period: str = "day",
|
|
130
|
+
start_date: str | None = None,
|
|
131
|
+
end_date: str | None = None,
|
|
132
|
+
time_zone: str | None = None,
|
|
133
|
+
) -> CalendarHistory:
|
|
134
|
+
"""Fetch calendar-based history for a site."""
|
|
135
|
+
params: dict[str, str] = {"kind": kind, "period": period}
|
|
136
|
+
if start_date:
|
|
137
|
+
params["start_date"] = start_date
|
|
138
|
+
if end_date:
|
|
139
|
+
params["end_date"] = end_date
|
|
140
|
+
if time_zone:
|
|
141
|
+
params["time_zone"] = time_zone
|
|
142
|
+
data = await self._client.get(
|
|
143
|
+
f"/api/1/energy_sites/{site_id}/calendar_history",
|
|
144
|
+
params=params,
|
|
145
|
+
)
|
|
146
|
+
return CalendarHistory.model_validate(data.get("response", {}))
|