Advertisement
mattfarley

Automatic.com Websocket Notifications in Linux

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