Skip to content

Instantly share code, notes, and snippets.

@kmcphillips
Last active September 1, 2020 15:01
Show Gist options
  • Save kmcphillips/5a267bddede993b9b666d874d0254030 to your computer and use it in GitHub Desktop.
Save kmcphillips/5a267bddede993b9b666d874d0254030 to your computer and use it in GitHub Desktop.
Poll for Tesla chargers in stock and emails when found
/poller.log
/.env
/last_run
# 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