Зимовка кактусов с онлайн контролем температуры

#########################################################################################

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 (»« »«.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 = (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 = »» % \ 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» }}»,»« Cactus Tracker

Cactus Tracker

{graph}

Heater:

heat from to °C
»«)).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 ()

#########################################################################################

© Habrahabr.ru