From 45fdc29615e20ed152b88d0bc5b4e8a5217554e5 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 20 Jun 2021 19:08:22 -0700 Subject: [PATCH 1/4] Cleaned up some stuff that was for test or just unsed. --- .gitignore | 2 + GardenModules/README.md | 3 + GardenModules/gardenServer/gardenServer.py | 61 +++- GardenModules/luxSensor/luxSensor.py | 2 +- GardenModules/prune/prune.py | 4 +- GardenModules/soilMoisture/soil.py | 4 +- {ModuleTests => Tests}/Data/luxData.csv | 6 +- .../GardenModules/GardenModule.py | 0 .../artificalLight/artificalLight.py | 0 .../GardenModules/email/email.py | 324 +++++++++--------- .../gardenServer/gardenServer.py | 0 .../GardenModules/luxSensor/luxSensor.py | 108 +++--- .../GardenModules/prune/prune.py | 50 +-- .../GardenModules/pump/pump.py | 182 +++++----- .../GardenModules/soilMoisture/soil.py | 152 ++++---- .../GardenModules/sunlightSensor/sunlight.py | 156 ++++----- Tests/README.md | 3 + {test => Tests}/alex.txt | 0 {test => Tests}/artificial_light_test.py | 0 {test => Tests}/camera_test.py | 0 {test => Tests}/database_test.py | 0 {test => Tests}/email_desktop.py | 0 {test => Tests}/email_test.py | 0 {test => Tests}/i2c-test.py | 38 +- {test => Tests}/image.jpeg | Bin {test => Tests}/lightTest.py | 0 {test => Tests}/light_sensor_test.py | 32 +- {ModuleTests => Tests}/luxTest.py | 42 +-- {test => Tests}/modules/soilMoisture/soil.py | 166 ++++----- {test => Tests}/opencv_test.py | 28 +- {test => Tests}/pump.py | 0 {ModuleTests => Tests}/pumpTest.py | 50 +-- {test => Tests}/soil_test.py | 0 {test => Tests}/sunlightLog.txt | 0 {test => Tests}/tempSensor.py | 0 {test => Tests}/test.jpg | Bin build_table.py | 106 ------ install.sh | 4 +- rc.local | 25 -- smartGarden.py | 129 +------ .../__pycache__/soil.cpython-35.pyc | Bin 1195 -> 0 bytes 41 files changed, 740 insertions(+), 937 deletions(-) create mode 100644 GardenModules/README.md rename {ModuleTests => Tests}/Data/luxData.csv (97%) rename {ModuleTests => Tests}/GardenModules/GardenModule.py (100%) rename {ModuleTests => Tests}/GardenModules/artificalLight/artificalLight.py (100%) rename {ModuleTests => Tests}/GardenModules/email/email.py (97%) rename {ModuleTests => Tests}/GardenModules/gardenServer/gardenServer.py (100%) rename {ModuleTests => Tests}/GardenModules/luxSensor/luxSensor.py (96%) rename {ModuleTests => Tests}/GardenModules/prune/prune.py (96%) rename {ModuleTests => Tests}/GardenModules/pump/pump.py (97%) rename {ModuleTests => Tests}/GardenModules/soilMoisture/soil.py (97%) rename {ModuleTests => Tests}/GardenModules/sunlightSensor/sunlight.py (97%) create mode 100644 Tests/README.md rename {test => Tests}/alex.txt (100%) rename {test => Tests}/artificial_light_test.py (100%) rename {test => Tests}/camera_test.py (100%) rename {test => Tests}/database_test.py (100%) rename {test => Tests}/email_desktop.py (100%) rename {test => Tests}/email_test.py (100%) rename {test => Tests}/i2c-test.py (96%) rename {test => Tests}/image.jpeg (100%) rename {test => Tests}/lightTest.py (100%) rename {test => Tests}/light_sensor_test.py (96%) rename {ModuleTests => Tests}/luxTest.py (95%) rename {test => Tests}/modules/soilMoisture/soil.py (96%) rename {test => Tests}/opencv_test.py (94%) rename {test => Tests}/pump.py (100%) rename {ModuleTests => Tests}/pumpTest.py (95%) rename {test => Tests}/soil_test.py (100%) rename {test => Tests}/sunlightLog.txt (100%) rename {test => Tests}/tempSensor.py (100%) rename {test => Tests}/test.jpg (100%) delete mode 100644 build_table.py delete mode 100755 rc.local delete mode 100644 test/modules/soilMoisture/__pycache__/soil.cpython-35.pyc diff --git a/.gitignore b/.gitignore index 64bf1ff..1f5144d 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,8 @@ *.log logs/ +#local +*.local* # MISC camera.sh manual_transfer.py diff --git a/GardenModules/README.md b/GardenModules/README.md new file mode 100644 index 0000000..96f9103 --- /dev/null +++ b/GardenModules/README.md @@ -0,0 +1,3 @@ +# Garden Modules + +This folder houses the gardent modules. Each module represents and manages some form of input or output, e.g. a soil moisture sensor. The idea behind this approach is to make adding features as simple as adding a new module. \ No newline at end of file diff --git a/GardenModules/gardenServer/gardenServer.py b/GardenModules/gardenServer/gardenServer.py index c4fb05e..bb4ab3b 100644 --- a/GardenModules/gardenServer/gardenServer.py +++ b/GardenModules/gardenServer/gardenServer.py @@ -5,7 +5,7 @@ import logging import time -app = Flask(__name__, template_folder='/home/pi/Desktop/smartGarden/smartGarden/ControlPanel', static_folder="/home/pi/Desktop/smartGarden/smartGarden/ControlPanel") +app = Flask(__name__, template_folder='./ControlPanel', static_folder="./ControlPanel") CORS(app) Debug(app) @@ -105,9 +105,56 @@ def get_light(): print(e) +@app.route('/soil') +def soil_route(): + try: + with open("./soilLog.txt") as file: + #lines = file.readlines() + table = "" + for count, line in reversed(list(enumerate(file))): + fields = line.split() + raw_value = fields[8] + percent = fields[3] + date = fields[4] + time = fields[5] + + if count % 2 == 0: + table = table + "" + else: + table = table + "" + + table = table + "" + date + "" + table = table + "" + time + "" + table = table + "" + percent + "" + table = table + "" + raw_value + "" + table = table + "" + return ''' + + + Soil Moisture - Smart Garden + + +

Soil Moisture Data

