Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change WeatherAgent to use Openweather instead of DarkSky (which is EOL) #2848

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ end
gem 'twilio-ruby', '~> 3.11.5' # TwilioAgent
gem 'ruby-growl', '~> 4.1.0' # GrowlAgent
gem 'net-ftp-list', '~> 3.2.8' # FtpsiteAgent
gem 'forecast_io', '~> 2.0.0' # WeatherAgent
gem 'rturk', '~> 2.12.1' # HumanTaskAgent
gem 'erector', github: 'dsander/erector', branch: 'rails6'
gem 'hipchat', '~> 1.2.0' # HipchatAgent
Expand Down
5 changes: 0 additions & 5 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -268,10 +268,6 @@ GEM
ffi (1.12.2)
font-awesome-sass (4.7.0)
sass (>= 3.2)
forecast_io (2.0.1)
faraday
hashie
multi_json
foreman (0.63.0)
dotenv (>= 0.7)
thor (>= 0.13.6)
Expand Down Expand Up @@ -714,7 +710,6 @@ DEPENDENCIES
feedjira (~> 3.1)
ffi (>= 1.9.4)
font-awesome-sass (~> 4.7.0)
forecast_io (~> 2.0.0)
foreman (~> 0.63.0)
geokit (~> 1.8.4)
geokit-rails (~> 2.2.0)
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Huginn is a system for building agents that perform automated tasks for you onli
* Watch for air travel or shopping deals
* Follow your project names on Twitter and get updates when people mention them
* Scrape websites and receive email when they change
* Connect to Adioso, HipChat, Basecamp, Growl, FTP, IMAP, Jabber, JIRA, MQTT, nextbus, Pushbullet, Pushover, RSS, Bash, Slack, StubHub, translation APIs, Twilio, Twitter, Wunderground, and Weibo, to name a few.
* Connect to Adioso, HipChat, Basecamp, Growl, FTP, IMAP, Jabber, JIRA, MQTT, nextbus, Pushbullet, Pushover, RSS, Bash, Slack, StubHub, translation APIs, Twilio, Twitter, OpenWeather, and Weibo, to name a few.
* Send digest email with things that you care about at specific times during the day
* Track counts of high frequency events and send an SMS within moments when they spike, such as the term "san francisco emergency"
* Send and receive WebHooks
Expand Down Expand Up @@ -142,9 +142,9 @@ See [private development instructions](https://github.com/huginn/huginn/wiki/Pri

#### Enable the WeatherAgent

In order to use the WeatherAgent you need an [API key with Wunderground](http://www.wunderground.com/weather/api/). Signup for one and then change the value of `api_key: your-key` in your seeded WeatherAgent.
In order to use the WeatherAgent you need an [API key with OpenWeather](https://home.openweathermap.org/api_keys). Signup for one and then change the value of `api_key: your-key` in your seeded WeatherAgent and setting the service key to `openweather`.

Note, Wunderground no longer offers free API keys. You can still use the WeatherAgent by setting the service key to darksky, and getting an [API key from DarkSky](https://darksky.net/dev).
Note, neither DarkSky nor Wunderground offers free API keys any more.

#### Disable SSL

Expand Down
101 changes: 59 additions & 42 deletions app/models/agents/weather_agent.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,16 @@ module Agents
class WeatherAgent < Agent
cannot_receive_events!

gem_dependency_check { defined?(ForecastIO) }

description <<-MD
The Weather Agent creates an event for the day's weather at a given `location`.

#{'## Include `forecast_io` in your Gemfile to use this Agent!' if dependencies_missing?}

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.
You can specify for which day you would like the weather forecast using the `which_day` option, where the number 1 represents today, 2 represents tomorrow, and so on. The default is 1 (today). If you schedule this to run at night, you probably want 2 (tomorrow). Weather forecast inforation is only returned for at most one week at a time.

The weather forecast information is provided by Dark Sky.
The weather forecast information is provided by [OpenWeather](https://home.openweathermap.org).

The `location` must be a comma-separated string of map co-ordinates (longitude, latitude). For example, San Francisco would be `37.7771,-122.4196`.

You must set up an [API key for Dark Sky](https://darksky.net/dev/) in order to use this Agent.
You must set up an [API key for OpenWeather](https://home.openweathermap.org/api_keys) in order to use this Agent.

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.

Expand All @@ -43,7 +39,7 @@ class WeatherAgent < Agent
},
"conditions": "Rain Showers",
"icon": "rain",
"icon_url": "https://icons-ak.wxug.com/i/c/k/rain.gif",
"icon_url": "http://openweathermap.org/img/wn/10d@2x.png",
"skyicon": "mostlycloudy",
...
}
Expand Down Expand Up @@ -93,10 +89,28 @@ def language
interpolated["language"].presence || "en"
end

def darksky?
interpolated["service"].presence && interpolated["service"].presence.downcase == "darksky"
end

def wunderground?
interpolated["service"].presence && interpolated["service"].presence.downcase == "wunderground"
end

def openweather_icon(code)
"http://openweathermap.org/img/wn/#{code}@2x.png"
end

def figure_rain_or_snow(rain, snow)
if rain.present? && (snow.nil? || (rain > snow))
"rain"
elsif snow.present?
"snow"
else
""
end
end

VALID_COORDS_REGEX = /^\s*-?\d{1,3}\.\d+\s*,\s*-?\d{1,3}\.\d+\s*$/

def validate_location
Expand All @@ -111,33 +125,36 @@ def validate_location
errors.add(
:base,
"Location #{location} is malformed. Location for " +
'Dark Sky must be in the format "-00.000,-00.00000". The ' +
'OpenWeather must be in the format "-00.000,-00.00000". The ' +
"number of decimal places does not matter.")
end
end

def validate_options
errors.add(:base, "The Weather Underground API has been disabled since Jan 1st 2018, please switch to DarkSky") if wunderground?
errors.add(:base, "The DarkSky API has been disabled since Aug 1st, 2020; please switch to OpenWeather.") if darksky?
errors.add(:base, "The Weather Underground API has been disabled since Jan 1st 2018; please switch to OpenWeather.") if wunderground?
validate_location
errors.add(:base, "api_key is required") unless interpolated['api_key'].present?
errors.add(:base, "which_day selection is required") unless which_day.present?
end

def dark_sky
def open_weather
if key_setup?
ForecastIO.api_key = interpolated['api_key']
onecall_endpoint = "http://api.openweathermap.org/data/2.5/onecall"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It sounds like new OpenWeatherMap account cannot use this API anymore. Do you think we can use https://api.openweathermap.org/data/2.5/weather instead? Doc: https://openweathermap.org/current

lat, lng = coordinates
ForecastIO.forecast(lat, lng, params: {lang: language.downcase})['daily']['data']
response = HTTParty.get("%s?units=imperial&appid=%s&lat=%s&lon=%s&lang=%s" %
[onecall_endpoint, interpolated['api_key'], lat, lng, language.downcase])
JSON.parse(response.body, object_class: OpenStruct).daily
end
end

def model(which_day)
value = dark_sky[which_day - 1]
value = open_weather[which_day - 1]
if value
timestamp = Time.at(value.time)
timestamp = Time.at(value.dt)
day = {
'date' => {
'epoch' => value.time.to_s,
'epoch' => value.dt.to_s,
'pretty' => timestamp.strftime("%l:%M %p %Z on %B %d, %Y"),
'day' => timestamp.day,
'month' => timestamp.month,
Expand All @@ -156,42 +173,42 @@ def model(which_day)
},
'period' => which_day.to_i,
'high' => {
'fahrenheit' => value.temperatureMax.round().to_s,
'epoch' => value.temperatureMaxTime.to_s,
'fahrenheit_apparent' => value.apparentTemperatureMax.round().to_s,
'epoch_apparent' => value.apparentTemperatureMaxTime.to_s,
'celsius' => ((5*(Float(value.temperatureMax) - 32))/9).round().to_s
'fahrenheit' => value.temp.max.round().to_s,
#'epoch' => value.temperatureMaxTime.to_s,
'fahrenheit_apparent' => value.feels_like.day.round().to_s,
#'epoch_apparent' => value.apparentTemperatureMaxTime.to_s,
'celsius' => ((5*(Float(value.temp.max) - 32))/9).round().to_s
},
'low' => {
'fahrenheit' => value.temperatureMin.round().to_s,
'epoch' => value.temperatureMinTime.to_s,
'fahrenheit_apparent' => value.apparentTemperatureMin.round().to_s,
'epoch_apparent' => value.apparentTemperatureMinTime.to_s,
'celsius' => ((5*(Float(value.temperatureMin) - 32))/9).round().to_s
'fahrenheit' => value.temp.min.round().to_s,
#'epoch' => value.temperatureMinTime.to_s,
'fahrenheit_apparent' => value.feels_like.night.round().to_s,
#'epoch_apparent' => value.apparentTemperatureMinTime.to_s,
'celsius' => ((5*(Float(value.temp.min) - 32))/9).round().to_s
},
'conditions' => value.summary,
'icon' => value.icon,
'conditions' => value.weather.first.description,
'icon' => openweather_icon(value.weather.first.icon),
'avehumidity' => (value.humidity * 100).to_i,
'sunriseTime' => value.sunriseTime.to_s,
'sunsetTime' => value.sunsetTime.to_s,
'moonPhase' => value.moonPhase.to_s,
'sunriseTime' => value.sunrise.to_s,
'sunsetTime' => value.sunset.to_s,
#'moonPhase' => value.moonPhase.to_s,
'precip' => {
'intensity' => value.precipIntensity.to_s,
'intensity_max' => value.precipIntensityMax.to_s,
'intensity_max_epoch' => value.precipIntensityMaxTime.to_s,
'probability' => value.precipProbability.to_s,
'type' => value.precipType
'intensity' => value.rain.to_s,
#'intensity_max' => value.precipIntensityMax.to_s,
#'intensity_max_epoch' => value.precipIntensityMaxTime.to_s,
#'probability' => value.precipProbability.to_s,
'type' => figure_rain_or_snow(value.rain, value.snow),
},
'dewPoint' => value.dewPoint.to_s,
'dewPoint' => value.dew_point.to_s,
'avewind' => {
'mph' => value.windSpeed.round().to_s,
'kph' => (Float(value.windSpeed) * 1.609344).round().to_s,
'degrees' => value.windBearing.to_s
'mph' => value.wind_speed.round().to_s,
'kph' => (Float(value.wind_speed) * 1.609344).round().to_s,
'degrees' => value.wind_deg.to_s
},
'visibility' => value.visibility.to_s,
'cloudCover' => value.cloudCover.to_s,
'cloudCover' => value.clouds.to_s,
'pressure' => value.pressure.to_s,
'ozone' => value.ozone.to_s
#'ozone' => value.ozone.to_s
}
return day
end
Expand Down
2 changes: 1 addition & 1 deletion db/seeds/seeder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def self.seed
user.save!

if DefaultScenarioImporter.seed(user)
puts "NOTE: The example 'SF Weather Agent' will not work until you edit it and put in a free API key from http://www.wunderground.com/weather/api/"
puts "NOTE: The example 'SF Weather Agent' will not work until you edit it and put in a free API key from https://home.openweathermap.org/api_keys."
puts "See the Huginn Wiki for more Agent examples! https://github.com/huginn/huginn/wiki"
else
raise('Unable to import the default scenario')
Expand Down
1,568 changes: 1,567 additions & 1 deletion spec/data_fixtures/weather.json

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions spec/models/agent_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ def receive(events)

describe ".receive!" do
before do
stub_request(:any, /darksky/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/weather.json")), :status => 200)
stub_request(:any, /openweather/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/weather.json")), :status => 200)
stub.any_instance_of(Agents::WeatherAgent).is_tomorrow?(anything) { true }
end

Expand Down Expand Up @@ -910,19 +910,20 @@ def @agent.receive_webhook(params)

describe ".drop_pending_events" do
before do
stub_request(:any, /darksky/).to_return(body: File.read(Rails.root.join("spec/data_fixtures/weather.json")), status: 200)
stub_request(:any, /openweather/).to_return(body: File.read(Rails.root.join("spec/data_fixtures/weather.json")), status: 200)
end

it "should drop pending events while the agent was disabled when set to true" do
agent1 = agents(:bob_weather_agent)
stub.any_instance_of(Agents::TriggerAgent).matches? {true}
agent2 = agents(:bob_rain_notifier_agent)

expect {
expect {
Agent.async_check(agent1.id)
Agent.receive!
}.to change { agent1.events.count }.by(1)
}.to change { agent2.events.count }.by(0)
}.to change { agent2.events.count }.by(1)

agent2.disabled = true
agent2.save!
Expand Down
2 changes: 1 addition & 1 deletion spec/models/agents/email_agent_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def get_message_part(mail, content_type)
end

it "can receive complex events and send them on" do
stub_request(:any, /darksky/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/weather.json")), :status => 200)
stub_request(:any, /openweather/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/weather.json")), :status => 200)
stub.any_instance_of(Agents::WeatherAgent).is_tomorrow?(anything) { true }
@checker.sources << agents(:bob_weather_agent)

Expand Down
2 changes: 1 addition & 1 deletion spec/models/agents/email_digest_agent_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def get_message_part(mail, content_type)
end

it "can receive complex events and send them on" do
stub_request(:any, /darksky/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/weather.json")), :status => 200)
stub_request(:any, /openweather/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/weather.json")), :status => 200)
stub.any_instance_of(Agents::WeatherAgent).is_tomorrow?(anything) { true }
@checker.sources << agents(:bob_weather_agent)

Expand Down
34 changes: 17 additions & 17 deletions spec/models/agents/weather_agent_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@
end
end

let :dark_sky_agent do
let :weather_agent do
Agents::WeatherAgent.create(
name: "weather from dark sky",
name: "weather from openweather",
options: {
:location => "37.779329,-122.41915",
:service => "darksky",
:service => "openweather",
:which_day => 1,
:api_key => "test"
}
Expand All @@ -44,23 +44,23 @@
expect(agent.working?).to be_falsey
end

context "dark sky" do
context "openweather" do
it "validates the location properly" do
expect(dark_sky_agent.options["location"]).to eq "37.779329,-122.41915"
expect(dark_sky_agent).to be_valid
dark_sky_agent.options["location"] = "37.779329, -122.41915" # with a space
expect(dark_sky_agent).to be_valid
dark_sky_agent.options["location"] = "94103" # a zip code
expect(dark_sky_agent).to_not be_valid
dark_sky_agent.options["location"] = "37.779329,-122.41915"
expect(dark_sky_agent.options["location"]).to eq "37.779329,-122.41915"
expect(dark_sky_agent).to be_valid
expect(weather_agent.options["location"]).to eq "37.779329,-122.41915"
expect(weather_agent).to be_valid
weather_agent.options["location"] = "37.779329, -122.41915" # with a space
expect(weather_agent).to be_valid
weather_agent.options["location"] = "94103" # a zip code
expect(weather_agent).to_not be_valid
weather_agent.options["location"] = "37.779329,-122.41915"
expect(weather_agent.options["location"]).to eq "37.779329,-122.41915"
expect(weather_agent).to be_valid
end
it "fails cases that pass the first test but are invalid" do
dark_sky_agent.options["location"] = "137.779329, -122.41915" # too high latitude
expect(dark_sky_agent).to_not be_valid
dark_sky_agent.options["location"] = "37.779329, -522.41915" # too low longitude
expect(dark_sky_agent).to_not be_valid
weather_agent.options["location"] = "137.779329, -122.41915" # too high latitude
expect(weather_agent).to_not be_valid
weather_agent.options["location"] = "37.779329, -522.41915" # too low longitude
expect(weather_agent).to_not be_valid
end
end

Expand Down