forked from gglockner/teslajson
-
Notifications
You must be signed in to change notification settings - Fork 2
/
tesla_poller
executable file
·443 lines (346 loc) · 15.1 KB
/
tesla_poller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
#!/usr/bin/python
import teslajson
import time
import json
import traceback
import argparse
from threading import Thread, Lock
import sys
import subprocess
import socket
import Queue
import faulthandler
import signal
args = None
master_connection = None
master_lock = Lock()
# Time intervals of importance to program operation
intervals = { "inactive": 60, "to_sleep": 900, "charging": 90, "running": 30, "recent": 60, "prep": 60, "Unknown": 15, "any_poll": 10000, "running_poll": 300, "charging_poll": 900, "recent_interval": 500 }
def monitor_socket(sock, queues, vlist):
"""Monitor RPC socket forever"""
while True:
data, addr = sock.recvfrom(1024)
if args.verbose:
print("# %d Received socket data: %s"%(time.time(), str(data)))
if data is None:
return
# Try to decode what was sent us
try:
dvar = json.loads(data)
except ValueError:
if args.verbose:
print("# %d Could not parse socket data"%time.time())
continue
# Check for known commands
if 'cmd' not in dvar:
if args.verbose:
print("# %d No command in socket data"%time.time())
continue
if dvar['cmd'] not in ('autocondition','quit'):
if args.verbose:
print("# %d Unknown command socket data"%time.time())
continue
# Resolve what car this command applies to
if 'carid' not in dvar:
if 'carpos' not in dvar:
dvar['carpos'] = 0
try:
dvar['carid'] = vlist[dvar['carpos']]['id']
except KeyError:
if args.verbose:
print("# %d Unknown vehicle position"%time.time())
continue
# Use pipe to car
try:
q = queues[dvar['carid']]
except KeyError:
if args.verbose:
print("# %d Unknown vehicle id"%time.time())
continue
# Write command to car queue
q.put(dvar)
# Special case, quit command applies to me too
if dvar['cmd'] == 'quit':
sys.exit(0)
def handle_queue(v, q, data, outs):
"""Handle RPC requests, at a per-vehicle level"""
# Is there anything to do?
try:
dvar = q.get_nowait()
except Queue.Empty:
return
# Simple command, go away
if dvar['cmd'] == 'quit':
sys.exit(0)
# Autocondition--increase charging limit and start A/C
if dvar['cmd'] == "autocondition":
# Charge state may be relative, get the most recent data if we don't already have it
if "charge_state" not in data:
wake(v)
data = data_request(v, "all")
# Find the default charge level
level = 85 if data['charge_state']['charge_limit_soc'] <= 80 else data['charge_state']['charge_limit_soc'] + 5
if level > 100:
level = 100
# Default start A/C
temp = True
# User-specified charge level
if "level" in dvar:
try:
level = int(dvar["level"])
except ValueError:
if args.verbose:
print("# %d Bad condition level"%time.time())
return
# User-specified A/C setting
if "temp" in dvar:
if dvar["temp"]:
temp = True
else:
temp = False
# Wake car
wake(v)
message="# %d Command processing: "%time.time()
if "autoresetlimit" not in dvar or not dvar["autoresetlimit"]:
autoresetlimit = data['charge_state']['charge_limit_soc']
else:
autoresetlimit = int(dvar["autoresetlimit"])
# Set charge level
if level:
command(v, "set_charge_limit", {"percent": level})
if autoresetlimit > 0:
outs['reset_charge_limit'] = autoresetlimit
message += "Charge limit set to %d from %d (w/reset %d). "%(level, outs['reset_charge_limit'] if 'reset_charge_limit' in outs else -1, autoresetlimit)
if args.verbose:
print("# %d Setting charge limit to %d (Reset limit is %d)"%(time.time(),level,autoresetlimit))
# Set A/C state
if temp:
command(v, "auto_conditioning_start")
message += "Conditioning on. "
if args.verbose:
print("# %d Turning on conditioning"%time.time())
else:
command(v, "auto_conditioning_stop")
message += "Conditioning off. "
if args.verbose:
print("# %d Turning off conditioning"%time.time())
W.write(message+"\n")
return True
def handle_outstanding(v, data, outs):
"""Handle tasks we have been asked to do in the future"""
if "reset_charge_limit" in outs:
# If we see reset_charge_limit, reset it back to the requested limit as soon as car is driving
if "drive_state" in data:
if data["drive_state"]["shift_state"] is not None:
if args.verbose:
print("# %d Resetting charge limit to %d"%(time.time(), outs["reset_charge_limit"]))
W.write("# %d Resetting charge limit to %d"%(time.time(), outs["reset_charge_limit"]))
command(v, "set_charge_limit", {"percent": outs["reset_charge_limit"]})
del(outs["reset_charge_limit"])
nexthour = 0
def output_maintenance():
"""Move to the next output file, if applicable"""
global nexthour, W
# Don't bother doing output maintenance if we are not saving data
if not args.outdir:
return
cur = time.time()
# Ensure we don't have multi-vehicle output direct race conditions
with master_lock:
if cur < nexthour:
return
if W is not None:
W.close()
nexthour = (int(cur / 3600)+1) * 3600
fname = time.strftime("%Y-%m-%d.json", time.gmtime(cur))
pname = "%s/%s"%(args.outdir, fname)
W = open(pname, "a", 0)
subprocess.call(["ln", "-sf", fname, "%s/cur.json"%args.outdir])
def refresh_vehicles(args, debug=False):
"""Connect to service and get list of vehicles"""
c = teslajson.Connection(email=args.email, password=args.password, access_token=args.token, tokens_file=args.tokenfile, proxy_url=args.proxy_url, proxy_user=args.proxy_user, proxy_password=args.proxy_password, retries=10, debug=debug)
if args.verbose:
print("# %d Vehicles: %s\n"%(time.time(), str(c.vehicles)))
return c
def data_request(vehicle, type, datawrap=None):
"""Get data from the vehicle, with retries on failure"""
if type == "all":
vdata = vehicle.data_all()
else:
vdata = vehicle.data_request(type)
if type and datawrap:
ndata = dict(datawrap)
ndata[type] = vdata
vdata = ndata
vdata['retrevial_time'] = int(time.time())
return vdata
def command(vehicle, *args, **kvargs):
"""Run a command on the vehicle, with retries on failure"""
return vehicle.command(*args, **kvargs)
def wake(vehicle):
"""Try really hard to wake vehicle up"""
wake_tries = 0
while wake_tries < 10000:
wake_tries += 1
output_maintenance()
vdata = data_request(vehicle, None)
W.write(json.dumps(vdata)+"\n")
if vdata["state"] not in ("asleep","offline","inactive"):
return vdata
if args.verbose:
W.write("# Waking... (%d times so far) at %d\n"%(wake_tries,time.time()))
vehicle.wake_up()
W.write("# Could not wake %s\n"%vehicle['display_name'])
return None
def monitor_sleep(vehicle, queue, data, outstanding, stime):
"""Sleep for a time, but polling queue and returning early if we did something"""
while stime > 0:
delta = 5 if stime > 5 else stime
time.sleep(delta)
stime -= delta
if queue:
if handle_queue(vehicle, queue, data, outstanding):
return
def monitor_vehicle(vehicle, args, queue):
"""Monitor a vehicle, forever, printing json about current status"""
state = args.state
backoff = 1
last_all = 0
last_active = 0
outstanding = {}
output_maintenance()
wake(vehicle)
basedata = data_request(vehicle, None)
# Loop to handle exceptions, with bounded expoential backoff to prevent Tesla from getting overly mad if we are polling too often
while True:
try:
# Loop monitoring vehicle state
while True:
# Handle output file
output_maintenance()
if state == "Unknown":
what = "all"
elif state == "charging":
what = "charge_state"
elif state == "running":
what = "drive_state"
elif state == "inactive":
what = None
elif state == "prep":
what = "all"
elif state == "recent":
what = "all"
elif state == "to_sleep":
what = None
else:
raise Exception("Unknown state %s"%str(state))
# Handle periodic all-data info refresh
all_interval = intervals.get(state+"_poll",intervals["any_poll"])
if last_all + all_interval <= time.time():
what = "all"
if what == "all":
last_all = time.time()
# Handle asleep vehicles
if state == "inactive" and what is not None:
wake(vehicle)
# Get the data
vdata = data_request(vehicle, what, datawrap=basedata)
W.write(json.dumps(vdata)+"\n")
backoff = 1
# Figure out what state we are now in
if vdata["state"] in ("asleep","offline","inactive"):
# Car is asleep
state = "inactive"
elif state == "to_sleep":
# We were trying to go to sleep but did not, why?
state = "Unknown"
elif state == "inactive" and what != 'all':
# Car was asleep, figure out what it is doing now
state = "Unknown"
else:
# Assume we are trying to go to sleep, will update otherwise
state = "to_sleep"
# If we have recently been doing something interesting
if last_active + intervals["recent_interval"] > time.time():
state = "recent"
# If we are currently preparing (or actually) doing something interesting
if "climate_state" in vdata and vdata["climate_state"]["is_climate_on"]:
state = "recent"
last_active = time.time()
# If we are currently charging
if "charge_state" in vdata and vdata["charge_state"]["charger_power"] is not None and vdata["charge_state"]["charger_power"] > 0:
state = "charging"
last_active = time.time()
# If we are currently driving
if "drive_state" in vdata and vdata["drive_state"]["shift_state"] is not None:
state = "running"
last_active = time.time()
if args.verbose:
W.write("# %d STATE: %s sleep(%s) last_all=%d last_active=%d what=%s\n"%(time.time(), state, intervals[state], last_all, last_active,str(what)))
if queue:
handle_queue(vehicle, queue, vdata, outstanding)
if outstanding:
handle_outstanding(vehicle, vdata, outstanding)
# Mostly sleep for state interval
if monitor_sleep(vehicle, queue, vdata, outstanding, intervals[state]):
state = "Unknown"
if args.verbose:
W.write("# %d QSTATE: %s\n"%(time.time(), state))
except Exception as e:
W.write("# %d Exception: %s\n"%(time.time(), str(e)))
traceback.print_exc()
backoff += 1
if backoff > 3:
backoff = 3
intrvl = 6 * 10**backoff
W.write("# %d Disaster sleep for %d\n"%(time.time(),intrvl))
time.sleep(intrvl)
# Do no wake here--except will kill perm
state = "Unknown"
parser = argparse.ArgumentParser()
parser.add_argument('--verbose', '-v', action='count', help='Increasing levels of verbosity')
parser.add_argument('--intervals',action='append',type=lambda x: x.split('='), help="Set important intervals name=secs for names in %s"%str(intervals.keys()))
parser.add_argument('--email', default=None, help='Tesla email for authentication option 1')
parser.add_argument('--password', default=None, help='Tesla password for authentication option 1')
parser.add_argument('--tokenfile', '--tokens_file', default=None, help='File containing access token json for tesla service, authentication option 2')
parser.add_argument('--token', '--access_token', default=None, help='Access token for tesla service, authentication option 3')
parser.add_argument('--proxy_url', default=None, help='URL for optional web proxy')
parser.add_argument('--proxy_user', default=None, help='Username for optional web proxy')
parser.add_argument('--proxy_password', default=None, help='Password for optional web proxy')
parser.add_argument('--state', default="Unknown", help="Start by assuming we are in named state")
parser.add_argument('--outdir', default=None, help='Directory to output log files')
parser.add_argument('--cmd_address', default=None, help='address:Port number to receive UDP commands on')
args = parser.parse_args()
W = None if args.outdir else sys.stdout
if not args.token and not args.tokenfile and not args.password:
print('''Must supply --token or --tokenfile or --email and --password''')
sys.exit(1)
# Let us see where we are stalled
faulthandler.register(signal.SIGUSR1) #pylint: disable=no-member
if args.intervals:
args.intervals = dict(args.intervals)
for x in args.intervals:
args.intervals[x] = int(args.intervals[x])
intervals.update(args.intervals)
master_connection = refresh_vehicles(args, debug=True if args.verbose > 2 else False)
if len(master_connection.vehicles) < 1:
raise Exception("No vehicles to monitor")
if args.cmd_address:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
dest = args.cmd_address.split(":")
dest[1] = int(dest[1])
sock.bind(tuple(dest))
queues = dict([(v['id'], Queue.Queue()) for v in master_connection.vehicles])
Thread(target=monitor_socket, args=(sock,queues,master_connection.vehicles)).start()
else:
sock = None
queues = dict([(v['id'], None) for v in master_connection.vehicles])
if len(master_connection.vehicles) == 1:
monitor_vehicle(master_connection.vehicles[0], args, queues[master_connection.vehicles[0]['id']])
else:
tlist = []
for vehicle in master_connection.vehicles:
t = Thread(target=monitor_vehicle, args=(vehicle,args,sock,queues[vehicle['id']])).start()
tlist.append(t)
for t in tlist:
t.join()