Jay Hickey

Technology, life, and fascinating web encounters.

Bringing Dark Sky to the Mac with PySky

I love the Dark Sky app for iOS. It's the best way to find out if rain or snow is imminent. Weather is unpredictable and inaccurate, but Dark Sky has honestly never failed me. It's amazing.

I've been thinking about how nice it would be to have Dark Sky on my Mac too. Since school has been pretty busy, I haven't been able to work on personal projects like this as I'd like. But I finally played with Dark Sky's newly released API—specifically Ryan Larrabure's Python wrapper. It was extremely easy to pull in all of the natural English language forecast information and display it cleanly on my Desktop with GeekTool. The output looked like this:

Rain, 44° F

Rain for 45 min

Moderate rain

Moderate chance of rain

Right after I finished it, Dr. Drang made an awesome post on plotting Dark Sky data with matplotlib. This inspired me to dig a little deeper and incorporate a precipitation intensity plot. Huge thanks go out to him for such a great post. Here's what the intensity plot looks like:

I also decided to incorporate Dr. Drang's method of using standard Python JSON decoding, instead of Ryan's Python wrapper. This saves the extra step of installing another module.

PySky is my result of these shenanigans. Here's it is:

# -*- coding: utf-8 -*-
import json
import urllib
from os import environ
from sys import exit, argv
import matplotlib.pyplot as plt

# If desired, enter lat & long as arguments
  lat = argv[1]
  lon = argv[2]
except IndexError:
  lat = 39.200932
  lon = -84.376009

# Get my API key and construct the URL
  with open(environ['HOME'] + '/.darksky') as rcfile:
    for line in rcfile:
      k, v = line.split(':')
      if k.strip() == 'APIkey':
        APIkey = v.strip()
    dsURL = 'https://api.darkskyapp.com/v1/forecast/%s/%s,%s' \
          % (APIkey, lat, lon)
except (IOError, NameError):
  print "Failed to get API key"

# Get the data from Dark Sky.
  jsonString = urllib.urlopen(dsURL).read()
  weather = json.loads(jsonString)
except (IOError, ValueError):
  print "Connection failure to %s" % dsURL

print 'NOW\n' +  str(weather['currentSummary']).capitalize() + ', '\
    + str(weather['currentTemp']) + u'° F'.encode('utf8') + '\n'
print 'NEXT HOUR \n' + weather['hourSummary'].capitalize() + '\n'

# Highest intensity in the next 3 hours.
hrsType = [ i['type'] for i in weather['dayPrecipitation'][1:4] ]
hrsProb = [ i['probability'] for i in weather['dayPrecipitation'][1:4] ]

chance = max(hrsProb)
probIndex = hrsProb.index(chance)

if chance > 0.8:
  nextThreeHrs = '%s' % (str(hrsType[probIndex])).capitalize()
elif chance > 0.5:
  nextThreeHrs = '%s likely' % (str(hrsType[probIndex])).capitalize()
elif chance > 0.2:
  nextThreeHrs = 'Possible %s' % str(hrsType[probIndex])
  nextThreeHrs = 'No precipitation'

print  'FOLLOWING 3 HRS\n' + nextThreeHrs.capitalize() + '\n'
print 'NEXT 24 HRS \n' + weather['daySummary'].capitalize() + '\n'

# Hourly intensity information for plot.
intensity = [x['intensity'] for x in weather['hourPrecipitation']]
time = [(x['time'] - weather['hourPrecipitation'][0]['time'])/60 \
     for x in weather['hourPrecipitation']]

# Plot dashed lines at intensity ranges.
plt.hlines(15, 0, 59, colors='black', linestyles='dashed')
plt.hlines(30, 0, 59, colors='black', linestyles='dashed')
plt.hlines(45, 0, 59, colors='black', linestyles='dashed')

# Plot the values.
plt.fill_between(time, intensity, color='#ffff00', alpha = .8, linewidth=2)
plt.axis([0, 59, 0, 60], frameon = True)
plt.xticks([10, 20, 30, 40, 50])
plt.yticks([7.5, 22.5, 37.5, 52.5], ['sporadic', 'light', 'moderate', 'heavy'],\
plt.tick_params('y', length=0)
plt.xlabel('Minutes from now')

# Save out to a png image.
plt.savefig('/Volumes/MacHD/Projects/darksky-scripts/ds-rain-1.png', dpi=220, \
  transparent=True, bbox_inches='tight')

GitHub is where you can download the source and view instructions to both run the script and display it on your Desktop with GeekTool—the latter of which makes PySky really useful. It looks like this on my Mac:

An explanation of the beginning of the script—reading in the latitude and longitude coordinates, getting the API key in from the ~/.darksky file, and retrieving JSON data from the Dark Sky—is better explained in Dr. Drang's post. He also does a great job at describing the plot routine.

I really like the presentation of data in the Dark Sky iOS apps, so I wanted PySky to show almost all same the info:

Displaying the English language forecast information was really all I had to do. The Hourly Forecast API Docs and some simple introspection told me most of what I needed to know about the weather variable containing the returned JSON object from Dark Sky.

"Now" and "Next Hour" information was easily retrieved with weather['currentSummary'] and weather['hourSummary']. I don't find "Overnight", "Morning", and "Afternoon" particularly useful, so they're replaced with "Next 24 Hours" data from weather['daySummary'].

For the "Following 3 Hours" information, I used weather['dayPrecipitation'] data. The docs explain that dayPrecipitation contains an array of precipitation predictions for the next 24 hours. The size of dayPrecipitation is 25, where dayPrecipitation[0] is the current precipitation, and dayPrecipitation[24] is 24 hours from now. Each dayPrecipitation[n] contains a dictionary with precipitation probability, type, temp, and time:

"dayPrecipitation": [
    { "probability": 1.0,
      "type": "rain",
      "temp": 65,
      "time": 1325607311 },

    { "probability": 0.84,
      "type": "rain",
      "temp": 65,
      "time": 1325610911 },

    { "probability": 0.8,
      "type": "rain",
      "temp": 65,
      "time": 1325614511 },

We just want the type and probability for hours [1:4]1, so I created a list of each like so:

hrsType = [ i['type'] for i in weather['dayPrecipitation'][1:4] ]
hrsProb = [ i['probability'] for i in weather['dayPrecipitation'][1:4] ]

The next line, chance = max(hrsProb), finds the maximum probability of precipitation, and probIndex = hrsProb.index(max(hrsProb)) finds the index (hour) of that maximum. Then we just find the type of precipitation at this index and print it out with terms such as "likely" or "possible" depending on the max probability:

if chance > 0.8:
  nextThreeHrs = '%s' % (str(hrsType[probIndex])).capitalize()
elif chance > 0.5:
  nextThreeHrs = '%s likely' % (str(hrsType[probIndex])).capitalize()
elif chance > 0.2:
  nextThreeHrs = 'Possible %s' % str(hrsType[probIndex])
  nextThreeHrs = 'No precipitation'

I haven't tested this enough to be sure that 0.8, 0.5, and 0.2 are proper probability thresholds, but they've been fine so far for me.

I think this might be one of the quickest and most basic scrips I've written, but it's also one of the most useful to me. Having the precipitation forecast at a quick glance on my computer will come in handy every day when I check the weather in the morning. I'm always looking for ways to make my programs better, so if you have any suggestions for improvements please get in touch.

  1. [1:4] means from element 1 up to, but not including 4.