├── .gitignore ├── README.md ├── config.json ├── logger.py └── speedcomplainer.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # speedcomplainer 2 | A python app that will test your internet connection and then complain to your service provider (and log to a data store if you'd like) 3 | 4 | ## Configuration 5 | Configuration is handled by a basic JSON file. Things that can be configured are: 6 | * twitter 7 | * twitterToken: This is your app access token 8 | * twitterConsumerKey: This is your Consumer Key (API Key) 9 | * twitterTokenSecret: This is your Access Token Secret 10 | * TwitterConsumerSecret: This is your Consumer Secret (API Secret) 11 | * tweetTo: This is a account (or list of accounts) that will be @ mentioned (include the @!) 12 | * internetSpeed: This is the speed (in MB/sec) you're paying for (and presumably not getting). 13 | * tweetThresholds: This is a list of messages that will be tweeted when you hit a threshold of crappiness. Placeholders are: 14 | * {tweetTo} - The above tweetTo configuration. 15 | * {internetSpeed} - The above internetSpeed configuration. 16 | * {downloadResult} - The poor download speed you're getting 17 | 18 | Threshold Example (remember to limit your messages to 140 characters or less!): 19 | ``` 20 | "tweetThresholds": { 21 | "5": [ 22 | "Hey {tweetTo} I'm paying for {internetSpeed}Mb/s but getting only {downloadResult} Mb/s?!? Shame.", 23 | "Oi! {tweetTo} $100+/month for {internetSpeed}Mbit/s and I only get {downloadResult} Mbit/s? How does that seem fair?" 24 | ], 25 | "12.5": [ 26 | "Uhh {tweetTo} for $100+/month I expect better than {downloadResult}Mbit/s when I'm paying for {internetSpeed}Mbit/s. Fix your network!", 27 | "Hey {tweetTo} why am I only getting {downloadResult}Mb/s when I pay for {internetSpeed}Mb/s? $100+/month for this??" 28 | ], 29 | "25": [ 30 | "Well {tweetTo} I guess {downloadResult}Mb/s is better than nothing, still not worth $100/mnth when I expect {internetSpeed}Mb/s" 31 | ] 32 | } 33 | ``` 34 | 35 | Logging can be done to CSV files, with a log file for ping results and speed test results. 36 | 37 | CSV Logging config example: 38 | ``` 39 | "log": { 40 | "type": "csv", 41 | "files": { 42 | "ping": "pingresults.csv", 43 | "speed": "speedrestuls.csv" 44 | } 45 | } 46 | ``` 47 | 48 | ## Usage 49 | > python speedcomplainer.py 50 | 51 | Or to run in the background: 52 | 53 | > python speedcomplainer.py > /dev/null & 54 | 55 | 56 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "twitter": { 3 | "twitterToken": "", 4 | "twitterConsumerKey": "", 5 | "twitterTokenSecret": "", 6 | "twitterConsumerSecret": "" 7 | }, 8 | "tweetTo": "", 9 | "internetSpeed": "50", 10 | "tweetThresholds": { 11 | "5": [ 12 | "Hey {tweetTo} I'm paying for {internetSpeed}Mb/s but getting only {downloadResult} Mb/s?!? Shame.", 13 | "Oi! {tweetTo} $100+/month for {internetSpeed}Mbit/s and I only get {downloadResult} Mbit/s? How does that seem fair?" 14 | ], 15 | "12.5": [ 16 | "Uhh {tweetTo} for $100+/month I expect better than {downloadResult}Mbit/s when I'm paying for {internetSpeed}Mbit/s. Fix your network!", 17 | "Hey {tweetTo} why am I only getting {downloadResult}Mb/s when I pay for {internetSpeed}Mb/s? $100+/month for this??" 18 | ], 19 | "25": [ 20 | "Well {tweetTo} I guess {downloadResult}Mb/s is better than nothing, still not worth $100/mnth when I expect {internetSpeed}Mb/s" 21 | ] 22 | }, 23 | "log": { 24 | "type": "csv", 25 | "files": { 26 | "ping": "pingresults.csv", 27 | "speed": "speedrestuls.csv" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /logger.py: -------------------------------------------------------------------------------- 1 | 2 | class Logger(object): 3 | def __init__(self, type, config): 4 | if type == 'csv': 5 | self.logger = CsvLogger(config['filename']) 6 | 7 | def log(self, logItems): 8 | self.logger.log(logItems) 9 | 10 | class CsvLogger(object): 11 | def __init__(self, filename): 12 | self.filename = filename 13 | 14 | def log(self, logItems): 15 | with open(self.filename, "a") as logfile: 16 | logfile.write("%s\n" % ','.join(logItems)) -------------------------------------------------------------------------------- /speedcomplainer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | from datetime import datetime 5 | import daemon 6 | import signal 7 | import threading 8 | import twitter 9 | import json 10 | import random 11 | from logger import Logger 12 | 13 | shutdownFlag = False 14 | 15 | def main(filename, argv): 16 | print "======================================" 17 | print " Starting Speed Complainer! " 18 | print " Lets get noisy! " 19 | print "======================================" 20 | 21 | global shutdownFlag 22 | signal.signal(signal.SIGINT, shutdownHandler) 23 | 24 | monitor = Monitor() 25 | 26 | while not shutdownFlag: 27 | try: 28 | 29 | monitor.run() 30 | 31 | for i in range(0, 5): 32 | if shutdownFlag: 33 | break 34 | time.sleep(1) 35 | 36 | except Exception as e: 37 | print 'Error: %s' % e 38 | sys.exit(1) 39 | 40 | sys.exit() 41 | 42 | def shutdownHandler(signo, stack_frame): 43 | global shutdownFlag 44 | print 'Got shutdown signal (%s: %s).' % (signo, stack_frame) 45 | shutdownFlag = True 46 | 47 | class Monitor(): 48 | def __init__(self): 49 | self.lastPingCheck = None 50 | self.lastSpeedTest = None 51 | 52 | def run(self): 53 | if not self.lastPingCheck or (datetime.now() - self.lastPingCheck).total_seconds() >= 60: 54 | self.runPingTest() 55 | self.lastPingCheck = datetime.now() 56 | 57 | if not self.lastSpeedTest or (datetime.now() - self.lastSpeedTest).total_seconds() >= 3600: 58 | self.runSpeedTest() 59 | self.lastSpeedTest = datetime.now() 60 | 61 | def runPingTest(self): 62 | pingThread = PingTest() 63 | pingThread.start() 64 | 65 | def runSpeedTest(self): 66 | speedThread = SpeedTest() 67 | speedThread.start() 68 | 69 | class PingTest(threading.Thread): 70 | def __init__(self, numPings=3, pingTimeout=2, maxWaitTime=6): 71 | super(PingTest, self).__init__() 72 | self.numPings = numPings 73 | self.pingTimeout = pingTimeout 74 | self.maxWaitTime = maxWaitTime 75 | self.config = json.load(open('./config.json')) 76 | self.logger = Logger(self.config['log']['type'], { 'filename': self.config['log']['files']['ping'] }) 77 | 78 | def run(self): 79 | pingResults = self.doPingTest() 80 | self.logPingResults(pingResults) 81 | 82 | def doPingTest(self): 83 | response = os.system("ping -c %s -W %s -w %s 8.8.8.8 > /dev/null 2>&1" % (self.numPings, (self.pingTimeout * 1000), self.maxWaitTime)) 84 | success = 0 85 | if response == 0: 86 | success = 1 87 | return { 'date': datetime.now(), 'success': success } 88 | 89 | def logPingResults(self, pingResults): 90 | self.logger.log([ pingResults['date'].strftime('%Y-%m-%d %H:%M:%S'), str(pingResults['success'])]) 91 | 92 | class SpeedTest(threading.Thread): 93 | def __init__(self): 94 | super(SpeedTest, self).__init__() 95 | self.config = json.load(open('./config.json')) 96 | self.logger = Logger(self.config['log']['type'], { 'filename': self.config['log']['files']['speed'] }) 97 | 98 | def run(self): 99 | speedTestResults = self.doSpeedTest() 100 | self.logSpeedTestResults(speedTestResults) 101 | self.tweetResults(speedTestResults) 102 | 103 | def doSpeedTest(self): 104 | # run a speed test 105 | result = os.popen("/usr/local/bin/speedtest-cli --simple").read() 106 | if 'Cannot' in result: 107 | return { 'date': datetime.now(), 'uploadResult': 0, 'downloadResult': 0, 'ping': 0 } 108 | 109 | # Result: 110 | # Ping: 529.084 ms 111 | # Download: 0.52 Mbit/s 112 | # Upload: 1.79 Mbit/s 113 | 114 | resultSet = result.split('\n') 115 | pingResult = resultSet[0] 116 | downloadResult = resultSet[1] 117 | uploadResult = resultSet[2] 118 | 119 | pingResult = float(pingResult.replace('Ping: ', '').replace(' ms', '')) 120 | downloadResult = float(downloadResult.replace('Download: ', '').replace(' Mbit/s', '')) 121 | uploadResult = float(uploadResult.replace('Upload: ', '').replace(' Mbit/s', '')) 122 | 123 | return { 'date': datetime.now(), 'uploadResult': uploadResult, 'downloadResult': downloadResult, 'ping': pingResult } 124 | 125 | def logSpeedTestResults(self, speedTestResults): 126 | self.logger.log([ speedTestResults['date'].strftime('%Y-%m-%d %H:%M:%S'), str(speedTestResults['uploadResult']), str(speedTestResults['downloadResult']), str(speedTestResults['ping']) ]) 127 | 128 | 129 | def tweetResults(self, speedTestResults): 130 | thresholdMessages = self.config['tweetThresholds'] 131 | message = None 132 | for (threshold, messages) in thresholdMessages.items(): 133 | threshold = float(threshold) 134 | if speedTestResults['downloadResult'] < threshold: 135 | message = messages[random.randint(0, len(messages) - 1)].replace('{tweetTo}', self.config['tweetTo']).replace('{internetSpeed}', self.config['internetSpeed']).replace('{downloadResult}', str(speedTestResults['downloadResult'])) 136 | 137 | if message: 138 | api = twitter.Api(consumer_key=self.config['twitter']['twitterConsumerKey'], 139 | consumer_secret=self.config['twitter']['twitterConsumerSecret'], 140 | access_token_key=self.config['twitter']['twitterToken'], 141 | access_token_secret=self.config['twitter']['twitterTokenSecret']) 142 | if api: 143 | status = api.PostUpdate(message) 144 | 145 | class DaemonApp(): 146 | def __init__(self, pidFilePath, stdout_path='/dev/null', stderr_path='/dev/null'): 147 | self.stdin_path = '/dev/null' 148 | self.stdout_path = stdout_path 149 | self.stderr_path = stderr_path 150 | self.pidfile_path = pidFilePath 151 | self.pidfile_timeout = 1 152 | 153 | def run(self): 154 | main(__file__, sys.argv[1:]) 155 | 156 | if __name__ == '__main__': 157 | main(__file__, sys.argv[1:]) 158 | 159 | workingDirectory = os.path.basename(os.path.realpath(__file__)) 160 | stdout_path = '/dev/null' 161 | stderr_path = '/dev/null' 162 | fileName, fileExt = os.path.split(os.path.realpath(__file__)) 163 | pidFilePath = os.path.join(workingDirectory, os.path.basename(fileName) + '.pid') 164 | from daemon import runner 165 | dRunner = runner.DaemonRunner(DaemonApp(pidFilePath, stdout_path, stderr_path)) 166 | dRunner.daemon_context.working_directory = workingDirectory 167 | dRunner.daemon_context.umask = 0o002 168 | dRunner.daemon_context.signal_map = { signal.SIGTERM: 'terminate', signal.SIGUP: 'terminate' } 169 | dRunner.do_action() 170 | 171 | 172 | 173 | --------------------------------------------------------------------------------