diff --git a/backend/automations.json b/backend/automations.json new file mode 100644 index 0000000..1a66100 --- /dev/null +++ b/backend/automations.json @@ -0,0 +1,12 @@ +{ + "automation_example": [ + { + "id": 1, + "device": "absolute device name goes here", + "name": "Name goes here", + "enabled": true, + "triggers": "time goes here (in 24 hour format)", + "action": "function call goes here" + } + ] +} \ No newline at end of file diff --git a/backend/automations.py b/backend/automations.py index e69de29..cc1b2f4 100644 --- a/backend/automations.py +++ b/backend/automations.py @@ -0,0 +1,58 @@ +import devices_json as dj +import schedule + +automationFile = "automations.json" + +import json +import schedule + +automationFile = "automations.json" +updates = [] + + +def loadAutomations(): + try: + with open(automationFile, "r") as JSONfile: + return json.load(JSONfile) + except FileNotFoundError: + updates.append("Error: automations.json not found!") + return {"automations": []} + + +def saveAutomations(data): + with open(automationFile, "w") as JSONfile: + json.dump(data, JSONfile, indent=2) + + +def addAutomation(automation): # Add automation to automations.json + data = loadAutomations() + automations = data.get("automations", []) + automations.append(automation) + saveAutomations(data) + updates.append(f"Added {automation['name']} to the automation table!") + + +# Manipulation of automation schedules down below +def changeAutomationStatus(id): + data = loadAutomations() + automations = data.get("automations", []) + for automation in automations: + if automation["id"] == id: + automation["enabled"] = not automation["enabled"] + saveAutomations(data) + updates.append(f"Changed status of {automation['name']} to {automation['enabled']}!") + return + updates.append("Error: Automation with specified ID not found (backend error!)") + return + +def deleteAutomation(id): + data = loadAutomations() + automations = data.get("automations", []) + for automation in automations: + if automation["id"] == id: + automations.remove(automation) + saveAutomations(data) + updates.append(f"Deleted {automation['name']} from the automation table!") + return + updates.append("Error: Automation with specified ID not found (backend error!)") + return diff --git a/backend/devices_json.py b/backend/devices_json.py index a50e677..042560b 100644 --- a/backend/devices_json.py +++ b/backend/devices_json.py @@ -106,6 +106,9 @@ def sumRating(): devices = data.get("smart_home_devices", []) return sum(device["power_rating"] for device in devices) +def deviceFunctions(): # Returns the list of device functions for scheduling purposes and possibly miscellaneous purposes + return ["Set Oven Timer", "Change Device Status"] + async def updateDevices(): while True: data = loadJSON() diff --git a/backend/fastAPI.py b/backend/fastAPI.py index e6591de..77ef1dd 100644 --- a/backend/fastAPI.py +++ b/backend/fastAPI.py @@ -71,3 +71,8 @@ def get_user_data(): return {"error": f"User database file not found"} except json.JSONDecodeError: return {"error": "Error decoding JSON data"} + +@app.get("/device_functions") +def get_device_functions(): + """Returns the list of device functions.""" + return {"functions": dj.deviceFunctions()} \ No newline at end of file diff --git a/backend/users.py b/backend/users.py new file mode 100644 index 0000000..8f9cac3 --- /dev/null +++ b/backend/users.py @@ -0,0 +1 @@ +selected_user = "" \ No newline at end of file diff --git a/database/.env b/database/.env new file mode 100644 index 0000000..260588c --- /dev/null +++ b/database/.env @@ -0,0 +1,3 @@ +FIREBASE_PROJECT_ID=powerhouse-62f4d +FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC+dm+isAoRCwvI\nzuymf7bLtZ0ig1YLLVpcEsG36wX2JjXuvCptgQ35VKUX1rE5t7ChIt7Rv/c8+Z3o\nr1fSCS9Xowq64pmX5MXzyI8WXsolg+zoSGxBo+puwSkqfnfo5ozRaUz7YjkosGIM\nX2+A3+jAf1bge6X3CMXerTer1dr9XqOtA+JQ01rkb47Hvx+ty2KrDuVMw/b0ERbv\nIa5P7e7fVklF7yFNEGdcZck5+X6Dc79otvH1WFg82to2Id42lnt2zvEZRRJdIugU\nDelt41LHGiQ8fZzrUMH2ztSPuDjSOwL1EMscZaIlULOl34uH4yIifnEC4N+X56l9\nmGbFQDvzAgMBAAECggEAXCmSVn5puJqIPsjGYcD+Eq/UlA1KdwkgptoarCUJUvVl\ndprNywQ+X05yvsjQJFY7HZXyjTufZv2AyEYEsGlqY7VM7WbWHNaCZTdA6fm/Ro0C\nd9oHrHGlcKd+hIYC/wuEy5ZxPllmwEYtg8AwhAOVLITR5Llgc1vm55wZCsjmeWBa\n/feT1HVPH550NZyGvnmokrnq74kpEKG/pEHB1cG9hkJOPwLoTbEIE3eHDBI2xSaI\nlGOBNEi21q8isYLXSjPCzHINBr+/59VbkJGMHBgIYUDLgAUdZbAzvL3PQTyPCrPH\nIrCU1lg/KOJNjnsFr1196thtaDyfjwppII02j+0SjQKBgQDiZD1XdDyUiBAb1Zdc\nILvojGRro3clSTZtTVpu+l60IldY4rrQ8uhBnpO0bQQflt0iq8N4IC6QsCAOzK6U\noH+6ZfgYL5AZJKU5D5h+lNdMO963X1M+7NmqiXJygYLrh2wSpXceIjJHh3HxoYn9\npu6J5zgtTbJ/AffNB5aYQZUl9wKBgQDXX0TjHkpGXxrSYgGgWtNu5VfNnbDluKKt\nbQ2s6BNC7H74IGMF4/CmkzJk+u3t4Kqy6fiLcdC11NNbMc1WjOft8X2laGQRxL+8\n6SwwpuAEtYffwv5R8XzhTzaxQ0QykOZHF0KoXU90U52FLhnO4CabD/FBNDTGwyrl\n81WWJItq5QKBgQCkNI6lkyKpnTMJ7Vd2tCt3VfU0eYFe4wZ+KyAP5LuyLkJqrzFP\ngxBhIBMESBo4i2Vj02/Y/oan3YezFFUAapWaboGPkVgUVJcelyhFj9HFcpttJ9Aq\nkQKkBkOjuDpseXIy6B9JFQahCSXlXATJnT3henstqPBx3MyOwxVA5Di5KwKBgHuc\nFD9+LQcCorRIx8IvHtaxDpnWfab7N3Lt+kFY2WU5sw3aGLtSvdX8+Zl3Y4N5xL73\nBk2SP0V9JulaRg38xUiNp519sqF4GMvl0BUEUeKiBZ/pnGmPfisaebt3bfbPrzry\nILHdDDMpomjDstBHWiD5H3Ba9Ed30HE2HdMUmmf5AoGAVXZD00+Z2MPZZXNmIoNt\nl2p5y+M/UCm8GP93287DaqVzeW1kpbwNmMPAgwUr0rrv+k/EpOUpgZbtCmttx3F5\nsTnX7nt3MwDTXUrtRMqVkahzWpf2KlgicQzbeH2CrtvdvLchAry0x3DQ0QAJWZ6O\nSCPTXOwvEU3x63TkwO+Ogbs=\n-----END PRIVATE KEY-----\n" +FIREBASE_CLIENT_EMAIL=firebase-adminsdk-fbsvc@powerhouse-62f4d.iam.gserviceaccount.com diff --git a/database/devices_db.py b/database/devices_db.py index 08d9c67..46f02c0 100644 --- a/database/devices_db.py +++ b/database/devices_db.py @@ -1,63 +1,135 @@ -import firebase_admin -from firebase_admin import credentials -from firebase_admin import firestore +import os +import time import json -import os - +import firebase_admin +from firebase_admin import credentials, firestore +import watchdog +from watchdog.observers import Observer +from watchdog.events import FileSystemEventHandler from dotenv import load_dotenv +from hashlib import sha256 + +# Load environment variables +load_dotenv() + +# Firebase credentials +FIREBASE_PROJECT_ID = os.getenv('FIREBASE_PROJECT_ID') +FIREBASE_PRIVATE_KEY = os.getenv('FIREBASE_PRIVATE_KEY').replace('\\n', '\n') # Fix newlines +FIREBASE_CLIENT_EMAIL = os.getenv('FIREBASE_CLIENT_EMAIL') + +firebase_config = { + "type": "service_account", + "project_id": FIREBASE_PROJECT_ID, + "private_key": FIREBASE_PRIVATE_KEY, + "client_email": FIREBASE_CLIENT_EMAIL, + "token_uri": "https://oauth2.googleapis.com/token" +} + +# Initialize Firebase Admin SDK if not already initialized +if not firebase_admin._apps: + cred = credentials.Certificate(firebase_config) + firebase_admin.initialize_app(cred) -cred = credentials.Certificate("/Users/annerinjeri/Downloads/powerhouse-62f4d-firebase-adminsdk-fbsvc-9a2e946c98.json") -firebase_admin.initialize_app(cred) +# Firestore reference db = firestore.client() -load_dotenv() +# Path to the JSON file +JSON_FILE_PATH = os.path.join(os.path.dirname(__file__), "../backend/devices.json") + +# Track last JSON hash to avoid unnecessary updates +last_json_hash = None + + +def hash_file_content(file_path): + """Calculate SHA256 hash of the file content.""" + with open(file_path, "r") as f: + content = f.read() + return sha256(content.encode()).hexdigest() + + +def update_firestore(): + """Sync JSON data with Firestore.""" + global last_json_hash + + new_hash = hash_file_content(JSON_FILE_PATH) + if new_hash == last_json_hash: + print("No changes detected in JSON. Skipping Firestore update.") + return # Skip update if the content hasn't changed -LAST_MODIFIED_TIME = 0 + last_json_hash = new_hash # Update hash -def set_doc_data(coll_ref, file): - rel_path=rel_path = os.path.join(os.path.dirname(__file__), f"../backend/{file}") - JSONfile = open(rel_path, "r") - data = json.load(JSONfile) - JSONfile.close() + with open(JSON_FILE_PATH, "r") as f: + data = json.load(f) + # Filter devices that are "connected" filtered_devices = [ - {key: device[key] for key in ["id", "name", "ip"]} - for device in data["smart_home_devices"] - - if device.get("connection_status") == "connected" + {key: device[key] for key in ["id", "name", "ip"]} + for device in data["smart_home_devices"] + if device.get("connection_status") == "connected" ] - for i in range(len(filtered_devices)): - coll_ref.document().set(filtered_devices[i]) - -def delete_collection(coll_ref): - - count_query = coll_ref.count() - query_result = count_query.get() - coll_size=query_result[0][0].value - - if coll_size == 0: - return - - docs = coll_ref.list_documents(page_size=coll_size) - deleted = 0 - - for doc in docs: - print(f"Deleting doc {doc.id} => {doc.get().to_dict()}") - doc.delete() - deleted = deleted + 1 - - if deleted >= coll_size: - return delete_collection(coll_ref) - -def is_json_updated(file): - global LAST_MODIFIED_TIME - current_modified_time = os.path.getmtime(file) - if current_modified_time > LAST_MODIFIED_TIME: - LAST_MODIFIED_TIME = current_modified_time - return True - return False - - -delete_collection(db.collection("Devices")) -set_doc_data(db.collection("Devices"),"devices.json") \ No newline at end of file + existing_devices = {doc.id: doc.to_dict() for doc in db.collection("Devices").stream()} + + added_devices = [] + modified_devices = [] + deleted_devices = [] + + new_device_ids = {str(device["id"]) for device in filtered_devices} + + # Add or update devices + for device in filtered_devices: + device_id = str(device["id"]) + if device_id not in existing_devices: + db.collection("Devices").document(device_id).set(device) + added_devices.append(device) + elif existing_devices[device_id] != device: + db.collection("Devices").document(device_id).set(device) + modified_devices.append(device) + + # Delete devices not in the JSON anymore + for device_id in existing_devices.keys(): + if device_id not in new_device_ids: + db.collection("Devices").document(device_id).delete() + deleted_devices.append(device_id) + + # Logging updates + if added_devices: + print(f"Added devices: {added_devices}") + if modified_devices: + print(f"Updated devices: {modified_devices}") + if deleted_devices: + print(f"Deleted devices: {deleted_devices}") + + if not (added_devices or modified_devices or deleted_devices): + print("No changes detected in Firestore.") + + +class JSONFileHandler(FileSystemEventHandler): + """Watches the JSON file for modifications.""" + + def on_modified(self, event): + """Triggered when JSON file is modified.""" + if event.src_path.endswith("devices.json"): + print("Detected JSON file update. Syncing with Firestore...") + update_firestore() + + +def start_file_watcher(): + """Start watching the JSON file for changes.""" + event_handler = JSONFileHandler() + observer = Observer() + observer.schedule(event_handler, path=os.path.dirname(JSON_FILE_PATH), recursive=False) + observer.start() + + try: + while True: + time.sleep(1) # Keep script running + except KeyboardInterrupt: + observer.stop() + observer.join() + + +if __name__ == "__main__": + print("Starting file watcher for devices.json...") + start_file_watcher() + diff --git a/database/users_db.json b/database/users_db.json index f6482f5..090407a 100644 --- a/database/users_db.json +++ b/database/users_db.json @@ -1,35 +1,38 @@ { "users": [ { + "user_id": 1, "user_name": "John Doe", "user_password": "1415", "allocated_devices": [ - "device1", - "device2", - "device3", - "device4", - "device5", - "device6" + "1", + "2", + "3", + "4", + "5", + "6" ], "user_role": "super_user" }, { + "user_id": 2, "user_name": "Jane Doe", "user_password": "2005", "allocated_devices": [ - "device3", - "device4" + "3", + "4" ], "user_role": "sub_user" }, { + "user_id": 3, "user_name": "John Smith", "user_password": "0007", "allocated_devices": [ - "device5", - "device6" + "5", + "6" ], "user_role": "sub_user" } ] -} \ No newline at end of file +} diff --git a/database/users_db.py b/database/users_db.py new file mode 100644 index 0000000..0b09e05 --- /dev/null +++ b/database/users_db.py @@ -0,0 +1,34 @@ +import firebase_admin +from firebase_admin import credentials, firestore +import json +import os +#rom dotenv import load_dotenv + +cred = credentials.Certificate("/Users/annerinjeri/Downloads/powerhouse-62f4d-firebase-adminsdk-fbsvc-9a2e946c98.json") +firebase_admin.initialize_app(cred) +db = firestore.client() + +#load_dotenv() +def_set_doc_data(collref,) +# Load JSON file +with open("users.json", "r") as file: + data = json.load(file) + +# Upload users to Firestore with device references +for user in data["users"]: + user_id = user["user_name"].replace(" ", "_").lower() # Unique Firestore ID + user_ref = db.collection("Users").document(user_id) + + # Convert device IDs into Firestore document references + device_refs = [db.collection("Devices").document(device_id) for device_id in user["allocated_devices"]] + + user_data = { + "user_name": user["user_name"], + "user_password": user["user_password"], # Password stored as plain text (⚠ Not recommended for production) + "allocated_devices": device_refs, # Store Firestore document references + "user_role": user["user_role"] + } + + user_ref.set(user_data) + print(f"User {user['user_name']} added with device references!") + diff --git a/frontend/src/app/automations/page.jsx b/frontend/src/app/automations/page.jsx index f988c66..8d4e928 100644 --- a/frontend/src/app/automations/page.jsx +++ b/frontend/src/app/automations/page.jsx @@ -1,11 +1,131 @@ "use client"; -import React from "react"; +import React, { useState } from "react"; import Breadcrumb from "../ui/dashboard/breadcrumbs"; +import { + Box, + Button, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + TextField, + MenuItem, +} from "@mui/material"; + +import AddIcon from "@mui/icons-material/Add"; +import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; +import { TimePicker } from "@mui/x-date-pickers/TimePicker"; // Use TimePicker instead +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import dayjs from "dayjs"; const Automations = () => { + const [openDialog, setOpenDialog] = useState(false); + const [name, setName] = useState(""); + const [trigger, setTrigger] = useState(dayjs()); + const [condition, setCondition] = useState(""); + + const conditions = ["Temperature > 25°C", "Motion Detected", "Light On", "Door Open"]; + + // Open the dialog + const handleOpenDialog = () => { + setOpenDialog(true); + }; + + // Close the dialog + const handleCloseDialog = () => { + setOpenDialog(false); + setName(""); + setTrigger(dayjs()); + setCondition(""); + }; + + // Handle form submission + const handleSave = () => { + console.log("Saving Automation:", { name, trigger, condition }); + handleCloseDialog(); + }; + return (