Last active
September 1, 2020 15:01
-
-
Save kmcphillips/5a267bddede993b9b666d874d0254030 to your computer and use it in GitHub Desktop.
Poll for Tesla chargers in stock and emails when found
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/poller.log | |
/.env | |
/last_run |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Scrapes for chargers in the canadian Tesla store and notifies when found. | |
# | |
# Set it up in the cron to run every few minutes: | |
# */2 * * * * /bin/bash -l -c 'cd /home/example/tesla_poller && ruby tesla_poller.rb' >/dev/null 2>&1 | |
# | |
# The `.env` file should have entries that look kinda like this: | |
# SMTP_ADDRESS=smtp.example.com | |
# SMTP_DOMAIN=example.com | |
# [email protected] | |
# SMTP_PASSWORD=password | |
# [email protected] | |
# SMTP_FROM="Tesla Scraper Bot" <[email protected]> | |
# SMTP_PORT=587 | |
# NOTIFIER_TYPE=email | |
# | |
# You should be able to use gmail app passwords, or whatever service you use. You can notify with something else if you write a different Notifier class. | |
require "bundler/inline" | |
gemfile do | |
source "https://rubygems.org" | |
gem "pry", "0.13.1" | |
gem "httparty", "0.18.1" | |
gem "dotenv", "2.7.6" | |
gem "mail", "2.7.1" | |
end | |
require "dotenv/load" | |
require "logger" | |
Log = Logger.new(File.join(File.dirname(__FILE__), 'poller.log')) | |
class ConsoleNotifier | |
attr_reader :logger | |
def initialize | |
@logger = Log | |
end | |
def notify(title:, message:) | |
puts "[ConsoleNotifier] title:#{ title } message:#{ message }" | |
@logger.info("[ConsoleNotifier] title:#{ title } message:#{ message }") | |
end | |
end | |
Mail.defaults do | |
delivery_method :smtp, { | |
address: ENV["SMTP_ADDRESS"], | |
port: ENV["SMTP_PORT"], | |
domain: ENV["SMTP_DOMAIN"], | |
user_name: ENV["SMTP_USERNAME"], | |
password: ENV["SMTP_PASSWORD"], | |
authentication: 'plain', | |
enable_starttls_auto: true | |
} | |
end | |
class EmailNotifier | |
attr_reader :logger | |
def initialize | |
@logger = Log | |
end | |
def notify(title:, message:) | |
@logger.info("[EmailNotifier] title:#{ title } message:#{ message }") | |
mail = Mail.new( | |
to: ENV["EMAIL_NOTIFIER_RECIPIENT"], | |
from: ENV["SMTP_FROM"], | |
subject: title, | |
body: message, | |
charset: 'UTF-8', | |
) | |
mail.deliver | |
end | |
end | |
class Response | |
attr_reader :result | |
def initialize | |
@result = nil | |
@path = "https://shop.tesla.com/en_ca/inventory.json" | |
@headers = { | |
"Accept" => "application/json, text/javascript, */*; q=0.01", | |
"Accept-Language" => "en-CA,en;q=0.8,en-US;q=0.6,fr-CA;q=0.4,nl;q=0.2", | |
"Connection" => "keep-alive", | |
"Content-Type" => "application/json", | |
"Host" => "shop.tesla.com", | |
"Origin" => "https://shop.tesla.com", | |
"Referer" => "https://shop.tesla.com/en_ca/product/wall-connector", | |
"TE" => "Trailers", | |
"User-Agent" => "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0", | |
"X-Requested-With" => "XMLHttpRequest", | |
} | |
@request_body = "[\"1457768-01-F\",\"1457768-00-F\"]" | |
end | |
def fetch | |
@result = HTTParty.post(@path, headers: @headers, body: @request_body) | |
success? | |
end | |
def success? | |
@result && @result.success? | |
end | |
def code | |
@result && @result.code | |
end | |
def in_stock? | |
node = @result.find { |r| r["skuCode"] == "1457768-00-F" } # 00 is the 8.5, 01 is the longer one | |
raise "Cannot find item in response" unless node | |
node["purchasable"] # also "error"=>"Out of stock" | |
end | |
end | |
class LastRun | |
def initialize | |
@file_path = File.join(File.dirname(__FILE__), 'last_run') | |
end | |
def read | |
begin | |
File.open(@file_path, "r") { |f| f.read } | |
rescue Errno::ENOENT => e | |
nil | |
end | |
end | |
def write(str) | |
File.open(@file_path, "w") { |f| f.write(str.to_s) } != 0 | |
end | |
def change?(incoming) | |
previous = read | |
write(incoming) | |
previous != incoming.to_s | |
end | |
end | |
class Runner | |
attr_reader :logger, :notifier, :response | |
def initialize | |
@notifier = case ENV["NOTIFIER_TYPE"] | |
when "console" then ConsoleNotifier.new | |
when "email" then EmailNotifier.new | |
else | |
raise "Unknown NOTIFIER_TYPE=#{ ENV['NOTIFIER_TYPE'] }" | |
end | |
@response = Response.new | |
@last_run = LastRun.new | |
@logger = Log | |
end | |
def run | |
begin | |
response.fetch | |
if response.success? | |
logger.info("success response") | |
if response.in_stock? | |
logger.info("found in stock") | |
if @last_run.change?("in_stock") | |
notifier.notify( | |
title: "Tesla wall charger in stock!", | |
message: "https://shop.tesla.com/en_ca/product/wall-connector", | |
) | |
end | |
else | |
@last_run.write("out_of_stock") | |
logger.info("out of stock") | |
end | |
0 | |
else | |
logger.error("response error") | |
logger.error(response.result.body) | |
if @last_run.change?("error") | |
notifier.notify( | |
title: "Tesla scraper error", | |
message: "code: #{ response.code }\n\n#{ response.result.body }", | |
) | |
end | |
1 | |
end | |
rescue => e | |
if @last_run.change?("error") | |
notifier.notify( | |
title: "Tesla scraper error", | |
message: e.inspect, | |
) | |
end | |
1 | |
end | |
end | |
end | |
exit Runner.new.run |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment