#!/usr/bin/env python3 from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.request import Request, urlopen from time import strftime, sleep from datetime import datetime from threading import Thread from json import dumps from sys import exit debug = 0 def xrange(low,high): ''' Recreates the xrange I love from the 2.7 version of Python. ''' return iter(range(low,high)) def log_it(valve, state): ''' Creates a simple log entry for each operation. ''' fd = open('blossom.log', 'a+') if (state == 1): fd.write("{:s} -> Opened valve: {:s}\n".format(str(strftime("%Y-%m-%dT%H:%M:%S")), str(valve))) else: fd.write("{:s} -> Closed valve: {:s}\n".format(str(strftime("%Y-%m-%dT%H:%M:%S")), str(valve))) fd.close() return class Blossom: ''' Instructs the Blossom to perform a desired operation. ''' def __init__(self): ''' Defines a shared variable. ''' self.apiHost = "[redacted]" # IP Address of Blossom return def open_valve(self, valveNumber): ''' Instructs Blossom to open a valve. ''' request = Request("http://{:s}/bloom/valve".format(str(self.apiHost)), dumps({"valve":valveNumber,"inverter":1}).encode('ascii')) fd = urlopen(request) return fd.close() def close_valve(self, valveNumber): ''' Instructs Blossom to close a valve. ''' request = Request("http://{:s}/bloom/valve".format(str(self.apiHost)), dumps({"valve":valveNumber,"inverter":0}).encode('ascii')) fd = urlopen(request) return fd.close() class Handler(BaseHTTPRequestHandler): ''' Decides how to handle each request. ''' def do_GET(self): ''' In the case of HTTP GET, the request type of checked and an appropriate response is returned. ''' uuid = "[redacted]" # Blossom UUID goes here if ("firmware-check" not in self.path): self.send_response(200) self.send_header('Content-Type', 'application/json') self.send_header('Vary', 'Accept') self.send_header('Connection', 'Close') self.end_headers() else: self.send_response(404) self.end_headers() if (self.path == '/device/v1/server/'): self.wfile.write(str('{"ts": "{:s}", "pst_tz_offset": -8.0, "pst_tz_seconds": -28800}'.format(str(strftime("%Y-%m-%dT%H:%M:%S"))))) elif ("api" in self.path and "device" in self.path and uuid in self.path): if ("api" in self.path and "parameters" in self.path): self.wfile.write(str('{"stats_freq": 3600, "pn_keepalive": 1, "uap_debug": 1, "wave_boost": 1, "ota_freq": 3600, "current_time":"{:s}", "build": 1042, "opn_trip": 40}'.format(str(strftime("%Y-%m-%dT%H:%M:%S"))))) else: self.wfile.write(str('{"channel": "channel_{:s}", "current_time": "{:s}", "tz_offset": -8.0, "tz_seconds": -28800, "sch_load_time": 24900, "fetch_lead": 3600}'.format(str(uuid), str(strftime("%Y-%m-%dT%H:%M:%S"))))) return class Monitor: ''' Monitors the current datetime and watering schedule. ''' def __init__(self): self.front_scheduleDays = ['Tue', 'Thu', 'Sat'] self.front_scheduleTimes = [400, 2330] self.front_zones = {'zones': [1, 2, 3], 'water_times': [60, 60, 120]} self.back_scheduleDays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'] self.back_scheduleTimes = [530, 2355] self.back_zones = {'zones': [8, 9, 10, 11, 12], 'water_times': [30, 30, 30, 30, 0]} self.days = {'Mon': 0, 'Tue': 1, 'Wed': 2, 'Thu': 3, 'Fri': 4, 'Sat': 5, 'Sun': 6} return def run(self): ''' Continually checks the current datetime and decides whether or not to water. ''' print(datetime.now(), " -> Starting") while True: print(datetime.now(), " -> Checking schedule") self.current_day = datetime.today().weekday() self.current_time = int(datetime.now().strftime('%H%M')) for schedule_day in self.front_scheduleDays: if self.days[schedule_day] is self.current_day: for schedule_time in self.front_scheduleTimes: if debug == 1: print(datetime.now(), " -> [F] Checking if ", self.current_time, " is in ", xrange(schedule_time-1, schedule_time+1)) if self.current_time in xrange(schedule_time-1, schedule_time+1): for zone_info in zip(self.front_zones['zones'], self.front_zones['water_times']): if (zone_info[1] > 0): print(datetime.now(), " -> [F] Opening valve: ", zone_info[0]) log_it(zone_info[0], 1) Blossom().open_valve(zone_info[0]) sleep(zone_info[1]) print(datetime.now(), " -> [F] Closing valve: ", zone_info[0]) log_it(zone_info[0], 0) Blossom().close_valve(zone_info[0]) for schedule_day in self.back_scheduleDays: if self.days[schedule_day] is self.current_day: for schedule_time in self.back_scheduleTimes: if debug == 1: print(datetime.now(), " -> [B] Checking if ", self.current_time, " is in ", xrange(schedule_time-1, schedule_time+1)) if self.current_time in xrange(schedule_time-1, schedule_time+1): for zone_info in zip(self.back_zones['zones'], self.back_zones['water_times']): if (zone_info[1] > 0): print(datetime.now(), " -> [B] Opening valve: ", zone_info[0]) log_it(zone_info[0], 1) Blossom().open_valve(zone_info[0]) sleep(zone_info[1]) print(datetime.now(), " -> [B] Closing valve: ", zone_info[0]) log_it(zone_info[0], 0) Blossom().close_valve(zone_info[1]) sleep(30) return def main(): ''' Spawn a thread for datetime monitoring. Serve the API. ''' Thread(name='monitor-schedule', target=Monitor().run, args=()).start() try: server = HTTPServer(('', 81), Handler) server.serve_forever() except KeyboardInterrupt: print("^C received, shutting down the web server") server.socket.close() return exit(1) if __name__ == "__main__": main()