onair.py
  1  # SPDX-FileCopyrightText: 2018 Phillip Burgess for Adafruit Industries
  2  #
  3  # SPDX-License-Identifier: MIT
  4  
  5  #!/usr/bin/python
  6  
  7  # "ON AIR" sign controller for Raspberry Pi.  Polls Ustream and Google+
  8  # Hangouts for online status, activates PowerSwitch Tail II as needed,
  9  # turns on lamp.  Requires RPi.GPIO library.
 10  #
 11  # Written by Phil Burgess / Paint Your Dragon for Adafruit Industries.
 12  #
 13  # Adafruit invests time and resources providing this open source code,
 14  # please support Adafruit and open-source hardware by purchasing products
 15  # from Adafruit!
 16  #
 17  # Resources:
 18  # http://www.adafruit.com/products/268
 19  # http://www.markertek.com/Studio-Gear/Studio-Warning-Lights-Signs.xhtml
 20  
 21  import bisect, calendar, json, time, urllib
 22  import RPi.GPIO as GPIO
 23  
 24  pin = 24   # PowerSwitch Tail connects to this GPIO pin
 25  
 26  # Ustream settings -----------------------------------------------------------
 27  # 'uKey' is your Developer API key (request one at developer.ustream.tv).
 28  # 'uChannel' is Ustream channel to monitor.
 29  uKey     =  'PUT_USTREAM_DEVELOPER_API_KEY_KERE'
 30  uChannel =  'adafruit-industries'
 31  uUrl     = ('http://api.ustream.tv/json/channel/' + uChannel +
 32              '/getValueOf/status?key='             + uKey )
 33  
 34  # Google+ settings -----------------------------------------------------------
 35  # 'gKey' is your API key from the Google APIs API Access page (need to switch
 36  # on G+ on API Services page first).  'gId' is the account ID to monitor (can
 37  # find this in the URL of your profile page, mild nuisance but ID is used
 38  # because user name is not guaranteed unique.
 39  gKey =  'PUT_GOOGLE_API_KEY_HERE'
 40  gId  =  '112526208786662512291' # Adafruit account ID
 41  gUrl = ('https://www.googleapis.com/plus/v1/people/' + gId +
 42          '/activities/public?maxResults=4&' +
 43          'fields=items(title,published),nextPageToken&key=' + gKey)
 44  gOn  =  'is hanging out'  # This phrase in title indicates an active hangout
 45  
 46  # List of starting times (HH:MM) and polling frequency (seconds) -------------
 47  # This is to provide a more responsive 'on air' switchover time without
 48  # blowing through search bandwidth limits.  Use more frequent checks during
 49  # known 'likely to switch' periods, infrequent during 'out of office' times.
 50  # Currently follows same pattern each day; doesn't have a weekly schedule.
 51  times = [
 52    ("06:00",  60),  # 6am, office hours starting, poll once per minute
 53    ("21:25",  10),  # 9:25pm, Show & Tell starting soon, poll 6X/minute
 54    ("21:35",  30),  # S&T underway, reduce polling to 2X per minute
 55    ("21:55",  10),  # 9:55pm, AAE starting soon, poll 6X/minute again
 56    ("22:05",  30),  # AAE underway, slow polling to 2X per minute
 57    ("23:10",  60),  # AAE over (plus extra), return to once per minute
 58    ("00:00", 900) ] # After midnight, gone home, poll every 15 minutes
 59  
 60  def req(url): # Open connection, read and deserialize JSON document ----------
 61  	connection = urllib.urlopen(url)
 62  	try:     data = json.load(connection)
 63  	finally: connection.close()
 64  	return data
 65  
 66  def paginate(pageToken): # ---------------------------------------------------
 67  	global gOnline, latestPost, timeThreshold
 68  
 69  	# Output from Google+ API is paginated, limited to 20 items max.
 70  	# We can't necessarily read everything in one pass, may need to
 71  	# make repeated calls passing a "page token" from one to the next.
 72  	response = req(
 73  	  (gUrl + '&pageToken=' + pageToken) if pageToken else gUrl)
 74  	for item in response['items']:
 75  		utcTime = time.strptime(
 76  		  item['published'].split('.', 1)[0],
 77  		  '%Y-%m-%dT%H:%M:%S')
 78  		seconds = calendar.timegm(utcTime) # UTC -> epoch
 79  		if seconds > latestPost:
 80  			# Keep track of time of most recent post.
 81  			# If the time threshold is more than 24 hours
 82  			# before this, it can be moved up -- otherwise
 83  			# searches will eventually reach all the way
 84  			# back to the last hangout which may be a full
 85  			# week prior.
 86  			latestPost = seconds
 87  			t = latestPost - 60 * 60 * 24
 88  			if(timeThreshold < t): timeThreshold = t
 89  		if seconds < timeThreshold:
 90  			# Time threshold reached, no on-air message
 91  			# found in any posts newer than the threshold,
 92  			# no hangout occurring.  Stop search.  Save
 93  			# time of latest post as new threshold;
 94  			# no need to search earlier than that for
 95  			# hangouts, they won't appear retroactively.
 96  			# (Items are in reverse chronological order.)
 97  			gOnline       = False
 98  			timeThreshold = latestPost
 99  			return None # Stop search; no further pages
