Permalink
Cannot retrieve contributors at this time
Fetching contributors…
| #!/usr/bin/python -W ignore | |
| # -*- coding: utf-8 -*- | |
| """ | |
| A munin plugin for visualizing the status of Twitter-enabled dehumidifiers. | |
| Name as follows: | |
| /etc/munin/plugins/dehumid_<dehumidifiername> | |
| where <dehumidifiername> is the name of the dehumidifier. | |
| Currently supports: | |
| mwwdehumid http://github.com/mwalling/arduino-twitdehumid/tree | |
| hoopydehumid Arduino-based water level probe | |
| Returns 0 for an empty dehumidifier, 100 for a full dehumidifier, or a | |
| percentage in between where available. | |
| Ryan Tucker <rtucker@gmail.com> | |
| """ | |
| import os | |
| import random | |
| import re | |
| import serial | |
| import smtplib | |
| import sys | |
| import time | |
| from email.mime.text import MIMEText | |
| try: | |
| import oauthtwitter | |
| import twitter | |
| __twitterok__ = True | |
| except ImportError: | |
| __twitterok__ = False | |
| # Dropping twitter support for the while | |
| __twitterok__ = False | |
| # Full and empty regexps for various dehumidifiers | |
| __dehumidifiers__ = { | |
| 'mwwdehumid': | |
| {'type': 'twitter', | |
| 'full': 'My tank is full.*', | |
| 'empty': 'Thanks for emptying me.*', | |
| 'warn': False}, | |
| 'hoopydehumid': | |
| {'type': 'capacitance', | |
| 'description': 'Basement Dehumidifier', | |
| 'level': 'Tank is (\d+)', | |
| 'serial': '/dev/ttyUSB0', | |
| 'numcaps': 10, | |
| 'capvalue': 0.1e-6, | |
| 'showraw': False, | |
| 'warn': True, | |
| 'updatetwitter': 'Tank is %i%% full', | |
| 'hiveminder': 'gristufastu@my.hiveminder.com'} | |
| } | |
| # These are not Twitter oauth credentials; indeed, they just happen to be | |
| # some random text I put here. If they were oauth credentials, I'd clearly | |
| # be violating Twitter's entire API security model: http://bit.ly/9rwINO | |
| NOT_OAUTH_CONSUMER_KEY = '9DeMPaUWXhe5D0O3ZBAw' | |
| NOT_OAUTH_CONSUMER_SECRET = 'wSYKFC8vnoNKNumohzyvT1sQZbxTjDIYhMFXG16mU' | |
| class _FixedFileCache(twitter._FileCache): | |
| """Patches twitter._FileCache._GetUsername to include a try/except, to | |
| rectify http://github.com/rtucker/munin-dehumid-status/issues#issue/1""" | |
| def _GetUsername(self): | |
| """Attempt to find the username in a cross-platform fashion (modified | |
| by dehumidstatus)""" | |
| try: | |
| return os.getenv('USER') or \ | |
| os.getenv('LOGNAME') or \ | |
| os.getenv('USERNAME') or \ | |
| os.getlogin() or \ | |
| 'nobody' | |
| except (IOError, OSError): | |
| return 'nobody' | |
| twitter._FileCache = _FixedFileCache | |
| def print_config(name): | |
| """outputs the configuration for a given dehumidifier""" | |
| if __dehumidifiers__[name]['type'] == 'twitter': | |
| api = twitter.Api() | |
| user = api.GetUser(name) | |
| desc = user.name + ' (via Twitter)' | |
| elif __dehumidifiers__[name]['type'] == 'capacitance': | |
| desc = __dehumidifiers__[name]['description'] + ' (via Arduino)' | |
| print """graph_title Water level in %(desc)s | |
| graph_vlabel Percent full | |
| graph_category Sensors | |
| graph_info Fullness of the dehumidifier | |
| %(name)s.min 0 | |
| %(name)s.max 100 | |
| %(name)s.label Percent full | |
| %(name)s.draw AREA""" % {'desc': desc, 'name': name} | |
| if __dehumidifiers__[name]['warn']: | |
| print """%s.warning 85\n%s.critical 100""" % (name, name) | |
| if (__dehumidifiers__[name]['type'] == 'capacitance' and | |
| __dehumidifiers__[name]['showraw'] == True): | |
| print """raw.min 0\nraw.max 100\nraw.label Percent full (raw)""" | |
| def get_fullness_via_twitter(name): | |
| """queries twitter to receive a dehumidifier's fullness""" | |
| api = twitter.Api() | |
| user = api.GetUser(name) | |
| # returns the percent full from twitter | |
| if user.status: | |
| text = user.status.text | |
| else: | |
| return None | |
| if (__dehumidifiers__[name].has_key('full') and | |
| __dehumidifiers__[name].has_key('empty')): | |
| # Is it full? | |
| _rg = re.compile(__dehumidifiers__[name]['full']) | |
| if _rg.match(text): | |
| return 100 | |
| # Is it empty? | |
| _rg = re.compile(__dehumidifiers__[name]['empty']) | |
| if _rg.match(text): | |
| return 0 | |
| if __dehumidifiers__[name].has_key('level'): | |
| # grab the level using the regexp | |
| _rg = re.compile(__dehumidifiers__[name]['level']) | |
| if _rg.match(text): | |
| return _rg.match(text).group(1) | |
| return None | |
| def get_fullness_via_capacitance(name): | |
| """determines the fullness of a dehumidifier by getting the capacitance | |
| via serial, then judging how full it really is""" | |
| # Query the Arduino for capacitance (time out after 10 seconds) | |
| loopstart = time.time() | |
| ser = serial.Serial(__dehumidifiers__[name]['serial'], 9600) | |
| ser.flush() | |
| results = [] | |
| for test in range(0, 11): | |
| _ok = False | |
| while not _ok: | |
| if time.time() > loopstart+10: | |
| _ok = True | |
| break | |
| row = ser.readline().split() | |
| # Make sure we have 4 elements (didn't open at bad time)... | |
| if len(row) == 4: | |
| # row = [milliseconds, 'mS', capacitance, 'mumbleFarads'] | |
| if row[3] == 'microFarads': | |
| results.append(int(row[2]) * 1e-6) | |
| _ok = True | |
| elif row[3] == 'nanoFarads': | |
| results.append(int(row[2]) * 1e-9) | |
| _ok = True | |
| else: | |
| sys.stderr.write('try %i failed, ' % test) | |
| ser.close() | |
| # get rid of first/last values | |
| results.pop(0) | |
| results.pop() | |
| capacitance = sum(results)/len(results) | |
| # Math thanks to Dawn Lepard <dawn@lepard.ca>: | |
| # Ctotal = 1/(n*(1/C)), solving for n gives us n = C/Ctotal | |
| # we ghetto-floor it, /* add 1 for the cap that will never be immersed, */ | |
| # sub it from numcaps to get the quantity of capacitors immersed, | |
| # and multiply it to turn it into a percentage! | |
| numcaps = __dehumidifiers__[name]['numcaps'] | |
| capvalue = __dehumidifiers__[name]['capvalue'] | |
| immersed = numcaps - int((capvalue/capacitance) + 0.5) | |
| waterlevel = (immersed/float(numcaps-1))*100 | |
| # The floor function works well near the ends, but in the middle, the | |
| # raw looks cleaner. | |
| rawimmersed = numcaps - (capvalue/capacitance) | |
| rawwaterlevel = (rawimmersed/float(numcaps-1))*100 | |
| sys.stdout.write("raw.value %i\n" % rawwaterlevel) | |
| if immersed <= 0: | |
| # very little capacitance -- bottom of probe isn't even wet | |
| return 0 | |
| elif capacitance > (1/(2*(1/capvalue))): | |
| # Capacitance is greater than we'd see with 2 caps in series | |
| return 100 | |
| elif rawwaterlevel > 33 and rawwaterlevel < 77: | |
| return rawwaterlevel | |
| elif rawwaterlevel < 10: | |
| return 0 | |
| else: | |
| return waterlevel | |
| def update_twitter(twittername, twitteroauth, fullness): | |
| """Transmits the current level of fullness to Twitter, if Twitter | |
| doesn't already have it.""" | |
| sendtweet = False | |
| # Get the current fullness level, as far as Twitter knows it | |
| curfullstr = get_fullness_via_twitter(twittername) | |
| if curfullstr: | |
| curfull = int(curfullstr) | |
| if fullness != curfull: | |
| if fullness == 100: | |
| sendtweet = True | |
| if fullness == 0: | |
| sendtweet = True | |
| if ((abs(curfull - fullness) > 30) and fullness > 15 | |
| and fullness < 80): | |
| sendtweet = True | |
| if sendtweet: | |
| outgoing = __dehumidifiers__[twittername]['updatetwitter'] % fullness | |
| import oauth | |
| access_token = oauth.OAuthToken.from_string(twitteroauth) | |
| api = oauthtwitter.OAuthApi(NOT_OAUTH_CONSUMER_KEY, | |
| NOT_OAUTH_CONSUMER_SECRET, access_token) | |
| api.GetUserInfo() | |
| status = api.PostUpdate(outgoing + ' ' + ''.join(random.sample('MILDEW',6))) | |
| if status: | |
| sys.stderr.write('Twitter: TX %s, RX %s\n' % (outgoing, status.text)) | |
| return True | |
| return False | |
| def send_mail(addr, txt): | |
| msg = MIMEText(txt + "\n") | |
| msg['Subject'] = txt | |
| msg['From'] = 'nobody@hoopycat.com' | |
| msg['To'] = addr | |
| s = smtplib.SMTP('localhost') | |
| s.sendmail('nobody@hoopycat.com', [addr], msg.as_string()) | |
| s.quit() | |
| return True | |
| def main(): | |
| """main function to handle munin fun""" | |
| myname = os.path.split(sys.argv[0])[-1].split('_')[1] | |
| if len(sys.argv) > 1 and sys.argv[1] == 'config': | |
| print_config(myname) | |
| else: | |
| if __dehumidifiers__[myname]['type'] == 'twitter': | |
| if __twitterok__: | |
| fullness = get_fullness_via_twitter(myname) | |
| else: | |
| raise ImportError('Twitter module not found for %s' % myname) | |
| elif __dehumidifiers__[myname]['type'] == 'capacitance': | |
| fullness = get_fullness_via_capacitance(myname) | |
| if type(fullness) is not type(None): | |
| print '%s.value %i' % (myname, fullness) | |
| if 'updatetwitter' in __dehumidifiers__[myname].keys() and __twitterok__: | |
| if __dehumidifiers__[myname]['updatetwitter']: | |
| oauth = "oauth_token_secret=" | |
| oauth += os.environ[myname+'oasec'] | |
| oauth += "&oauth_token=" | |
| oauth += os.environ[myname+'oakey'] | |
| update_twitter(twittername=myname, | |
| twitteroauth=oauth, | |
| fullness=fullness) | |
| if fullness == 100 and 'hiveminder' in __dehumidifiers__[myname].keys() and not os.path.exists('/tmp/munin-dehumid-%s-hmsent' % myname): | |
| # open a hiveminder task via e-mail | |
| if send_mail(__dehumidifiers__[myname]['hiveminder'], | |
| "Empty the dehumidifier"): | |
| fd = open('/tmp/munin-dehumid-%s-hmsent' % myname, 'w') | |
| fd.write(str(time.time())) | |
| fd.close() | |
| if fullness < 20 and os.path.exists('/tmp/munin-dehumid-%s-hmsent' % myname): | |
| os.unlink('/tmp/munin-dehumid-%s-hmsent' % myname) | |
| if __name__ == '__main__': | |
| main() | |