Merge branch 'main' of https://repos.hackathon.bz.it/2025-summer/team-2
This commit is contained in:
commit
9195618db8
18 changed files with 242 additions and 35 deletions
BIN
.DS_Store
vendored
BIN
.DS_Store
vendored
Binary file not shown.
BIN
PlantDashboard/.DS_Store
vendored
BIN
PlantDashboard/.DS_Store
vendored
Binary file not shown.
BIN
PlantDashboard/.cache.sqlite
Normal file
BIN
PlantDashboard/.cache.sqlite
Normal file
Binary file not shown.
Binary file not shown.
BIN
PlantDashboard/__pycache__/plant_meteo.cpython-313.pyc
Normal file
BIN
PlantDashboard/__pycache__/plant_meteo.cpython-313.pyc
Normal file
Binary file not shown.
|
@ -6,7 +6,8 @@ import os
|
||||||
from datetime import datetime, date
|
from datetime import datetime, date
|
||||||
from plant_model import PlantGrowthModel
|
from plant_model import PlantGrowthModel
|
||||||
from data_handler import DataHandler
|
from data_handler import DataHandler
|
||||||
from tkcalendar import DateEntry
|
from tkcalendar import DateEntry, Calendar
|
||||||
|
from plant_meteo import HappyMeteo
|
||||||
|
|
||||||
class PlantGrowthDashboard:
|
class PlantGrowthDashboard:
|
||||||
def __init__(self, root):
|
def __init__(self, root):
|
||||||
|
@ -15,7 +16,11 @@ class PlantGrowthDashboard:
|
||||||
self.root.geometry("1000x800") # More square dimensions
|
self.root.geometry("1000x800") # More square dimensions
|
||||||
self.root.configure(bg='#f0f0f0')
|
self.root.configure(bg='#f0f0f0')
|
||||||
|
|
||||||
image = Image.open("public/transparentLogo.png")
|
image = Image.open("public/logoTransparent.png")
|
||||||
|
|
||||||
|
desired_size = (128, 128)
|
||||||
|
image = image.resize(desired_size, Image.Resampling.LANCZOS)
|
||||||
|
|
||||||
# Convert to PhotoImage
|
# Convert to PhotoImage
|
||||||
icon = ImageTk.PhotoImage(image)
|
icon = ImageTk.PhotoImage(image)
|
||||||
# Set as window icon
|
# Set as window icon
|
||||||
|
@ -24,6 +29,7 @@ class PlantGrowthDashboard:
|
||||||
# Initialize components
|
# Initialize components
|
||||||
self.plant_model = PlantGrowthModel()
|
self.plant_model = PlantGrowthModel()
|
||||||
self.data_handler = DataHandler()
|
self.data_handler = DataHandler()
|
||||||
|
self.happyMeteo = HappyMeteo()
|
||||||
|
|
||||||
# Variables - fixed plant type
|
# Variables - fixed plant type
|
||||||
self.current_plant = "tomato" # Fixed plant type
|
self.current_plant = "tomato" # Fixed plant type
|
||||||
|
@ -143,6 +149,8 @@ class PlantGrowthDashboard:
|
||||||
command=lambda x, p=param: self.on_param_change(p))
|
command=lambda x, p=param: self.on_param_change(p))
|
||||||
scale.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(4, 4))
|
scale.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(4, 4))
|
||||||
|
|
||||||
|
setattr(self, f"{param}_scale", scale)
|
||||||
|
|
||||||
# Value label
|
# Value label
|
||||||
value_label = ttk.Label(param_frame, text=f"{self.env_params[param].get():.1f}", width=5, font=('Arial', 8))
|
value_label = ttk.Label(param_frame, text=f"{self.env_params[param].get():.1f}", width=5, font=('Arial', 8))
|
||||||
value_label.pack(side=tk.RIGHT)
|
value_label.pack(side=tk.RIGHT)
|
||||||
|
@ -163,28 +171,33 @@ class PlantGrowthDashboard:
|
||||||
spacer.grid(row=row, column=0, columnspan=2, pady=10)
|
spacer.grid(row=row, column=0, columnspan=2, pady=10)
|
||||||
|
|
||||||
# Add this after the parameters section
|
# Add this after the parameters section
|
||||||
ttk.Label(control_frame, text="Final date of growth:",
|
ttk.Label(control_frame, text="Final date of growth (choose a date):",
|
||||||
font=('Arial', 9, 'bold')).grid(row=row, column=0, columnspan=2, sticky=tk.W, pady=(10, 4))
|
font=('Arial', 9, 'bold')).grid(row=row, column=0, columnspan=2, sticky=tk.W, pady=(10, 4))
|
||||||
|
|
||||||
row += 1
|
row += 1
|
||||||
# Compact date entry with calendar popup
|
# Compact date entry with calendar popup
|
||||||
# To get the selected dat simply call self.date_entry.get_date()
|
# To get the selected dat simply call self.calendar.get_date()
|
||||||
self.date_entry = DateEntry(control_frame,
|
# self.date_entry
|
||||||
width=12,
|
self.calendar = Calendar(control_frame, selectmode='day',
|
||||||
background='darkblue',
|
year=2025, month=8, day=1,
|
||||||
foreground='white',
|
font=('Arial', 8))
|
||||||
borderwidth=2,
|
self.calendar.grid(row=row, column=0, columnspan=2, pady=2)
|
||||||
font=('Arial', 8),
|
|
||||||
date_pattern='dd/mm/yyyy',
|
|
||||||
state='readonly', # Add this
|
|
||||||
cursor='hand2') # Add this
|
|
||||||
self.date_entry.grid(row=row, column=0, columnspan=2, sticky=tk.W, pady=2)
|
|
||||||
|
|
||||||
row += 1
|
row += 1
|
||||||
|
|
||||||
control_frame.columnconfigure(0, weight=1)
|
control_frame.columnconfigure(0, weight=1)
|
||||||
control_frame.columnconfigure(1, weight=1)
|
control_frame.columnconfigure(1, weight=1)
|
||||||
|
|
||||||
|
def disable_parameter(self, param):
|
||||||
|
scale = getattr(self, f"{param}_scale", None)
|
||||||
|
if scale:
|
||||||
|
scale.configure(state='disabled')
|
||||||
|
|
||||||
|
def enable_parameter(self, param):
|
||||||
|
scale = getattr(self, f"{param}_scale", None)
|
||||||
|
if scale:
|
||||||
|
scale.configure(state='normal')
|
||||||
|
|
||||||
def setup_visualization_panel(self, parent):
|
def setup_visualization_panel(self, parent):
|
||||||
viz_frame = ttk.LabelFrame(parent, text="Plant Visualization", padding="6")
|
viz_frame = ttk.LabelFrame(parent, text="Plant Visualization", padding="6")
|
||||||
viz_frame.grid(row=1, column=1, sticky=(tk.W, tk.E, tk.N, tk.S), padx=3)
|
viz_frame.grid(row=1, column=1, sticky=(tk.W, tk.E, tk.N, tk.S), padx=3)
|
||||||
|
@ -325,14 +338,18 @@ class PlantGrowthDashboard:
|
||||||
if current_mode == "controlled":
|
if current_mode == "controlled":
|
||||||
print("Switched to Controlled mode")
|
print("Switched to Controlled mode")
|
||||||
# Enable all parameter controls
|
# Enable all parameter controls
|
||||||
|
self.enable_parameter("humidity")
|
||||||
|
self.enable_parameter("brightness")
|
||||||
|
self.enable_parameter("temperature")
|
||||||
|
|
||||||
# No need to call the meteo api
|
# No need to call the meteo api
|
||||||
|
|
||||||
elif current_mode == "open":
|
elif current_mode == "open":
|
||||||
print("Switched to Open mode")
|
print("Switched to Open mode")
|
||||||
# Disable most parameter controls (temp, humidity, light)
|
# Disable most parameter controls (temp, humidity, light)
|
||||||
|
self.disable_parameter("humidity")
|
||||||
# Call the meteo api to retrieve all the parameters, set variable meteo_values to retrieve when submitiing all
|
self.disable_parameter("brightness")
|
||||||
# Inside the retrieving of all the data check if the mode is one, select which data to use
|
self.disable_parameter("temperature")
|
||||||
|
|
||||||
def on_param_change(self, param):
|
def on_param_change(self, param):
|
||||||
value = self.env_params[param].get()
|
value = self.env_params[param].get()
|
||||||
|
@ -354,7 +371,7 @@ class PlantGrowthDashboard:
|
||||||
|
|
||||||
self.ambient_mode.set("controlled")
|
self.ambient_mode.set("controlled")
|
||||||
|
|
||||||
self.date_entry.set_date(date.today())
|
self.calendar.set_date(date.today())
|
||||||
|
|
||||||
def update_parameter_label(self, param, value):
|
def update_parameter_label(self, param, value):
|
||||||
"""Update the value label for a specific parameter"""
|
"""Update the value label for a specific parameter"""
|
||||||
|
@ -387,11 +404,25 @@ class PlantGrowthDashboard:
|
||||||
def submit_plant_data(self):
|
def submit_plant_data(self):
|
||||||
"""Submit plant information and photo"""
|
"""Submit plant information and photo"""
|
||||||
try:
|
try:
|
||||||
# Get current parameters
|
start_date = datetime.now().date()
|
||||||
|
end_date = self.calendar.get_date()
|
||||||
|
|
||||||
|
time_lapse = end_date - start_date
|
||||||
|
days_difference = time_lapse.days
|
||||||
|
|
||||||
params = {param: var.get() for param, var in self.env_params.items()}
|
params = {param: var.get() for param, var in self.env_params.items()}
|
||||||
params['plant_type'] = self.current_plant
|
params['plant_type'] = self.current_plant
|
||||||
params['ambient_mode'] = self.ambient_mode.get()
|
params['ambient_mode'] = self.ambient_mode.get()
|
||||||
|
|
||||||
|
current_mode = self.ambient_mode.get()
|
||||||
|
happy_data = 0
|
||||||
|
if current_mode == "open":
|
||||||
|
happy_data = self.happyMeteo.openMeteoCall(days_difference)
|
||||||
|
|
||||||
|
excluded_params = {"humidity", "temperature", "brightness"}
|
||||||
|
params = {param: var.get() for param, var in self.env_params.items()
|
||||||
|
if param not in excluded_params}
|
||||||
|
|
||||||
# Create submission data
|
# Create submission data
|
||||||
submission_data = {
|
submission_data = {
|
||||||
'timestamp': datetime.now().isoformat(),
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
@ -399,9 +430,12 @@ class PlantGrowthDashboard:
|
||||||
'baseline_image_path': self.baseline_image_path,
|
'baseline_image_path': self.baseline_image_path,
|
||||||
'plant_info': self.plant_info_text.get(1.0, tk.END),
|
'plant_info': self.plant_info_text.get(1.0, tk.END),
|
||||||
'start date': datetime.now().date().isoformat(),
|
'start date': datetime.now().date().isoformat(),
|
||||||
'end_date': self.date_entry.get_date().isoformat()
|
'end_date': self.calendar.get_date().isoformat()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if current_mode == "open":
|
||||||
|
submission_data['meteoForecast'] = happy_data
|
||||||
|
|
||||||
#Remove plant_info_text
|
#Remove plant_info_text
|
||||||
self.plant_info_text.delete(1.0, tk.END)
|
self.plant_info_text.delete(1.0, tk.END)
|
||||||
|
|
||||||
|
|
80
PlantDashboard/plant_meteo.py
Normal file
80
PlantDashboard/plant_meteo.py
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import openmeteo_requests
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import requests_cache
|
||||||
|
from retry_requests import retry
|
||||||
|
import geocoder
|
||||||
|
|
||||||
|
|
||||||
|
class HappyMeteo:
|
||||||
|
def __init__(self):
|
||||||
|
# Setup the Open-Meteo API client with cache and retry on error
|
||||||
|
cache_session = requests_cache.CachedSession('.cache', expire_after = 3600)
|
||||||
|
retry_session = retry(cache_session, retries = 5, backoff_factor = 0.2)
|
||||||
|
self.openmeteo = openmeteo_requests.Client(session = retry_session)
|
||||||
|
|
||||||
|
def get_current_location(self):
|
||||||
|
"""Get current location using IP geolocation"""
|
||||||
|
try:
|
||||||
|
g = geocoder.ip('me')
|
||||||
|
if g.ok:
|
||||||
|
latitude = g.latlng[0]
|
||||||
|
longitude = g.latlng[1]
|
||||||
|
print(f"Latitude: {latitude}")
|
||||||
|
print(f"Longitude: {longitude}")
|
||||||
|
print(f"Address: {g.address}")
|
||||||
|
return latitude, longitude
|
||||||
|
else:
|
||||||
|
print("Could not determine location")
|
||||||
|
return None, None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error getting location: {e}")
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def openMeteoCall(self, timeLapse):
|
||||||
|
lat, lon = self.get_current_location()
|
||||||
|
|
||||||
|
# Make sure all required weather variables are listed here
|
||||||
|
# The order of variables in hourly or daily is important to assign them correctly below
|
||||||
|
url = "https://api.open-meteo.com/v1/forecast"
|
||||||
|
params = {
|
||||||
|
"latitude": lat,
|
||||||
|
"longitude": lon,
|
||||||
|
"daily": ["weather_code", "temperature_2m_mean", "rain_sum", "showers_sum", "precipitation_sum", "daylight_duration", "relative_humidity_2m_mean"],
|
||||||
|
"timezone": "auto",
|
||||||
|
"forecast_days": timeLapse
|
||||||
|
}
|
||||||
|
responses = self.openmeteo.weather_api(url, params=params)
|
||||||
|
|
||||||
|
# Process first location. Add a for-loop for multiple locations or weather models
|
||||||
|
response = responses[0]
|
||||||
|
|
||||||
|
# Process daily data. The order of variables needs to be the same as requested.
|
||||||
|
daily = response.Daily()
|
||||||
|
daily_weather_code = daily.Variables(0).ValuesAsNumpy()
|
||||||
|
daily_temperature_2m_mean = daily.Variables(1).ValuesAsNumpy()
|
||||||
|
daily_rain_sum = daily.Variables(2).ValuesAsNumpy()
|
||||||
|
daily_showers_sum = daily.Variables(3).ValuesAsNumpy()
|
||||||
|
daily_precipitation_sum = daily.Variables(4).ValuesAsNumpy()
|
||||||
|
daily_daylight_duration = daily.Variables(5).ValuesAsNumpy()
|
||||||
|
daily_relative_humidity_2m_mean = daily.Variables(6).ValuesAsNumpy()
|
||||||
|
|
||||||
|
# Return comprehensive data structure
|
||||||
|
return {
|
||||||
|
"daily_data": {
|
||||||
|
"weather_code": daily_weather_code.tolist(),
|
||||||
|
"temperature_2m_mean": daily_temperature_2m_mean.tolist(),
|
||||||
|
"rain_sum": daily_rain_sum.tolist(),
|
||||||
|
"showers_sum": daily_showers_sum.tolist(),
|
||||||
|
"precipitation_sum": daily_precipitation_sum.tolist(),
|
||||||
|
"daylight_duration": daily_daylight_duration.tolist(),
|
||||||
|
"relative_humidity_2m_mean": daily_relative_humidity_2m_mean.tolist()
|
||||||
|
},
|
||||||
|
"summary": {
|
||||||
|
"avg_temperature": float(daily_temperature_2m_mean.mean()),
|
||||||
|
"total_precipitation": float(daily_precipitation_sum.sum()),
|
||||||
|
"avg_humidity": float(daily_relative_humidity_2m_mean.mean()),
|
||||||
|
"total_daylight_hours": float(daily_daylight_duration.sum() / 3600) # Convert seconds to hours
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
BIN
PlantDashboard/public/.DS_Store
vendored
BIN
PlantDashboard/public/.DS_Store
vendored
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 7.7 KiB |
BIN
PlantDashboard/public/logo.png
Normal file
BIN
PlantDashboard/public/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 126 KiB |
BIN
PlantDashboard/public/logoTransparent.png
Normal file
BIN
PlantDashboard/public/logoTransparent.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 133 KiB |
Binary file not shown.
Before Width: | Height: | Size: 84 KiB |
|
@ -30,6 +30,10 @@ memory-profiler>=0.60.0
|
||||||
|
|
||||||
tkcalendar
|
tkcalendar
|
||||||
|
|
||||||
|
openmeteo-requests
|
||||||
|
requests-cache
|
||||||
|
retry-requests
|
||||||
|
geocoder
|
||||||
# Note: tkinter comes pre-installed with most Python distributions
|
# Note: tkinter comes pre-installed with most Python distributions
|
||||||
# If tkinter is not available, install it using your system package manager:
|
# If tkinter is not available, install it using your system package manager:
|
||||||
# Ubuntu/Debian: sudo apt-get install python3-tk
|
# Ubuntu/Debian: sudo apt-get install python3-tk
|
||||||
|
|
BIN
__pycache__/script.cpython-311.pyc
Normal file
BIN
__pycache__/script.cpython-311.pyc
Normal file
Binary file not shown.
BIN
basilico.jpg
Normal file
BIN
basilico.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 MiB |
42
forDashboardIntegration.py
Normal file
42
forDashboardIntegration.py
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
from datetime import datetime
|
||||||
|
from script import PlantPredictor
|
||||||
|
|
||||||
|
def dashboard_plant_prediction(image_path, start_date, end_date, additional_notes=""):
|
||||||
|
"""
|
||||||
|
Simple function for dashboard integration
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Calculate days
|
||||||
|
start_dt = datetime.strptime(start_date, "%Y-%m-%d")
|
||||||
|
end_dt = datetime.strptime(end_date, "%Y-%m-%d")
|
||||||
|
days = (end_dt - start_dt).days
|
||||||
|
|
||||||
|
if days <= 0:
|
||||||
|
return {"success": False, "error": "Invalid date range"}
|
||||||
|
|
||||||
|
# Create predictor and run
|
||||||
|
predictor = PlantPredictor()
|
||||||
|
result = predictor.dashboard_plant_prediction(image_path, days, additional_notes)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
return {"success": True, "result": result}
|
||||||
|
else:
|
||||||
|
return {"success": False, "error": "No result"}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
# Test
|
||||||
|
if __name__ == "__main__":
|
||||||
|
result = dashboard_plant_prediction(
|
||||||
|
"./basilico.jpg",
|
||||||
|
"2024-08-01",
|
||||||
|
"2024-08-08",
|
||||||
|
"Test plant"
|
||||||
|
)
|
||||||
|
|
||||||
|
if result["success"]:
|
||||||
|
print(" SUCCESS!")
|
||||||
|
else:
|
||||||
|
print(f" ERROR: {result['error']}")
|
15
path.txt
15
path.txt
|
@ -1,18 +1,5 @@
|
||||||
ROADMAP
|
ROADMAP
|
||||||
|
-format the data that will came to the user
|
||||||
-differences between the three modes:
|
|
||||||
Open mode:
|
|
||||||
-the temp, humidity, light, water parameters are setted by the meteo api, the rest is setted by the user
|
|
||||||
-all the parameters are controlled by the user
|
|
||||||
SemiControlled mode:
|
|
||||||
-the user choose how to set the parameters
|
|
||||||
-all parameters free
|
|
||||||
|
|
||||||
Controlled mode:
|
|
||||||
-all the values are set by the user
|
|
||||||
-the user choose which parameters are free and which are controlled by the meteo api
|
|
||||||
|
|
||||||
-make the calendar widget working
|
|
||||||
|
|
||||||
-final updates of README.md, hackathon page, forgejo, github page, small design adjustments.
|
-final updates of README.md, hackathon page, forgejo, github page, small design adjustments.
|
||||||
|
|
||||||
|
|
60
script.py
Normal file
60
script.py
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
from datetime import datetime
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
class PlantPredictor:
|
||||||
|
def dashboard_plant_prediction(
|
||||||
|
image_path: str,
|
||||||
|
start_date: str,
|
||||||
|
end_date: str,
|
||||||
|
additional_notes: str = ""
|
||||||
|
) -> dict:
|
||||||
|
try:
|
||||||
|
# Calcola giorni
|
||||||
|
start_dt = datetime.strptime(start_date, "%Y-%m-%d")
|
||||||
|
end_dt = datetime.strptime(end_date, "%Y-%m-%d")
|
||||||
|
days = (end_dt - start_dt).days
|
||||||
|
if days <= 0:
|
||||||
|
return {"success": False, "error": "End date must be after start date", "days": days}
|
||||||
|
|
||||||
|
# Log
|
||||||
|
print(f"Dashboard prediction request: {start_date} to {end_date} ({days} days) image={image_path}")
|
||||||
|
if additional_notes:
|
||||||
|
print(f"Notes: {additional_notes}")
|
||||||
|
|
||||||
|
# Inizializza il predictor e chiama il metodo
|
||||||
|
predictor = PlantPredictor()
|
||||||
|
result = predictor.predict_plant_growth(image_path, days, additional_notes)
|
||||||
|
|
||||||
|
# Unwrap risultato tuple
|
||||||
|
if isinstance(result, tuple) and len(result) == 5:
|
||||||
|
_img, conditions, weather_df, plant_type, plant_health = result
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"plant_analysis": {"plant_type": plant_type, "plant_health": plant_health},
|
||||||
|
"weather_conditions": conditions,
|
||||||
|
"weather_data_shape": weather_df.shape,
|
||||||
|
"parameters_used": {"start_date": start_date, "end_date": end_date, "days": days, "notes": additional_notes, "image": image_path},
|
||||||
|
"prediction_summary": {
|
||||||
|
"temperature_range": f"{conditions['avg_temp_min']}–{conditions['avg_temp_max']}°C",
|
||||||
|
"total_rain": f"{conditions['total_rain']}mm",
|
||||||
|
"sunshine_hours": f"{conditions['total_sunshine_hours']}h"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {"success": False, "error": "Invalid result from PlantPredictor", "result": result}
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
return {"success": False, "error": f"Date format error: {e}"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"success": False, "error": f"Unexpected error: {e}"}
|
||||||
|
|
||||||
|
# Esempio di test
|
||||||
|
if __name__ == '__main__':
|
||||||
|
res = dashboard_plant_prediction(
|
||||||
|
image_path='./basilico.jpg',
|
||||||
|
start_date='2024-08-01',
|
||||||
|
end_date='2024-08-08',
|
||||||
|
additional_notes='Indoor day 3'
|
||||||
|
)
|
||||||
|
print(res)
|
Loading…
Add table
Add a link
Reference in a new issue