├── .gitignore ├── README.md ├── config.json └── squeaky-wheel.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.iws 3 | *.xml 4 | .idea 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Squeaky-Wheel 2 | Automatically run speed tests and tweet @ your ISP if they are garbage. 3 | ### Overview: 4 | Python Script that utilizes selenium web driver to scrape speed test results and 5 | tweet them at your ISP if they are below a given value. 6 | ###### This script compiles and logs: 7 | * Download Speed in Mbps 8 | * Upload Speed in Mbps 9 | * Latency in msec 10 | * Jitter in msec 11 | 12 | #### [MLab (Measurement Lab)](http://www.measurementlab.net/) is used for the speedtest. 13 | ##### It's the same test that Google is using in its new native speedtest feature: 14 | 15 | ![Google Test](https://s24.postimg.org/vdhuc51p1/image.png) 16 | 17 | The MLab's test is located @ http://www.measurementlab.net/tools/ndt/ , 18 | but the data is scraped from http://www.measurementlab.net/p/ndt-ws.html 19 | because the test is served up through an iframe. 20 | 21 | The [Tweepy](http://www.tweepy.org/) Library is used to access the twitter API. 22 | You will need your own API keys and access tokens from https://dev.twitter.com/. 23 | 24 | #### Sample Output: 25 | ![Sample Tweet](https://s23.postimg.org/hv52aukjv/image.png) 26 | 27 | ### Setup: 28 | ##### Dependencies: 29 | `Selenium` 30 | `geckodriver` 31 | `Tweepy` 32 | 33 | ##### Config: 34 | Config options can be set in **config.json**: 35 | 36 | ###### Bandwidth 37 | Set "download" to the download speed you are supposed to get (in Mbps). 38 | Set "upload" to the upload speed you are supposed to get (in Mbps). 39 | ###### Twitter 40 | Put in your Twitter API keys and Access tokens in the "twitter" section. 41 | ###### Margin 42 | The margin sets how much leeway there is for normal fluxuation. 43 | Example: a margin of ".5" triggers an exception and tweet if speeds dip 44 | below 50% of promised speeds (".6" == 60%, etc). 45 | **Must be between 0 and 1**. 46 | ###### ISP: 47 | Set "isp" to your ISP's twitter handle. 48 | 49 | | ISP | Twitter Handle | 50 | | --- | --- | 51 | | Comcast | @comcast | 52 | | Comcast Support | @comcastcares | 53 | | Spectrum / Time Warner | @GetSpectrum | 54 | | Verizon Fios | @verizonfios | 55 | | Verizon Support | @VerizonSupport | 56 | | AT&T | @ATT | 57 | | AT&T Support | @ATTCares | 58 | | CenturyLink | @CenturyLink | 59 | | CenturyLink Support | @CenturyLinkHelp | 60 | | Cox Communications | @CoxComm | 61 | | Cox Support | @CoxCommHelp | 62 | | Frontier | @FrontierCorp | 63 | ###### Log: 64 | Change the name / location of the .log file. Default location is local directory 65 | ###### Selenium Driver 66 | Change the driver type to one of either 'chrome' or 'firefox'. Leaving 67 | empty or not set will use Firefox. If Firefox is not installed in the 68 | expected location, set the driver binary to the full path to the 69 | Firefox executable. Ex `C:\Program Files (x86)\Mozilla Firefox\Firefox.exe` 70 | 71 | You must have installed the proper driver. See the [downloads section here](http://www.seleniumhq.org/download/) 72 | ## Usage: 73 | `python3 ~/squeaky-wheel.py` 74 | 75 | Set it as a cron job to run every x minutes/ hours: 76 | `0,30 * * * * python3 ~/squeaky-wheel.py` 77 | 78 | ## Support: 79 | Drop me a line - mrbenpappas@gmail.com 80 | Or visit my [site](http://mrbenpappas.com) 81 | ## License: 82 | ##### The MIT License (MIT) 83 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bandwidth":{ 3 | "download": "100", 4 | "upload": "10" 5 | }, 6 | "twitter": 7 | { 8 | "twitter_token": "", 9 | "twitter_consumer_key": "", 10 | "twitter_token_secret": "", 11 | "twitter_consumer_secret": "" 12 | }, 13 | "margin": ".7", 14 | "isp": "@comcast", 15 | "log": 16 | { 17 | "name": "squeaky-wheel.log" 18 | }, 19 | "driver": 20 | { 21 | "type" : "firefox", 22 | "binary" : "/usr/bin/firefox" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /squeaky-wheel.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import time 4 | import tweepy 5 | import os.path 6 | from enum import Enum 7 | from selenium import webdriver 8 | from selenium.common.exceptions import TimeoutException, WebDriverException 9 | from selenium.webdriver.common.by import By 10 | from selenium.webdriver.support import expected_conditions as EC 11 | from selenium.webdriver.support.ui import WebDriverWait 12 | from selenium.webdriver.firefox.firefox_binary import FirefoxBinary 13 | 14 | CONIFG_FILENAME = "config.json" 15 | 16 | 17 | class DriverType(Enum): 18 | driver = 0 19 | firefox = 1 20 | chrome = 2 21 | 22 | class Config(object): 23 | 24 | with open(CONIFG_FILENAME, "r") as j: 25 | config = json.load(j) 26 | 27 | # set download, upload, isp, twitter api data, and other defaults in config.json 28 | 29 | download = float(config["bandwidth"]["download"]) 30 | upload = float(config["bandwidth"]["upload"]) 31 | margin = float(config["margin"]) 32 | isp = config["isp"] 33 | 34 | twitter_token = config["twitter"]["twitter_token"] 35 | twitter_token_secret = config["twitter"]["twitter_token_secret"] 36 | twitter_consomer_key = config["twitter"]["twitter_consumer_key"] 37 | twitter_consumer_secret = config["twitter"]["twitter_consumer_secret"] 38 | 39 | log = config["log"]["name"] 40 | 41 | driver_type = None 42 | binary_path = None 43 | 44 | # Check the configuration file for existence of a 'driver' field 45 | if DriverType.driver.name in config: 46 | # If the driver indicated is "firefox", set for that 47 | if config[DriverType.driver.name]["type"] == DriverType.firefox.name: 48 | driver_type = DriverType.firefox 49 | binary_path = config[DriverType.driver.name]["binary"] 50 | # Check if the provided binary path is valid 51 | if len(binary_path) > 0 and not os.path.isfile(binary_path): 52 | # Invalid binary path, try to use the default 53 | driver_type = None 54 | binary_path = None 55 | # Check for "chrome", no binary path needed 56 | elif config[DriverType.driver.name]["type"] == DriverType.chrome.name: 57 | driver_type = DriverType.chrome 58 | # No driver, or other missing, try the default (which is Firefox right now) 59 | 60 | date = ("Data logged: {:%Y-%b-%d %H:%M:%S}".format(datetime.datetime.now())) 61 | 62 | 63 | class Log(object): 64 | 65 | config = Config() 66 | 67 | def write_to_log(self, input): 68 | with open(self.config.log, "a") as f: 69 | f.write(input) 70 | 71 | 72 | class SpeedTest(object): 73 | 74 | def __init__(self, config:Config): 75 | self.download = "" 76 | self.upload = "" 77 | self.latency = "" 78 | self.jitter = "" 79 | self.log = Log() 80 | self.config = config 81 | 82 | self.driver = None 83 | self.wait = None 84 | try: 85 | # Check for specific driver configured 86 | if self.config.driver_type is not None: 87 | # Create a Firefox driver 88 | if self.config.driver_type == DriverType.firefox: 89 | # Point to a configured binary for FF 90 | if self.config.binary_path is not None: 91 | self.driver = webdriver.Firefox(firefox_binary=FirefoxBinary(config.binary_path)) 92 | else: 93 | self.driver = webdriver.Firefox() 94 | # Create a Chrome driver (no specific executable needed) 95 | elif self.config.driver_type == DriverType.chrome: 96 | self.driver = webdriver.Chrome() 97 | # No specific driver configured, try to use Firefox 98 | else: 99 | self.driver = webdriver.Firefox() 100 | self.wait = WebDriverWait(self.driver, 5) 101 | except WebDriverException as e: 102 | self.log.write_to_log("Driver creation failed {:%Y-%b-%d %H:%M:%S}\n".format(datetime.datetime.now())) 103 | self.log.write_to_log(str(e)) 104 | 105 | def valid_driver(self): 106 | return self.driver is not None 107 | 108 | def run_test(self): 109 | if self.driver is None or self.wait is None: 110 | return 111 | 112 | self.driver.get("https://www.measurementlab.net/p/ndt-ws.html") 113 | 114 | try: 115 | button = self.wait.until(EC.element_to_be_clickable( 116 | (By.ID, "start-button"))) 117 | button.click() 118 | 119 | except TimeoutException: 120 | self.log.write_to_log("-- Button not found --") 121 | 122 | def store_test_values(self): 123 | if self.driver is None: 124 | return 125 | 126 | try: 127 | self.upload = str(self.driver.find_element_by_id("upload-speed").text) 128 | self.download = str(self.driver.find_element_by_id("download-speed").text) 129 | self.latency = self.driver.find_element_by_id("latency").text 130 | self.jitter = self.driver.find_element_by_id("jitter").text 131 | except TimeoutException: 132 | self.log.write_to_log("-- could not find test values --") 133 | 134 | def __del__(self): 135 | if self.driver is not None: 136 | self.driver.quit() 137 | 138 | def __exit__(self, exc_type, exc_val, exc_tb): 139 | if self.driver is not None: 140 | self.driver.quit() 141 | 142 | 143 | class Twitter(object): 144 | 145 | def __init__(self): 146 | self.config = Config() 147 | self.log = Log() 148 | auth = tweepy.OAuthHandler(self.config.twitter_consomer_key, 149 | self.config.twitter_consumer_secret) 150 | auth.set_access_token(self.config.twitter_token, 151 | self.config.twitter_token_secret) 152 | 153 | try: 154 | self.api = tweepy.API(auth) 155 | except: 156 | self.log.write_to_log("-- " + self.config.date + " --\n" 157 | "Twitter Auth failed \n" 158 | "-------------------- \n") 159 | 160 | class Output(object): 161 | def __init__(self, config:Config, speedtest:SpeedTest, twitter:Twitter): 162 | self.config = config 163 | self.log = Log() 164 | self.twitter = twitter 165 | self.speedtest = speedtest 166 | self.config_download = self.config.download 167 | self.config_upload = self.config.upload 168 | self.margin = self.config.margin 169 | self.isp = self.config.isp 170 | self.speedtest_download = self.speedtest.download 171 | self.speedtest_upload = self.speedtest.upload 172 | 173 | def test_results(self): 174 | 175 | # If the driver creation failed, these will be empty strings 176 | if self.speedtest_download == "" or self.speedtest_upload == "": 177 | self.log.write_to_log("Speed test values invalid\n") 178 | self.log.write_to_log("Down {} : Up {}\n".format(self.speedtest_download, self.speedtest_upload)) 179 | return 180 | 181 | if (float(self.speedtest_download) < self.config_download * self.margin or 182 | float(self.speedtest_upload) < self.config_upload * self.margin): 183 | 184 | try: 185 | 186 | self.twitter.api.update_status(self.isp + " Hey what gives! I pay for " + 187 | str(self.config_download) + " Mbps download and " + 188 | str(self.config_upload) + " Mbps upload. Why am I only getting " + 189 | self.speedtest_download + " Mbps down and " + 190 | self.speedtest_upload + " Mbps up?") 191 | 192 | self.log.write_to_log("-- " + self.config.date + " --\n" 193 | "- ERROR: Bandwidth not in spec - \n" 194 | "Download: " + self.speedtest_download + " Mbps \n" 195 | "Upload: " + self.speedtest_upload + " Mbps \n" 196 | "Latency: " + self.speedtest.latency + " msec round trip time \n" 197 | "Jitter: " + self.speedtest.jitter + " msec \n" 198 | "-------------------- \n") 199 | 200 | except: 201 | self.log.write_to_log("-- " + self.config.date + " --\n" 202 | "Twitter post / logging failed \n" 203 | "-------------------- \n") 204 | 205 | else: 206 | self.log.write_to_log("-- " + self.config.date + " --\n" 207 | "- Bandwidth in spec - \n" 208 | "Download: " + self.speedtest_download + " Mbps \n" 209 | "Upload: " + self.speedtest_upload + " Mbps \n" 210 | "Latency: " + self.speedtest.latency + " msec round trip time \n" 211 | "Jitter: " + self.speedtest.jitter + "\n" 212 | "-------------------- \n") 213 | 214 | 215 | if __name__ == "__main__": 216 | # Parse the configuration JSON file 217 | config_data = Config() 218 | # Create SpeedTest 219 | speedtest = SpeedTest(config_data) 220 | # Run test 221 | speedtest.run_test() 222 | if speedtest.valid_driver(): 223 | time.sleep(35) 224 | # Store speed test values 225 | speedtest.store_test_values() 226 | # Create Twitter object 227 | twitter = Twitter() 228 | # Create Output 229 | output = Output(config_data, speedtest, twitter) 230 | # Tweet/log results 231 | output.test_results() 232 | --------------------------------------------------------------------------------