#########################################################################################
import io, os, re, traceback
import BaseHTTPServer, urlparse, base64
import dateutil.parser
import matplotlib, numpy
import scipy.interpolate
from matplotlib import pylab
from itertools import groupby
from datetime import datetime, timedelta
HOST = "stepan.local"
PORT = 8080
USERNAME = "cactus"
PASSWORD = "forever"
LOGFILE = "cactuslog.txt"
CMDFILE = "cactuscmd.txt"
FONT = "Arial"
FONT_SIZE = 12
GRAPH_STEP_SEC = 300
STATS_DAYS_NUM = 7
SMOOTH_WINDOW = 3
MAGIC = 10101
# time difference in seconds between real time and log time
LOG_TIME_OFFSET_SEC = 3600
OFF, ON, AUTO = 0, 1, 2
#########################################################################################
class CactusHandler(BaseHTTPServer.BaseHTTPRequestHandler):
def do_GET(self):
if not self.authorize(): return
url = urlparse.urlparse(self.path)
query = urlparse.parse_qs(url.query)
pending = False
if "mode" in query and "hfrom" in query and "hto" in query:
pending = True
try:
mode = int(query["mode"][0])
heaterFrom = float(query["hfrom"][0])
heaterTo = float(query["hto"][0])
self.update_params(mode, heaterFrom, heaterTo)
except:
traceback.print_exc()
if self.path in [ "/cactus.png", "/favicon.ico" ]:
self.send_image(self.path)
else:
self.send_page(pending)
self.wfile.close()
def authorize(self):
if self.headers.getheader("Authorization") == None:
return self.send_auth()
else:
auth = self.headers.getheader("Authorization")
code = re.match(r"Basic (\S+)", auth)
if not code: return self.send_auth()
data = base64.b64decode(code.groups(0)[0])
code = re.match(r"(.*):(.*)", data)
if not code: return self.send_auth()
user, password = code.groups(0)[0], code.groups(0)[1]
if user != USERNAME or password != PASSWORD:
return self.send_auth()
return True
def send_auth(self):
self.send_response(401)
self.send_header("WWW-Authenticate", "Basic realm=\"Cactus\"")
self.send_header("Content-type", "text/html")
self.end_headers()
self.send_default()
self.wfile.close()
return False
def send_default(self):
self.wfile.write("""
<html>
<body style="background:url(data:image/png;base64,{imageCode}) repeat;">
</body>
</html>""".format(imageCode = "iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAA" +
"AJ0lEQVQIW2NkwA7+M2IR/w8UY0SXAAuCFCNLwAWRJVAEYRIYgiAJALsgBgYb" +
"CawOAAAAAElFTkSuQmCC"))
def address_string(self):
host, port = self.client_address[:2]
return host
def update_params(self, mode, heaterFrom, heaterTo):
if max(mode, heaterFrom, heaterTo) >= MAGIC:
print "invalid params values"
return
fout = open(CMDFILE, "w")
fout.write("%d %d %.1f %.1f" % (MAGIC, mode, heaterFrom, heaterTo))
fout.close()
def send_image(self, path):
filename = os.path.basename(path)
name, ext = os.path.splitext(filename)
fimage = open(filename)
self.send_response(200)
format = { ".png" : "png", ".ico" : "x-icon" }
self.send_header("Content-type", "image/" + format[ext])
self.send_header("Content-length", os.path.getsize(filename))
self.end_headers()
self.wfile.write(fimage.read())
fimage.close()
def fix_time(self, X):
time = X[0].timetuple()
if time.tm_hour == 0 and time.tm_min <= 11:
X[0] -= timedelta(seconds = time.tm_min * 60 + time.tm_sec)
time = X[-1].timetuple()
if time.tm_hour == 23 and time.tm_min >= 49:
offset = (60 - time.tm_min - 1) * 60 + (60 - time.tm_sec - 1)
X[-1] += timedelta(seconds = offset)
def send_page(self, pending):
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
data, flog = [ ], None
while not flog:
try: flog = open(LOGFILE)
except: traceback.print_exc()
mode, heater, heaterFrom, heaterTo = AUTO, 0, 5, 10
for s in flog:
row = tuple(s.strip().split(","))
offset = timedelta(seconds = LOG_TIME_OFFSET_SEC)
date = dateutil.parser.parse(row[0]) + offset
temp = float(row[1])
if len(row) == 3:
heater = int(row[2])
elif len(row) >= 3:
mode, heater = int(row[2]), int(row[3])
heaterFrom, heaterTo = float(row[4]), float(row[5])
data.append((date, temp, heater))
nowDate = datetime.now().date()
Yavg = [ [] for foo in numpy.arange(0, 24 * 3600, GRAPH_STEP_SEC) ]
matplotlib.rc("font", family = FONT, size = FONT_SIZE)
fig = pylab.figure(figsize = (964 / 100.0, 350 / 100.0), dpi = 100)
ax = pylab.axes()
for date, points in groupby(data, lambda foo: foo[0].date().isoformat()):
X, Y, H = zip(*points)
deltaDays = (nowDate - X[0].date()).days
if deltaDays > STATS_DAYS_NUM: continue
if len(X) == 1: continue
# convert to same day data
alpha = [1.0, 0.5, 0.3, 0][min(3, deltaDays)]
X = Xsrc = [ datetime.combine(nowDate, foo.time()) for foo in X ]
self.fix_time(X)
# resample X and Y
P = [ (foo - X[0]).seconds for foo in X ]
Q = numpy.arange(0, P[-1], GRAPH_STEP_SEC)
X = [ X[0] + timedelta(seconds = int(foo)) for foo in Q ]
fresample = scipy.interpolate.interp1d(P, Y)
Y = fresample(Q)
# smooth Y
Y = [ Y[0] ] * SMOOTH_WINDOW + list(Y) + [ Y[-1] ] * SMOOTH_WINDOW
window = numpy.ones(SMOOTH_WINDOW * 2 + 1) / float(SMOOTH_WINDOW * 2 + 1)
Y = numpy.convolve(Y, window, 'same')
Y = Y[SMOOTH_WINDOW:-SMOOTH_WINDOW]
fresample = scipy.interpolate.interp1d(Q, Y)
# gather points for stats curve
for i in range(len(Q)):
Yavg[i].append(Y[i])
# plot stats curve
if deltaDays == 3:
self.fix_time(X)
Ymin = [ min(foo or [0]) for foo in Yavg ][:len(X)]
Ymax = [ max(foo or [0]) for foo in Yavg ][:len(X)]
pylab.fill(X + list(reversed(X)), Ymax + list(reversed(Ymin)),
color = "blue", alpha = 0.10)
if alpha == 0: continue
pylab.plot(X, Y, linewidth = 2, color = "blue", alpha = alpha)
# draw heater
for heater, points in groupby(zip(Xsrc, H), lambda foo: foo[1] != 0):
XX, H = zip(*points)
if heater:
p = min(Q[-1], (XX[0] - X[0]).seconds)
x1, y1 = XX[0], fresample(p)
if (XX[-1] - XX[0]).seconds > 600:
x2 = XX[0] + timedelta(seconds = 600)
y2 = fresample(p + 600)
arrow = dict(facecolor = "red", width = 2, headwidth = 6,
frac = 0.40, alpha = alpha, edgecolor = "red")
ax.annotate("", xy = (x2, y2), xytext = (x1, y1),
arrowprops = arrow)
else:
ax.plot(x1, y1, "ro", markersize = 4, mec = "red", alpha = alpha)
ax.xaxis_date()
ax.xaxis.set_major_formatter(matplotlib.dates.DateFormatter("%H:%M"))
ax.xaxis.set_major_locator(matplotlib.dates.HourLocator())
ax.xaxis.grid(True, "major")
ax.yaxis.grid(True, "major")
ax.tick_params(axis = "both", which = "major", direction = "out", labelright = True)
ax.tick_params(axis = "x", which = "major", labelsize = 8)
ax.grid(which = "major", alpha = 1.0)
fig.autofmt_xdate()
pylab.tight_layout()
image = io.BytesIO()
pylab.savefig(image, format = "png")
pylab.clf()
image.seek(0)
graph = "<img src='data:image/png;base64,%s'/>" % \
base64.b64encode(image.getvalue())
image.close()
pending = pending or os.path.isfile(CMDFILE)
self.wfile.write(re.sub(r"{\s", r"{{ ", re.sub(r"\s}", r" }}", """
<html>
<head>
<title>Cactus Tracker</title>
<meta http-equiv="refresh" content="{pending};URL='/'">
<style>
body {
font-family: {font}, sans-serif; font-size: {fontSize}pt;
width: 964px; margin: 47px 30px 0 30px; padding: 0;
background-color: white; color: #262626;
}
h1 {
font-size: 24pt; margin: 0; padding-bottom: 4px;
border-bottom: 2px dotted #262626; margin-bottom: 26px;
}
p { margin-left: 38px; margin-bottom: 20px; }
input {
font-family: {font}, sans-serif; font-size: {fontSize}pt;
border: 2px solid #262626; padding: 2px 6px;
}
button {
font-family: {font}, sans-serif; font-size: {fontSize}pt;
padding: 4px 8px; border: 2px solid #262626; border-radius: 10px;
background-color: white; color: #262626; margin: 0 3px;
}
form { display: inline-block; }
.selected, button:hover:not([disabled]) {
cursor: pointer; background-color: #262626; color: white;
}
.selected:hover { cursor: default; }
.heater { width: 50px; text-align: center; margin: 0 3px; }
.pending { opacity: 0.5; }
.hidden { display: none; }
</style>
</head>
<body>
<h1>Cactus Tracker</h1>
<div>{graph}</div>
<div>
<form action="/" class="{transparent}">
<p>Heater:
<button type="submit" name="mode"
class="{modeOn}" value="1" {disabled}> on </button>
<button type="submit" name="mode"
class="{modeOff}" value="0" {disabled}> off </button>
<button type="submit" name="mode"
class="{modeAuto}" value="2" {disabled}> auto </button>
<input type="hidden" name="hfrom" value="{heaterFrom:.0f}"/>
<input type="hidden" name="hto" value="{heaterTo:.0f}"/>
</form>
<form action="/" class="{transparent} {heaterAuto}">
<span style="margin-left: 30px;">
<input type="hidden" name="mode" value="{mode}"/>
heat from
<input name="hfrom" class="heater" maxlength=2
value="{heaterFrom:.0f}" {disabled}/>
to <input name="hto" class="heater" maxlength=2
value="{heaterTo:.0f}" {disabled}/>
°C
<button type="submit" style="visibility: hidden;" {disabled}/>
</span>
</form>
</div>
<div style="position: absolute; top: 7px; left: 760px;">
<img src="cactus.png">
</div>
</body>
</html>
""")).format(
font = FONT,
fontSize = FONT_SIZE,
graph = graph,
mode = mode,
heaterFrom = heaterFrom,
heaterTo = heaterTo,
modeOff = (mode == OFF) and "selected" or "",
modeOn = (mode == ON) and "selected" or "",
modeAuto = (mode == AUTO) and "selected" or "",
pending = pending and "20" or "1200",
disabled = pending and "disabled=true" or "",
transparent = pending and "pending" or "",
heaterAuto = (mode != AUTO) and "hidden" or ""))
#########################################################################################
server = BaseHTTPServer.HTTPServer((HOST, PORT), CactusHandler)
server.serve_forever()
#########################################################################################