# Project export: The Botanical Bots

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: CruzHacks 2025
- Tagline: Botanical Bots is a battery-powered plant care system that waters and monitors your plant. Users can select their plant through a web app and track info such as moisture, pH, and temperature.
- Devpost: https://devpost.com/software/the-botanical-bots
- GitHub: https://github.com/RTryantaylor/Botanical_Bots
- Result: winner (Sustainability Hacks)
- Team: 3 GitHub contributor(s) — Andrew Kuznetsov (12 commits), ChadxBaker52 (9 commits), Ryan Taylor (2 commits)

## Devpost submission (written by the team)

No Devpost description available.

## README (from the GitHub repository)

﻿# Botanical_Bots


## Detected evidence (automated analysis)

Indexed codebase: 17 recognized source files, 38 KB.
- CSS (language) — detected in the code
- Flask (technology) — detected in the code
- HTML (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- TypeScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (36 of 36)

```
.gitignore
backend/graph_sensor_data.py
backend/jsons/hours_sensor_data.json
backend/jsons/latest_sensor_data.json
backend/jsons/plant_types.json
backend/jsons/todays_sensor_data.json
backend/jsons/weeks_sensor_data.json
backend/main.py
botanical_bots/botanical_bots.ino
esp_32_check/esp_32_check.ino
frontend/.env
frontend/.gitignore
frontend/package.json
frontend/public/index.html
frontend/public/manifest.json
frontend/public/robots.txt
frontend/README.md
frontend/src/AboutUsScreenComps/AboutUsScreen.css
frontend/src/AboutUsScreenComps/AboutUsScreen.tsx
frontend/src/App.tsx
frontend/src/GraphScreenComps/GraphScreen.tsx
frontend/src/GraphScreenComps/LineGraph.tsx
frontend/src/HomeScreenComps/HomeScreen.css
frontend/src/HomeScreenComps/HomeScreen.tsx
frontend/src/HomeScreenComps/PlantSelector.tsx
frontend/src/index.css
frontend/src/index.tsx
frontend/src/NavHeader.css
frontend/src/NavHeader.tsx
frontend/tsconfig.json
README.md
requirements.txt
stl_files/base_top.stl
stl_files/base.stl
stl_files/electronics_holder_top.stl
stl_files/electronics_holder.stl
```

### Dependencies

- frontend/package.json: @testing-library/dom@^10.4.0, @testing-library/jest-dom@^6.6.3, @testing-library/react@^16.3.0, @testing-library/user-event@^13.5.0, @types/jest@^27.5.2, @types/node@^16.18.126, @types/react@^19.1.1, @types/react-dom@^19.1.2, axios@^1.8.4, react@^19.1.0, react-dom@^19.1.0, react-scripts@5.0.1, react-select@^5.10.1, recharts@^2.15.2, typescript@^4.9.5, web-vitals@^2.1.4
- requirements.txt: Flask@==2.3.3, flask-cors@==4.0.0, requests@==2.32.3

### Recent commits (newest first)

- touch up about us
- touch up main backend
- resetting backend/jsons/todays_sensor_data.json
- Merge pull request #1 from RTryantaylor/frontend
- final Changes
- pushing final changes
- Pushing graph changes, and beautifying frontend
- pushing hardcoded plant data
- changed checkWatering to hybrid
- Touched up some graph stuff, as well as added hour graphs
- fixed checkWatering()
- added get request
- all sensors working, sending data to server
- Finished adding hardcoded plants, displaying more info on hompage
- Possibly fully working graphs
- basic setup
- Added graphs for all variables not cleaned up, only for today's values
- Merge branch 'main' of github.com:RTryantaylor/Botanical_Bots
- changes
- add stl files to github

## Key source files (fetched from GitHub, selected and truncated for size)

### requirements.txt

```
Flask==2.3.3
flask-cors==4.0.0
requests==2.32.3
```

### frontend/package.json

```
{
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "proxy": "http://192.168.1.159:8085",
  "dependencies": {
    "@testing-library/dom": "^10.4.0",
    "@testing-library/jest-dom": "^6.6.3",
    "@testing-library/react": "^16.3.0",
    "@testing-library/user-event": "^13.5.0",
    "@types/jest": "^27.5.2",
    "@types/node": "^16.18.126",
    "@types/react": "^19.1.1",
    "@types/react-dom": "^19.1.2",
    "axios": "^1.8.4",
    "react": "^19.1.0",
    "react-dom": "^19.1.0",
    "react-scripts": "5.0.1",
    "react-select": "^5.10.1",
    "recharts": "^2.15.2",
    "typescript": "^4.9.5",
    "web-vitals": "^2.1.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}
```

### backend/main.py

```python
from flask import Flask, jsonify, request
from flask_cors import CORS

import json

from graph_sensor_data import graph_sensor_data


app = Flask(__name__)
CORS(app)  # Allows requests from React

@app.route("/get_sensor_data")
def get_sensor_data():
    """
    POLLED by frontend every 5 seconds
    
    returns: latest sensor data from local json
    """

    with open("jsons/latest_sensor_data.json", "r") as f:
        data = json.load(f)
    
    return data
    

@app.route("/put_sensor_data", methods=['PUT'])
def put_sensor_data():
    r_data = request.get_json()

    print("data:", r_data)

    # opens local json
    with open("jsons/latest_sensor_data.json", "r") as f:
        j_data = json.load(f)

    # moves data from request json to local json
    j_data["moisture"] = ((4096 - r_data.get("moisture")) / 200)  # moisture function
    j_data["ph"] = r_data.get("ph")
    j_data["temp"] = (r_data.get("temp") * 9/5) + 32 # converting to fahrenheit
    j_data["light"] = r_data.get("light")

    # writes new data to local json
    with open("jsons/latest_sensor_data.json", "w") as f:
        json.dump(j_data, f, indent=4)

    graph_sensor_data(j_data)

    return jsonify({"status": "OK"}), 200

@app.route("/get_hour_graph", methods=['GET'])
def get_hours_graph_entries():
    """
    Called by frontend returns values for each variable for each minute for the hour
    """
    with open("jsons/hours_sensor_data.json", 'r') as f:
        j_data = json.load(f)

    return j_data

@app.route("/get_today_graph", methods=['GET'])
def get_todays_graph_entries():
    """
    Called by frontend returns values for each variable for each hour for today
    """
    with open("jsons/todays_sensor_data.json", 'r') as f:
        j_data = json.load(f)
    
    return j_data

@app.route("/get_week_graph", methods=['GET'])
def get_weeks_graph_entries():
    """
    Called by frontend returns values for each variable for each day in a week
    """
    with open("jsons/weeks_sensor_data.json", 'r') as f:
        j_data = json.load(f)

    return j_data

@app.route("/get_plant_types", methods=['GET'])
def get_plant_types():
    """
    Called by frontend returns plant types and variable values
    and currently selected plant
    """
    with open("jsons/plant_types.json", 'r') as f:
        j_data = json.load(f)

    return j_data

@app.route("/set_selected_plant", methods=['PUT'])
def set_selected_plant():
    r_data = request.get_json()

    # update local json
    with open("jsons/plant_types.json", 'r') as f:
        j_data = json.load(f)
    
    j_data["curr_selected"] = r_data.get("plant")

    with open("jsons/plant_types.json", 'w') as f:
        json.dump(j_data, f, indent=4)

    return jsonify({"status": "OK"}), 200 

@app.route("/get_selected_plant", methods=['GET'])
def get_selected_plant():
    """
    Called by ESP32 to get settings for curr selected plant
    """
    with open("jsons/plant_types.json", 'r') as f:
        j_data = json.load(f)
    

    plant_types = j_data.get("types", {})
    for plant in plant_types:
        if plant == j_data["curr_selected"]:
            return {"moisture": plant_types[plant]["moisture"]}
    
    return {"moisture": 50} # default
    

if __name__ == "__main__":
    app.run(host="0.0.0.0", debug=True, port=8085)
```

### frontend/src/index.tsx

```typescript
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

```

### frontend/src/App.tsx

```typescript
import React from 'react';
import { useState, useEffect } from 'react';
import axios from "axios";

import NavHeader from "./NavHeader";
import HomeScreen from './HomeScreenComps/HomeScreen';
import AboutUsScreen from './AboutUsScreenComps/AboutUsScreen';
import GraphScreen from './GraphScreenComps/GraphScreen';

function App() {
  const [currentScreen, setCurrentScreen] = useState("Home");
  const renderScreen = () => {
    switch (currentScreen) {
      case "Home":
        return <HomeScreen />;
      case "Graphs":
        return <GraphScreen />;
      case "AboutUs":
        return <AboutUsScreen />;
      default:
        return <h2>Page Not Found</h2>;
    }
  };

  const [sensorData, setSensorData] = useState({
    temp: 0,
    ph: 0,
    moisture: 0,
    light: 0,
  })
  
  useEffect(() => {
    const fetchData = () => {
      axios.get("http://192.168.1.159:8085/get_sensor_data")
        .then(res => setSensorData(res.data))
        .catch(err => console.error("Error fetching sensor data:", err));
    };

    fetchData(); // Get immediately on mount
    const interval = setInterval(fetchData, 5000); // Every 5s

    return () => clearInterval(interval); // Cleanup on unmount
  }, [])

  return (
    <div>
      <NavHeader onNavigate={setCurrentScreen} />
      <div style={{ padding: "2rem" }}>{renderScreen()}</div>
    </div>
  );
}

export default App;

```

### backend/graph_sensor_data.py

```python
from datetime import datetime, timedelta
import json


def graph_sensor_data(r_data):
    """
    Graphs the latest sensor data, updating:
    - hours_sensor_data.json (minute-by-minute entries within current hour)
    - todays_sensor_data.json (hourly averages for the current day)
    - weeks_sensor_data.json (daily averages for the last 7 days)
    """

    now = datetime.now()
    current_date = now.date()
    current_date_str = str(current_date)
    current_hour = str(now.hour).zfill(2)
    current_minute = str(now.minute).zfill(2)

    # Load existing data
    with open("jsons/hours_sensor_data.json", "r") as f:
        hours_j_data = json.load(f)

    with open("jsons/todays_sensor_data.json", "r") as f:
        todays_j_data = json.load(f)

    # --- HOURS SENSOR DATA HANDLING ---
    if (
        (hours_j_data["date"] == current_date_str and hours_j_data["hour"] == current_hour)
        or (hours_j_data["date"] == "" and hours_j_data["hour"] == "")
    ):
        print("IN IF")
        # Still in the same hour — just log a new minute entry
        hours_j_data["date"] = current_date_str
        hours_j_data["hour"] = current_hour
        entries = hours_j_data.get("entries", {})

        if current_minute not in entries:
            entries[current_minute] = {
                "moisture": r_data.get("moisture"),
                "ph": r_data.get("ph"),
                "temp": r_data.get("temp"),
                "light": r_data.get("light"),
            }
            hours_j_data["entries"] = entries

            with open("jsons/hours_sensor_data.json", "w") as f:
                json.dump(hours_j_data, f, indent=4)

    elif hours_j_data.get("date") == current_date_str and hours_j_data.get("hour") != current_hour:
        print("IN ELIF")
        # Hour has changed → average and dump into today's data
        entries = hours_j_data.get("entries", {})
        totals = { "moisture": 0, "ph": 0, "temp": 0, "light": 0 }
        count = 0

        for minute_data in entries.values():
            for key in totals:
                totals[key] += minute_data.get(key, 0)
            count += 1

        if count > 0:
            # --- DAY TRANSITION CHECK BEFORE UPDATING TODAY ---
            if todays_j_data.get("date") not in [current_date_str, ""]:
                # Load and update weeks data
                with open("jsons/weeks_sensor_data.json", "r") as f:
                    weeks_j_data = json.load(f)

                weeks_entries = weeks_j_data.get("entries", {})
                seven_days_ago = current_date - timedelta(days=6)

                for date_str in list(weeks_entries.keys()):
                    try:
                        date_obj = datetime.strptime(date_str, "%Y-%m-%d").date()
                        if date_obj < seven_days_ago:
                            del weeks_entries[date_str]
                    except ValueError:
                        continue

                # Compute yesterday's average
                prev_entries = todays_j_data.get("entries", {})
                day_totals = { "moisture": 0, "ph": 0, "temp": 0, "light": 0 }
                day_count = 0

                for hour_data in prev_entries.values():
                    for key in day_totals:
                        day_totals[key] += hour_data.get(key, 0)
                    day_count += 1

                if day_count > 0:
                    weeks_entries[todays_j_data["date"]] = {
                        key: round(day_totals[key] / day_count, 2) for key in day_totals
                    }

                weeks_j_data["entries"] = weeks_entries

                with open("jsons/weeks_sensor_data.json", "w") as f:
                    json.dump(weeks_j_data, f, indent=4)

                # Reset today's data
                todays_j_data = {
                    "date": current_date_str,
                    "entries": {}
                }

            # --- Add this past hour's average to today's data ---
            todays_entries = todays_j_data.get("entries", {})
            todays_entries[hours_j_data["hour"]] = {
                key: round(totals[key] / count, 2) for key in totals
            }
            todays_j_data["entries"] = todays_entries
            todays_j_data["date"] = current_date_str

            with open("jsons/todays_sensor_data.json", "w") as f:
                json.dump(todays_j_data, f, indent=4)

        # --- Reset the hour data for the new hour ---
        hours_j_data = {
            "date": current_date_str,
            "hour": current_hour,
            "entries": {
                current_minute: {
                    "moisture": r_data.get("moisture"),
                    "ph": r_data.get("ph"),
                    "temp": r_data.get("temp"),
                    "light": r_data.get("light"),
                }
            }
        }

        with open("jsons/hours_sensor_data.json", "w") as f:
            json.dump(hours_j_data, f, indent=4)
    else:
        # hour and day wrong!
        # wipe hour_sensor_data.json and update it
        print("IN ELSE")
        hours_j_data["date"] = current_date_str
        hours_j_data["hour"] = current_hour

        hours_j_data["entries"][current_minute] = {
            "moisture": r_data.get("moisture"),
                "ph": r_data.get("ph"),
                "temp": r_data.get("temp"),
                "light": r_data.get("light"),
        }

        with open("jsons/hours_sensor_data.json", "w") as f:
            json.dump(hours_j_data, f, indent=4)
```

### frontend/src/index.css

```css
body {
  background-color: #F0EAD6;
  margin: 0;
  font-family: 'Inter', 'Roboto', 'Helvetica Neue', sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}
```

### frontend/src/NavHeader.css

```css
.nav-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    background-color: #885920;
    color: white;
    padding: 1.5rem 2rem;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.nav-left {
    font-size: 1.1rem;
    font-weight: bold;
}

.nav-item {
    cursor: pointer;
    font-weight: 500;
    transition: color 0.2s ease;
}

.nav-item:hover {
    color: #30a31b;
}

.nav-right {
    display: flex;
    gap: 12rem;
    margin-right: 10rem;
}
```

### frontend/src/NavHeader.tsx

```typescript
import React from "react";
import "./NavHeader.css";

const NavHeader = ({ onNavigate }: { onNavigate: (screen: string) => void }) => {
  return (
    <header className="nav-header">
      <nav className="nav-left">
        <strong>Botanical Bots</strong>
      </nav>
      <div className="nav-right">
        <span className="nav-item" onClick={() => onNavigate("Home")}><b>Home</b></span>
        <span className="nav-item" onClick={() => onNavigate("Graphs")}><b>Graphs</b></span>
        <span className="nav-item" onClick={() => onNavigate("AboutUs")}><b>About Us</b></span>
      </div>
    </header>
  );
};

export default NavHeader;

```

### frontend/public/index.html

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>React App</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>

```

[7 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]