1
0

weather_agent.rb 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. require 'date'
  2. require 'cgi'
  3. module Agents
  4. class WeatherAgent < Agent
  5. cannot_receive_events!
  6. gem_dependency_check { defined?(Wunderground) && defined?(ForecastIO) }
  7. description <<-MD
  8. The Weather Agent creates an event for the day's weather at a given `location`.
  9. #{'## Include `forecast_io` and `wunderground` in your Gemfile to use this Agent!' if dependencies_missing?}
  10. You also must select when you would like to get the weather forecast for using the `which_day` option, where the number 1 represents today, 2 represents tomorrow and so on. Weather forecast inforation is only returned for at most one week at a time.
  11. The weather forecast information can be provided by either Wunderground or Dark Sky. To choose which `service` to use, enter either `darksky` or `wunderground`.
  12. The `location` should be:
  13. * For Wunderground: A US zipcode, or any location that Wunderground supports. To find one, search [wunderground.com](https://wunderground.com) and copy the location part of the URL. For example, a result for San Francisco gives `https://www.wunderground.com/US/CA/San_Francisco.html` and London, England gives `https://www.wunderground.com/q/zmw:00000.1.03772`. The locations in each are `US/CA/San_Francisco` and `zmw:00000.1.03772`, respectively.
  14. * For Dark Sky: `location` must be a comma-separated string of map co-ordinates (longitude, latitude). For example, San Francisco would be `37.7771,-122.4196`.
  15. You must set up an [API key for Wunderground](https://www.wunderground.com/weather/api/) in order to use this Agent with Wunderground.
  16. You must set up an [API key for Dark Sky](https://darksky.net/dev/) in order to use this Agent with Dark Sky.
  17. Set `expected_update_period_in_days` to the maximum amount of time that you'd expect to pass between Events being created by this Agent.
  18. If you want to see the returned texts in your language, set the `language` parameter in ISO 639-1 format.
  19. MD
  20. event_description <<-MD
  21. Events look like this:
  22. {
  23. "location": "12345",
  24. "date": {
  25. "epoch": "1357959600",
  26. "pretty": "10:00 PM EST on January 11, 2013"
  27. },
  28. "high": {
  29. "fahrenheit": "64",
  30. "celsius": "18"
  31. },
  32. "low": {
  33. "fahrenheit": "52",
  34. "celsius": "11"
  35. },
  36. "conditions": "Rain Showers",
  37. "icon": "rain",
  38. "icon_url": "https://icons-ak.wxug.com/i/c/k/rain.gif",
  39. "skyicon": "mostlycloudy",
  40. ...
  41. }
  42. MD
  43. default_schedule "8pm"
  44. def working?
  45. event_created_within?((interpolated['expected_update_period_in_days'].presence || 2).to_i) && !recent_error_logs? && key_setup?
  46. end
  47. def key_setup?
  48. interpolated['api_key'].present? && interpolated['api_key'] != "your-key" && interpolated['api_key'] != "put-your-key-here"
  49. end
  50. def default_options
  51. {
  52. 'service' => 'wunderground',
  53. 'api_key' => 'your-key',
  54. 'location' => '94103',
  55. 'which_day' => '1',
  56. 'language' => 'EN',
  57. 'expected_update_period_in_days' => '2'
  58. }
  59. end
  60. def check
  61. if key_setup?
  62. create_event :payload => model(weather_provider, which_day).merge('location' => location)
  63. end
  64. end
  65. private
  66. def weather_provider
  67. interpolated["service"].presence || "wunderground"
  68. end
  69. # a check to see if the weather provider is wunderground.
  70. def wunderground?
  71. weather_provider.downcase == "wunderground"
  72. end
  73. # a check to see if the weather provider is one of the valid aliases for Dark Sky.
  74. def dark_sky?
  75. ["dark_sky", "darksky", "forecast_io", "forecastio"].include? weather_provider.downcase
  76. end
  77. def which_day
  78. (interpolated["which_day"].presence || 1).to_i
  79. end
  80. def location
  81. interpolated["location"].presence || interpolated["zipcode"]
  82. end
  83. def coordinates
  84. location.split(',').map { |e| e.to_f }
  85. end
  86. def language
  87. interpolated['language'].presence || 'EN'
  88. end
  89. VALID_COORDS_REGEX = /^\s*-?\d{1,3}\.\d+\s*,\s*-?\d{1,3}\.\d+\s*$/
  90. def validate_location
  91. errors.add(:base, "location is required") unless location.present?
  92. return if wunderground?
  93. if location =~ VALID_COORDS_REGEX
  94. lat, lon = coordinates
  95. errors.add :base, "too low of a latitude" unless lat > -90
  96. errors.add :base, "too big of a latitude" unless lat < 90
  97. errors.add :base, "too low of a longitude" unless lon > -180
  98. errors.add :base, "too high of a longitude" unless lon < 180
  99. else
  100. errors.add(
  101. :base,
  102. "Location #{location} is malformed. Location for " +
  103. 'Dark Sky must be in the format "-00.000,-00.00000". The ' +
  104. "number of decimal places does not matter.")
  105. end
  106. end
  107. def validate_options
  108. errors.add(:base, "service must be set to 'darksky' or 'wunderground'") unless wunderground? || dark_sky?
  109. validate_location
  110. errors.add(:base, "api_key is required") unless interpolated['api_key'].present?
  111. errors.add(:base, "which_day selection is required") unless which_day.present?
  112. end
  113. def wunderground
  114. if key_setup?
  115. forecast = Wunderground.new(interpolated['api_key'], language: language.upcase).forecast_for(location)
  116. merged = {}
  117. forecast['forecast']['simpleforecast']['forecastday'].each { |daily| merged[daily['period']] = daily }
  118. forecast['forecast']['txt_forecast']['forecastday'].each { |daily| (merged[daily['period']] || {}).merge!(daily) }
  119. merged
  120. end
  121. end
  122. def dark_sky
  123. if key_setup?
  124. ForecastIO.api_key = interpolated['api_key']
  125. lat, lng = coordinates
  126. ForecastIO.forecast(lat, lng, params: {lang: language.downcase})['daily']['data']
  127. end
  128. end
  129. def model(weather_provider,which_day)
  130. if wunderground?
  131. wunderground[which_day]
  132. elsif dark_sky?
  133. dark_sky.each do |value|
  134. timestamp = Time.at(value.time)
  135. if (timestamp.to_date - Time.now.to_date).to_i == which_day
  136. day = {
  137. 'date' => {
  138. 'epoch' => value.time.to_s,
  139. 'pretty' => timestamp.strftime("%l:%M %p %Z on %B %d, %Y"),
  140. 'day' => timestamp.day,
  141. 'month' => timestamp.month,
  142. 'year' => timestamp.year,
  143. 'yday' => timestamp.yday,
  144. 'hour' => timestamp.hour,
  145. 'min' => timestamp.strftime("%M"),
  146. 'sec' => timestamp.sec,
  147. 'isdst' => timestamp.isdst ? 1 : 0 ,
  148. 'monthname' => timestamp.strftime("%B"),
  149. 'monthname_short' => timestamp.strftime("%b"),
  150. 'weekday_short' => timestamp.strftime("%a"),
  151. 'weekday' => timestamp.strftime("%A"),
  152. 'ampm' => timestamp.strftime("%p"),
  153. 'tz_short' => timestamp.zone
  154. },
  155. 'period' => which_day.to_i,
  156. 'high' => {
  157. 'fahrenheit' => value.temperatureMax.round().to_s,
  158. 'epoch' => value.temperatureMaxTime.to_s,
  159. 'fahrenheit_apparent' => value.apparentTemperatureMax.round().to_s,
  160. 'epoch_apparent' => value.apparentTemperatureMaxTime.to_s,
  161. 'celsius' => ((5*(Float(value.temperatureMax) - 32))/9).round().to_s
  162. },
  163. 'low' => {
  164. 'fahrenheit' => value.temperatureMin.round().to_s,
  165. 'epoch' => value.temperatureMinTime.to_s,
  166. 'fahrenheit_apparent' => value.apparentTemperatureMin.round().to_s,
  167. 'epoch_apparent' => value.apparentTemperatureMinTime.to_s,
  168. 'celsius' => ((5*(Float(value.temperatureMin) - 32))/9).round().to_s
  169. },
  170. 'conditions' => value.summary,
  171. 'icon' => value.icon,
  172. 'avehumidity' => (value.humidity * 100).to_i,
  173. 'sunriseTime' => value.sunriseTime.to_s,
  174. 'sunsetTime' => value.sunsetTime.to_s,
  175. 'moonPhase' => value.moonPhase.to_s,
  176. 'precip' => {
  177. 'intensity' => value.precipIntensity.to_s,
  178. 'intensity_max' => value.precipIntensityMax.to_s,
  179. 'intensity_max_epoch' => value.precipIntensityMaxTime.to_s,
  180. 'probability' => value.precipProbability.to_s,
  181. 'type' => value.precipType
  182. },
  183. 'dewPoint' => value.dewPoint.to_s,
  184. 'avewind' => {
  185. 'mph' => value.windSpeed.round().to_s,
  186. 'kph' => (Float(value.windSpeed) * 1.609344).round().to_s,
  187. 'degrees' => value.windBearing.to_s
  188. },
  189. 'visibility' => value.visibility.to_s,
  190. 'cloudCover' => value.cloudCover.to_s,
  191. 'pressure' => value.pressure.to_s,
  192. 'ozone' => value.ozone.to_s
  193. }
  194. return day
  195. end
  196. end
  197. end
  198. end
  199. end
  200. end