Skip to content

Instantly share code, notes, and snippets.

@awood45
Last active November 29, 2020 22:26
Show Gist options
  • Save awood45/512ac02f0044a9856ca44d0cb767518f to your computer and use it in GitHub Desktop.
Save awood45/512ac02f0044a9856ca44d0cb767518f to your computer and use it in GitHub Desktop.
Adverse weather notification app - the highlights.
require 'net/http'
require 'aws-sdk-sns'
LATITUDE = ENV["LATITUDE"]
LONGITUDE = ENV["LONGITUDE"]
NOTIFICATION_TOPIC = ENV["TOPIC_ARN"]
ENDPOINT = URI("https://api.weather.gov/points/#{LATITUDE},#{LONGITUDE}")
SNS = Aws::SNS::Client.new
NOTIFICATION_THRESHOLD = 15
def lambda_handler(event:, context:)
forecast_uri = get_forecast_uri
resp = get_json_resp(forecast_uri)
# By convention, the first "period" is the "today/now" forecast, which is what we want.
wind_speed = resp["properties"]["periods"].first["windSpeed"]
puts "[INFO] Raw wind speed value: #{wind_speed}"
top_speed = wind_speed.match(/(\d+)\D*(\d+)*/)[1..-1].map { |i| i.to_i }.max
if top_speed >= NOTIFICATION_THRESHOLD
puts "[INFO] Top wind speed of #{top_speed} meets notification threshold. Sending warning notification."
message = "Wind speed forecasted as #{wind_speed}, which is in excess of warning threshold."
adverse_notification(message)
else
puts "[INFO] Top wind speed of #{top_speed} below notification threshold. Skip."
end
end
def get_forecast_uri
resp = get_json_resp(ENDPOINT)
URI(resp["properties"]["forecast"])
end
def get_json_resp(endpoint)
resp = JSON.parse(Net::HTTP.get(endpoint))
puts "[INFO] Response from #{endpoint}:\n#{JSON.pretty_unparse(resp)}"
resp
end
def adverse_notification(message)
SNS.publish(
topic_arn: NOTIFICATION_TOPIC,
message: message
)
end
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: high-wind-checker
Parameters:
PhoneNumber:
Type: String
Description: Phone number to send adverse weather notifications to.
Latitude:
Type: String
Description: Latitude of weather check target, to 4 decimal places.
Longitude:
Type: String
Description: Longitude of weather check target, to 4 decimal places.
Globals:
Function:
Timeout: 5
Resources:
AdverseWeatherNotificationTopic:
Type: AWS::SNS::Topic
Properties:
DisplayName: WeatherChecker
Subscription:
- Protocol: sms
Endpoint: !Ref PhoneNumber
WeatherCheckerFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: weather/
Handler: app.lambda_handler
Runtime: ruby2.7
Policies:
- SNSPublishMessagePolicy:
TopicName: !GetAtt AdverseWeatherNotificationTopic.TopicName
Environment:
Variables:
LATITUDE: !Ref Latitude
LONGITUDE: !Ref Longitude
TOPIC_ARN: !Ref AdverseWeatherNotificationTopic
Events:
ScheduledNotification:
Type: Schedule
Properties:
Enabled: true
Schedule: "cron(0 0,1,2,3,4,5,6,17,18,19,20,21,22,23 * * ? *)" # 9 AM - 10 PM PST

I started from a sam init using Ruby 2.7. Run sam deploy --guided and provide a phone number (you may get texted to confirm your subscription), latitude, and longitude. These are passed in to the weather.gov APIs, and this particular app focuses on wind. Of course, you can modify the handler logic to alarm on whatever you happen to care about (the APIs are public, easy to run curl on them to explore what they return. If you are going to put this in a repo somewhere, you may also want to add samconfig.toml to your .gitignore so as to not broadcast your phone number, latitude, or longitude to the world.

In the future, I may enhance this to run on an hourly basis to collect "should notify" status overnight to be sent out in the morning, or to avoid re-texting the same adverse notification each hour on a high wind day. For that I might use something like AWS SSM (as a sort of cache of values like "when did I last send a text" or "has there been an adverse weather threshold since the last text notification" as key-value pairs) or AWS SQS (as an alternative, to send intent to notify into a queue each hour, and then a couple times a day flush the queue and send one or zero notifications).

This does not include all files, but it does have all files I modified from a hello world sam init invocation.

Additionally, this doesn't really deal with alarming, and any errors will just be allowed to crash the Lambda. I don't have availability needs (given that this has only me as a customer), and a hard crash doesn't really cause any problems for the app overall. If you were to scale this out, you'd probably want alarming or soft-fails on errors.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment