lib/barometer/weather_services/weather_bug.rb in barometer-0.5.0 vs lib/barometer/weather_services/weather_bug.rb in barometer-0.6.1
- old
+ new
@@ -1,6 +1,295 @@
module Barometer
-
+ #
+ # = WeatherBug
+ # www.weatherbug.com
+ #
+ # - key required: YES (api_code)
+ # - registration required: YES
+ # - supported countries: US (by zipcode), International (by coordinates)
+ #
+ # === performs geo coding
+ # - city: YES
+ # - coordinates: PARTIAL (just for weather station)
+ #
+ # === time info
+ # - sun rise/set: YES
+ # - provides timezone: NO, but provides a timezone short code and utc offset
+ # - requires TZInfo: NO
+ #
+ # == resources
+ # - API: http://weather.weatherbug.com/corporate/products/API/help.aspx
+ #
+ # === Possible queries:
+ # - http://[API_Code].api.wxbug.net:80/getLiveWeatherRSS.aspx?ACode=[API_Code]&OutputType=1&UnitType=1&zipCode=90210
+ #
+ # where query can be:
+ # - zipcode (US) [5 digits only]
+ # - coordinates (International)
+ #
+ # = WeatherBug.com terms of use
+ # ???
+ #
+ # == notes
+ # - WeatherBug also supports queries using "citycode" and "stationID", but these
+ # are specific to WeatherBug and un-supported by Barometer
+ #
class WeatherService::WeatherBug < WeatherService
+
+ @@api_code = nil
+
+ def self.keys=(keys)
+ raise ArgumentError unless keys.is_a?(Hash)
+ keys.each do |key, value|
+ @@api_code = value.to_s if key.to_s.downcase == "code"
+ end
+ end
+
+ #########################################################################
+ # PRIVATE
+ # If class methods could be private, the remaining methods would be.
+ #
+
+ def self._source_name; :weather_bug; end
+ def self._accepted_formats; [:short_zipcode, :coordinates]; end
+
+ def self._has_keys?; !@@api_code.nil?; end
+ def self._requires_keys?; true; end
+
+ def self._wet_icon_codes
+ codes = [5,6,8,9,11,12,14,15] + (18..22).to_a + [25] + (27..30).to_a +
+ [32,36] + (38..49).to_a + (52..63).to_a + (80..157).to_a +
+ (161..176).to_a
+ codes.collect {|c| c.to_s}
+ end
+ def self._sunny_icon_codes
+ codes = [0,2,3,4,7,26,31,64,65,75]
+ codes.collect {|c| c.to_s}
+ end
+
+ def self._build_extra(measurement, result, metric=true)
+ #raise ArgumentError unless measurement.is_a?(Data::Measurement)
+ #raise ArgumentError unless query.is_a?(Barometer::Query)
+
+ # use todays sun data for all future days
+ if measurement.forecast && measurement.current.sun
+ measurement.forecast.each do |forecast|
+ forecast.sun = measurement.current.sun
+ end
+ end
+
+ measurement
+ end
+
+ def self._parse_local_time(data)
+ Data::LocalTime.new(
+ data["aws:ob_date"]["aws:hour"]["hour_24"].to_i,
+ data["aws:ob_date"]["aws:minute"]["number"].to_i,
+ data["aws:ob_date"]["aws:second"]["number"].to_i
+ ) if data && data["aws:ob_date"]
+ end
+
+ def self._build_timezone(data)
+ if data && data["aws:ob_date"] && data["aws:ob_date"]["aws:time_zone"]
+ Data::Zone.new(data["aws:ob_date"]["aws:time_zone"]["abbrv"])
+ end
+ end
+
+ def self._build_current(data, metric=true)
+ raise ArgumentError unless data.is_a?(Hash)
+
+ current = Measurement::Current.new
+ # current.updated_at = Data::LocalDateTime.parse(data['observation_time']) if data['observation_time']
+ current.humidity = data['aws:humidity'].to_i
+ current.condition = data['aws:current_condition'] if data['aws:current_condition']
+ current.icon = data['aws:icon'].to_i.to_s if data['aws:icon']
+
+ current.temperature = Data::Temperature.new(metric)
+ current.temperature << data['aws:temp']
+
+ current.wind = Data::Speed.new(metric)
+ current.wind << data['aws:wind_speed'].to_f
+ current.wind.direction = data['aws:wind_direction']
+
+ current.pressure = Data::Pressure.new(metric)
+ current.pressure << data['aws:pressure']
+
+ current.dew_point = Data::Temperature.new(metric)
+ current.dew_point << data['aws:dew_point']
+
+ current.wind_chill = Data::Temperature.new(metric)
+ current.wind_chill << data['aws:feels_like']
+
+ current
+ end
+
+ def self._build_forecast(data, metric=true)
+ raise ArgumentError unless data.is_a?(Hash)
+ forecasts = Measurement::ForecastArray.new
+ # go through each forecast and create an instance
+ if data && data["aws:forecast"]
+ start_date = Date.parse(data['date'])
+ i = 0
+ data["aws:forecast"].each do |forecast|
+ forecast_measurement = Measurement::Forecast.new
+ icon_match = forecast['aws:image'].match(/cond(\d*)\.gif$/)
+ forecast_measurement.icon = icon_match[1].to_i.to_s if icon_match
+ forecast_measurement.date = start_date + i
+ forecast_measurement.condition = forecast['aws:short_prediction']
+
+ forecast_measurement.high = Data::Temperature.new(metric)
+ forecast_measurement.high << forecast['aws:high']
+
+ forecast_measurement.low = Data::Temperature.new(metric)
+ forecast_measurement.low << forecast['aws:low']
+
+ forecasts << forecast_measurement
+ i += 1
+ end
+ end
+ forecasts
+ end
+
+ def self._build_location(data, geo=nil)
+ raise ArgumentError unless data.is_a?(Hash)
+ raise ArgumentError unless (geo.nil? || geo.is_a?(Data::Geo))
+ location = Data::Location.new
+ # use the geocoded data if available, otherwise get data from result
+ if geo
+ location.city = geo.locality
+ location.state_code = geo.region
+ location.country = geo.country
+ location.country_code = geo.country_code
+ location.latitude = geo.latitude
+ location.longitude = geo.longitude
+ else
+ if data && data['aws:location']
+ location.city = data['aws:location']['aws:city']
+ location.state_code = data['aws:location']['aws:state']
+ location.zip_code = data['aws:location']['aws:zip']
+ end
+ end
+ location
+ end
+
+ def self._build_station(data)
+ raise ArgumentError unless data.is_a?(Hash)
+ station = Data::Location.new
+ station.id = data['aws:station_id']
+ station.name = data['aws:station']
+ station.city = data['aws:city_state'].split(',')[0].strip
+ station.state_code = data['aws:city_state'].split(',')[1].strip
+ station.country = data['aws:country']
+ station.zip_code = data['aws:station_zipcode']
+ station.latitude = data['aws:latitude']
+ station.longitude = data['aws:longitude']
+ station
+ end
+
+ def self._build_sun(data)
+ raise ArgumentError unless data.is_a?(Hash)
+ sun = nil
+ if data
+ if data['aws:sunrise']
+ rise = Data::LocalTime.new(
+ data['aws:sunrise']['aws:hour']['hour_24'].to_i,
+ data['aws:sunrise']['aws:minute']['number'].to_i,
+ data['aws:sunrise']['aws:second']['number'].to_i
+ )
+ end
+ if data['aws:sunset']
+ set = Data::LocalTime.new(
+ data['aws:sunset']['aws:hour']['hour_24'].to_i,
+ data['aws:sunset']['aws:minute']['number'].to_i,
+ data['aws:sunset']['aws:second']['number'].to_i
+ )
+ sun = Data::Sun.new(rise,set)
+ end
+ end
+ sun || Data::Sun.new
+ end
+
+ # override default _fetch behavior
+ # this service requires TWO seperate http requests (one for current
+ # and one for forecasted weather) ... combine the results
+ #
+ def self._fetch(query, metric=true)
+ result = []
+ result << _fetch_current(query,metric)
+ result << _fetch_forecast(query,metric)
+ result
+ end
+
+ # use HTTParty to get the current weather
+ #
+ def self._fetch_current(query, metric=true)
+ puts "fetch weatherbug current: #{query.q}" if Barometer::debug?
+
+ q = ( query.format.to_sym == :short_zipcode ?
+ { :zipCode => query.q } :
+ { :lat => query.q.split(',')[0], :long => query.q.split(',')[1] })
+
+ # httparty and the xml builder it uses miss some information
+ # 1st - get the raw response
+ # 2nd - manually get the missing information
+ # 3rd - let httparty build xml as normal
+ #
+ response = self.get(
+ "http://#{@@api_code}.api.wxbug.net/getLiveWeatherRSS.aspx",
+ :query => { :ACode => @@api_code,
+ :OutputType => "1", :UnitType => (metric ? '1' : '0')
+ }.merge(q),
+ :format => :plain,
+ :timeout => Barometer.timeout
+ )
+
+ # get icon
+ icon_match = response.match(/cond(\d*)\.gif/)
+ icon = icon_match[1] if icon_match
+
+ # get station zipcode
+ zip_match = response.match(/zipcode=\"(\d*)\"/)
+ zipcode = zip_match[1] if zip_match
+
+ # build xml
+ output = Crack::XML.parse(response)
+ output = output["aws:weather"]["aws:ob"]
+
+ # add missing data
+ output["aws:icon"] = icon
+ output["aws:station_zipcode"] = zipcode
+
+ output
+ end
+
+ # use HTTParty to get the current weather
+ #
+ def self._fetch_forecast(query, metric=true)
+ puts "fetch weatherbug forecast: #{query.q}" if Barometer::debug?
+
+ q = ( query.format.to_sym == :short_zipcode ?
+ { :zipCode => query.q } :
+ { :lat => query.q.split(',')[0], :long => query.q.split(',')[1] })
+
+ self.get(
+ "http://#{@@api_code}.api.wxbug.net/getForecastRSS.aspx",
+ :query => { :ACode => @@api_code,
+ :OutputType => "1", :UnitType => (metric ? '1' : '0')
+ }.merge(q),
+ :format => :xml,
+ :timeout => Barometer.timeout
+ )["aws:weather"]["aws:forecasts"]
+ end
+
+ # since we have two sets of data, override these calls to choose the
+ # right set of data
+ #
+ def self._current_result(data); data[0]; end
+ def self._forecast_result(data=nil); data[1]; end
+ def self._location_result(data=nil); data[1]; end
+ def self._station_result(data=nil); data[0]; end
+ def self._sun_result(data=nil); data[0]; end
+ def self._timezone_result(data=nil); data[0]; end
+ def self._time_result(data=nil); data[0]; end
+
end
-
end
\ No newline at end of file