100  		if gOn in item['title']:
101  			# On-air message found!  Set global gOnline
102  			# flag, set time threshold to hangout time;
103  			# need to keep testing back to this time to
104  			# confirm hangout is still running.  (If it
105  			# ends, title changes and will no longer
106  			# contain the on-air string.  There is no
107  			# separate event to indicate end of hangout.)
108  			gOnline       = True
109  			timeThreshold = seconds
110  			return None
111  	return response['nextPageToken'] # Continue search on next page
112  
113  # Startup --------------------------------------------------------------------
114  
115  GPIO.setwarnings(False)    # Don't bug me about existing pin state
116  GPIO.setmode(GPIO.BCM)     # Use Broadcom pin numbers
117  GPIO.setup(pin, GPIO.OUT)  # Enable output
118  GPIO.output(pin, GPIO.LOW) # Pin off by default
119  
120  # Convert times[] to a new list with units in integer minutes
121  mins = []
122  for t in times:
123  	x = t[0].split(":")
124  	mins.append([(int(x[0]) % 24) * 60 + int(x[1]), t[1]])
125  
126  mins.sort() # Sort in-place in increasing order
127  
128  # If first time is not midnight, insert an item there, duplicating the
129  # polling frequency of the last item in the list.
130  if mins[0][0] > 0: mins.insert(0, [0, mins[len(mins)-1][1]])
131  
132  timeThreshold = latestPost = 0
133  
134  while 1: # Main loop ---------------------------------------------------------
135  
136  	uOnline = gOnline = False
137  	startTime = time.time()
138  
139  	# Ustream broadcast query
140  	try:                 uOnline = req(uUrl)['results'] == 'live'
141  	except Exception as e: print ("Error: {0} : {1}").format(type(e), e.args)
142  
143  	# G+ hangout query
144  	try:
145  		pageToken = None
146  		while 1:
147  			pageToken = paginate(pageToken)
148  			if pageToken is None: break
149  	except Exception as e: print ("Error: {0} : {1}").format(type(e), e.args)
150  
151  	print ('G+ hangout: ') + ('online' if gOnline else 'offline')
152  	print ('Ustream   : ') + ('online' if uOnline else 'offline')
153  	GPIO.output(pin, GPIO.HIGH if uOnline or gOnline else GPIO.LOW)
154  
155  	# Delay before next query
156  	try:
157  		n = time.time()                      # NAO
158  		t = time.localtime(n)                # Local time struct
159  		m = t.tm_hour * 60 + t.tm_min        # Convert to minutes
160  		i = bisect.bisect(mins, [m, 60]) - 1 # mins[] list index
161  		d = mins[i][1] - (n - startTime)     # Time to next poll
162  		if d > 0:
163  			print ('Waiting ') + str(d) + ' seconds'
164  			time.sleep(d)
165  	except:
166  		time.sleep(60)