Skip to content
Find file
Fetching contributors…
Cannot retrieve contributors at this time
executable file 276 lines (244 sloc) 9.81 KB
#!/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()
Jump to Line
Something went wrong with that request. Please try again.