Merge branch 'forecast'

pull/28/head
Alpha Chen 10 years ago
commit e77422073c

@ -1,28 +0,0 @@
## 0.0.5 - 2014.11.22
### Changed
- Use `forecast-config` for managing API keys.
- Fixed bug when precipitation intensity/probability was all 0's.
## 0.0.4 - 2014.11.21
### Added
- Sparklines for precipitation intensity and probability for the next hour
(where applicable) and day.
### Changed
- Bugfix for when `DEFAULT_LAT_LONG` is set and `DEFAULT_LOCATION` is not.
## 0.0.3 - 2014.11.19
### Added
- Forecast now uses units appropriate to the location.
### Changed
- Fix `DEFAULT_LAT_LONG`.
## 0.0.2 - 2014.11.19
### Changed
- Remove minutely result for non-US locations since Forecast doesn't have this
data.
## 0.0.1 - 2014.11.18
### Added
- Initial release

@ -1,42 +0,0 @@
# Forecast Workflow for Alfred
![screenshot][screenshot]
[screenshot]: http://i.imgur.com/mxGnovo.png
# Requirements
- [Alfred](http://www.alfredapp.com/)
- [Alfred Powerpack](http://www.alfredapp.com/powerpack/)
- OS X Mavericks
# Installation
Download and install the [workflow][download].
[download]: https://github.com/kejadlen/forecast.alfredworkflow/releases/download/0.0.5/Forecast.alfredworkflow
Run `forecast-config VALUE` to set API keys and the default location:
- `FORECAST_API_KEY`: Get an API key [here][forecast-api-key].
- `GOOGLE_API_KEY`: Get an API key [here][google-api-key]. (Used for geocoding.
If you never need to search for a location, this can be omitted by using
`DEFAULT_LAT_LONG`.)
- `DEFAULT_LOCATION`: Ex. "Seattle, WA".
- `DEFAULT_LAT_LONG`: Only required if `GOOGLE_API_KEY` is unavailable, since
`DEFAULT_LOCATION` can't be geocoded. Format: `lat,long`.
[forecast-api-key]: https://developer.forecast.io/register
[google-api-key]: https://developers.google.com/maps/documentation/geocoding/#api_key
# TODO
- Handle errors gracefully
- Caching? (Probably unnecessary...)
- Use `Accept-Encoding: gzip` for Forecast calls
# Attributions
- [Climacons](http://adamwhitcroft.com/climacons/)
- [Forecast API](https://developer.forecast.io/docs/v2)
- [Google Geocoding API](https://developers.google.com/maps/documentation/geocoding/)

@ -1,89 +0,0 @@
require 'delegate'
require 'erb'
require 'yaml'
class Items < DelegateClass(Array)
attr_reader :items
def initialize
@items = []
super(@items)
end
def to_s
ERB.new(<<-XML).result(binding)
<?xml version="1.0"?>
<items>
<%= items.map {|item| item.to_s.split("\n").map {|line| ' ' << line }}.join("\n").strip %>
</items>
XML
end
end
class Item
attr_reader *%i[ uid arg valid
title subtitle icon ]
def initialize(**kwargs)
@uid = kwargs.fetch(:uid).to_s.encode(xml: :attr)
@arg = kwargs[:arg].to_s.encode(xml: :attr)
@valid = kwargs.fetch(:valid, false) ? 'yes' : 'no'
@title = kwargs.fetch(:title).encode(xml: :text)
@subtitle = kwargs[:subtitle] && kwargs[:subtitle].encode(xml: :text)
@icon = kwargs[:icon] && kwargs[:icon].encode(xml: :text)
end
def to_s
ERB.new(<<-XML, nil, '%>').result(binding)
<item arg=<%= arg %> uid=<%= uid %> valid="<%= valid %>">
<title><%= title %></title>
% if subtitle
<subtitle><%= subtitle %></subtitle>
% end
% if icon
<icon><%= icon %></icon>
% end
</item>
XML
end
end
module Alfred
class Config
def self.[](key)
config[key]
end
def self.[]=(key, value)
config[key] = value
end
def self.config
return @config if defined?(@config)
bundle_id = `/usr/libexec/PlistBuddy info.plist -c 'print :bundleid'`.strip
@config = self.new(bundle_id)
end
WORKFLOW_DATA = '~/Library/Application Support/Alfred 2/Workflow Data/'
attr_reader :path
attr_accessor :config
def initialize(bundle_id)
dir = File.expand_path(File.join(WORKFLOW_DATA, bundle_id))
Dir.mkdir(dir) unless Dir.exist?(dir)
@path = File.join(dir, 'config.yml')
@config = File.exist?(@path) ? YAML.load_file(@path) : {}
end
def [](key)
config.fetch(key) { '' }
end
def []=(key, value)
config[key] = value
File.write(path, YAML.dump(config))
end
end
end

@ -1,26 +0,0 @@
require_relative 'alfred'
OPTIONS = %w[ FORECAST_API_KEY
GOOGLE_API_KEY
DEFAULT_LOCATION
DEFAULT_LAT_LONG ]
input = ARGV.shift || ''
items = Items.new
OPTIONS.each do |option|
title = if input.empty?
"Unset #{option}"
else
"Set #{option} to #{input}"
end
items << Item.new(
uid: option,
arg: "Alfred::Config['#{option}'] = '#{input}'",
valid: true,
title: title,
subtitle: Alfred::Config[option],
)
end
puts items.to_s

@ -1,138 +0,0 @@
require 'delegate'
require 'erb'
require 'yaml'
require_relative 'alfred'
require_relative 'forecaster'
require_relative 'location'
require_relative 'spark'
ICONS = {
'clear-day' => 'Sun',
'clear-night' => 'Moon',
'rain' => 'Cloud-Rain',
'snow' => 'Cloud-Snow',
'sleet' => 'Cloud-Snow-Alt',
'wind' => 'Wind',
'fog' => 'Cloud-Fog',
'cloudy' => 'Cloud',
'partly-cloudy-day' => 'Cloud-Sun',
'partly-cloudy-night' => 'Cloud-Moon',
}
Precipitation = Struct.new(:intensity, :probability) do
def self.from_forecast(forecast)
self.new(*forecast.values_at('precipIntensity', 'precipProbability'))
end
def human_intensity
case intensity
when 0...0.002
'no'
when 0.002...0.017
'very light'
when 0.017...0.1
'light'
when 0.1...0.4
'moderate'
else
'heavy'
end
end
def to_s
"#{(probability*100).to_i}% chance of #{human_intensity} rain."
end
end
query = ARGV.shift || ''
location = if query.empty?
lat, long = Alfred::Config['DEFAULT_LAT_LONG'].split(?,).map(&:to_f)
Location.new(Alfred::Config['DEFAULT_LOCATION'], lat, long)
else
Location.new(query)
end
forecast = Forecaster.forecast(location)
items = Items.new
items << Item.new(
uid: :location,
arg: "#{location.lat.round(4)},#{location.long.round(4)}",
valid: true,
title: location.name,
icon: 'icons/forecast.ico',
)
currently = forecast['currently']
precip = Precipitation.from_forecast(currently)
subtitle = [ "#{currently['temperature'].round}°" ]
subtitle << "Feels like #{currently['apparentTemperature'].round}°"
subtitle << precip.to_s if precip.probability > 0
items << Item.new(
uid: :currently,
title: currently['summary'],
subtitle: subtitle.join(' · '),
icon: "icons/#{ICONS[currently['icon']]}.png",
)
minutely = forecast['minutely']
if minutely
intensity = minutely['data'].map {|m| 1000 * m['precipIntensity'] }
intensity = intensity.select.with_index {|_,i| i % 5 == 0 }
min, max = intensity.minmax
subtitle = ["#{min.round}\" #{Spark.new(intensity)} #{max.round}\""]
probability = minutely['data'].map {|m| (100 * m['precipProbability']).round }
probability = probability.select.with_index {|_,i| i % 5 == 0 }
min, max = probability.minmax
subtitle << "#{min}% #{Spark.new(probability, max: 100)} #{max}%"
items << Item.new(
uid: :minutely,
title: minutely['summary'],
subtitle: subtitle.join(' · '),
icon: "icons/#{ICONS[minutely['icon']]}.png",
)
end
hourly = forecast['hourly']
intensity = hourly['data'].map {|m| 1000 * m['precipIntensity'] }
intensity = intensity.select.with_index {|_,i| i % 4 == 0 }
min, max = intensity.minmax
subtitle = ["#{min.round}\" #{Spark.new(intensity)} #{max.round}\""]
probability = hourly['data'].map {|m| (100 * m['precipProbability']).round }
probability = probability.select.with_index {|_,i| i % 4 == 0 }
min, max = probability.minmax
subtitle << "#{min}% #{Spark.new(probability, max: 100)} #{max}%"
items << Item.new(
uid: :hourly,
title: hourly['summary'],
subtitle: subtitle.join(' · '),
icon: "icons/#{ICONS[hourly['icon']]}.png",
)
forecast['daily']['data'][1..6].each do |data|
wday = Time.at(data['time']).strftime('%A')
precip = Precipitation.from_forecast(data)
subtitle = [ "Low: #{data['apparentTemperatureMin'].round}°",
"High: #{data['apparentTemperatureMax'].round}°" ]
subtitle << precip.to_s if precip.probability > 0
items << Item.new(
uid: wday,
title: "#{wday} - #{data['summary']}",
subtitle: subtitle.join(' · '),
icon: "icons/#{ICONS[data['icon']]}.png",
)
end
puts items.to_s

@ -1,23 +0,0 @@
require 'json'
require 'open-uri'
require_relative 'alfred'
Forecaster = Struct.new(:api_key) do
def self.forecast(location)
forecaster.forecast(location)
end
def self.forecaster
return @forecaster if defined?(@forecaster)
@forecaster = self.new(Alfred::Config['FORECAST_API_KEY'])
end
def forecast(location)
lat, long = location.lat, location.long
url = "https://api.forecast.io/forecast/#{api_key}/#{lat},#{long}?units=auto"
response = JSON.load(open(url))
end
end

@ -1,30 +0,0 @@
require 'json'
require 'open-uri'
require 'uri'
require_relative 'alfred'
Geocoder = Struct.new(:api_key) do
def self.geocode(location)
geocoder.geocode(location)
end
def self.geocoder
return @geocoder if defined?(@geocoder)
@geocoder = self.new(Alfred::Config['GOOGLE_API_KEY'])
end
def geocode(location)
url = 'https://maps.googleapis.com/maps/api/geocode/json'
query = URI.encode_www_form(address: location, api_key: api_key)
response = JSON.load(open("#{url}?#{query}"))
result = response['results'][0]
name = result['formatted_address']
location = result['geometry']['location']
lat, long = location.values_at('lat', 'lng')
[name, lat, long]
end
end

@ -1,159 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>bundleid</key>
<string>com.kejadlen.forecast</string>
<key>category</key>
<string>Internet</string>
<key>connections</key>
<dict>
<key>2A5C0A87-204E-49EA-94A7-8E62BB4EFD8A</key>
<array>
<dict>
<key>destinationuid</key>
<string>5E2D96BA-31B2-4800-9A2B-B999285680A0</string>
<key>modifiers</key>
<integer>0</integer>
<key>modifiersubtext</key>
<string></string>
</dict>
</array>
<key>450E8285-E286-4D33-AADF-1ACF99F41031</key>
<array>
<dict>
<key>destinationuid</key>
<string>373F7A50-59E2-4225-B665-E63FBBBAF7E5</string>
<key>modifiers</key>
<integer>0</integer>
<key>modifiersubtext</key>
<string></string>
</dict>
</array>
</dict>
<key>createdby</key>
<string>Alpha Chen</string>
<key>description</key>
<string></string>
<key>disabled</key>
<false/>
<key>name</key>
<string>Forecast</string>
<key>objects</key>
<array>
<dict>
<key>config</key>
<dict>
<key>plusspaces</key>
<false/>
<key>url</key>
<string>http://forecast.io/#/f/{query}</string>
<key>utf8</key>
<true/>
</dict>
<key>type</key>
<string>alfred.workflow.action.openurl</string>
<key>uid</key>
<string>5E2D96BA-31B2-4800-9A2B-B999285680A0</string>
<key>version</key>
<integer>0</integer>
</dict>
<dict>
<key>config</key>
<dict>
<key>argumenttype</key>
<integer>1</integer>
<key>escaping</key>
<integer>127</integer>
<key>keyword</key>
<string>forecast</string>
<key>runningsubtext</key>
<string>Retriving location/weather...</string>
<key>script</key>
<string>ruby forecast.rb {query}</string>
<key>title</key>
<string>Forecast</string>
<key>type</key>
<integer>0</integer>
<key>withspace</key>
<true/>
</dict>
<key>type</key>
<string>alfred.workflow.input.scriptfilter</string>
<key>uid</key>
<string>2A5C0A87-204E-49EA-94A7-8E62BB4EFD8A</string>
<key>version</key>
<integer>0</integer>
</dict>
<dict>
<key>config</key>
<dict>
<key>escaping</key>
<integer>0</integer>
<key>script</key>
<string>ruby -r./alfred -e "{query}"</string>
<key>type</key>
<integer>0</integer>
</dict>
<key>type</key>
<string>alfred.workflow.action.script</string>
<key>uid</key>
<string>373F7A50-59E2-4225-B665-E63FBBBAF7E5</string>
<key>version</key>
<integer>0</integer>
</dict>
<dict>
<key>config</key>
<dict>
<key>argumenttype</key>
<integer>1</integer>
<key>escaping</key>
<integer>127</integer>
<key>keyword</key>
<string>forecast-config</string>
<key>script</key>
<string>ruby forecast-config.rb {query}</string>
<key>title</key>
<string>Configure the Forecast workflow</string>
<key>type</key>
<integer>0</integer>
<key>withspace</key>
<true/>
</dict>
<key>type</key>
<string>alfred.workflow.input.scriptfilter</string>
<key>uid</key>
<string>450E8285-E286-4D33-AADF-1ACF99F41031</string>
<key>version</key>
<integer>0</integer>
</dict>
</array>
<key>readme</key>
<string></string>
<key>uidata</key>
<dict>
<key>2A5C0A87-204E-49EA-94A7-8E62BB4EFD8A</key>
<dict>
<key>ypos</key>
<real>10</real>
</dict>
<key>373F7A50-59E2-4225-B665-E63FBBBAF7E5</key>
<dict>
<key>ypos</key>
<real>130</real>
</dict>
<key>450E8285-E286-4D33-AADF-1ACF99F41031</key>
<dict>
<key>ypos</key>
<real>130</real>
</dict>
<key>5E2D96BA-31B2-4800-9A2B-B999285680A0</key>
<dict>
<key>ypos</key>
<real>10</real>
</dict>
</dict>
<key>webaddress</key>
<string>http://github.com/kejadlen/forecast.alfredworkflow</string>
</dict>
</plist>

@ -1,16 +0,0 @@
require_relative 'geocoder'
class Location
attr_accessor :name, :lat, :long, :geocoder
def initialize(name, lat=nil, long=nil, geocoder=Geocoder)
@name, @lat, @long, @geocoder = name, lat, long, geocoder
geocode! unless lat && long
end
def geocode!
self.name, self.lat, self.long = geocoder.geocode(name)
end
end

@ -1,17 +0,0 @@
class Spark
# TICKS = %w[▁ ▂ ▃ ▄ ▅ ▆ ▇ █] # Alfred doesn't render the last bar correctly
# for some reason...
TICKS = %w[▁ ▂ ▃ ▄ ▅ ▆ ▇]
attr_reader :data, :min, :max
def initialize(data, **kwargs)
@data = data.map(&:round)
@min = kwargs.fetch(:min) { 0 }
@max = [(kwargs.fetch(:max) { data.max }).to_f, 1.0].max
end
def to_s
data.map {|i| TICKS[(TICKS.size - 1) * (i - min) / max] }.join
end
end

@ -1,22 +0,0 @@
require 'minitest/autorun'
require_relative 'alfred'
class TestConfig < Minitest::Test
def setup
@config = Alfred::Config.new('com.kejadlen.test')
end
def teardown
File.delete(@config.path) if File.exist?(@config.path)
end
def test_config
assert_nil @config[:foo]
@config[:foo] = 123
assert_equal 123, @config[:foo]
assert File.exist?(@config.path)
assert_equal '{:foo=>123}', File.read(@config.path)
end
end

@ -1,10 +0,0 @@
require 'minitest/autorun'
require_relative 'spark'
class TestSpark < Minitest::Test
def test_div_by_zero
spark = Spark.new([0, 0, 0])
assert_equal '▁▁▁', spark.to_s
end
end
Loading…
Cancel
Save