Created
August 2, 2014 05:39
-
-
Save rho333/705cf46da7a9e1587104 to your computer and use it in GitHub Desktop.
Simple Vodafone NZ usage scraper/tracker
This file contains 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
# Simple Python script to fetch usage data for Vodafone NZ customers and push | |
# some summary stats to their mobile phones/tablets via Boxcar. | |
# | |
# Disclaimer: I in no way guarantee the security, continued functionality or support of this software. | |
# I am in no way affiliated with Vodafone NZ. This software is licensed under the MIT License. | |
# | |
# ----------------------- | |
# | |
# The MIT License (MIT) | |
# | |
# Copyright (c) 2014 Richard Hofman | |
# | |
# Permission is hereby granted, free of charge, to any person obtaining a copy | |
# of this software and associated documentation files (the "Software"), to deal | |
# in the Software without restriction, including without limitation the rights | |
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
# copies of the Software, and to permit persons to whom the Software is | |
# furnished to do so, subject to the following conditions: | |
# | |
# The above copyright notice and this permission notice shall be included in | |
# all copies or substantial portions of the Software. | |
# | |
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
# THE SOFTWARE. | |
import sys | |
from calendar import monthrange | |
from datetime import datetime | |
import urllib, urllib2, cookielib | |
import lxml.html as etree | |
def main(args): | |
try: | |
u = args[1] | |
p = args[2] | |
except IndexError: | |
usage("Insufficient arguments provided.") | |
try: | |
mode = args[3] | |
except IndexError: | |
debug("No mode specified, assuming push alert mode.") | |
mode = "pushalert" | |
if mode == "pushalert": # Send summaries via Boxcar | |
get_authenticated_connection(u, p) | |
data = get_usage_data() | |
send_results(get_daily_usage(data)) | |
elif mode == "detailed": # Fetch detailed data on the current billing period and return it in JSON format | |
get_authenticated_connection(u, p) | |
data = get_usage_data() | |
print generate_report_data(data) | |
else: | |
fail("Unrecognised mode parameter specified.") | |
def fail(msg): | |
print "ERROR: " + msg | |
sys.exit(1) | |
def usage(msg=None): | |
import inspect | |
if msg: | |
print msg + "\n-------" | |
print "VFNZ Internet Usage Tracker\n" | |
print "Usage:" | |
print "python %s <username> <password> [(pushalert)|detailed]\n" % inspect.getfile(inspect.currentframe()) | |
sys.exit(1) | |
def debug(msg): | |
#print "DEBUG: " + msg # uncomment this line to print boring stuff to stdout | |
pass | |
# | |
# Handles initial log in to the My Vodafone portal | |
# | |
def get_authenticated_connection(username, password): | |
strings = {'myvf_url':'https://www.vodafone.co.nz/myvodafone', | |
'loginform_id':'fabLoginFormMain', | |
'un_input_name':'login', | |
'pw_input_name':'password' | |
} | |
# Set up urllib | |
vf_opener = urllib2.build_opener(urllib2.HTTPHandler(), urllib2.HTTPRedirectHandler(), urllib2.HTTPCookieProcessor(cookielib.CookieJar())) | |
urllib2.install_opener(vf_opener) | |
# Connect and get page | |
lp_conn = urllib2.urlopen(strings['myvf_url']) | |
lp = lp_conn.read() | |
lp_tree = etree.fromstring(lp) | |
# Find login form | |
lp_forms = lp_tree.forms | |
loginform = None | |
for f in lp_forms: | |
#print "ID: %s" % f.get("id") | |
if f.get("id") == strings['loginform_id']: | |
loginform = f | |
if loginform is not None: | |
debug("Logging in using login form.") | |
# Parse form and fill in appropriate fields | |
login_url = loginform.action | |
login_values = dict() | |
for input in loginform.inputs: | |
if input.name is not None: | |
if input.name.lower() == strings["un_input_name"] or input.name.lower() == strings["pw_input_name"]: | |
pass | |
else: | |
login_values[input.name] = input.value | |
#print input.name + " " + ("(static)" if input.type.lower() == "hidden" else "(userdefined)") | |
else: | |
debug("Logging in using hard-coded fallbacks.") | |
# Set literal values - riskier, but worth a try if we can't find the login form | |
login_url = "https://the.vodafone.co.nz/acnts/myaccounts.pl/login" | |
login_values = { | |
"cmd": "login", | |
"force_connection": "true", | |
"frames": "true", | |
"selected_tpl": "true", | |
"no_tcode": "true", | |
"login-type": "internet", | |
"login-password-cover": "Password" | |
} | |
login_values[strings["un_input_name"]] = username | |
login_values[strings["pw_input_name"]] = password | |
# Submit login information | |
login_data = urllib.urlencode(login_values) | |
myvf_conn = urllib2.urlopen(url=login_url, data=login_data) | |
myvf_page = myvf_conn.read() | |
# | |
# Fetches and parses data usage page, and returns a dictionary of dates => usage amounts (in MB). | |
# | |
def get_usage_data(): | |
usage_strings = {'usage_url':'https://the.vodafone.co.nz/acnts/myaccount-int.pl/usage', | |
'usage_form_action':'usage-data', | |
'usage_detail_url':'https://the.vodafone.co.nz/acnts/myaccount-int.pl/usage-data' | |
} | |
# Load usage page and parse into an lxml element tree | |
up = urllib2.urlopen(usage_strings['usage_url']).read() | |
up_tree = etree.fromstring(up) | |
# Find usage form (billing period selection) | |
up_forms = up_tree.forms | |
usage_form = None | |
for f in up_forms: | |
if f.action == usage_strings["usage_form_action"]: | |
usage_form = f | |
if usage_form is None: | |
fail("Could not find usage form - perhaps the page formatting has changed?") | |
# Get date ranges (billing periods) and login name from usage form dropdown | |
date_ranges = None | |
for i in usage_form.inputs: | |
if i.name == "range": | |
date_ranges = i | |
if i.name == "login": | |
login_name = i.value | |
if date_ranges is None: | |
fail("Could not find date ranges in usage form - perhaps the page formatting has changed?") | |
# Get date range options and pick current billing period | |
range_options = date_ranges.value_options | |
selected_range = range_options[0] # 0th entry is most recent | |
# Request detailed usage page for current billing period | |
usage_post_dict = {'login':login_name, 'range':selected_range} | |
data = urllib.urlencode(usage_post_dict) | |
dup = urllib2.urlopen(url=usage_strings['usage_detail_url'], data=data).read() | |
dup_tree = etree.fromstring(dup) | |
# Parse dates and usage values (in MB) from detailed usage page | |
usage_table_dates_el = dup_tree.xpath("//table[2]/tr/td[1]") | |
usage_table_dates = [] | |
usage_table_values_el = dup_tree.xpath("//table[2]/tr/td[2]") | |
usage_table_values = [] | |
for i in range(1, len(usage_table_dates_el)-1): | |
usage_table_dates.append(usage_table_dates_el[i].text_content().strip().lower()) | |
usage_table_values.append(usage_table_values_el[i].text_content().strip()) | |
# Relabel yesterday's entry as "yesterday" | |
if "today" not in usage_table_dates: | |
usage_table_dates[-2] = "yesterday" | |
else: | |
usage_table_dates[-3] = "yesterday" | |
# Map dates to their corresponding usage values | |
data = dict(zip(usage_table_dates, usage_table_values)) | |
# Handles corner case where "Today" entry does not appear just after midnight... | |
if "today" not in data: | |
data["today"] = "0.000" | |
return data | |
# | |
# Gets current day's usage from raw data, returns dictionary of data to be pushed. | |
# | |
def get_daily_usage(data): | |
# BEGIN CONFIGURATION OPTIONS | |
monthly_max = 150 * 1024 | |
send_summary_information = True | |
# END CONFIGURATION OPTIONS | |
usage_today = float(data["today"]) | |
usage_yesterday = float(data["yesterday"]) | |
cumulative_usage = float(data["total"]) | |
daily_max = (monthly_max-cumulative_usage)/((monthrange(datetime.now().year, datetime.now().month)[1])+1 - datetime.now().day) | |
is_over_daily_limit = usage_today>daily_max | |
results = {'usage_today': usage_today, | |
'usage_yesterday': usage_yesterday, | |
'cumulative_usage': cumulative_usage, | |
'monthly_max': monthly_max, | |
'daily_max': daily_max, | |
'is_over_daily_limit': is_over_daily_limit, | |
'send_summary_information':send_summary_information | |
} | |
return results | |
# | |
# Takes the push_data dictionary in and generates the push notification to be sent. | |
# Also contains list of hard-coded Boxcar account tokens to receive the message. | |
# | |
def send_results(push_data): | |
# BEGIN CONFIGURATION OPTIONS | |
# Put Boxcar Access Tokens here (comma-separated). | |
# For just one token, simply don't include a comma: ['mytoken'] | |
recipients = ['token1', 'token2'] | |
# END CONFIGURATION OPTIONS | |
if push_data["is_over_daily_limit"] or push_data["send_summary_information"]: | |
gb_tot_usage = push_data['cumulative_usage']/1024.0 | |
gb_today_usage = push_data['usage_today']/1024.0 | |
gb_yesterday_usage = push_data['usage_yesterday']/1024.0 | |
daily_max = push_data['daily_max']/1024.0 | |
monthly_max = push_data['monthly_max']/1024.0 | |
cumulative = "%.2f / %.2f GB (%.2f %%)" % (gb_tot_usage, monthly_max, 100*gb_tot_usage/monthly_max) | |
today = "%.2f / %.2f GB" % (gb_today_usage, daily_max) | |
yesterday = "%.2f / %.2f GB" % (gb_yesterday_usage, daily_max) | |
footer = "Please try to restrict your data usage where possible to avoid exceeding the cap." | |
content = '''<p>Usage for the month so far: %s.</p> \n | |
<p>Used today: %s (daily max)</p> | |
<p>Used yesterday: %s (daily max)</p> | |
<p>%s</p> | |
''' | |
body = content % (cumulative, today, yesterday, footer) | |
subject = "VF - INTERNET USAGE SUMMARY" if not push_data["is_over_daily_limit"] else "VF - DAILY LIMIT EXCEEDED" | |
for r in recipients: | |
send_boxcar_message(r, subject, body) | |
else: | |
debug("No warnings for today!") | |
# | |
# Sends notification to the given Boxcar account token | |
# | |
def send_boxcar_message(acctok, subject, body): | |
boxcar_data = { 'user_credentials': acctok, | |
'notification[title]': subject, | |
'notification[long_message]': body | |
} | |
urllib2.urlopen(url="https://new.boxcar.io/api/notifications", data=urllib.urlencode(boxcar_data)) | |
# | |
# Generates JSON-formatted data set for Chart.js (to be used for web UI) | |
# | |
def generate_report_data(data): | |
return "{}" | |
if __name__ == "__main__": | |
main(sys.argv) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment