[
  {
    "path": ".gitignore",
    "content": ".idea\n"
  },
  {
    "path": "README.md",
    "content": "# speedcomplainer\nA 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)\n\n## Configuration\nConfiguration is handled by a basic JSON file. Things that can be configured are:\n* twitter\n * twitterToken: This is your app access token\n * twitterConsumerKey: This is your Consumer Key (API Key)\n * twitterTokenSecret: This is your Access Token Secret\n * TwitterConsumerSecret: This is your Consumer Secret (API Secret)\n* tweetTo: This is a account (or list of accounts) that will be @ mentioned (include the @!)\n* internetSpeed: This is the speed (in MB/sec) you're paying for (and presumably not getting).\n* tweetThresholds: This is a list of messages that will be tweeted when you hit a threshold of crappiness. Placeholders are:\n * {tweetTo} - The above tweetTo configuration.\n * {internetSpeed} - The above internetSpeed configuration.\n * {downloadResult} - The poor download speed you're getting\n\nThreshold Example (remember to limit your messages to 140 characters or less!):\n```\n    \"tweetThresholds\": {\n        \"5\": [\n            \"Hey {tweetTo} I'm paying for {internetSpeed}Mb/s but getting only {downloadResult} Mb/s?!? Shame.\",\n            \"Oi! {tweetTo} $100+/month for {internetSpeed}Mbit/s and I only get {downloadResult} Mbit/s? How does that seem fair?\"\n        ],\n        \"12.5\": [\n            \"Uhh {tweetTo} for $100+/month I expect better than {downloadResult}Mbit/s when I'm paying for {internetSpeed}Mbit/s. Fix your network!\",\n            \"Hey {tweetTo} why am I only getting {downloadResult}Mb/s when I pay for {internetSpeed}Mb/s? $100+/month for this??\"\n        ],\n        \"25\": [\n            \"Well {tweetTo} I guess {downloadResult}Mb/s is better than nothing, still not worth $100/mnth when I expect {internetSpeed}Mb/s\"\n        ]\n    }\n```\n\nLogging can be done to CSV files, with a log file for ping results and speed test results. \n\nCSV Logging config example:\n```\n\"log\": {\n    \"type\": \"csv\",\n    \"files\": {\n        \"ping\": \"pingresults.csv\",\n        \"speed\": \"speedrestuls.csv\"\n    }\n}\n```\n\n## Usage\n> python speedcomplainer.py\n\nOr to run in the background:\n\n> python speedcomplainer.py > /dev/null &\n\n\n"
  },
  {
    "path": "config.json",
    "content": "{\n    \"twitter\": {\n        \"twitterToken\": \"\",\n        \"twitterConsumerKey\": \"\",\n        \"twitterTokenSecret\": \"\",\n        \"twitterConsumerSecret\": \"\"\n    },\n    \"tweetTo\": \"\",\n    \"internetSpeed\": \"50\",\n    \"tweetThresholds\": {\n        \"5\": [\n            \"Hey {tweetTo} I'm paying for {internetSpeed}Mb/s but getting only {downloadResult} Mb/s?!? Shame.\",\n            \"Oi! {tweetTo} $100+/month for {internetSpeed}Mbit/s and I only get {downloadResult} Mbit/s? How does that seem fair?\"\n        ],\n        \"12.5\": [\n            \"Uhh {tweetTo} for $100+/month I expect better than {downloadResult}Mbit/s when I'm paying for {internetSpeed}Mbit/s. Fix your network!\",\n            \"Hey {tweetTo} why am I only getting {downloadResult}Mb/s when I pay for {internetSpeed}Mb/s? $100+/month for this??\"\n        ],\n        \"25\": [\n            \"Well {tweetTo} I guess {downloadResult}Mb/s is better than nothing, still not worth $100/mnth when I expect {internetSpeed}Mb/s\"\n        ]\n    },\n    \"log\": {\n        \"type\": \"csv\",\n        \"files\": {\n            \"ping\": \"pingresults.csv\",\n            \"speed\": \"speedrestuls.csv\"\n        }\n    }\n}\n"
  },
  {
    "path": "logger.py",
    "content": "\nclass Logger(object):\n    def __init__(self, type, config):\n        if type == 'csv':\n            self.logger = CsvLogger(config['filename'])\n\n    def log(self, logItems):\n        self.logger.log(logItems)\n\nclass CsvLogger(object):\n    def __init__(self, filename):\n        self.filename = filename\n\n    def log(self, logItems):\n        with open(self.filename, \"a\") as logfile:\n            logfile.write(\"%s\\n\" % ','.join(logItems))"
  },
  {
    "path": "speedcomplainer.py",
    "content": "import os\r\nimport sys\r\nimport time\r\nfrom datetime import datetime\r\nimport daemon\r\nimport signal\r\nimport threading\r\nimport twitter\r\nimport json \r\nimport random\r\nfrom logger import Logger\r\n\r\nshutdownFlag = False\r\n\r\ndef main(filename, argv):\r\n    print \"======================================\"\r\n    print \" Starting Speed Complainer!           \"\r\n    print \" Lets get noisy!                      \"\r\n    print \"======================================\"\r\n\r\n    global shutdownFlag\r\n    signal.signal(signal.SIGINT, shutdownHandler)\r\n\r\n    monitor = Monitor()\r\n\r\n    while not shutdownFlag:\r\n        try:\r\n\r\n            monitor.run()\r\n\r\n            for i in range(0, 5):\r\n                if shutdownFlag:\r\n                    break\r\n                time.sleep(1)\r\n\r\n        except Exception as e:\r\n            print 'Error: %s' % e\r\n            sys.exit(1)\r\n\r\n    sys.exit()\r\n\r\ndef shutdownHandler(signo, stack_frame):\r\n    global shutdownFlag\r\n    print 'Got shutdown signal (%s: %s).' % (signo, stack_frame)\r\n    shutdownFlag = True\r\n\r\nclass Monitor():\r\n    def __init__(self):\r\n        self.lastPingCheck = None\r\n        self.lastSpeedTest = None\r\n\r\n    def run(self):\r\n        if not self.lastPingCheck or (datetime.now() - self.lastPingCheck).total_seconds() >= 60:\r\n            self.runPingTest()\r\n            self.lastPingCheck = datetime.now()\r\n\r\n        if not self.lastSpeedTest or (datetime.now() - self.lastSpeedTest).total_seconds() >= 3600:\r\n            self.runSpeedTest()\r\n            self.lastSpeedTest = datetime.now()\r\n\r\n    def runPingTest(self):\r\n        pingThread = PingTest()\r\n        pingThread.start()\r\n\r\n    def runSpeedTest(self):\r\n        speedThread = SpeedTest()\r\n        speedThread.start()\r\n\r\nclass PingTest(threading.Thread):\r\n    def __init__(self, numPings=3, pingTimeout=2, maxWaitTime=6):\r\n        super(PingTest, self).__init__()\r\n        self.numPings = numPings\r\n        self.pingTimeout = pingTimeout\r\n        self.maxWaitTime = maxWaitTime\r\n        self.config = json.load(open('./config.json'))\r\n        self.logger = Logger(self.config['log']['type'], { 'filename': self.config['log']['files']['ping'] })\r\n\r\n    def run(self):\r\n        pingResults = self.doPingTest()\r\n        self.logPingResults(pingResults)\r\n\r\n    def doPingTest(self):\r\n        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))\r\n        success = 0\r\n        if response == 0:\r\n            success = 1\r\n        return { 'date': datetime.now(), 'success': success }\r\n\r\n    def logPingResults(self, pingResults):\r\n        self.logger.log([ pingResults['date'].strftime('%Y-%m-%d %H:%M:%S'), str(pingResults['success'])])\r\n\r\nclass SpeedTest(threading.Thread):\r\n    def __init__(self):\r\n        super(SpeedTest, self).__init__()\r\n        self.config = json.load(open('./config.json'))\r\n        self.logger = Logger(self.config['log']['type'], { 'filename': self.config['log']['files']['speed'] })\r\n\r\n    def run(self):\r\n        speedTestResults = self.doSpeedTest()\r\n        self.logSpeedTestResults(speedTestResults)\r\n        self.tweetResults(speedTestResults)\r\n\r\n    def doSpeedTest(self):\r\n        # run a speed test\r\n        result = os.popen(\"/usr/local/bin/speedtest-cli --simple\").read()\r\n        if 'Cannot' in result:\r\n            return { 'date': datetime.now(), 'uploadResult': 0, 'downloadResult': 0, 'ping': 0 }\r\n\r\n        # Result:\r\n        # Ping: 529.084 ms\r\n        # Download: 0.52 Mbit/s\r\n        # Upload: 1.79 Mbit/s\r\n\r\n        resultSet = result.split('\\n')\r\n        pingResult = resultSet[0]\r\n        downloadResult = resultSet[1]\r\n        uploadResult = resultSet[2]\r\n\r\n        pingResult = float(pingResult.replace('Ping: ', '').replace(' ms', ''))\r\n        downloadResult = float(downloadResult.replace('Download: ', '').replace(' Mbit/s', ''))\r\n        uploadResult = float(uploadResult.replace('Upload: ', '').replace(' Mbit/s', ''))\r\n\r\n        return { 'date': datetime.now(), 'uploadResult': uploadResult, 'downloadResult': downloadResult, 'ping': pingResult }\r\n\r\n    def logSpeedTestResults(self, speedTestResults):\r\n        self.logger.log([ speedTestResults['date'].strftime('%Y-%m-%d %H:%M:%S'), str(speedTestResults['uploadResult']), str(speedTestResults['downloadResult']), str(speedTestResults['ping']) ])\r\n\r\n\r\n    def tweetResults(self, speedTestResults):\r\n        thresholdMessages = self.config['tweetThresholds']\r\n        message = None\r\n        for (threshold, messages) in thresholdMessages.items():\r\n            threshold = float(threshold)\r\n            if speedTestResults['downloadResult'] < threshold:\r\n                message = messages[random.randint(0, len(messages) - 1)].replace('{tweetTo}', self.config['tweetTo']).replace('{internetSpeed}', self.config['internetSpeed']).replace('{downloadResult}', str(speedTestResults['downloadResult']))\r\n\r\n        if message:\r\n            api = twitter.Api(consumer_key=self.config['twitter']['twitterConsumerKey'],\r\n                            consumer_secret=self.config['twitter']['twitterConsumerSecret'],\r\n                            access_token_key=self.config['twitter']['twitterToken'],\r\n                            access_token_secret=self.config['twitter']['twitterTokenSecret'])\r\n            if api:\r\n                status = api.PostUpdate(message)\r\n\r\nclass DaemonApp():\r\n    def __init__(self, pidFilePath, stdout_path='/dev/null', stderr_path='/dev/null'):\r\n        self.stdin_path = '/dev/null'\r\n        self.stdout_path = stdout_path\r\n        self.stderr_path = stderr_path\r\n        self.pidfile_path = pidFilePath\r\n        self.pidfile_timeout = 1\r\n\r\n    def run(self):\r\n        main(__file__, sys.argv[1:])\r\n\r\nif __name__ == '__main__':\r\n    main(__file__, sys.argv[1:])\r\n\r\n    workingDirectory = os.path.basename(os.path.realpath(__file__))\r\n    stdout_path = '/dev/null'\r\n    stderr_path = '/dev/null'\r\n    fileName, fileExt = os.path.split(os.path.realpath(__file__))\r\n    pidFilePath = os.path.join(workingDirectory, os.path.basename(fileName) + '.pid')\r\n    from daemon import runner\r\n    dRunner = runner.DaemonRunner(DaemonApp(pidFilePath, stdout_path, stderr_path))\r\n    dRunner.daemon_context.working_directory = workingDirectory\r\n    dRunner.daemon_context.umask = 0o002\r\n    dRunner.daemon_context.signal_map = { signal.SIGTERM: 'terminate', signal.SIGUP: 'terminate' }\r\n    dRunner.do_action()\r\n\r\n\r\n\r\n"
  }
]