Creating a Bot to Refill Parking Meters Using AWS Lambda

October 7, 2019

I get a shameful number of parking tickets from failing to refill parking meters on time. So one day I decided to make a bot that tops off the parking meter for me. Yes, I'm probably breaking some terms and conditions. But for now that's a risk I'm willing to accept.

So here's the situation. My car is parked on the street in a suburb of Washington DC. I need to refill the parking meter every two hours. These meters limit the parking reservation limit to two hours. So every two hours during business hours (9 AM to 6 PM, Monday to Friday) I need to refill the meter.

ParkMobile is one of a few apps that allow you to pay for parking on a device. But even with a convenience of re-reserving your parking space remotely from your computer of phone, it's still a pain to do every two hours on weekdays.

Here's what the ParkMobile UI looks like for reserving a parking space:

It's not too complicated at first glance.

You just need to set the zone id, select a location, selection a parking duration, select a time block and then click start parking.

Browser Automation

I opted to use chromeless to automate reserving a parking space. Chromeless is a browser automation library with an API akin to nightmare.js that uses Chrome Canary instead of Electron.

My rationale for using Chromeless is that I wanted to run the lot in AWS Lambda and not locally on my machine for obvious reasons.

The hardest part turned out to be logging in to ParkMobile. I encountered the following obstacles automating the login flow:

  • The actual ParkMobile login page (the page where you are prompted to enter your username and password) is https://dlweb.parkmobile.us/Phonixx/. However, navigating to this page directly does not work. Instead, you need to navigate to https://parkmobile.io/login and click "Zone Parking". That's because the parkmobile.io/login link sets a myriad of cookies (e.g., anti-csrf) that are needed.
  • ParkMobile did not like the default user-agent "Chromeless" (for obvious reasons). So I manually set the user-agent to something plausible.

Here's what the code looks like for logging in:

const INPUT = {
  LOGIN: 'input#ctl00_ContentPlaceHolder1_UcUserLoginControl1_userName',
  PASSWORD: 'input#ctl00_ContentPlaceHolder1_UcUserLoginControl1_password'
}
const login = async (chromeless) => {
  let cookies = await chromeless
    .setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36')
    .goto('https://parkmobile.io/login')
    .cookies()
  await chromeless
    .goto('https://dlweb.parkmobile.us/Phonixx/')
    .setCookies(cookies)
    .type(process.env.LOGIN, INPUT.LOGIN)
    .type(process.env.PASSWORD, INPUT.PASSWORD)
    .wait(500)
    .evaluate(() => {
      WebForm_DoPostBackWithOptions(new WebForm_PostBackOptions("ctl00$ContentPlaceHolder1$UcUserLoginControl1$lbLogon", "", true, "", "", false, true))
    })
}

The code above logins into ParkMobile. Here's what it looks like when run:

I won't bore you with the remaining implementation details. In brief I deployed the project to AWS lambda using serverless. I scheduled it to run every 5 minutes and used redis to keep track of state. I also used Twilio to send myself confirmation text messages every time the space was reserved.

AWS lambda makes cron jobs trivially easy:

functions:
  handler:
    events:
      - schedule: rate(5 minutes)

For the curious, here is the github repository for the project.