+ + + + + + + + ''' + table + ''' +
DateTimeSoil Moisture PercentSoil Moisture Raw Value
+ + + ''' + except Exception as e: + logging.warn("There was an exception returning soil data to rest endpoint: " + str(e)) + return "There was an exception: " + str(e) + + @app.route('/garden') def garden_route(): - with open("/home/pi/Desktop/smartGarden/smartGarden/logs/smartGardenLog.txt") as file: + with open("./logs/smartGardenLog.txt") as file: return file.read() @@ -119,31 +166,31 @@ def control_panel(): @app.route('/water') def control_panel_water(): - with open('/home/pi/Desktop/smartGarden/smartGarden/ControlPanel/water.html') as file: + with open('./ControlPanel/water.html') as file: return file.read() @app.route('/light') def control_panel_light(): - with open('/home/pi/Desktop/smartGarden/smartGarden/ControlPanel/light.html') as file: + with open('./ControlPanel/light.html') as file: return file.read() @app.route('/soilMoisture') def control_panel_soil_moisture(): - with open('/home/pi/Desktop/smartGarden/smartGarden/ControlPanel/soilMoisture.html') as file: + with open('./ControlPanel/soilMoisture.html') as file: return file.read() @app.route('/sun_css') def sun_css(): - with open('/home/pi/Desktop/smartGarden/smartGarden/ControlPanel/sun.css') as file: + with open('./ControlPanel/sun.css') as file: return file.read() @app.route('/status_css') def status_css(): - with open('/home/pi/Desktop/smartGarden/smartGarden/ControlPanel/status.css') as file: + with open('./ControlPanel/status.css') as file: return file.read() # End Control Panel Endpoints diff --git a/GardenModules/luxSensor/luxSensor.py b/GardenModules/luxSensor/luxSensor.py index 805ffd6..49b2b9f 100644 --- a/GardenModules/luxSensor/luxSensor.py +++ b/GardenModules/luxSensor/luxSensor.py @@ -19,7 +19,7 @@ def __init__(self, log, queue): self._sensor.gain = 0 self._grow_light_lux = 535 self._lux_interval = 120 - self._data_file = "/home/pi/Desktop/smartGarden/smartGarden/Data/luxData.csv" + self._data_file = "./Data/luxData.csv" self._log.info("Lux sensor start up successful") except Exception as exception: self._log.error("Lux sensor failed to start up.") diff --git a/GardenModules/prune/prune.py b/GardenModules/prune/prune.py index b4ba474..9142b21 100644 --- a/GardenModules/prune/prune.py +++ b/GardenModules/prune/prune.py @@ -3,7 +3,7 @@ def prune(file): lines = [] try: - logFile = open("/home/pi/Desktop/smartGarden/smartGarden/logs/" + file, "r") + logFile = open("./logs/" + file, "r") for line in logFile: lines.append(line) except Exception as e: @@ -12,7 +12,7 @@ def prune(file): logFile.close() try: - logFile = open("/home/pi/Desktop/smartGarden/smartGarden/logs/" + file, "w") + logFile = open("./logs/" + file, "w") if len(lines) > 5000: # Only keep the last 5000 lines for x in range(5000): diff --git a/GardenModules/soilMoisture/soil.py b/GardenModules/soilMoisture/soil.py index ba057c4..0b9684d 100644 --- a/GardenModules/soilMoisture/soil.py +++ b/GardenModules/soilMoisture/soil.py @@ -23,7 +23,7 @@ def __init__(self, log, queue): self.soilInterval = 120 # 1800 self._log.info("Channel: " + str(self.channel)) self.setName("soilThread") - self._data_file = "/home/pi/Desktop/smartGarden/smartGarden/Data/soilMoistureData.csv" + self._data_file = "./Data/soilMoistureData.csv" # Set Gain to 16 bits # Gain = 1 # Wet: 13884 Dry: 21680 @@ -69,7 +69,7 @@ def _checkSoil(self): self._log.exception("Error calculating soil moisture") try: - with open("/home/pi/Desktop/smartGarden/smartGarden/logs/soilLog.txt", "a+") as logFile: + with open("./logs/soilLog.txt", "a+") as logFile: logFile.write("Soil Moisture Level: " + str(self.getSoilPercentage()) + "% " + str( datetime.now()) + " Raw Value: " + str(self.channel.value) + "\n") except Exception as exception: diff --git a/ModuleTests/Data/luxData.csv b/Tests/Data/luxData.csv similarity index 97% rename from ModuleTests/Data/luxData.csv rename to Tests/Data/luxData.csv index 84f47c5..f4528b7 100644 --- a/ModuleTests/Data/luxData.csv +++ b/Tests/Data/luxData.csv @@ -1,3 +1,3 @@ -527.978462833967,2020-11-29 02:55:51.918946 -528.5072462839585,2020-11-29 02:56:02.070627 -529.5646851648721,2020-11-29 02:56:12.221543 +527.978462833967,2020-11-29 02:55:51.918946 +528.5072462839585,2020-11-29 02:56:02.070627 +529.5646851648721,2020-11-29 02:56:12.221543 diff --git a/ModuleTests/GardenModules/GardenModule.py b/Tests/GardenModules/GardenModule.py similarity index 100% rename from ModuleTests/GardenModules/GardenModule.py rename to Tests/GardenModules/GardenModule.py diff --git a/ModuleTests/GardenModules/artificalLight/artificalLight.py b/Tests/GardenModules/artificalLight/artificalLight.py similarity index 100% rename from ModuleTests/GardenModules/artificalLight/artificalLight.py rename to Tests/GardenModules/artificalLight/artificalLight.py diff --git a/ModuleTests/GardenModules/email/email.py b/Tests/GardenModules/email/email.py similarity index 97% rename from ModuleTests/GardenModules/email/email.py rename to Tests/GardenModules/email/email.py index 2620bbe..362f38a 100644 --- a/ModuleTests/GardenModules/email/email.py +++ b/Tests/GardenModules/email/email.py @@ -1,163 +1,163 @@ -from email.mime.text import MIMEText -from email.mime.multipart import MIMEMultipart -from email.mime.base import MIMEBase -from email.utils import formatdate -from email import encoders -from datetime import datetime -from datetime import timedelta -import smtplib, ssl -import logging - -def send_email(): - port = 465 # For SSL - password = "al.EX.91.27" - sender_email = "raspberry.pi.taffe@gmail.com" - receiver_email = "taffeAlexander@gmail.com" - message = MIMEMultipart("alternative") - message["Subject"] = "Garden update: " + formatdate(localtime=True) - message["From"] = sender_email - message["To"] = receiver_email - message["Date"] = formatdate(localtime=True) - soilMoisture = "No Data" - soilTimeStamp = "No Data" - soilIterator = 0 - soilMoistureArray = [] - soilTimeStampArray = [] - currentYMD = str(datetime.now()).split()[0] - - # Create the body of the message (a plain-text and an HTML version). - text = "Garden update plan text" - - try: - html = """\ - - - - - -

Garden Update

- - - - - - - - """ - soilLogArray = [] - with open("/home/pi/Desktop/smartGarden/smartGarden/logs/soilLog.txt", "r") as fp2: - for count, line in enumerate(fp2): - soilLogArray.append(line) - - for line in soilLogArray: - try: - splitLine = line.split() - if (currentYMD == splitLine[4]) and (len(splitLine) > 5): - soilMoistureArray.append(splitLine[3]) - soilTimeStampArray.append(splitLine[4] + " " + splitLine[5]) - except Exception as e: - logging.warn("Unable to parse soil moisture or time stamp for email") - logging.warn(e) - print("Error parsing soil log: " + str(e)) - - with open("/home/pi/Desktop/smartGarden/smartGarden/logs/sunlightLog.txt", "r") as fp: - for cnt, line in enumerate(fp): - lineArray = line.split() - highlightedRow = "" + highlightedRow + lineArray[0] + " " + lineArray[1] + "" - else: - row = "" + regularRow + lineArray[0] + " " + lineArray[1] + "" - - row = row + regularRow + lineArray[3] + " " + lineArray[4]+ "" - row = row + regularRow + soilMoisture +"%" - row = row + regularRow + soilTimeStamp + "" - else: - if "YES" in lineArray[0]: - row = "" + highlightedRow + lineArray[0] + " " + lineArray[1] + "" - else: - row = "" + greyRow + lineArray[0] + " " + lineArray[1] + "" - row = row + greyRow + lineArray[3] + " " + lineArray[4]+ "" - row = row + greyRow + soilMoisture + "%" - row = row + greyRow + soilTimeStamp + "" - html = html + row - elif currentYMD == lineArray[4]: - if cnt % 2 == 0: - if "YES" in lineArray[0]: - row = "" + highlightedRow + lineArray[0] + " " + lineArray[1] + " " + lineArray[2] + "" - else: - row = "" + regularRow + lineArray[0] + " " + lineArray[1] + "" - row = row + regularRow + lineArray[4] + " " + lineArray[5]+ "" - row = row + regularRow + soilMoisture + "%" - row = row + regularRow + soilTimeStamp + "" - else: - if "YES" in lineArray[0]: - row = "" + highlightedRow + lineArray[0] + " " + lineArray[1] + " " + lineArray[2] + "" - else: - row = "" + greyRow + lineArray[0] + " " + lineArray[1] + "" - row = row + greyRow + lineArray[4] + " " + lineArray[5]+ "" - row = row + greyRow + soilMoisture + "%" - row = row + greyRow + soilTimeStamp + "" - html = html + row - html = html + """\ -
Sunlight TimeStamp Soil Moisture TimeStamp
" - regularRow = "" - greyRow = "" - - if currentYMD == lineArray[3] or currentYMD == lineArray[4]: - if soilIterator < len(soilMoistureArray): - soilMoisture = soilMoistureArray[soilIterator] - soilTimeStamp = soilTimeStampArray[soilIterator] - soilIterator = soilIterator + 1 - print("Setting moisture: " + soilMoisture) - else: - soilMoisture = "NO DATA" - soilTimeStamp = "NO DATA" - if cnt % 2 == 0: - if "YES" in lineArray[0]: - row = "
- - - """ - except Exception as e: - logging.warn("There was an error reading html file. Defaulting to basic html page") - html = """\ - - - -

Garden Update

- - - """ - logging.warn(e) - print("Error sending email: " + str(e)) - - try: - #Open the file to be sent - attachment = open("/home/pi/Desktop/smartGarden/smartGarden/logs/smartGardenLog.txt", "rb") - p = MIMEBase('application', 'octet-stream') - p.set_payload((attachment).read()) - encoders.encode_base64(p) - - p.add_header('Content-Disposition', "attachment; filename=%s" % "GardenLog.txt") - message.attach(p) - except Exception as e: - logging.warn("There was an error opening attachment.") - logging.warn(e) - finally: - attachment.close() - - # Record the MIME types of both parts - text/plain and text/html. - part1 = MIMEText(text, 'plain') - part2 = MIMEText(html, 'html') - - # Attach parts into message container. - # According to RFC 2046, the last part of a multipart message, in this case - # the HTML message, is best and preferred. - message.attach(part1) - message.attach(part2) - - # Create a secure SSL context - context = ssl.create_default_context() - with smtplib.SMTP_SSL("smtp.gmail.com", port) as server: - server.login("raspberry.pi.taffe@gmail.com", password) - server.sendmail(sender_email, receiver_email, message.as_string()) +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from email.mime.base import MIMEBase +from email.utils import formatdate +from email import encoders +from datetime import datetime +from datetime import timedelta +import smtplib, ssl +import logging + +def send_email(): + port = 465 # For SSL + password = "al.EX.91.27" + sender_email = "raspberry.pi.taffe@gmail.com" + receiver_email = "taffeAlexander@gmail.com" + message = MIMEMultipart("alternative") + message["Subject"] = "Garden update: " + formatdate(localtime=True) + message["From"] = sender_email + message["To"] = receiver_email + message["Date"] = formatdate(localtime=True) + soilMoisture = "No Data" + soilTimeStamp = "No Data" + soilIterator = 0 + soilMoistureArray = [] + soilTimeStampArray = [] + currentYMD = str(datetime.now()).split()[0] + + # Create the body of the message (a plain-text and an HTML version). + text = "Garden update plan text" + + try: + html = """\ + + + + + +

Garden Update

+ + + + + + + + """ + soilLogArray = [] + with open("/home/pi/Desktop/smartGarden/smartGarden/logs/soilLog.txt", "r") as fp2: + for count, line in enumerate(fp2): + soilLogArray.append(line) + + for line in soilLogArray: + try: + splitLine = line.split() + if (currentYMD == splitLine[4]) and (len(splitLine) > 5): + soilMoistureArray.append(splitLine[3]) + soilTimeStampArray.append(splitLine[4] + " " + splitLine[5]) + except Exception as e: + logging.warn("Unable to parse soil moisture or time stamp for email") + logging.warn(e) + print("Error parsing soil log: " + str(e)) + + with open("/home/pi/Desktop/smartGarden/smartGarden/logs/sunlightLog.txt", "r") as fp: + for cnt, line in enumerate(fp): + lineArray = line.split() + highlightedRow = "" + highlightedRow + lineArray[0] + " " + lineArray[1] + "" + else: + row = "" + regularRow + lineArray[0] + " " + lineArray[1] + "" + + row = row + regularRow + lineArray[3] + " " + lineArray[4]+ "" + row = row + regularRow + soilMoisture +"%" + row = row + regularRow + soilTimeStamp + "" + else: + if "YES" in lineArray[0]: + row = "" + highlightedRow + lineArray[0] + " " + lineArray[1] + "" + else: + row = "" + greyRow + lineArray[0] + " " + lineArray[1] + "" + row = row + greyRow + lineArray[3] + " " + lineArray[4]+ "" + row = row + greyRow + soilMoisture + "%" + row = row + greyRow + soilTimeStamp + "" + html = html + row + elif currentYMD == lineArray[4]: + if cnt % 2 == 0: + if "YES" in lineArray[0]: + row = "" + highlightedRow + lineArray[0] + " " + lineArray[1] + " " + lineArray[2] + "" + else: + row = "" + regularRow + lineArray[0] + " " + lineArray[1] + "" + row = row + regularRow + lineArray[4] + " " + lineArray[5]+ "" + row = row + regularRow + soilMoisture + "%" + row = row + regularRow + soilTimeStamp + "" + else: + if "YES" in lineArray[0]: + row = "" + highlightedRow + lineArray[0] + " " + lineArray[1] + " " + lineArray[2] + "" + else: + row = "" + greyRow + lineArray[0] + " " + lineArray[1] + "" + row = row + greyRow + lineArray[4] + " " + lineArray[5]+ "" + row = row + greyRow + soilMoisture + "%" + row = row + greyRow + soilTimeStamp + "" + html = html + row + html = html + """\ +
Sunlight TimeStamp Soil Moisture TimeStamp
" + regularRow = "" + greyRow = "" + + if currentYMD == lineArray[3] or currentYMD == lineArray[4]: + if soilIterator < len(soilMoistureArray): + soilMoisture = soilMoistureArray[soilIterator] + soilTimeStamp = soilTimeStampArray[soilIterator] + soilIterator = soilIterator + 1 + print("Setting moisture: " + soilMoisture) + else: + soilMoisture = "NO DATA" + soilTimeStamp = "NO DATA" + if cnt % 2 == 0: + if "YES" in lineArray[0]: + row = "
+ + + """ + except Exception as e: + logging.warn("There was an error reading html file. Defaulting to basic html page") + html = """\ + + + +

Garden Update

+ + + """ + logging.warn(e) + print("Error sending email: " + str(e)) + + try: + #Open the file to be sent + attachment = open("/home/pi/Desktop/smartGarden/smartGarden/logs/smartGardenLog.txt", "rb") + p = MIMEBase('application', 'octet-stream') + p.set_payload((attachment).read()) + encoders.encode_base64(p) + + p.add_header('Content-Disposition', "attachment; filename=%s" % "GardenLog.txt") + message.attach(p) + except Exception as e: + logging.warn("There was an error opening attachment.") + logging.warn(e) + finally: + attachment.close() + + # Record the MIME types of both parts - text/plain and text/html. + part1 = MIMEText(text, 'plain') + part2 = MIMEText(html, 'html') + + # Attach parts into message container. + # According to RFC 2046, the last part of a multipart message, in this case + # the HTML message, is best and preferred. + message.attach(part1) + message.attach(part2) + + # Create a secure SSL context + context = ssl.create_default_context() + with smtplib.SMTP_SSL("smtp.gmail.com", port) as server: + server.login("raspberry.pi.taffe@gmail.com", password) + server.sendmail(sender_email, receiver_email, message.as_string()) logging.info("Email Sent "+ str(datetime.now())) \ No newline at end of file diff --git a/ModuleTests/GardenModules/gardenServer/gardenServer.py b/Tests/GardenModules/gardenServer/gardenServer.py similarity index 100% rename from ModuleTests/GardenModules/gardenServer/gardenServer.py rename to Tests/GardenModules/gardenServer/gardenServer.py diff --git a/ModuleTests/GardenModules/luxSensor/luxSensor.py b/Tests/GardenModules/luxSensor/luxSensor.py similarity index 96% rename from ModuleTests/GardenModules/luxSensor/luxSensor.py rename to Tests/GardenModules/luxSensor/luxSensor.py index 56d1c79..905eae8 100644 --- a/ModuleTests/GardenModules/luxSensor/luxSensor.py +++ b/Tests/GardenModules/luxSensor/luxSensor.py @@ -1,54 +1,54 @@ -import adafruit_tsl2561 -import busio -import board -import threading -from GardenModules.GardenModule import GardenModule -from datetime import datetime -import csv -import os - - -class LuxSensor(GardenModule): - def __init__(self, log, queue): - super().__init__(queue) - self._logging = log - self._i2c = busio.I2C(board.SCL, board.SDA) - self._sensor = adafruit_tsl2561.TSL2561(self._i2c) - self._sensor.gain = 0 - self._grow_light_lux = 535 - self._lux_interval = 10 - self._data_file = "/home/pi/Desktop/smartGarden/smartGarden/ModuleTests/Data/luxData.csv" - - def getLux(self): - return self._sensor.lux - - def isSunlight(self): - return self._sensor.lux > self._grow_light_lux - - def _saveReading(self): - if not os.path.exists(self._data_file): - with open(self._data_file, 'w+'): - pass - - with open(self._data_file, mode='a') as file: - writer = csv.writer(file, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL) - writer.writerow([self._sensor.lux, datetime.now()]) - - def run(self): - print("Starting lux sensor thread") - timer = threading.Event() - while not timer.wait(self._lux_interval): - self._logging.info(self) - print(self) - self._saveReading() - if self._sentinel.get(block=True): - print("Sentinel was triggered in soil thread.") - self._sentinel.put(True) - self._sentinel.task_done() - break - self._sentinel.put(False) - self._sentinel.task_done() - - def __str__(self): - return "Current lux reading: {}".format(self.getLux()) - +import adafruit_tsl2561 +import busio +import board +import threading +from GardenModules.GardenModule import GardenModule +from datetime import datetime +import csv +import os + + +class LuxSensor(GardenModule): + def __init__(self, log, queue): + super().__init__(queue) + self._logging = log + self._i2c = busio.I2C(board.SCL, board.SDA) + self._sensor = adafruit_tsl2561.TSL2561(self._i2c) + self._sensor.gain = 0 + self._grow_light_lux = 535 + self._lux_interval = 10 + self._data_file = "/home/pi/Desktop/smartGarden/smartGarden/ModuleTests/Data/luxData.csv" + + def getLux(self): + return self._sensor.lux + + def isSunlight(self): + return self._sensor.lux > self._grow_light_lux + + def _saveReading(self): + if not os.path.exists(self._data_file): + with open(self._data_file, 'w+'): + pass + + with open(self._data_file, mode='a') as file: + writer = csv.writer(file, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL) + writer.writerow([self._sensor.lux, datetime.now()]) + + def run(self): + print("Starting lux sensor thread") + timer = threading.Event() + while not timer.wait(self._lux_interval): + self._logging.info(self) + print(self) + self._saveReading() + if self._sentinel.get(block=True): + print("Sentinel was triggered in soil thread.") + self._sentinel.put(True) + self._sentinel.task_done() + break + self._sentinel.put(False) + self._sentinel.task_done() + + def __str__(self): + return "Current lux reading: {}".format(self.getLux()) + diff --git a/ModuleTests/GardenModules/prune/prune.py b/Tests/GardenModules/prune/prune.py similarity index 96% rename from ModuleTests/GardenModules/prune/prune.py rename to Tests/GardenModules/prune/prune.py index b4ba474..310657a 100644 --- a/ModuleTests/GardenModules/prune/prune.py +++ b/Tests/GardenModules/prune/prune.py @@ -1,26 +1,26 @@ -import logging - -def prune(file): - lines = [] - try: - logFile = open("/home/pi/Desktop/smartGarden/smartGarden/logs/" + file, "r") - for line in logFile: - lines.append(line) - except Exception as e: - print("Error reading log file: " + file + " for pruning") - finally: - logFile.close() - - try: - logFile = open("/home/pi/Desktop/smartGarden/smartGarden/logs/" + file, "w") - if len(lines) > 5000: - # Only keep the last 5000 lines - for x in range(5000): - index = len(lines) - 5001 + x - logFile.write(lines[index]) - except Exception as e: - print("Error writing log file: " + file) - finally: - logFile.close() - logging.info("Pruned Log: " + file) +import logging + +def prune(file): + lines = [] + try: + logFile = open("/home/pi/Desktop/smartGarden/smartGarden/logs/" + file, "r") + for line in logFile: + lines.append(line) + except Exception as e: + print("Error reading log file: " + file + " for pruning") + finally: + logFile.close() + + try: + logFile = open("/home/pi/Desktop/smartGarden/smartGarden/logs/" + file, "w") + if len(lines) > 5000: + # Only keep the last 5000 lines + for x in range(5000): + index = len(lines) - 5001 + x + logFile.write(lines[index]) + except Exception as e: + print("Error writing log file: " + file) + finally: + logFile.close() + logging.info("Pruned Log: " + file) print("Pruned log: " + file) \ No newline at end of file diff --git a/ModuleTests/GardenModules/pump/pump.py b/Tests/GardenModules/pump/pump.py similarity index 97% rename from ModuleTests/GardenModules/pump/pump.py rename to Tests/GardenModules/pump/pump.py index 6c147ca..b218331 100644 --- a/ModuleTests/GardenModules/pump/pump.py +++ b/Tests/GardenModules/pump/pump.py @@ -1,91 +1,91 @@ -import RPi.GPIO as GPIO -import time -import threading -from GardenModules.GardenModule import GardenModule -from GardenModules.soilMoisture.soil import SoilMoisture -from datetime import datetime - - -class WaterPump(GardenModule): - def __init__(self, log, queue, soil_moisture_sensor): - super().__init__(queue) - self.logging = log - self._pwm = 70 - self._pin = 18 - self._pumpInterval = 60 - self.setName("pumpThread") - self.soilMoisture = soil_moisture_sensor - - def _run(self, runtime=None, dutyCycle=50): - if runtime == None: - raise Exception("The value of run_time for the water pump was none.") - try: - self._setup(self._pin) - self._togglePin(self._pin) - p = GPIO.PWM(self._pin, self._pwm) - p.start(dutyCycle) - time.sleep(runtime) - GPIO.output(self._pin, GPIO.LOW) - p.stop() - self.logging.info("Watered plants at: " + str(datetime.now())) - except Exception as exception: - self.logging.warn("There was an error watering the plants.") - self.logging.warn(exception) - self.logging.info(self._printWatered()) - - def _run_sequence(self): - self._run(10, 100) - time.sleep(5) - self._run(3, 70) - time.sleep(5) - self._run(2, 50) - - def run(self): - try: - print("Starting pump thread.") - # self._run_sequence() - timer = threading.Event() - while not timer.wait(self._pumpInterval): - if self.soilMoisture.getSoilPercentage() < 40: - # self._run_sequence() - print("Watering because soil moisture is at: {}\n".format(self.soilMoisture.getSoilPercentage()) - + self._printWatered()) - else: - print("Skipping watering because soil moisture is at: {:.2f}%".format( - self.soilMoisture.getSoilPercentage())) - # TODO Refactor sentinel to be part of the while loop so that when it is triggered the loop ends. - if self._sentinel.get(block=True): - self.logging.info("Sentinel was triggered in pump thread.") - self._sentinel.put(True) - self._sentinel.task_done() - break - self._sentinel.put(False) - self._sentinel.task_done() - except Exception as exception: - self.logging.info("There was an exception in the pump thread: ") - self.logging.info(exception) - - def setInterval(self, interval): - self._pumpInterval = interval - - def getInterval(self): - return self._pumpInterval - - def _togglePin(self, pin): - GPIO.output(pin, GPIO.HIGH) - GPIO.output(pin, GPIO.LOW) - - def _setup(self, pin): - GPIO.setmode(GPIO.BCM) - GPIO.setup(pin, GPIO.OUT) - - def _printWatered(self): - return """ - ,d - 88 - 8b db d8 ,adPPYYba, MM88MMM ,adPPYba, 8b,dPPYba, - `8b d88b d8' "" `Y8 88 a8P_____88 88P' "Y8 - `8b d8'`8b d8' ,adPPPPP88 88 8PP""""""" 88 - `8bd8' `8bd8' 88, ,88 88, "8b, ,aa 88 - YP YP `"8bbdP"Y8 "Y888 `"Ybbd8"' 88 - """ +import RPi.GPIO as GPIO +import time +import threading +from GardenModules.GardenModule import GardenModule +from GardenModules.soilMoisture.soil import SoilMoisture +from datetime import datetime + + +class WaterPump(GardenModule): + def __init__(self, log, queue, soil_moisture_sensor): + super().__init__(queue) + self.logging = log + self._pwm = 70 + self._pin = 18 + self._pumpInterval = 60 + self.setName("pumpThread") + self.soilMoisture = soil_moisture_sensor + + def _run(self, runtime=None, dutyCycle=50): + if runtime == None: + raise Exception("The value of run_time for the water pump was none.") + try: + self._setup(self._pin) + self._togglePin(self._pin) + p = GPIO.PWM(self._pin, self._pwm) + p.start(dutyCycle) + time.sleep(runtime) + GPIO.output(self._pin, GPIO.LOW) + p.stop() + self.logging.info("Watered plants at: " + str(datetime.now())) + except Exception as exception: + self.logging.warn("There was an error watering the plants.") + self.logging.warn(exception) + self.logging.info(self._printWatered()) + + def _run_sequence(self): + self._run(10, 100) + time.sleep(5) + self._run(3, 70) + time.sleep(5) + self._run(2, 50) + + def run(self): + try: + print("Starting pump thread.") + # self._run_sequence() + timer = threading.Event() + while not timer.wait(self._pumpInterval): + if self.soilMoisture.getSoilPercentage() < 40: + # self._run_sequence() + print("Watering because soil moisture is at: {}\n".format(self.soilMoisture.getSoilPercentage()) + + self._printWatered()) + else: + print("Skipping watering because soil moisture is at: {:.2f}%".format( + self.soilMoisture.getSoilPercentage())) + # TODO Refactor sentinel to be part of the while loop so that when it is triggered the loop ends. + if self._sentinel.get(block=True): + self.logging.info("Sentinel was triggered in pump thread.") + self._sentinel.put(True) + self._sentinel.task_done() + break + self._sentinel.put(False) + self._sentinel.task_done() + except Exception as exception: + self.logging.info("There was an exception in the pump thread: ") + self.logging.info(exception) + + def setInterval(self, interval): + self._pumpInterval = interval + + def getInterval(self): + return self._pumpInterval + + def _togglePin(self, pin): + GPIO.output(pin, GPIO.HIGH) + GPIO.output(pin, GPIO.LOW) + + def _setup(self, pin): + GPIO.setmode(GPIO.BCM) + GPIO.setup(pin, GPIO.OUT) + + def _printWatered(self): + return """ + ,d + 88 + 8b db d8 ,adPPYYba, MM88MMM ,adPPYba, 8b,dPPYba, + `8b d88b d8' "" `Y8 88 a8P_____88 88P' "Y8 + `8b d8'`8b d8' ,adPPPPP88 88 8PP""""""" 88 + `8bd8' `8bd8' 88, ,88 88, "8b, ,aa 88 + YP YP `"8bbdP"Y8 "Y888 `"Ybbd8"' 88 + """ diff --git a/ModuleTests/GardenModules/soilMoisture/soil.py b/Tests/GardenModules/soilMoisture/soil.py similarity index 97% rename from ModuleTests/GardenModules/soilMoisture/soil.py rename to Tests/GardenModules/soilMoisture/soil.py index 25775ec..434af72 100644 --- a/ModuleTests/GardenModules/soilMoisture/soil.py +++ b/Tests/GardenModules/soilMoisture/soil.py @@ -1,76 +1,76 @@ -import board -import busio -import adafruit_ads1x15.ads1115 as ADS -import threading -from adafruit_ads1x15.analog_in import AnalogIn -from GardenModules.GardenModule import GardenModule -from datetime import datetime -from queue import Queue - - -class SoilMoisture(GardenModule): - def __init__(self, log, queue): - super().__init__(queue) - self.log = log - self._i2c = busio.I2C(board.SCL, board.SDA) - self._ads = ADS.ADS1115(self._i2c) - self._ads.gain = 1 - self.channel = AnalogIn(self._ads, ADS.P0) - self.soilInterval = 1800 # 1800 - self.log.info("Channel: " + str(self.channel)) - self.setName("soilThread") - - # Set Gain to 16 bits - # Gain = 1 # Wet: 13884 Dry: 21680 - # Gain = 2/3 # Wet: 11031 Dry: 14989 - self.queue = Queue() - self.sum = 0 - self.window_size = 5 - self.percentage = 0 - self.average_soil_value = 0 - for x in range(0,5): - self._checkSoil() - def _checkSoil(self): - try: - # I am using a moving average to remove sensor noise. - value = self.channel.value - if self.queue.qsize() >= self.window_size: - self.sum += value - self.sum -= self.queue.get() - self.average_soil_value = self.sum / self.window_size - self.percentage = ((self.sum / self.window_size) / 21680) * 100 - self.log.info("Soil Moisture Level: {} | Averaged Value: {:.2f}%".format(self.sum / self.window_size, self.getSoilPercentage())) - print("Soil Moisture Level: {} | Averaged Value: {:.2f}%".format(self.sum / self.window_size, self.getSoilPercentage())) - else: - self.queue.put(value) - self.sum += value - - except Exception as exception: - self.log.exception("Error calculating soil moisture") - - try: - with open("/home/pi/Desktop/smartGarden/smartGarden/logs/soilLog.txt", "a+") as logFile: - logFile.write("Soil Moisture Level: " + str(self.getSoilPercentage()) + "% " + str( - datetime.now()) + " Raw Value: " + str(self.channel.value) + "\n") - except Exception as exception: - self.log.exception("Error writing soil moisture level") - - def run(self): - self._checkSoil() - timer = threading.Event() - while not timer.wait(self.soilInterval): - self._checkSoil() - # TODO create a function for the sentinel - if self._sentinel.get(block=True): - print("Sentinel was triggered in soil thread.") - self._sentinel.put(True) - self._sentinel.task_done() - break - self._sentinel.put(False) - self._sentinel.task_done() - - def getSoilPercentage(self): - return 100 - self.percentage - - def getSoilValue(self): - return self.sum / self.window_size +import board +import busio +import adafruit_ads1x15.ads1115 as ADS +import threading +from adafruit_ads1x15.analog_in import AnalogIn +from GardenModules.GardenModule import GardenModule +from datetime import datetime +from queue import Queue + + +class SoilMoisture(GardenModule): + def __init__(self, log, queue): + super().__init__(queue) + self.log = log + self._i2c = busio.I2C(board.SCL, board.SDA) + self._ads = ADS.ADS1115(self._i2c) + self._ads.gain = 1 + self.channel = AnalogIn(self._ads, ADS.P0) + self.soilInterval = 1800 # 1800 + self.log.info("Channel: " + str(self.channel)) + self.setName("soilThread") + + # Set Gain to 16 bits + # Gain = 1 # Wet: 13884 Dry: 21680 + # Gain = 2/3 # Wet: 11031 Dry: 14989 + self.queue = Queue() + self.sum = 0 + self.window_size = 5 + self.percentage = 0 + self.average_soil_value = 0 + for x in range(0,5): + self._checkSoil() + def _checkSoil(self): + try: + # I am using a moving average to remove sensor noise. + value = self.channel.value + if self.queue.qsize() >= self.window_size: + self.sum += value + self.sum -= self.queue.get() + self.average_soil_value = self.sum / self.window_size + self.percentage = ((self.sum / self.window_size) / 21680) * 100 + self.log.info("Soil Moisture Level: {} | Averaged Value: {:.2f}%".format(self.sum / self.window_size, self.getSoilPercentage())) + print("Soil Moisture Level: {} | Averaged Value: {:.2f}%".format(self.sum / self.window_size, self.getSoilPercentage())) + else: + self.queue.put(value) + self.sum += value + + except Exception as exception: + self.log.exception("Error calculating soil moisture") + + try: + with open("/home/pi/Desktop/smartGarden/smartGarden/logs/soilLog.txt", "a+") as logFile: + logFile.write("Soil Moisture Level: " + str(self.getSoilPercentage()) + "% " + str( + datetime.now()) + " Raw Value: " + str(self.channel.value) + "\n") + except Exception as exception: + self.log.exception("Error writing soil moisture level") + + def run(self): + self._checkSoil() + timer = threading.Event() + while not timer.wait(self.soilInterval): + self._checkSoil() + # TODO create a function for the sentinel + if self._sentinel.get(block=True): + print("Sentinel was triggered in soil thread.") + self._sentinel.put(True) + self._sentinel.task_done() + break + self._sentinel.put(False) + self._sentinel.task_done() + + def getSoilPercentage(self): + return 100 - self.percentage + + def getSoilValue(self): + return self.sum / self.window_size diff --git a/ModuleTests/GardenModules/sunlightSensor/sunlight.py b/Tests/GardenModules/sunlightSensor/sunlight.py similarity index 97% rename from ModuleTests/GardenModules/sunlightSensor/sunlight.py rename to Tests/GardenModules/sunlightSensor/sunlight.py index d99843a..621e0fa 100644 --- a/ModuleTests/GardenModules/sunlightSensor/sunlight.py +++ b/Tests/GardenModules/sunlightSensor/sunlight.py @@ -1,78 +1,78 @@ -import logging -import sqlite3 -from datetime import datetime -from datetime import timedelta -import RPi.GPIO as GPIO - -def insertSunlightRecord(message,time1, time2): - insert_command = "INSERT INTO sunlight VALUES (\'" + message + "\',\'"+ time1 + "\',\'" + time2 + "\');" - try: - conn = sqlite3.connect('/home/pi/Desktop/smartGarden/smartGarden/gardenDatabase.db') - cursor = conn.cursor() - cursor.execute(insert_command) - conn.commit() - logging.info("Inserted sunlight record") - return cursor.lastrowid - except Exception as e: - logging.warn(e) - finally: - conn.close() - -def prune(): - time = datetime.now() - pruneDate = str(time - timedelta(days=30)).split() - - delete_command = "DELETE FROM sunlight WHERE record_timestamp LIKE '" + pruneDate[0] + "%';" - #select_command = "SELECT * FROM sunlight WHERE record_timestamp LIKE '" + pruneDate[0] + "%';" - #print("Prune Date: " + str(pruneDate)) - try: - conn = sqlite3.connect('/home/pi/Desktop/smartGarden/smartGarden/gardenDatabase.db') - cursor = conn.cursor() - cursor.execute(delete_command) - conn.commit() - except Exception as e: - print("Error: " + str(e)) - finally: - conn.close() - - -def check_sunlight(): - artificialLightHours = False - f = None - try: - GPIO.setmode(GPIO.BCM) - GPIO.setup(4, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) - f = open("/home/pi/Desktop/smartGarden/smartGarden/logs/sunlightLog.txt", "a+") - time = datetime.now() - dateTimeString = str(time) - cleanDateString = str(time + timedelta(days=30)) - currentTimeStamp = str(datetime.now()).split()[1] - currentHour = currentTimeStamp.split(':')[0] - hourAsInt = int(currentHour) - - if hourAsInt >= 18: - f.write("YES Artificial Sunlight at: " + dateTimeString + "\n") - insertSunlightRecord("YES Artificial Sunlight",dateTimeString,cleanDateString) - artificialLightHours = True - logging.info("Checked artificial sunlight") - if hourAsInt >=6 and hourAsInt <= 12: - f.write("YES Artificial Sunlight at: " + dateTimeString + "\n") - insertSunlightRecord("YES Artificial Sunlight",dateTimeString,cleanDateString) - artificialLightHours = True - logging.info("Checked artificial sunlight") - - if not artificialLightHours: - if not GPIO.input(4): - f.write("YES Natural Sunlight at: " + dateTimeString + "\n") - insertSunlightRecord("Yes Sunlight",dateTimeString,cleanDateString) - logging.info("Checked natural sunlight") - else: - f.write("NO Sunlight at: " + dateTimeString + "\n") - insertSunlightRecord("No Sunlight", dateTimeString, cleanDateString) - logging.info("Checked natural sunlight") - except Exception as e: - logging.warn("There was an error writing to file.") - logging.warn(e) - finally: - f.close() - +import logging +import sqlite3 +from datetime import datetime +from datetime import timedelta +import RPi.GPIO as GPIO + +def insertSunlightRecord(message,time1, time2): + insert_command = "INSERT INTO sunlight VALUES (\'" + message + "\',\'"+ time1 + "\',\'" + time2 + "\');" + try: + conn = sqlite3.connect('/home/pi/Desktop/smartGarden/smartGarden/gardenDatabase.db') + cursor = conn.cursor() + cursor.execute(insert_command) + conn.commit() + logging.info("Inserted sunlight record") + return cursor.lastrowid + except Exception as e: + logging.warn(e) + finally: + conn.close() + +def prune(): + time = datetime.now() + pruneDate = str(time - timedelta(days=30)).split() + + delete_command = "DELETE FROM sunlight WHERE record_timestamp LIKE '" + pruneDate[0] + "%';" + #select_command = "SELECT * FROM sunlight WHERE record_timestamp LIKE '" + pruneDate[0] + "%';" + #print("Prune Date: " + str(pruneDate)) + try: + conn = sqlite3.connect('/home/pi/Desktop/smartGarden/smartGarden/gardenDatabase.db') + cursor = conn.cursor() + cursor.execute(delete_command) + conn.commit() + except Exception as e: + print("Error: " + str(e)) + finally: + conn.close() + + +def check_sunlight(): + artificialLightHours = False + f = None + try: + GPIO.setmode(GPIO.BCM) + GPIO.setup(4, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) + f = open("/home/pi/Desktop/smartGarden/smartGarden/logs/sunlightLog.txt", "a+") + time = datetime.now() + dateTimeString = str(time) + cleanDateString = str(time + timedelta(days=30)) + currentTimeStamp = str(datetime.now()).split()[1] + currentHour = currentTimeStamp.split(':')[0] + hourAsInt = int(currentHour) + + if hourAsInt >= 18: + f.write("YES Artificial Sunlight at: " + dateTimeString + "\n") + insertSunlightRecord("YES Artificial Sunlight",dateTimeString,cleanDateString) + artificialLightHours = True + logging.info("Checked artificial sunlight") + if hourAsInt >=6 and hourAsInt <= 12: + f.write("YES Artificial Sunlight at: " + dateTimeString + "\n") + insertSunlightRecord("YES Artificial Sunlight",dateTimeString,cleanDateString) + artificialLightHours = True + logging.info("Checked artificial sunlight") + + if not artificialLightHours: + if not GPIO.input(4): + f.write("YES Natural Sunlight at: " + dateTimeString + "\n") + insertSunlightRecord("Yes Sunlight",dateTimeString,cleanDateString) + logging.info("Checked natural sunlight") + else: + f.write("NO Sunlight at: " + dateTimeString + "\n") + insertSunlightRecord("No Sunlight", dateTimeString, cleanDateString) + logging.info("Checked natural sunlight") + except Exception as e: + logging.warn("There was an error writing to file.") + logging.warn(e) + finally: + f.close() + diff --git a/Tests/README.md b/Tests/README.md new file mode 100644 index 0000000..d6e2759 --- /dev/null +++ b/Tests/README.md @@ -0,0 +1,3 @@ +# Module Tests + +This folder is used to test new modules or just anything to do with the garden. \ No newline at end of file diff --git a/test/alex.txt b/Tests/alex.txt similarity index 100% rename from test/alex.txt rename to Tests/alex.txt diff --git a/test/artificial_light_test.py b/Tests/artificial_light_test.py similarity index 100% rename from test/artificial_light_test.py rename to Tests/artificial_light_test.py diff --git a/test/camera_test.py b/Tests/camera_test.py similarity index 100% rename from test/camera_test.py rename to Tests/camera_test.py diff --git a/test/database_test.py b/Tests/database_test.py similarity index 100% rename from test/database_test.py rename to Tests/database_test.py diff --git a/test/email_desktop.py b/Tests/email_desktop.py similarity index 100% rename from test/email_desktop.py rename to Tests/email_desktop.py diff --git a/test/email_test.py b/Tests/email_test.py similarity index 100% rename from test/email_test.py rename to Tests/email_test.py diff --git a/test/i2c-test.py b/Tests/i2c-test.py similarity index 96% rename from test/i2c-test.py rename to Tests/i2c-test.py index 179dacb..599e29c 100644 --- a/test/i2c-test.py +++ b/Tests/i2c-test.py @@ -1,20 +1,20 @@ -import board -import busio -import adafruit_ads1x15.ads1115 as ADS -from adafruit_ads1x15.analog_in import AnalogIn -import time - -i2c = busio.I2C(board.SCL, board.SDA) -# Soil moisture is address 0x4a -# Light is address 48 -soil_ads = ADS.ADS1115(i2c, address=0x4a) - -light_ads = ADS.ADS1115(i2c) -light_ads.gain = 2/3 -chan1 = AnalogIn(soil_ads, ADS.P0) -chan2 = AnalogIn(light_ads, ADS.P0) - -while(True): - #print("soil value: " + str(chan1.value) + " soil voltage: " + str(chan1.voltage)) - print("light value: " + str(chan2.value) + " light voltage: " + str(chan2.voltage)) +import board +import busio +import adafruit_ads1x15.ads1115 as ADS +from adafruit_ads1x15.analog_in import AnalogIn +import time + +i2c = busio.I2C(board.SCL, board.SDA) +# Soil moisture is address 0x4a +# Light is address 48 +soil_ads = ADS.ADS1115(i2c, address=0x4a) + +light_ads = ADS.ADS1115(i2c) +light_ads.gain = 2/3 +chan1 = AnalogIn(soil_ads, ADS.P0) +chan2 = AnalogIn(light_ads, ADS.P0) + +while(True): + #print("soil value: " + str(chan1.value) + " soil voltage: " + str(chan1.voltage)) + print("light value: " + str(chan2.value) + " light voltage: " + str(chan2.voltage)) time.sleep(1) \ No newline at end of file diff --git a/test/image.jpeg b/Tests/image.jpeg similarity index 100% rename from test/image.jpeg rename to Tests/image.jpeg diff --git a/test/lightTest.py b/Tests/lightTest.py similarity index 100% rename from test/lightTest.py rename to Tests/lightTest.py diff --git a/test/light_sensor_test.py b/Tests/light_sensor_test.py similarity index 96% rename from test/light_sensor_test.py rename to Tests/light_sensor_test.py index bd74ff7..69d8133 100644 --- a/test/light_sensor_test.py +++ b/Tests/light_sensor_test.py @@ -1,16 +1,16 @@ -import board -import busio -import adafruit_tsl2561 -import time - -i2c = busio.I2C(board.SCL, board.SDA) -sensor = adafruit_tsl2561.TSL2561(i2c) -sensor.gain = 0 - -while True: - print('Lux: {}'.format(sensor.lux)) - print('Broadband: {}'.format(sensor.broadband)) - print('Infrared: {}'.format(sensor.infrared)) - print('gain: {}'.format(sensor.gain)) - print('Luminosity: {}\n'.format(sensor.luminosity)) - time.sleep(1) +import board +import busio +import adafruit_tsl2561 +import time + +i2c = busio.I2C(board.SCL, board.SDA) +sensor = adafruit_tsl2561.TSL2561(i2c) +sensor.gain = 0 + +while True: + print('Lux: {}'.format(sensor.lux)) + print('Broadband: {}'.format(sensor.broadband)) + print('Infrared: {}'.format(sensor.infrared)) + print('gain: {}'.format(sensor.gain)) + print('Luminosity: {}\n'.format(sensor.luminosity)) + time.sleep(1) diff --git a/ModuleTests/luxTest.py b/Tests/luxTest.py similarity index 95% rename from ModuleTests/luxTest.py rename to Tests/luxTest.py index a24e660..0500e32 100644 --- a/ModuleTests/luxTest.py +++ b/Tests/luxTest.py @@ -1,21 +1,21 @@ -import logging -import queue - -from GardenModules.luxSensor.luxSensor import LuxSensor - -logging.basicConfig(filename="testLog.log", level=logging.INFO) -sentinel = queue.Queue() -sentinel.put(False) - -sensor = LuxSensor(logging, sentinel) -sensor.start() - -while True: - try: - pass - except KeyboardInterrupt: - sentinel.put(True) - print("Ending test") - break - -sensor.join() +import logging +import queue + +from GardenModules.luxSensor.luxSensor import LuxSensor + +logging.basicConfig(filename="testLog.log", level=logging.INFO) +sentinel = queue.Queue() +sentinel.put(False) + +sensor = LuxSensor(logging, sentinel) +sensor.start() + +while True: + try: + pass + except KeyboardInterrupt: + sentinel.put(True) + print("Ending test") + break + +sensor.join() diff --git a/test/modules/soilMoisture/soil.py b/Tests/modules/soilMoisture/soil.py similarity index 96% rename from test/modules/soilMoisture/soil.py rename to Tests/modules/soilMoisture/soil.py index fb4ce22..cc85195 100644 --- a/test/modules/soilMoisture/soil.py +++ b/Tests/modules/soilMoisture/soil.py @@ -1,83 +1,83 @@ -import board -import busio -import adafruit_ads1x15.ads1115 as ADS -from adafruit_ads1x15.analog_in import AnalogIn -import sys -import time -import keyboard -from queue import Queue - -i2c = busio.I2C(board.SCL, board.SDA) -print("i2c configured") -ads = ADS.ADS1115(i2c, address=0x48) -ads.gain = 1 -print("ADS configured with gain: " + str(ads.gain)) -chan = AnalogIn(ads, ADS.P0) - -def test_min_max(): - min_value = sys.maxsize - max_value = -sys.maxsize - try: - while True: - value = chan.value - voltage = chan.voltage - max_value = max([value, int(max_value)]) - min_value = min([value, int(min_value)]) - print("value: {}\nvoltage: {}\nmin: {}\nmax: {}\n".format(value,voltage, min_value, max_value)) - time.sleep(0.5) - except KeyboardInterrupt: - print("\nfinal min: {}\n final max: {}".format(min_value, max_value)) - -def test_soil_moving_avg(): - window_size = int(input("Please input the window size: ")) - sum = 0 - buffer = Queue() - while True: - value = chan.value - try: - if buffer.qsize() >= window_size: - sum += value - sum -= buffer.get() - percentage = ((sum / window_size) / 21680) * 100 - print("Moisture level: {}\n percentage: {:.2f}".format(sum/window_size, percentage)) - buffer.put(value) - time.sleep(0.2) - else: - buffer.put(value) - sum += value - - if keyboard.is_pressed('q'): - print("Exiting...") - sys.exit() - except Exception as e: - print(e) - break - -def test_soil(): - - lowest = chan.value - highest = lowest - min = 1061 - max = 14667 - started = False - weight = 40 - valInt = 0 - while True: - if started: - rawVal = chan.value - val = (rawVal - min) / (max - min) - val = val * 100 - newValInt = round(val, 5) - rawVal = (weight * newValInt) + ((1 - weight) * valInt) - else: - rawVal = chan.value - #Normalization - val = (rawVal - min) / (max - min) - #Convert to a percentage - val = val * 100 - valInt = round(val, 5) - - print("Voltage: " + str(chan.voltage) + "\t\tValue: " + str(chan.value)) - - if not started: - started = True +import board +import busio +import adafruit_ads1x15.ads1115 as ADS +from adafruit_ads1x15.analog_in import AnalogIn +import sys +import time +import keyboard +from queue import Queue + +i2c = busio.I2C(board.SCL, board.SDA) +print("i2c configured") +ads = ADS.ADS1115(i2c, address=0x48) +ads.gain = 1 +print("ADS configured with gain: " + str(ads.gain)) +chan = AnalogIn(ads, ADS.P0) + +def test_min_max(): + min_value = sys.maxsize + max_value = -sys.maxsize + try: + while True: + value = chan.value + voltage = chan.voltage + max_value = max([value, int(max_value)]) + min_value = min([value, int(min_value)]) + print("value: {}\nvoltage: {}\nmin: {}\nmax: {}\n".format(value,voltage, min_value, max_value)) + time.sleep(0.5) + except KeyboardInterrupt: + print("\nfinal min: {}\n final max: {}".format(min_value, max_value)) + +def test_soil_moving_avg(): + window_size = int(input("Please input the window size: ")) + sum = 0 + buffer = Queue() + while True: + value = chan.value + try: + if buffer.qsize() >= window_size: + sum += value + sum -= buffer.get() + percentage = ((sum / window_size) / 21680) * 100 + print("Moisture level: {}\n percentage: {:.2f}".format(sum/window_size, percentage)) + buffer.put(value) + time.sleep(0.2) + else: + buffer.put(value) + sum += value + + if keyboard.is_pressed('q'): + print("Exiting...") + sys.exit() + except Exception as e: + print(e) + break + +def test_soil(): + + lowest = chan.value + highest = lowest + min = 1061 + max = 14667 + started = False + weight = 40 + valInt = 0 + while True: + if started: + rawVal = chan.value + val = (rawVal - min) / (max - min) + val = val * 100 + newValInt = round(val, 5) + rawVal = (weight * newValInt) + ((1 - weight) * valInt) + else: + rawVal = chan.value + #Normalization + val = (rawVal - min) / (max - min) + #Convert to a percentage + val = val * 100 + valInt = round(val, 5) + + print("Voltage: " + str(chan.voltage) + "\t\tValue: " + str(chan.value)) + + if not started: + started = True diff --git a/test/opencv_test.py b/Tests/opencv_test.py similarity index 94% rename from test/opencv_test.py rename to Tests/opencv_test.py index e4ef9a9..2971d2e 100644 --- a/test/opencv_test.py +++ b/Tests/opencv_test.py @@ -1,15 +1,15 @@ -import cv2 - -vid_cap = cv2.VideoCapture(0) -vid_cap.set(3, 1280) -vid_cap.set(4, 720) - -if not vid_cap.isOpened(): - raise Exception("could not open video device") - -for x in range(10): - ret, frame = vid_cap.read() - -cv2.imwrite("test.jpg", frame) - +import cv2 + +vid_cap = cv2.VideoCapture(0) +vid_cap.set(3, 1280) +vid_cap.set(4, 720) + +if not vid_cap.isOpened(): + raise Exception("could not open video device") + +for x in range(10): + ret, frame = vid_cap.read() + +cv2.imwrite("test.jpg", frame) + vid_cap.release() \ No newline at end of file diff --git a/test/pump.py b/Tests/pump.py similarity index 100% rename from test/pump.py rename to Tests/pump.py diff --git a/ModuleTests/pumpTest.py b/Tests/pumpTest.py similarity index 95% rename from ModuleTests/pumpTest.py rename to Tests/pumpTest.py index 3154bfe..9599473 100644 --- a/ModuleTests/pumpTest.py +++ b/Tests/pumpTest.py @@ -1,25 +1,25 @@ -import queue -import logging - -from GardenModules.pump.pump import WaterPump -from GardenModules.soilMoisture.soil import SoilMoisture - -logging.basicConfig(filename="testLog.log", level=logging.INFO) -sentinel = queue.Queue() -sentinel.put(False) - -soilMoistureSensor = SoilMoisture(logging, sentinel) -pump = WaterPump(logging, sentinel, soilMoistureSensor) - -soilMoistureSensor.start() -pump.start() - -while True: - try: - pass - except KeyboardInterrupt: - sentinel.put(True) - print("Ending test") - break - -pump.join() +import queue +import logging + +from GardenModules.pump.pump import WaterPump +from GardenModules.soilMoisture.soil import SoilMoisture + +logging.basicConfig(filename="testLog.log", level=logging.INFO) +sentinel = queue.Queue() +sentinel.put(False) + +soilMoistureSensor = SoilMoisture(logging, sentinel) +pump = WaterPump(logging, sentinel, soilMoistureSensor) + +soilMoistureSensor.start() +pump.start() + +while True: + try: + pass + except KeyboardInterrupt: + sentinel.put(True) + print("Ending test") + break + +pump.join() diff --git a/test/soil_test.py b/Tests/soil_test.py similarity index 100% rename from test/soil_test.py rename to Tests/soil_test.py diff --git a/test/sunlightLog.txt b/Tests/sunlightLog.txt similarity index 100% rename from test/sunlightLog.txt rename to Tests/sunlightLog.txt diff --git a/test/tempSensor.py b/Tests/tempSensor.py similarity index 100% rename from test/tempSensor.py rename to Tests/tempSensor.py diff --git a/test/test.jpg b/Tests/test.jpg similarity index 100% rename from test/test.jpg rename to Tests/test.jpg diff --git a/build_table.py b/build_table.py deleted file mode 100644 index 2c9e865..0000000 --- a/build_table.py +++ /dev/null @@ -1,106 +0,0 @@ -def build_table(ymd): - soilLogArray = [] - with open("/home/pi/Desktop/smartGarden/smartGarden/soilLog.txt", "r") as fp2: - for count, line in enumerate(fp2): - soilLogArray.append(line) - - with open("/home/pi/Desktop/smartGarden/smartGarden/sunlightLog.txt", "r") as fp: - for cnt, line in enumerate(fp): - lineArray = line.split() - currentYMD = str(datetime.now()).split()[0] - soilMoisture = "No Data" - soilTimeStamp = "No Data" - if cnt < len(soilLogArray): - try: - splitLine = soilLogArray[cnt].split() - soilMoisture = splitLine[3] - soilTimeStamp = splitLine[4] + " " + splitLine[5] - print("soilMoisture: " + soilMoisture) - print("soil time stamp: " + soilTimeStamp) - except Exception as e: - logging.warn("Unable to parse soil moisture or time stamp for email.") - logging.warn(e) - - if currentYMD == lineArray[3]: - if cnt % 2 == 0: - if "YES" in lineArray[0]: - row = "" + lineArray[0] + " " + lineArray[1] + "" - else: - row = "" + lineArray[0] + " " + lineArray[1] + "" - - row = row + "" + lineArray[3] + " " + lineArray[4]+ "" - row = row + "" + soilMoisture + "" - row = row + "" + soilTimeStamp + "" - else: - if "YES" in lineArray[0]: - row = "" + lineArray[0] + " " + lineArray[1] + "" - else: - row = "" + lineArray[0] + " " + lineArray[1] + "" - row = row + "" + lineArray[3] + " " + lineArray[4]+ "" - row = row + "" + soilMoisture + "" - row = row + "" + soilTimeStamp + "" - html = html + row - elif currentYMD == lineArray[4]: - if cnt % 2 == 0: - if "YES" in lineArray[0]: - row = "" + lineArray[0] + " " + lineArray[1] + " " + lineArray[2] + "" - else: - row = "" + lineArray[0] + " " + lineArray[1] + "" - row = row + "" + lineArray[4] + " " + lineArray[5]+ "" - row = row + "" + soilMoisture + "" - row = row + "" + soilTimeStamp + "" - else: - if "YES" in lineArray[0]: - row = "" + lineArray[0] + " " + lineArray[1] + " " + lineArray[2] + "" - else: - row = "" + lineArray[0] + " " + lineArray[1] + "" - row = row + "" + lineArray[4] + " " + lineArray[5]+ "" - row = row + "" + soilMoisture + "" - row = row + "" + soilTimeStamp + "" - html = html + row - html = html + """\ - - - - """ - -def build_sunlight_row(line, lineCount, currentYMD): - -lineArray = line.split() -sunlightText = "" -date = "" - -if currentYMD == lineArray[3]: - sunlightText = lineArray[0] + " " + lineArray[1] -elif currentYMD == lineArray[4]: - sunlightText = lineArray[0] + " " + lineArray[1] + " " + lineArray[2] - -hightlighted_row = "" -regular_row = "" -grey_row = "" -html = "" - - -if currentYMD == lineArray[3]: - if cnt % 2 == 0: - if "YES" in lineArray[0]: - row = hightlighted_row + sunlightText + "" - else: - row = regular_row + sunlightText + "" - else: - if "YES" in lineArray[0]: - row = "" + sunlightText + "" - else: - row = "" + sunlightText + "" - html = html + row -elif currentYMD == lineArray[4]: - if cnt % 2 == 0: - if "YES" in lineArray[0]: - row = "" + lineArray[0] + " " + lineArray[1] + " " + lineArray[2] + "" - else: - row = "" + lineArray[0] + " " + lineArray[1] + "" - else: - if "YES" in lineArray[0]: - row = "" + lineArray[0] + " " + lineArray[1] + " " + lineArray[2] + "" - else: - row = "" + lineArray[0] + " " + lineArray[1] + "" \ No newline at end of file diff --git a/install.sh b/install.sh index 9472e70..f43b2be 100755 --- a/install.sh +++ b/install.sh @@ -14,5 +14,5 @@ sudo apt-get install libjasper-dev sudo apt-get install libqtgui4 sudo apt-get install libqt4-test #paper trail -wget -qO - --header="X-Papertrail-Token: 5z2e1Jt2DIcHx9gjLcE" \ -https://papertrailapp.com/destinations/17877732/setup.sh | sudo bash +wget -qO - --header="X-Papertrail-Token: " \ +https://papertrailapp.com/destinations//setup.sh | sudo bash diff --git a/rc.local b/rc.local deleted file mode 100755 index 5105a66..0000000 --- a/rc.local +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/sh -e -# -# rc.local -# -# This script is executed at the end of each multiuser runlevel. -# Make sure that the script will "exit 0" on success or any other -# value on error. -# -# In order to enable or disable this script just change the execution -# bits. -# -# By default this script does nothing. - -# Print the IP address -_IP=$(hostname -I) || true -if [ "$_IP" ]; then - printf "My IP address is %s\n" "$_IP" -fi -exec 2> /home/pi/rc_local_logs.log -exec 1>&2 -set -x -sudo ./home/pi/go/src/github.com/papertrail/remote_syslog2/build/remote_syslog2/remote_syslog -p 35718 -d logs4.papertrailapp.com --pid-file=/var/run/remote_syslog.pid /home/pi/Desktop/smartGarden/smartGarden/logs/smartGardenLog.log & -sudo python3 /home/pi/Desktop/smartGarden/smartGarden/smartGarden.py &>> /home/pi/Desktop/smartGarden/smartGarden/smartGardenLog.txt & -#sudo /home/pi/go/src/github.com/elastic/beats/filebeat/filebeat -c /home/pi/go/src/github.com/elastic/beats/filebeat/filebeat.yml -e & -exit 0 diff --git a/smartGarden.py b/smartGarden.py index 5c5ec8b..36c59e2 100644 --- a/smartGarden.py +++ b/smartGarden.py @@ -31,128 +31,12 @@ image_count = 0 SHUTDOWN_FLAG = False -app = Flask(__name__, template_folder='/home/pi/Desktop/smartGarden/smartGarden/ControlPanel', - static_folder="/home/pi/Desktop/smartGarden/smartGarden/ControlPanel") +app = Flask(__name__, template_folder='./ControlPanel', + static_folder="./ControlPanel") CORS(app) Debug(app) -def create_folder(): - filename = str(datetime.now()).replace(" ", "-") - dateArray = filename.split('-') - ymd = dateArray[0] + "-" + dateArray[1] + "-" + dateArray[2] - try: - os.mkdir("/home/pi/Desktop/smartGarden/smartGarden/images/" + ymd) - except FileExistsError: - pass - finally: - return ymd - - -def zipdir(path, ziph): - logging.info("zipping path: " + path) - for root, dirs, files in os.walk(path): - logging.info("root: " + root) - for file in files: - ziph.write(os.path.join(root, file)) - - -def send_folder(ymd): - time1 = datetime.now() - logging.info("Zipping File... " + str(datetime.now())) - baseFolder = ymd - baseFolder = "/home/pi/Desktop/smartGarden/smartGarden/images/" + baseFolder - ymd = baseFolder + ".zip" - currentDirectory = os.path.dirname(os.path.realpath(__file__)) - os.chdir("/home/pi/Desktop/smartGarden/smartGarden/images") - zf = zipfile.ZipFile(ymd, mode='w', compression=zipfile.ZIP_LZMA) - try: - zipdir(baseFolder, zf) - finally: - zf.close() - logging.info("Sending images...") - try: - scp_command = "SSHPASS='al.EX.91.27' sshpass -e scp " + ymd + " alext@192.168.0.20:D:\\\\smartGarden\\\\Images" - except Exception as e: - logging.warn("There was an error sending the file to Cacutar") - logging.warn(e) - try: - os.system(scp_command) - os.system("rm " + ymd) - # os.system("rm -r " + baseFolder) - os.chdir(currentDirectory) - time2 = datetime.now() - diff = time2 - time1 - logging.info("It took (mins, seconds): " + str(divmod(diff.total_seconds(), 60)) + " to transfer " + str(ymd)) - except Exception as e: - logging.warn("There was an error deleting the folders and moving back a directory") - logging.warn(e) - - -def take_pics(ymd, number=1): - for x in range(number): - logging.info("Taking image " + str(x + 1) + " out of " + str(number)) - filename = str(datetime.now()).replace(" ", "-") - filename = filename.replace(":", "-") - filename = filename.replace(".", "-") - filename = filename + ".jpg" - # Take image - # old was 800x600 - vid_cap = cv2.VideoCapture(0) - vid_cap.set(3, 1280) - vid_cap.set(4, 720) - if not vid_cap.isOpened(): - logging.warn("Error opening video device using opencv") - else: - print("Taking picture") - for x in range(10): - ret, image = vid_cap.read() - cv2.imwrite("/home/pi/Desktop/smartGarden/smartGarden/images/" + ymd + "/" + str(filename), image) - vid_cap.release() - # print("Sending picture to: " + "/home/pi/Desktop/smartGarden/smartGarden/images/" + ymd + "/" + str(filename)) - # myCmd = 'fswebcam -q -i 0 -r 1280x720 /home/pi/Desktop/smartGarden/smartGarden/images/' + ymd + "/" + str(filename) - # os.system(myCmd) - - -def run_camera(send_folder): - ymd = create_folder() - if send_folder: - logging.info("Sending folder") - yesterday = datetime.now() - timedelta(days=1) - filename = str(yesterday).replace(" ", "-") - dateArray = filename.split('-') - ymd = dateArray[0] + "-" + dateArray[1] + "-" + dateArray[2] - send_folder(ymd) - ymd = create_folder() - take_pics(ymd) - - -def camera_thread(): - # TODO ADD ANOTHER THREAD FOR SENDING IMAGES TO CACTUAR PC - ymd = create_folder() - timer = threading.Event() - # run_camera(send_folder=False) - send_folder = False - sent_folder = False - while not timer.wait(CAMERA_TIME_SECONDS) and not SHUTDOWN_FLAG: - try: - time = str(datetime.now()).split() - hour = str(time[1].split(':')[0]) - except Exception as e: - print("Error parsing date for camera.") - print(e) - if hour == "00" and not sent_folder: - send_folder = True - sent_folder = True - elif hour != "00" and sent_folder: - sent_folder = False - else: - send_folder = False - run_camera(False) - if SHUTDOWN_FLAG: - break - - def prune_logs_thread(): # prune.prune("smartGardenLog.txt") timer = threading.Event() @@ -167,7 +51,7 @@ def prune_logs_thread(): if __name__ == "__main__": - logFile = "/home/pi/Desktop/smartGarden/smartGarden/logs/smartGardenLog.log" + logFile = "./logs/smartGardenLog.log" if not os.path.exists(logFile): with open(logFile, 'w+'): pass @@ -181,17 +65,12 @@ def prune_logs_thread(): pump = WaterPump(logging, sentinel, soilMoistureSensor) luxSensor = None - try: - luxSensor = LuxSensor(logging, sentinel) - except Exception as e: - logging.error("Failed to start lux sensor") - logging.error(e) + luxSensor = LuxSensor(logging, sentinel) tempSensor = TempSensor(logging, sentinel) artificialLight = ArtificialLight(logging, sentinel) server = GardenServer(sentinel, pump, luxSensor, soilMoistureSensor, tempSensor) signal.signal(signal.SIGINT, server.shutDownGarden) - # thread4 = threading.Thread(target=camera_thread) thread8 = threading.Thread(target=prune_logs_thread) print("Starting threads at time: " + str(datetime.now()) + "...") diff --git a/test/modules/soilMoisture/__pycache__/soil.cpython-35.pyc b/test/modules/soilMoisture/__pycache__/soil.cpython-35.pyc deleted file mode 100644 index 2eca134e3c5505be3991068c8dee54b20db99ffd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1195 zcmb7D%Wl&^6uo1|kIAEHOHoRZkT*z;kQ!3a1yzNpT2Ylpg%nhzkx*n}H*rYpFf(aN zWUtDa|KKb5fc}9OY*^(NSaE0iAQdYd&z<``_nsN2R;d&VKOcQqDFFO}xyL~LIWBR4 zBEXlx0D#@-1Q@yY8_+j_6|`x9xm~X07!s70ja^2EWo(|8Tu^+c20mu8~P5Upo|QsF(4(-c6Ejck}9+B zs%TB87nMU5l{A#06A@NHA*iKGuv7VO3A*iTcjd;N>ZHts)PxLqJCI?b3z-c8g$&si zA#))pK}O;12c!=02jr>>r@+^O)zsG8{j3CO34$`Lpe7fzfxVDy8B!Ng3@Bu=SUj;0IIX2x4r4z%Ukw3JwNtDC_R~YQ9QMkjbVD445Z%;x4q~gUVZfH zDZq=_I-+s~J;S~*vs4p$e4RO^lpgzoF&4e6S99y*SVRdk4>$KvPkbIQv$gjc#r|$? zjd&Ex+{6i)gQD4NZn1)zEjNYaTz%urc>6fFP8A|^uI9-to{Zz5USTFOW~LtqR!}oD zdyq^*A(_*Qx;<2KbEv?E{uy(G^tlWJW=}#4N@gSIVT?QXCpd9#qRnU=Dg*uBfiGeI z-!B`zWEeI^QDZ+8pJXy>h#>+UAeAt_rAuts7$(7Z5Q>IKqQRRa5)yAh8yll9IaMib z6|xap?4SU$N{U96P*O9NNs-iWr^KH7nW$5uVpVd_pvJO+nz3%!gx|sXy31YMxPyYN z`hkDK$B}H~nKaLuTN`Ru$Ld@uyr}Zl{>{_>_jVNPQ`g9v|D0IH%G}+#WcW;d0YY^y KlQK@WX#4?XC Date: Sun, 20 Jun 2021 19:40:20 -0700 Subject: [PATCH 2/4] Added a bunch of READMEs --- ControlPanel/README.md | 6 +++++- GardenModules/artificalLight/README.md | 3 +++ GardenModules/gardenServer/README.md | 3 +++ GardenModules/luxSensor/README.md | 3 +++ GardenModules/pump/README.md | 3 +++ GardenModules/soilMoisture/README.md | 3 +++ GardenModules/tempSensor/README.md | 3 +++ images/ControlPanel.PNG | Bin 0 -> 55156 bytes 8 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 GardenModules/artificalLight/README.md create mode 100644 GardenModules/gardenServer/README.md create mode 100644 GardenModules/luxSensor/README.md create mode 100644 GardenModules/pump/README.md create mode 100644 GardenModules/soilMoisture/README.md create mode 100644 GardenModules/tempSensor/README.md create mode 100644 images/ControlPanel.PNG diff --git a/ControlPanel/README.md b/ControlPanel/README.md index a5e9fbb..c155d51 100644 --- a/ControlPanel/README.md +++ b/ControlPanel/README.md @@ -1 +1,5 @@ -This is an website built to directly interface with the garden and display status from the various modules in the system. \ No newline at end of file +# Control Panel + +This is an website built to directly interface with the garden and display status from the various modules in the system. + +![alt text](https://raw.github.com/ataffe/smartGarden/master/images/ControlPanel.PNG) \ No newline at end of file diff --git a/GardenModules/artificalLight/README.md b/GardenModules/artificalLight/README.md new file mode 100644 index 0000000..befcf3d --- /dev/null +++ b/GardenModules/artificalLight/README.md @@ -0,0 +1,3 @@ +# Artificial Light Sensor + +This is a module to control a relay that controls artificial lighting for plants. This module was built for the [SunFounder 2 Channel DC 5V Relay Module with Optocoupler Low Level Trigger Expansion Board for Arduino](https://www.amazon.com/gp/product/B00E0NTPP4/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&psc=1) \ No newline at end of file diff --git a/GardenModules/gardenServer/README.md b/GardenModules/gardenServer/README.md new file mode 100644 index 0000000..b509106 --- /dev/null +++ b/GardenModules/gardenServer/README.md @@ -0,0 +1,3 @@ +# Garden Server + +This is a server that uses Flask to remotely interface with the garden. This includes a heartbeat REST endpoint, that can be used to detect if a sensor is still working. \ No newline at end of file diff --git a/GardenModules/luxSensor/README.md b/GardenModules/luxSensor/README.md new file mode 100644 index 0000000..21f33b5 --- /dev/null +++ b/GardenModules/luxSensor/README.md @@ -0,0 +1,3 @@ +# Lux Sensor + +This is a module for a [Adafruit TSL2591 High Dynamic Range Digital Light Sensor.](https://www.amazon.com/gp/product/B00XW2OFWW/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&psc=1) \ No newline at end of file diff --git a/GardenModules/pump/README.md b/GardenModules/pump/README.md new file mode 100644 index 0000000..130604b --- /dev/null +++ b/GardenModules/pump/README.md @@ -0,0 +1,3 @@ +# Water Pump Module + +This module is to control a DC water pump connected to a [DROK 200203 DC 5-36V 400W Dual Large Power MOS Transistor Driving Module](https://www.amazon.com/gp/product/B01J78FX9S/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&psc=1) [Here](https://www.amazon.com/gp/product/B07DW4WRV8/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&psc=1) is the pump used. \ No newline at end of file diff --git a/GardenModules/soilMoisture/README.md b/GardenModules/soilMoisture/README.md new file mode 100644 index 0000000..aef895c --- /dev/null +++ b/GardenModules/soilMoisture/README.md @@ -0,0 +1,3 @@ +# Soil Moisture Sensor + +This is a module to control a [Gikfun Capacitive Soil Moisture Sensor](https://www.amazon.com/gp/product/B07H3P1NRM/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&psc=1) via I2C. The soil moisture sensor is connected to a [Onyehn ADS1115 16 Byte 4 Channel I2C IIC Analog-to-Digital ADC Converter](https://www.amazon.com/gp/product/B07L3Q7N7T/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&psc=1) \ No newline at end of file diff --git a/GardenModules/tempSensor/README.md b/GardenModules/tempSensor/README.md new file mode 100644 index 0000000..fc4d7e5 --- /dev/null +++ b/GardenModules/tempSensor/README.md @@ -0,0 +1,3 @@ +# Temperature Sensor + +This is a module to read data from a [DFRobot Waterproof DS18B20 Temperature Sensor Kit](https://www.amazon.com/gp/product/B07434MB77/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&psc=1) \ No newline at end of file diff --git a/images/ControlPanel.PNG b/images/ControlPanel.PNG new file mode 100644 index 0000000000000000000000000000000000000000..4b634f9ef24bad37f23aa58900d048f4ad48522d GIT binary patch literal 55156 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 Date: Sun, 20 Jun 2021 19:44:32 -0700 Subject: [PATCH 3/4] Update README.md --- ControlPanel/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ControlPanel/README.md b/ControlPanel/README.md index c155d51..3284c98 100644 --- a/ControlPanel/README.md +++ b/ControlPanel/README.md @@ -2,4 +2,4 @@ This is an website built to directly interface with the garden and display status from the various modules in the system. -![alt text](https://raw.github.com/ataffe/smartGarden/master/images/ControlPanel.PNG) \ No newline at end of file +![alt text](https://raw.github.com/ataffe/smartGarden/Dev/images/ControlPanel.PNG) From 10104479c1ce667b9a091f6851cd4de63c6c2d99 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 20 Jun 2021 19:47:51 -0700 Subject: [PATCH 4/4] Update README.md --- ControlPanel/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ControlPanel/README.md b/ControlPanel/README.md index 3284c98..0b8ee62 100644 --- a/ControlPanel/README.md +++ b/ControlPanel/README.md @@ -2,4 +2,6 @@ This is an website built to directly interface with the garden and display status from the various modules in the system. +## **Warning:** This will probably be replaced with [Home Assistant](https://www.home-assistant.io/) in the future. + ![alt text](https://raw.github.com/ataffe/smartGarden/Dev/images/ControlPanel.PNG)