Repository: james-atkinson/speedcomplainer
Branch: master
Commit: 0692520772c7
Files: 5
Total size: 10.0 KB
Directory structure:
gitextract_0i8yc2am/
├── .gitignore
├── README.md
├── config.json
├── logger.py
└── speedcomplainer.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
.idea
================================================
FILE: README.md
================================================
# speedcomplainer
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)
## Configuration
Configuration is handled by a basic JSON file. Things that can be configured are:
* twitter
* twitterToken: This is your app access token
* twitterConsumerKey: This is your Consumer Key (API Key)
* twitterTokenSecret: This is your Access Token Secret
* TwitterConsumerSecret: This is your Consumer Secret (API Secret)
* tweetTo: This is a account (or list of accounts) that will be @ mentioned (include the @!)
* internetSpeed: This is the speed (in MB/sec) you're paying for (and presumably not getting).
* tweetThresholds: This is a list of messages that will be tweeted when you hit a threshold of crappiness. Placeholders are:
* {tweetTo} - The above tweetTo configuration.
* {internetSpeed} - The above internetSpeed configuration.
* {downloadResult} - The poor download speed you're getting
Threshold Example (remember to limit your messages to 140 characters or less!):
```
"tweetThresholds": {
"5": [
"Hey {tweetTo} I'm paying for {internetSpeed}Mb/s but getting only {downloadResult} Mb/s?!? Shame.",
"Oi! {tweetTo} $100+/month for {internetSpeed}Mbit/s and I only get {downloadResult} Mbit/s? How does that seem fair?"
],
"12.5": [
"Uhh {tweetTo} for $100+/month I expect better than {downloadResult}Mbit/s when I'm paying for {internetSpeed}Mbit/s. Fix your network!",
"Hey {tweetTo} why am I only getting {downloadResult}Mb/s when I pay for {internetSpeed}Mb/s? $100+/month for this??"
],
"25": [
"Well {tweetTo} I guess {downloadResult}Mb/s is better than nothing, still not worth $100/mnth when I expect {internetSpeed}Mb/s"
]
}
```
Logging can be done to CSV files, with a log file for ping results and speed test results.
CSV Logging config example:
```
"log": {
"type": "csv",
"files": {
"ping": "pingresults.csv",
"speed": "speedrestuls.csv"
}
}
```
## Usage
> python speedcomplainer.py
Or to run in the background:
> python speedcomplainer.py > /dev/null &
================================================
FILE: config.json
================================================
{
"twitter": {
"twitterToken": "",
"twitterConsumerKey": "",
"twitterTokenSecret": "",
"twitterConsumerSecret": ""
},
"tweetTo": "",
"internetSpeed": "50",
"tweetThresholds": {
"5": [
"Hey {tweetTo} I'm paying for {internetSpeed}Mb/s but getting only {downloadResult} Mb/s?!? Shame.",
"Oi! {tweetTo} $100+/month for {internetSpeed}Mbit/s and I only get {downloadResult} Mbit/s? How does that seem fair?"
],
"12.5": [
"Uhh {tweetTo} for $100+/month I expect better than {downloadResult}Mbit/s when I'm paying for {internetSpeed}Mbit/s. Fix your network!",
"Hey {tweetTo} why am I only getting {downloadResult}Mb/s when I pay for {internetSpeed}Mb/s? $100+/month for this??"
],
"25": [
"Well {tweetTo} I guess {downloadResult}Mb/s is better than nothing, still not worth $100/mnth when I expect {internetSpeed}Mb/s"
]
},
"log": {
"type": "csv",
"files": {
"ping": "pingresults.csv",
"speed": "speedrestuls.csv"
}
}
}
================================================
FILE: logger.py
================================================
class Logger(object):
def __init__(self, type, config):
if type == 'csv':
self.logger = CsvLogger(config['filename'])
def log(self, logItems):
self.logger.log(logItems)
class CsvLogger(object):
def __init__(self, filename):
self.filename = filename
def log(self, logItems):
with open(self.filename, "a") as logfile:
logfile.write("%s\n" % ','.join(logItems))
================================================
FILE: speedcomplainer.py
================================================
import os
import sys
import time
from datetime import datetime
import daemon
import signal
import threading
import twitter
import json
import random
from logger import Logger
shutdownFlag = False
def main(filename, argv):
print "======================================"
print " Starting Speed Complainer! "
print " Lets get noisy! "
print "======================================"
global shutdownFlag
signal.signal(signal.SIGINT, shutdownHandler)
monitor = Monitor()
while not shutdownFlag:
try:
monitor.run()
for i in range(0, 5):
if shutdownFlag:
break
time.sleep(1)
except Exception as e:
print 'Error: %s' % e
sys.exit(1)
sys.exit()
def shutdownHandler(signo, stack_frame):
global shutdownFlag
print 'Got shutdown signal (%s: %s).' % (signo, stack_frame)
shutdownFlag = True
class Monitor():
def __init__(self):
self.lastPingCheck = None
self.lastSpeedTest = None
def run(self):
if not self.lastPingCheck or (datetime.now() - self.lastPingCheck).total_seconds() >= 60:
self.runPingTest()
self.lastPingCheck = datetime.now()
if not self.lastSpeedTest or (datetime.now() - self.lastSpeedTest).total_seconds() >= 3600:
self.runSpeedTest()
self.lastSpeedTest = datetime.now()
def runPingTest(self):
pingThread = PingTest()
pingThread.start()
def runSpeedTest(self):
speedThread = SpeedTest()
speedThread.start()
class PingTest(threading.Thread):
def __init__(self, numPings=3, pingTimeout=2, maxWaitTime=6):
super(PingTest, self).__init__()
self.numPings = numPings
self.pingTimeout = pingTimeout
self.maxWaitTime = maxWaitTime
self.config = json.load(open('./config.json'))
self.logger = Logger(self.config['log']['type'], { 'filename': self.config['log']['files']['ping'] })
def run(self):
pingResults = self.doPingTest()
self.logPingResults(pingResults)
def doPingTest(self):
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))
success = 0
if response == 0:
success = 1
return { 'date': datetime.now(), 'success': success }
def logPingResults(self, pingResults):
self.logger.log([ pingResults['date'].strftime('%Y-%m-%d %H:%M:%S'), str(pingResults['success'])])
class SpeedTest(threading.Thread):
def __init__(self):
super(SpeedTest, self).__init__()
self.config = json.load(open('./config.json'))
self.logger = Logger(self.config['log']['type'], { 'filename': self.config['log']['files']['speed'] })
def run(self):
speedTestResults = self.doSpeedTest()
self.logSpeedTestResults(speedTestResults)
self.tweetResults(speedTestResults)
def doSpeedTest(self):
# run a speed test
result = os.popen("/usr/local/bin/speedtest-cli --simple").read()
if 'Cannot' in result:
return { 'date': datetime.now(), 'uploadResult': 0, 'downloadResult': 0, 'ping': 0 }
# Result:
# Ping: 529.084 ms
# Download: 0.52 Mbit/s
# Upload: 1.79 Mbit/s
resultSet = result.split('\n')
pingResult = resultSet[0]
downloadResult = resultSet[1]
uploadResult = resultSet[2]
pingResult = float(pingResult.replace('Ping: ', '').replace(' ms', ''))
downloadResult = float(downloadResult.replace('Download: ', '').replace(' Mbit/s', ''))
uploadResult = float(uploadResult.replace('Upload: ', '').replace(' Mbit/s', ''))
return { 'date': datetime.now(), 'uploadResult': uploadResult, 'downloadResult': downloadResult, 'ping': pingResult }
def logSpeedTestResults(self, speedTestResults):
self.logger.log([ speedTestResults['date'].strftime('%Y-%m-%d %H:%M:%S'), str(speedTestResults['uploadResult']), str(speedTestResults['downloadResult']), str(speedTestResults['ping']) ])
def tweetResults(self, speedTestResults):
thresholdMessages = self.config['tweetThresholds']
message = None
for (threshold, messages) in thresholdMessages.items():
threshold = float(threshold)
if speedTestResults['downloadResult'] < threshold:
message = messages[random.randint(0, len(messages) - 1)].replace('{tweetTo}', self.config['tweetTo']).replace('{internetSpeed}', self.config['internetSpeed']).replace('{downloadResult}', str(speedTestResults['downloadResult']))
if message:
api = twitter.Api(consumer_key=self.config['twitter']['twitterConsumerKey'],
consumer_secret=self.config['twitter']['twitterConsumerSecret'],
access_token_key=self.config['twitter']['twitterToken'],
access_token_secret=self.config['twitter']['twitterTokenSecret'])
if api:
status = api.PostUpdate(message)
class DaemonApp():
def __init__(self, pidFilePath, stdout_path='/dev/null', stderr_path='/dev/null'):
self.stdin_path = '/dev/null'
self.stdout_path = stdout_path
self.stderr_path = stderr_path
self.pidfile_path = pidFilePath
self.pidfile_timeout = 1
def run(self):
main(__file__, sys.argv[1:])
if __name__ == '__main__':
main(__file__, sys.argv[1:])
workingDirectory = os.path.basename(os.path.realpath(__file__))
stdout_path = '/dev/null'
stderr_path = '/dev/null'
fileName, fileExt = os.path.split(os.path.realpath(__file__))
pidFilePath = os.path.join(workingDirectory, os.path.basename(fileName) + '.pid')
from daemon import runner
dRunner = runner.DaemonRunner(DaemonApp(pidFilePath, stdout_path, stderr_path))
dRunner.daemon_context.working_directory = workingDirectory
dRunner.daemon_context.umask = 0o002
dRunner.daemon_context.signal_map = { signal.SIGTERM: 'terminate', signal.SIGUP: 'terminate' }
dRunner.do_action()
gitextract_0i8yc2am/ ├── .gitignore ├── README.md ├── config.json ├── logger.py └── speedcomplainer.py
SYMBOL INDEX (27 symbols across 2 files)
FILE: logger.py
class Logger (line 2) | class Logger(object):
method __init__ (line 3) | def __init__(self, type, config):
method log (line 7) | def log(self, logItems):
class CsvLogger (line 10) | class CsvLogger(object):
method __init__ (line 11) | def __init__(self, filename):
method log (line 14) | def log(self, logItems):
FILE: speedcomplainer.py
function main (line 15) | def main(filename, argv):
function shutdownHandler (line 42) | def shutdownHandler(signo, stack_frame):
class Monitor (line 47) | class Monitor():
method __init__ (line 48) | def __init__(self):
method run (line 52) | def run(self):
method runPingTest (line 61) | def runPingTest(self):
method runSpeedTest (line 65) | def runSpeedTest(self):
class PingTest (line 69) | class PingTest(threading.Thread):
method __init__ (line 70) | def __init__(self, numPings=3, pingTimeout=2, maxWaitTime=6):
method run (line 78) | def run(self):
method doPingTest (line 82) | def doPingTest(self):
method logPingResults (line 89) | def logPingResults(self, pingResults):
class SpeedTest (line 92) | class SpeedTest(threading.Thread):
method __init__ (line 93) | def __init__(self):
method run (line 98) | def run(self):
method doSpeedTest (line 103) | def doSpeedTest(self):
method logSpeedTestResults (line 125) | def logSpeedTestResults(self, speedTestResults):
method tweetResults (line 129) | def tweetResults(self, speedTestResults):
class DaemonApp (line 145) | class DaemonApp():
method __init__ (line 146) | def __init__(self, pidFilePath, stdout_path='/dev/null', stderr_path='...
method run (line 153) | def run(self):
Condensed preview — 5 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (11K chars).
[
{
"path": ".gitignore",
"chars": 6,
"preview": ".idea\n"
},
{
"path": "README.md",
"chars": 2208,
"preview": "# speedcomplainer\nA python app that will test your internet connection and then complain to your service provider (and l"
},
{
"path": "config.json",
"chars": 1135,
"preview": "{\n \"twitter\": {\n \"twitterToken\": \"\",\n \"twitterConsumerKey\": \"\",\n \"twitterTokenSecret\": \"\",\n "
},
{
"path": "logger.py",
"chars": 434,
"preview": "\nclass Logger(object):\n def __init__(self, type, config):\n if type == 'csv':\n self.logger = CsvLogg"
},
{
"path": "speedcomplainer.py",
"chars": 6411,
"preview": "import os\r\nimport sys\r\nimport time\r\nfrom datetime import datetime\r\nimport daemon\r\nimport signal\r\nimport threading\r\nimpor"
}
]
About this extraction
This page contains the full source code of the james-atkinson/speedcomplainer GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 5 files (10.0 KB), approximately 2.5k tokens, and a symbol index with 27 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.