mattfarley

Automatic.com Websocket Notifications in Linux

Oct 29th, 2016
338
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 11.57 KB | None | 0 0
  1. #!/usr/bin/python
  2.  
  3. # Dependency: https://github.com/invisibleroads/socketIO-client
  4. # Install client first: sudo pip install -U socketIO-client
  5.  
  6. # Disclaimer:
  7. #   -I'm a Python novice
  8. #
  9. # Usage:
  10. #   -Edit the car{} variable below, change the id's to your Automatic car id's  
  11. #   -Edit the googleMapsApiKey
  12. #   -Start from (Linux) command-line to begin listening for websocket notifications: https://developer.automatic.com/api-reference/#real-time-events
  13. #
  14. # Notes:
  15. #   -The notification occurs in the function: 'notifyNetwork' at the end of this file. This notification is unique to my network. It places popups on all the TV's and Desktops, flashes the lights, and causes Alexa to read the message outloud (all at once)
  16. #   -There's a function in there that notifies my carpool partner that I've left the house in the morning, this probably doesn't apply to you
  17. #   -There's a lot of code to account for the fact that (a) automatic sends notifications out of order, so I only show new ones, and (b) notifications often come late :(
  18. #   -The logic for car starting/stopping = always notify, but the logic for location_updated is to only notify me if the car is on its way home, and then only repeat an update of ETA if the car has made 50% progress home since its last notification
  19. #   -The two cars configured below are our Honda and Scion, but the 3rd car represents the Automatic app dashboard test events
  20. #
  21. # Video of this in action:
  22. #   -(end of the video, last 15 seconds) --> https://www.youtube.com/watch?v=y09_YaduvEk
  23. #
  24. #
  25.  
  26. import logging
  27. logging.getLogger('socketIO-client').setLevel(logging.DEBUG)
  28. import sys
  29. import datetime
  30. import time
  31. import subprocess
  32. import json
  33. import requests
  34. import os
  35. from socketIO_client import SocketIO, LoggingNamespace
  36.  
  37. """
  38.    Initialize global vars ****************************************************
  39. """
  40. vehicleId = ""
  41. history = {}
  42. history_counter = 0
  43. history_limit = 400
  44. event = {}
  45. car = {}
  46. googleMapsApiKey = 'ABC123'
  47.  
  48. # Tia's honda setup
  49. car['C_905zzzzzzzzzzzzz'] = {}
  50. car['C_905zzzzzzzzzzzzz']['name'] = 'Honda Minivan'
  51. car['C_905zzzzzzzzzzzzz']['previousMilesFromHome'] = 0.0
  52. car['C_905zzzzzzzzzzzzz']['currentMilesFromHome'] = 0.0
  53. # Matt's Scion setup
  54. car['C_d8daaaaaaaaaaaaa'] = {}
  55. car['C_d8daaaaaaaaaaaaa']['name'] = 'Scion tC'
  56. car['C_d8daaaaaaaaaaaaa']['previousMilesFromHome'] = 0.0
  57. car['C_d8daaaaaaaaaaaaa']['currentMilesFromHome'] = 0.0
  58. # Lambo
  59. car['C_40ebbbbbbbbbbbbb'] = {}
  60. car['C_40ebbbbbbbbbbbbb']['name'] = 'Lamborghini'
  61. car['C_40ebbbbbbbbbbbbb']['previousMilesFromHome'] = 0.0
  62. car['C_40ebbbbbbbbbbbbb']['currentMilesFromHome'] = 0.0
  63.  
  64. """
  65.    Triage websocket events, decide what to do ****************************************************
  66. """
  67. # *args can accept multiple arguments, coming through as args[0], args[1], etc
  68. def triage(*args):
  69.  
  70.     # Debug:
  71.     print(args[0])    
  72.  
  73.     # Any globals that will be modified must be called out
  74.     global event
  75.     global history
  76.     global history_counter
  77.     global vehicleId
  78.    
  79.     # Car ID to isolate tracking without cross-talk confusion
  80.     vehicleId = args[0]['vehicle']['id']
  81.  
  82.     # Test for new event but occurs prior to an event we already received (thus can be disregarded, even though we missed it, tan-pis)
  83.     # Will error on the first event since there's no last event    
  84.     try:
  85.         newTimestamp = datetime.datetime.strptime(args[0]['location']['created_at'], '%Y-%m-%dT%H:%M:%S.%fZ');
  86.         lastTimestamp = datetime.datetime.strptime(event[vehicleId]['location']['created_at'], '%Y-%m-%dT%H:%M:%S.%fZ');
  87.         if newTimestamp < lastTimestamp:
  88.             print("Skipping historic lagging event, we already received newer events: " + args[0]['type'])
  89.             return
  90.     except Exception, e:
  91.         print("FYI - Exception testing newTimestamp vs Old Timestamp: " + str(e))
  92.         pass
  93.  
  94.     # Capture all the json data in event
  95.     event[vehicleId] = args[0];
  96.  
  97.     # Notify Carpool that I'm started my car (if it's early in the morning and we're turning the car on)
  98.     if event[vehicleId]['type'] == 'ignition:on':    
  99.         if datetime.time(5,20) <= datetime.datetime.now().time() <= datetime.time(6,15):
  100.             if datetime.datetime.today().weekday() < 5:
  101.                 notifyCarpool()
  102.    
  103.     # Test for duplicate
  104.     if event[vehicleId]['id'] in history.values():
  105.         print("Skipping duplicate: " + event[vehicleId]['type'])
  106.         return
  107.  
  108.     # New notification, record it to history
  109.     if history_counter > history_limit: history_counter = 0
  110.     history[history_counter] = event[vehicleId]['id']
  111.     history_counter += 1    
  112.  
  113.     # Share what we're dealing with
  114.     print("\n" + event[vehicleId]['type'] + " @ " + datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
  115.  
  116.     # Google Location Geocode and Distance Matrix ETA
  117.     ETA = getETA()
  118.     location = getLocation()
  119.    
  120.     if event[vehicleId]['type'] == 'ignition:on':
  121.         car[vehicleId]['previousMilesFromHome'] = 0.0
  122.         car[vehicleId]['currentMilesFromHome'] = 0.0
  123.         notifyNetwork(car[vehicleId]['name'] + " Started", "Ignition on at " + location + ", " + ETA)
  124.  
  125.     if event[vehicleId]['type'] == 'ignition:off':
  126.         car[vehicleId]['previousMilesFromHome'] = 0.0
  127.         car[vehicleId]['currentMilesFromHome'] = 0.0
  128.         notifyNetwork(car[vehicleId]['name'] + " Parked", "Ignition off at " + location)
  129.  
  130.     if event[vehicleId]['type'] == 'location:updated':
  131.        
  132.         print('PreviousMiles = ' + str(car[vehicleId]['previousMilesFromHome']));
  133.         print('CurrentMiles = ' + str(car[vehicleId]['currentMilesFromHome']));
  134.        
  135.         try:
  136.             percentChange = ((car[vehicleId]['previousMilesFromHome'] - car[vehicleId]['currentMilesFromHome']) / car[vehicleId]['previousMilesFromHome']) * 100
  137.         except Exception, e:
  138.             # Divide by zero when car[vehicleId]['previousMilesFromHome'] is zero
  139.             percentChange = 0.0
  140.             car[vehicleId]['previousMilesFromHome'] = car[vehicleId]['currentMilesFromHome']            
  141.            
  142.         # Only notify en route if there is a significant change in progress towards the house (or we're less than a mile away)
  143.         if percentChange > 50.0 or car[vehicleId]['currentMilesFromHome'] == 0.0:
  144.             car[vehicleId]['previousMilesFromHome'] = car[vehicleId]['currentMilesFromHome']
  145.             notifyNetwork(car[vehicleId]['name'] + " En Route", "At " + location + ", " + ETA)
  146.         else:
  147.             print("Skipping due to lack of progress *towards* home; percent change = " + str(percentChange))
  148.     return    
  149.  
  150.  
  151. """
  152.    Let Adam know I'm coming ****************************************************
  153. """
  154. def notifyCarpool():
  155.     print("Texting Adam farley that we are leaving now...")
  156.    
  157.     # Notify Adam
  158.     command = 'sms 1235551212 "Farley car started @ ' + datetime.datetime.now().strftime("%H:%M") + '. I assume they are coming for you. -Jarvis"'    
  159.     with open(os.devnull, 'wb') as devnull:
  160.         subprocess.check_call(command, shell=True, stdout=devnull, stderr=subprocess.STDOUT)            
  161.  
  162.     # Notify Matt                
  163.     command = 'sms 1235551111 "Farley car started @ ' + datetime.datetime.now().strftime("%H:%M") + '. I assume they are coming for you. -Jarvis"'    
  164.     with open(os.devnull, 'wb') as devnull:
  165.         subprocess.check_call(command, shell=True, stdout=devnull, stderr=subprocess.STDOUT)            
  166.                
  167.     return
  168.  
  169. """
  170.    Websocket Error ****************************************************
  171. """
  172. def on_error(*args):
  173.     print('on_error', args)
  174.  
  175. """
  176.    Get ETA via Google Distance Matrix ****************************************************
  177. """
  178. def getETA():
  179.  
  180.     global vehicleId
  181.     global googleMapsApiKey
  182.  
  183.     try:
  184.         response = requests.get('https://maps.googleapis.com/maps/api/distancematrix/json?origins='+str(event[vehicleId]['location']['lat'])+','+str(event[vehicleId]['location']['lon'])+'&destinations=Houston,%20TX&key='+googleMapsApiKey+'&units=imperial')
  185.         json_data = json.loads(response.text)
  186.        
  187.         text = json_data['rows'][0]['elements'][0]['duration']['text']
  188.         seconds = json_data['rows'][0]['elements'][0]['duration']['value']
  189.        
  190.         timestamp = datetime.datetime.strptime(event[vehicleId]['location']['created_at'], '%Y-%m-%dT%H:%M:%S.%fZ')
  191.         timestamp = timestamp - datetime.timedelta(hours=5)
  192.        
  193.         # Give ETA as event timestamp plus google travel time to home
  194.         ETA = timestamp + datetime.timedelta(seconds=seconds)
  195.        
  196.         # Record last ETA Distance and this ETA Distance to help determine if we'll notify en route
  197.         distance = str(json_data['rows'][0]['elements'][0]['distance']['text']).split()
  198.         if distance[1] == 'mi':
  199.             car[vehicleId]['currentMilesFromHome'] = float(distance[0]);
  200.         else:
  201.             car[vehicleId]['currentMilesFromHome'] = 0.0;
  202.        
  203.         return text + " / " + str(car[vehicleId]['currentMilesFromHome']) + " miles from home, ETA " + ETA.strftime("%I:%M %p")
  204.     except Exception, e:
  205.         print 'Location unknown, distance matrix calculation failed: ' + str(e)
  206.         return 'unknown ETA'
  207.  
  208. """
  209.    Google Geocode Location ****************************************************
  210. """
  211. def getLocation():
  212.  
  213.     global vehicleId
  214.     global googleMapsApiKey
  215.  
  216.     text = 'unknown location'    
  217.  
  218.     try:
  219.         response = requests.get('https://maps.googleapis.com/maps/api/geocode/json?latlng='+str(event[vehicleId]['location']['lat'])+','+str(event[vehicleId]['location']['lon'])+'&key='+googleMapsApiKey)
  220.         json_data = json.loads(response.text)
  221.  
  222.  
  223.         #print(json_data['results'][0]['address_components'])
  224.         for component in json_data['results'][0]['address_components']:
  225.             if component['types'][0] == 'route':
  226.                 text = component['long_name']
  227.     except Exception, e:
  228.         print('Location unknown, geocode failed: ' + str(e))
  229.         pass
  230.    
  231.     return text
  232.  
  233. """
  234.    Notify Network ****************************************************
  235. """
  236. def notifyNetwork(header, message):
  237.  
  238.     global vehicleId
  239.  
  240.     # Notifying network
  241.     # $1 = Icon
  242.     # $2 = Prefix
  243.     # $3 = Subject / Header
  244.     # $4 = Message
  245.     # $5 = Type / App
  246.     command  = '/mnt/documents/bin/notify_network /mnt/documents/Icons/' + event[vehicleId]['vehicle']['id'] + '.png '
  247.     command += '"' + header + ' " '
  248.    
  249.     # If this event occurred more than a few minutes ago, then add a timestamp
  250.     timestamp = datetime.datetime.strptime(event[vehicleId]['location']['created_at'], '%Y-%m-%dT%H:%M:%S.%fZ')
  251.     timestamp = timestamp - datetime.timedelta(hours=5) # API sends them five hours later than CST
  252.     tsNow = datetime.datetime.now()
  253.     d1_ts = time.mktime(timestamp.timetuple())
  254.     d2_ts = time.mktime(tsNow.timetuple())    
  255.     minutesAgo = int(d2_ts-d1_ts) / 60
  256.     if minutesAgo > 4:
  257.         command += '"- ' + timestamp.strftime("%I:%M %p") + '" '    
  258.     else:
  259.         command += '"" '
  260.    
  261.     command += '"' + message + '" '
  262.     command += '"automatic" '    
  263.     with open(os.devnull, 'wb') as devnull:
  264.         subprocess.check_call(command, shell=True, stdout=devnull, stderr=subprocess.STDOUT)    
  265.  
  266.  
  267.  
  268. """
  269.    Main Execution ****************************************************
  270. """
  271. io = SocketIO('https://stream.automatic.com', 443, LoggingNamespace, params={'token': 'clientid:zzzzzzzzzzzzzzzzzzzzz'})
  272. #io.on('trip:finished', triage)
  273. io.on('location:updated', triage)
  274. io.on('ignition:on', triage)
  275. io.on('ignition:off', triage)
  276. io.on('error', on_error)
  277. io.wait()
Advertisement
Add Comment
Please, Sign In to add comment