diff --git a/Python/Socket_server_x_test.py b/Python/Socket_server_x_test.py new file mode 100644 index 0000000..9fc574f --- /dev/null +++ b/Python/Socket_server_x_test.py @@ -0,0 +1,15 @@ +import socket + +HOST = 'localhost' +PORT = 12345 + +with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.connect((HOST, PORT)) + + # Se l'autenticazione è abilitata + s.sendall(b'admin\n') + s.sendall(b'password123\n') + + s.sendall(b'status\n') # Invia il comando + data = s.recv(1024) + print(f"Received: {data.decode()}") \ No newline at end of file diff --git a/Python/cron_include.py b/Python/cron_include.py new file mode 100644 index 0000000..b3d7662 --- /dev/null +++ b/Python/cron_include.py @@ -0,0 +1,434 @@ +import os +import re +import logging +from crontab import CronTab, CronItem # Importa le classi necessarie da python-crontab + +# --- Mock/Placeholder per le dipendenze esterne --- +# In un'applicazione reale, queste funzioni verrebbero fornite dal tuo sistema piGarden principale. + +# Configura un logger di base per le funzioni di log +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + +def log_write(log_type, level, message): + """ + Simula la funzione log_write dal tuo script Bash. + In un'applicazione reale, useresti il modulo logging di Python. + """ + if level == "info": + logging.info(f"[{log_type}] {message}") + elif level == "warning": + logging.warning(f"[{log_type}] {message}") + elif level == "error": + logging.error(f"[{log_type}] {message}") + else: + logging.debug(f"[{log_type}] {message}") + +def trigger_event(event_name, *args): + """ + Simula la funzione trigger_event dal tuo script Bash. + """ + log_write("event", "info", f"Triggered event: {event_name} with args: {args}") + # Qui potresti aggiungere la logica per chiamare handler di eventi reali + +def alias_exists(alias_name): + """ + Simula la funzione alias_exists. + Dovrebbe essere integrata con la tua configurazione delle elettrovalvole. + Per questo esempio, restituisce True solo per alias "1" a "6". + """ + try: + num = int(alias_name) + return 1 <= num <= EV_TOTAL # Assumiamo EV_TOTAL sia definito globalmente o passato + except ValueError: + return False + +# EV_TOTAL deve essere definito o passato, simuliamo un valore qui +EV_TOTAL = 6 +# Percorso dello script principale (es. piGarden.py) +# Questo dovrebbe essere il percorso assoluto del tuo script piGarden principale +# In un'applicazione reale, lo passeresti dalla tua classe PiGarden +PI_GARDEN_SCRIPT_PATH = "/home/pi/piGarden/piGarden.py" + +# --- Classe CronManager --- + +class CronManager: + def __init__(self, script_path, ev_total_val, log_writer, event_trigger, alias_checker): + self.script_path = script_path + self.ev_total = ev_total_val + self.log_write = log_writer + self.trigger_event = event_trigger + self.alias_exists = alias_checker + self.cron_user = True # Gestisce il crontab dell'utente corrente + + def _get_crontab(self): + """Ottiene l'oggetto CronTab per l'utente corrente.""" + try: + return CronTab(user=self.cron_user) + except Exception as e: + self.log_write("cron", "error", f"Impossibile accedere al crontab: {e}") + raise + + def cron_del(self, cron_type, cron_arg=""): + """ + Elimina una tipologia di schedulazione dal crontab dell'utente. + :param cron_type: Tipologia del cron (es. "init", "open", "close"). + :param cron_arg: Argomento della tipologia (es. alias dell'elettrovalvola). + """ + if not cron_type: + self.log_write("cron", "error", "Tipo cron vuoto") + print("Tipo cron vuoto", file=os.sys.stderr) + return False + + crontab = self._get_crontab() + jobs_to_remove = [] + + # Il tuo script Bash usa commenti START/END. + # Possiamo cercare lavori che contengono questi commenti. + # Alternativamente, si potrebbe assegnare un commento specifico ad ogni job creato. + start_comment_pattern = re.compile(rf"^# START cron {re.escape(cron_type)} {re.escape(cron_arg)}$") + end_comment_pattern = re.compile(rf"^# END cron {re.escape(cron_type)} {re.escape(cron_arg)}$") + + found_block = False + in_block = False + for job in list(crontab.jobs): # Iterate over a copy because we might modify + if start_comment_pattern.match(job.comment or ""): + in_block = True + found_block = True + jobs_to_remove.append(job) # Include the START comment job itself + elif end_comment_pattern.match(job.comment or ""): + if in_block: + jobs_to_remove.append(job) # Include the END comment job itself + in_block = False + elif in_block: + jobs_to_remove.append(job) + + if not found_block: + print(f"{cron_type} {cron_arg} cron non presente", file=os.sys.stderr) + return True # Considerato un successo se non c'è nulla da eliminare + + self.trigger_event("cron_del_before", cron_type, cron_arg) + + for job in jobs_to_remove: + crontab.remove(job) + + try: + crontab.write() + self.log_write("cron", "info", f"Cron '{cron_type} {cron_arg}' eliminato con successo.") + self.trigger_event("cron_del_after", cron_type, cron_arg) + return True + except Exception as e: + self.log_write("cron", "error", f"Errore durante la scrittura del crontab: {e}") + print(f"Errore durante la scrittura del crontab: {e}", file=os.sys.stderr) + return False + + def _get_cron_command(self, cron_type, cron_arg, cron_arg2): + """Determina il comando Bash da eseguire per il cron job.""" + base_command = f"{self.script_path}" + + if cron_type == "init": + return f"{base_command} init" + elif cron_type == "start_socket_server": + return f"{base_command} start_socket_server force" + elif cron_type == "check_rain_online": + return f"{base_command} check_rain_online 2> /tmp/check_rain_online.err" + elif cron_type == "check_rain_sensor": + return f"{base_command} check_rain_sensor 2> /tmp/check_rain_sensor.err" + elif cron_type == "close_all_for_rain": + return f"{base_command} close_all_for_rain 2> /tmp/close_all_for_rain.err 1> /dev/null" + elif cron_type == "open": + return f"{base_command} open {cron_arg}" + elif cron_type == "open_in": + return f"{base_command} open {cron_arg} {cron_arg2}" + elif cron_type == "open_in_stop": + return f"{base_command} close {cron_arg}" + elif cron_type == "close": + return f"{base_command} close {cron_arg}" + else: + self.log_write("cron", "error", f"Tipo cron errato: {cron_type}") + print(f"Tipo cron errato: {cron_type}", file=os.sys.stderr) + raise ValueError(f"Tipo cron errato: {cron_type}") + + def cron_add(self, cron_type, minute="*", hour="*", dom="*", month="*", dow="*", cron_arg="", cron_arg2=""): + """ + Aggiunge una schedulazione nel crontab dell'utente. + :param cron_type: Tipologia del cron. + :param minute: Minuto (0-59, *, */n, @reboot). + :param hour: Ora (0-23, *, */n). + :param dom: Giorno del mese (1-31, *, */n). + :param month: Mese (1-12, *, */n). + :param dow: Giorno della settimana (0-6, *, */n). + :param cron_arg: Primo argomento specifico della tipologia. + :param cron_arg2: Secondo argomento specifico della tipologia (es. "disabled"). + """ + if not cron_type: + self.log_write("cron", "error", "Tipo cron vuoto") + print("Tipo cron vuoto", file=os.sys.stderr) + return False + + # Elimina prima qualsiasi blocco esistente per garantire l'idempotenza + self.cron_del(cron_type, cron_arg) + + crontab = self._get_crontab() + + # Determina il comando e se deve essere disabilitato + cron_command = self._get_cron_command(cron_type, cron_arg, cron_arg2) + cron_disabled = (cron_arg2 == "disabled") + + # Crea i commenti START e END per il blocco + start_comment = f"# START cron {cron_type} {cron_arg}" + end_comment = f"# END cron {cron_type} {cron_arg}" + + # Aggiungi il commento START + job_start = crontab.new(command=f"echo '{start_comment}'", comment=start_comment) + job_start.minute.every(1) # Un cron job fittizio per il commento START + job_start.enabled = False # Disabilita il job commento + + # Aggiungi il job principale + job = crontab.new(command=cron_command) + if minute == "@reboot": + job.set_every("reboot") + else: + job.minute.on(minute) + job.hour.on(hour) + job.dom.on(dom) + job.month.on(month) + job.dow.on(dow) + job.enabled = not cron_disabled + job.comment = f"piGarden {cron_type} {cron_arg}" # Un commento più descrittivo per il job reale + + # Aggiungi il commento END + job_end = crontab.new(command=f"echo '{end_comment}'", comment=end_comment) + job_end.minute.every(1) # Un cron job fittizio per il commento END + job_end.enabled = False # Disabilita il job commento + + try: + crontab.write() + self.log_write("cron", "info", f"Cron '{cron_type} {cron_arg}' aggiunto con successo: {job.render()}") + self.trigger_event("cron_add_after", cron_type, cron_arg, job.render()) + return True + except Exception as e: + self.log_write("cron", "error", f"Errore durante la scrittura del crontab: {e}") + print(f"Errore durante la scrittura del crontab: {e}", file=os.sys.stderr) + return False + + def cron_get(self, cron_type, cron_arg=""): + """ + Legge una tipologia di schedulazione dal crontab dell'utente. + :param cron_type: Tipologia del cron. + :param cron_arg: Argomento della tipologia. + :return: Stringa contenente le schedulazioni trovate, separate da newline. + """ + if not cron_type: + self.log_write("cron", "error", "Tipo cron vuoto") + print("Tipo cron vuoto", file=os.sys.stderr) + return "" + + crontab = self._get_crontab() + found_jobs = [] + + # Cerca i job principali che corrispondono al tipo e all'argomento + for job in crontab.jobs: + if job.comment and job.comment.startswith(f"piGarden {cron_type} {cron_arg}"): + found_jobs.append(job.render()) + + return "\n".join(found_jobs) + + # --- Funzioni wrapper per tipi di cron specifici --- + + def set_cron_init(self): + self.cron_del("init") # Assicurati che non ci siano duplicati + self.cron_add("init", minute="@reboot") + + def del_cron_init(self): + self.cron_del("init") + + def set_cron_start_socket_server(self): + self.cron_del("start_socket_server") + self.cron_add("start_socket_server", minute="@reboot") + + def del_cron_start_socket_server(self): + self.cron_del("start_socket_server") + + def set_cron_check_rain_sensor(self): + self.cron_del("check_rain_sensor") + self.cron_add("check_rain_sensor", minute="*") # Ogni minuto + + def del_cron_check_rain_sensor(self): + self.cron_del("check_rain_sensor") + + def set_cron_check_rain_online(self): + self.cron_del("check_rain_online") + self.cron_add("check_rain_online", minute="*/3") # Ogni 3 minuti + + def del_cron_check_rain_online(self): + self.cron_del("check_rain_online") + + def set_cron_close_all_for_rain(self): + self.cron_del("close_all_for_rain") + self.cron_add("close_all_for_rain", minute="*/5") # Ogni 5 minuti + + def del_cron_close_all_for_rain(self): + self.cron_del("close_all_for_rain") + + def add_cron_open(self, alias, minute, hour, dom, month, dow, disabled=""): + if not self.alias_exists(alias): + self.log_write("cron", "error", f"Alias '{alias}' non trovato") + print(f"Alias '{alias}' non trovato", file=os.sys.stderr) + return False + self.cron_add("open", minute, hour, dom, month, dow, alias, disabled) + return True + + def del_cron_open(self, alias): + if not self.alias_exists(alias): + self.log_write("cron", "error", f"Alias '{alias}' non trovato") + print(f"Alias '{alias}' non trovato", file=os.sys.stderr) + return False + self.cron_del("open", alias) + return True + + def get_cron_open(self, alias): + if not self.alias_exists(alias): + self.log_write("cron", "error", f"Alias '{alias}' non trovato") + print(f"Alias '{alias}' non trovato", file=os.sys.stderr) + return "" + return self.cron_get("open", alias) + + def del_cron_open_in(self, alias): + if not self.alias_exists(alias): + self.log_write("cron", "error", f"Alias '{alias}' non trovato") + print(f"Alias '{alias}' non trovato", file=os.sys.stderr) + return False + self.cron_del("open_in", alias) + self.cron_del("open_in_stop", alias) + return True + + def get_cron_close(self, alias): + if not self.alias_exists(alias): + self.log_write("cron", "error", f"Alias '{alias}' non trovato") + print(f"Alias '{alias}' non trovato", file=os.sys.stderr) + return "" + return self.cron_get("close", alias) + + def add_cron_close(self, alias, minute, hour, dom, month, dow, disabled=""): + if not self.alias_exists(alias): + self.log_write("cron", "error", f"Alias '{alias}' non trovato") + print(f"Alias '{alias}' non trovato", file=os.sys.stderr) + return False + self.cron_add("close", minute, hour, dom, month, dow, alias, disabled) + return True + + def del_cron_close(self, alias): + if not self.alias_exists(alias): + self.log_write("cron", "error", f"Alias '{alias}' non trovato") + print(f"Alias '{alias}' non trovato", file=os.sys.stderr) + return False + self.cron_del("close", alias) + return True + + def cron_disable_all_open_close(self): + crontab = self._get_crontab() + for i in range(1, self.ev_total + 1): + alias = str(i) # Assumendo che gli alias siano i numeri delle EV + # Disabilita le schedulazioni di apertura + for job in list(crontab.jobs): + if job.comment and job.comment.startswith(f"piGarden open {alias}") and job.enabled: + job.enabled = False + self.log_write("cron", "info", f"Disabilitato cron 'open' per alias {alias}: {job.render()}") + + # Disabilita le schedulazioni di chiusura + for job in list(crontab.jobs): + if job.comment and job.comment.startswith(f"piGarden close {alias}") and job.enabled: + job.enabled = False + self.log_write("cron", "info", f"Disabilitato cron 'close' per alias {alias}: {job.render()}") + try: + crontab.write() + self.log_write("cron", "info", "Tutte le schedulazioni di apertura/chiusura disabilitate.") + return True + except Exception as e: + self.log_write("cron", "error", f"Errore durante la disabilitazione dei cron: {e}") + return False + + def cron_enable_all_open_close(self): + crontab = self._get_crontab() + for i in range(1, self.ev_total + 1): + alias = str(i) # Assumendo che gli alias siano i numeri delle EV + # Abilita le schedulazioni di apertura + for job in list(crontab.jobs): + if job.comment and job.comment.startswith(f"piGarden open {alias}") and not job.enabled: + job.enabled = True + self.log_write("cron", "info", f"Abilitato cron 'open' per alias {alias}: {job.render()}") + + # Abilita le schedulazioni di chiusura + for job in list(crontab.jobs): + if job.comment and job.comment.startswith(f"piGarden close {alias}") and not job.enabled: + job.enabled = True + self.log_write("cron", "info", f"Abilitato cron 'close' per alias {alias}: {job.render()}") + try: + crontab.write() + self.log_write("cron", "info", "Tutte le schedulazioni di apertura/chiusura abilitate.") + return True + except Exception as e: + self.log_write("cron", "error", f"Errore durante l'abilitazione dei cron: {e}") + return False + +# --- Esempio di utilizzo (per testare la classe CronManager) --- +if __name__ == "__main__": + # Inizializza il gestore cron con le dipendenze mock + cron_manager = CronManager( + script_path=PI_GARDEN_SCRIPT_PATH, + ev_total_val=EV_TOTAL, + log_writer=log_write, + event_trigger=trigger_event, + alias_checker=alias_exists + ) + + print("--- Test Cron Manager ---") + + # Esempio: Aggiungi un cron per l'inizializzazione + print("\nAggiungo cron 'init'...") + cron_manager.set_cron_init() + + # Esempio: Aggiungi un cron per aprire l'elettrovalvola "1" ogni giorno alle 7:00 + print("\nAggiungo cron 'open' per EV 1 alle 07:00...") + cron_manager.add_cron_open("1", "0", "7", "*", "*", "*") + + # Esempio: Aggiungi un cron per chiudere l'elettrovalvola "2" ogni 5 minuti (disabilitato) + print("\nAggiungo cron 'close' per EV 2 ogni 5 minuti (disabilitato)...") + cron_manager.add_cron_close("2", "*/5", "*", "*", "*", "*", "disabled") + + # Esempio: Ottieni i cron per l'elettrovalvola "1" + print("\nCron 'open' per EV 1:") + print(cron_manager.get_cron_open("1")) + + # Esempio: Ottieni i cron per l'elettrovalvola "2" + print("\nCron 'close' per EV 2:") + print(cron_manager.get_cron_close("2")) + + # Esempio: Disabilita tutti i cron di apertura/chiusura + print("\nDisabilito tutti i cron di apertura/chiusura...") + cron_manager.cron_disable_all_open_close() + + # Verifica lo stato dopo la disabilitazione + print("\nCron 'open' per EV 1 dopo disabilitazione:") + print(cron_manager.get_cron_open("1")) # Dovrebbe mostrare il job ma disabilitato + + # Esempio: Abilita tutti i cron di apertura/chiusura + print("\nAbilito tutti i cron di apertura/chiusura...") + cron_manager.cron_enable_all_open_close() + + # Verifica lo stato dopo l'abilitazione + print("\nCron 'open' per EV 1 dopo abilitazione:") + print(cron_manager.get_cron_open("1")) # Dovrebbe mostrare il job abilitato + + # Esempio: Elimina un cron specifico + print("\nElimino cron 'init'...") + cron_manager.del_cron_init() + + print("\nElimino cron 'open' per EV 1...") + cron_manager.del_cron_open("1") + + print("\nElimino cron 'close' per EV 2...") + cron_manager.del_cron_close("2") + + print("\n--- Test Completato ---") + print("Controlla il tuo crontab con 'crontab -l' per vedere le modifiche.") diff --git a/Python/drv.include.py b/Python/drv.include.py new file mode 100644 index 0000000..44d07cb --- /dev/null +++ b/Python/drv.include.py @@ -0,0 +1,487 @@ +import os +import re +import logging +import subprocess # Per eseguire comandi esterni come 'gpio' se necessario + +# --- Mock/Placeholder per le dipendenze esterne e configurazione --- +# In un'applicazione reale, queste verrebbero fornite dal tuo sistema piGarden principale. + +# Configura un logger di base per le funzioni di log +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + +def log_write(log_type, level, message): + """Simula la funzione log_write dal tuo script Bash.""" + if level == "info": + logging.info(f"[{log_type}] {message}") + elif level == "warning": + logging.warning(f"[{log_type}] {message}") + elif level == "error": + logging.error(f"[{log_type}] {message}") + else: + logging.debug(f"[{log_type}] {message}") + +def message_write(msg_type, message): + """Simula la funzione message_write dal tuo script Bash.""" + if msg_type == 'info': + logging.info(f"[message] INFO: {message}") + elif msg_type == 'warning': + logging.warning(f"[message] WARNING: {message}") + elif msg_type == 'success': + logging.info(f"[message] SUCCESS: {message}") + +# Variabili di configurazione simulate (dovrebbero venire dal piGarden.conf) +# Queste saranno passate alla classe DriverManager +mock_config = { + "EV_TOTAL": 6, + "SUPPLY_GPIO_1": 2, + "SUPPLY_GPIO_2": 3, + "RAIN_GPIO": 25, + "WEATHER_SERVICE": "drv:openweathermap", # Esempio di driver di servizio meteo + "RELE_GPIO_CLOSE": 0, + "RELE_GPIO_OPEN": 1, + "SUPPLY_GPIO_POS": 0, + "SUPPLY_GPIO_NEG": 1, + "GPIO": "/usr/local/bin/gpio", # Percorso al comando gpio (wiringPi) + "CUT": "/usr/bin/cut", # Percorso al comando cut + "LOG_OUTPUT_DRV_FILE": "/tmp/piGarden.drv.log", # Percorso per il log dei driver + # Elettrovalvole di esempio per setup_drv + "EV1_GPIO": "17", + "EV2_GPIO": "drv:custom_rele", # Esempio di un GPIO gestito da un driver custom + "EV3_GPIO": "22", + "EV4_GPIO": "18", + "EV5_GPIO": "23", + "EV6_GPIO": "24", +} + +# --- Classe DriverManager --- + +class DriverManager: + def __init__(self, config, log_writer, message_writer): + self.config = config + self.log_write = log_writer + self.message_write = message_writer + self.list_drv = [] # Lista dei driver attivi rilevati + + # Percorsi degli strumenti esterni (dal config) + self.gpio_cmd = self.config.get("GPIO") + self.cut_cmd = self.config.get("CUT") + self.log_output_drv_file = self.config.get("LOG_OUTPUT_DRV_FILE") + + # Inizializza il file di log dei driver se non esiste + if not os.path.exists(self.log_output_drv_file): + open(self.log_output_drv_file, 'a').close() + + # Configura un logger specifico per l'output dei driver, come nello script Bash + self.drv_logger = logging.getLogger('driver_output') + self.drv_logger.setLevel(logging.INFO) + # Rimuovi handler esistenti per evitare duplicati se chiamato più volte + if not self.drv_logger.handlers: + drv_handler = logging.FileHandler(self.log_output_drv_file, mode='a') + drv_formatter = logging.Formatter('%(asctime)s %(message)s') + drv_handler.setFormatter(drv_formatter) + self.drv_logger.addHandler(drv_handler) + self.drv_logger.propagate = False # Evita che i log vadano al logger root + + # Placeholder per le funzioni GPIO dirette (sostituire con RPi.GPIO o gpiozero) + # Esempio con subprocess per il comando 'gpio' (meno Pythonico ma più fedele al Bash) + self._gpio_write = lambda gpio_id, value: self._run_gpio_command("write", gpio_id, value) + self._gpio_mode = lambda gpio_id, mode: self._run_gpio_command("mode", gpio_id, mode) + self._gpio_read = lambda gpio_id: self._run_gpio_command("read", gpio_id) + + # Inizializza i driver al momento della creazione dell'istanza + self.setup_drv() + + def _run_gpio_command(self, action, gpio_id, value=None): + """Esegue un comando 'gpio' tramite subprocess.""" + cmd = [self.gpio_cmd, "-g", action, str(gpio_id)] + if value is not None: + cmd.append(str(value)) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + self.drv_logger.info(f"GPIO command '{' '.join(cmd)}' output: {result.stdout.strip()}") + return result.stdout.strip() + except subprocess.CalledProcessError as e: + self.log_write("drv", "error", f"Errore GPIO command '{' '.join(cmd)}': {e.stderr.strip()}") + self.message_write("warning", f"Errore GPIO: {e.stderr.strip()}") + return None # O solleva un'eccezione + + def setup_drv(self): + """ + Funzione eseguita ad ogni avvio, include i driver e lancia le funzioni di setup. + """ + self.list_drv = [] # Azzera la lista dei driver + + # Raccoglie i nomi dei driver utilizzati per le elettrovalvole + ev_total = self.config.get("EV_TOTAL", 0) + for i in range(1, ev_total + 1): + gpio_val = self.config.get(f"EV{i}_GPIO", "") + if gpio_val.startswith("drv:"): + drv = gpio_val.split(":")[1] + if drv not in self.list_drv: + self.list_drv.append(drv) + + # Raccoglie i nomi dei driver utilizzati per gli altri gpio e servizi + for key in ["SUPPLY_GPIO_1", "SUPPLY_GPIO_2", "RAIN_GPIO", "WEATHER_SERVICE"]: + gpio_val = self.config.get(key, "") + if isinstance(gpio_val, str) and gpio_val.startswith("drv:"): + drv = gpio_val.split(":")[1] + if drv not in self.list_drv: + self.list_drv.append(drv) + + # Simula l'inclusione dei file dei driver e l'esecuzione della funzione di setup + for drv in self.list_drv: + # In un'applicazione reale, qui potresti caricare moduli Python specifici + # per ogni driver o chiamare metodi dedicati. + # Per ora, simuliamo la chiamata a drv__setup + setup_func_name = f"drv_{drv}_setup" + if hasattr(self, setup_func_name) and callable(getattr(self, setup_func_name)): + self.drv_logger.info(f"{setup_func_name}") + try: + getattr(self, setup_func_name)() + except Exception as e: + self.log_write("drv", "error", f"Errore in {setup_func_name}: {e}") + else: + self.drv_logger.info(f"Nessuna funzione di setup trovata per driver: {drv}") + + def get_driver_callback(self, function_name, driver_id): + """ + Restituisce il nome del metodo interno da richiamare per una specifica funzione del driver. + """ + if isinstance(driver_id, str) and driver_id.startswith("drv:"): + drv = driver_id.split(":")[1] + if drv not in self.list_drv: + return "drvnotfound" + return f"drv_{drv}_{function_name}" + return None # Nessun driver specifico, useremo il GPIO diretto + + # --- Implementazioni delle funzioni drv_* --- + + # Esempio di un driver custom (simulato) + def drv_custom_rele_rele_init(self, gpio_id): + self.drv_logger.info(f"Custom Relè Driver: Inizializzazione {gpio_id}") + # Logica specifica per il relè custom + # Esempio: self.custom_rele_board.init(gpio_id) + return True + + def drv_custom_rele_rele_close(self, gpio_id): + self.drv_logger.info(f"Custom Relè Driver: Chiusura {gpio_id}") + # Logica specifica per il relè custom + # Esempio: self.custom_rele_board.set_state(gpio_id, 'closed') + return True + + def drv_custom_rele_rele_open(self, gpio_id): + self.drv_logger.info(f"Custom Relè Driver: Apertura {gpio_id}") + # Logica specifica per il relè custom + # Esempio: self.custom_rele_board.set_state(gpio_id, 'open') + return True + + def drv_openweathermap_rain_online_get(self, driver_id): + self.drv_logger.info(f"OpenWeatherMap Driver: Recupero dati meteo online per {driver_id}") + # Qui faresti una chiamata API reale a OpenWeatherMap + # Esempio: + # import requests + # api_key = self.config.get("OPENWEATHERMAP_KEY") + # location = self.config.get("OPENWEATHERMAP_LOCATION") + # url = f"http://api.openweathermap.org/data/2.5/weather?{location}&appid={api_key}" + # try: + # response = requests.get(url) + # response.raise_for_status() # Solleva un'eccezione per errori HTTP + # data = response.json() + # # Estrai lo stato della pioggia da 'data' + # if 'rain' in data and data['rain']: + # return "1" # Indica pioggia + # return "0" # Nessuna pioggia + # except requests.exceptions.RequestException as e: + # self.log_write("drv", "error", f"Errore OpenWeatherMap API: {e}") + # self.message_write("warning", "Errore servizio meteo online") + # return "" + return "0" # Mock return + + def drv_rele_init(self, gpio_id): + """Inizializza un relè e lo porta nello stato aperto.""" + fnc_name = self.get_driver_callback("rele_init", gpio_id) + + if fnc_name is None: # Nessun driver specifico, usa GPIO diretto + self._gpio_write(gpio_id, self.config.get("RELE_GPIO_OPEN")) + self._gpio_mode(gpio_id, "out") + elif fnc_name == "drvnotfound": + self.log_write("drv", "error", f"Driver non trovato: {gpio_id}") + self.message_write("warning", f"Driver non trovato: {gpio_id}") + else: + # Chiama la funzione del driver dinamico + if hasattr(self, fnc_name) and callable(getattr(self, fnc_name)): + self.drv_logger.info(f"{fnc_name} arg:{gpio_id}") + try: + getattr(self, fnc_name)(gpio_id) + except Exception as e: + self.log_write("drv", "error", f"Errore in {fnc_name}: {e}") + else: + self.log_write("drv", "error", f"Funzione driver '{fnc_name}' non implementata.") + self.message_write("warning", f"Funzione driver '{fnc_name}' non implementata.") + + + def drv_rele_close(self, gpio_id): + """Chiude un relè.""" + fnc_name = self.get_driver_callback("rele_close", gpio_id) + + if fnc_name is None: + self._gpio_write(gpio_id, self.config.get("RELE_GPIO_CLOSE")) + elif fnc_name == "drvnotfound": + self.log_write("drv", "error", f"Driver non trovato: {gpio_id}") + self.message_write("warning", f"Driver non trovato: {gpio_id}") + return False # Fallimento + else: + if hasattr(self, fnc_name) and callable(getattr(self, fnc_name)): + self.drv_logger.info(f"{fnc_name} arg:{gpio_id}") + try: + getattr(self, fnc_name)(gpio_id) + except Exception as e: + self.log_write("drv", "error", f"Errore in {fnc_name}: {e}") + return False + else: + self.log_write("drv", "error", f"Funzione driver '{fnc_name}' non implementata.") + self.message_write("warning", f"Funzione driver '{fnc_name}' non implementata.") + return False + return True # Successo + + def drv_rele_open(self, gpio_id): + """Apre un relè.""" + fnc_name = self.get_driver_callback("rele_open", gpio_id) + + if fnc_name is None: + self._gpio_write(gpio_id, self.config.get("RELE_GPIO_OPEN")) + elif fnc_name == "drvnotfound": + self.log_write("drv", "error", f"Driver non trovato: {gpio_id}") + self.message_write("warning", f"Driver non trovato: {gpio_id}") + return False + else: + if hasattr(self, fnc_name) and callable(getattr(self, fnc_name)): + self.drv_logger.info(f"{fnc_name} arg:{gpio_id}") + try: + getattr(self, fnc_name)(gpio_id) + except Exception as e: + self.log_write("drv", "error", f"Errore in {fnc_name}: {e}") + return False + else: + self.log_write("drv", "error", f"Funzione driver '{fnc_name}' non implementata.") + self.message_write("warning", f"Funzione driver '{fnc_name}' non implementata.") + return False + return True + + def drv_supply_bistable_init(self, idx1, idx2): + """Inizializza i relè che gestiscono l'alimentazione per le valvole bistabili.""" + fnc1_name = self.get_driver_callback("supply_bistable_init", idx1) + fnc2_name = self.get_driver_callback("supply_bistable_init", idx2) + + if fnc1_name is None: + self._gpio_write(idx1, 0) + self._gpio_mode(idx1, "out") + elif fnc1_name == "drvnotfound": + self.log_write("drv", "error", f"Driver non trovato: {idx1}") + self.message_write("warning", f"Driver non trovato: {idx1}") + return + else: + if hasattr(self, fnc1_name) and callable(getattr(self, fnc1_name)): + self.drv_logger.info(f"{fnc1_name} arg:{idx1}") + try: + getattr(self, fnc1_name)(idx1) + except Exception as e: + self.log_write("drv", "error", f"Errore in {fnc1_name}: {e}") + else: + self.log_write("drv", "error", f"Funzione driver '{fnc1_name}' non implementata.") + self.message_write("warning", f"Funzione driver '{fnc1_name}' non implementata.") + + if fnc2_name is None: + self._gpio_write(idx2, 0) + self._gpio_mode(idx2, "out") + elif fnc2_name == "drvnotfound": + self.log_write("drv", "error", f"Driver non trovato: {idx2}") + self.message_write("warning", f"Driver non trovato: {idx2}") + else: + if hasattr(self, fnc2_name) and callable(getattr(self, fnc2_name)): + self.drv_logger.info(f"{fnc2_name} arg:{idx2}") + try: + getattr(self, fnc2_name)(idx2) + except Exception as e: + self.log_write("drv", "error", f"Errore in {fnc2_name}: {e}") + else: + self.log_write("drv", "error", f"Funzione driver '{fnc2_name}' non implementata.") + self.message_write("warning", f"Funzione driver '{fnc2_name}' non implementata.") + + def drv_supply_positive(self, idx1, idx2): + """Imposta la tensione positiva per le elettrovalvole bistabili.""" + fnc1_name = self.get_driver_callback("supply_positive", idx1) + fnc2_name = self.get_driver_callback("supply_positive", idx2) + + if fnc1_name is None: + self._gpio_write(idx1, self.config.get("SUPPLY_GPIO_POS")) + elif fnc1_name == "drvnotfound": + self.log_write("drv", "error", f"Driver non trovato: {idx1}") + self.message_write("warning", f"Driver non trovato: {idx1}") + return + else: + if hasattr(self, fnc1_name) and callable(getattr(self, fnc1_name)): + self.drv_logger.info(f"{fnc1_name} arg:{idx1}") + try: + getattr(self, fnc1_name)(idx1) + except Exception as e: + self.log_write("drv", "error", f"Errore in {fnc1_name}: {e}") + else: + self.log_write("drv", "error", f"Funzione driver '{fnc1_name}' non implementata.") + self.message_write("warning", f"Funzione driver '{fnc1_name}' non implementata.") + + if fnc2_name is None: + self._gpio_write(idx2, self.config.get("SUPPLY_GPIO_POS")) + elif fnc2_name == "drvnotfound": + self.log_write("drv", "error", f"Driver non trovato: {idx2}") + self.message_write("warning", f"Driver non trovato: {idx2}") + else: + if hasattr(self, fnc2_name) and callable(getattr(self, fnc2_name)): + self.drv_logger.info(f"{fnc2_name} arg:{idx2}") + try: + getattr(self, fnc2_name)(idx2) + except Exception as e: + self.log_write("drv", "error", f"Errore in {fnc2_name}: {e}") + else: + self.log_write("drv", "error", f"Funzione driver '{fnc2_name}' non implementata.") + self.message_write("warning", f"Funzione driver '{fnc2_name}' non implementata.") + + def drv_supply_negative(self, idx1, idx2): + """Imposta la tensione negativa per le elettrovalvole bistabili.""" + fnc1_name = self.get_driver_callback("supply_negative", idx1) + fnc2_name = self.get_driver_callback("supply_negative", idx2) + + if fnc1_name is None: + self._gpio_write(idx1, self.config.get("SUPPLY_GPIO_NEG")) + elif fnc1_name == "drvnotfound": + self.log_write("drv", "error", f"Driver non trovato: {idx1}") + self.message_write("warning", f"Driver non trovato: {idx1}") + return + else: + if hasattr(self, fnc1_name) and callable(getattr(self, fnc1_name)): + self.drv_logger.info(f"{fnc1_name} arg:{idx1}") + try: + getattr(self, fnc1_name)(idx1) + except Exception as e: + self.log_write("drv", "error", f"Errore in {fnc1_name}: {e}") + else: + self.log_write("drv", "error", f"Funzione driver '{fnc1_name}' non implementata.") + self.message_write("warning", f"Funzione driver '{fnc1_name}' non implementata.") + + if fnc2_name is None: + self._gpio_write(idx2, self.config.get("SUPPLY_GPIO_NEG")) + elif fnc2_name == "drvnotfound": + self.log_write("drv", "error", f"Driver non trovato: {idx2}") + self.message_write("warning", f"Driver non trovato: {idx2}") + else: + if hasattr(self, fnc2_name) and callable(getattr(self, fnc2_name)): + self.drv_logger.info(f"{fnc2_name} arg:{idx2}") + try: + getattr(self, fnc2_name)(idx2) + except Exception as e: + self.log_write("drv", "error", f"Errore in {fnc2_name}: {e}") + else: + self.log_write("drv", "error", f"Funzione driver '{fnc2_name}' non implementata.") + self.message_write("warning", f"Funzione driver '{fnc2_name}' non implementata.") + + def drv_rain_sensor_init(self, gpio_id): + """Inizializza il sensore della pioggia.""" + fnc_name = self.get_driver_callback("rain_sensor_init", gpio_id) + + if fnc_name is None: + self._gpio_mode(gpio_id, "in") + elif fnc_name == "drvnotfound": + self.log_write("drv", "error", f"Driver non trovato: {gpio_id}") + self.message_write("warning", f"Driver non trovato: {gpio_id}") + else: + if hasattr(self, fnc_name) and callable(getattr(self, fnc_name)): + self.drv_logger.info(f"{fnc_name} arg:{gpio_id}") + try: + getattr(self, fnc_name)(gpio_id) + except Exception as e: + self.log_write("drv", "error", f"Errore in {fnc_name}: {e}") + else: + self.log_write("drv", "error", f"Funzione driver '{fnc_name}' non implementata.") + self.message_write("warning", f"Funzione driver '{fnc_name}' non implementata.") + + def drv_rain_sensor_get(self, gpio_id): + """Legge lo stato del sensore della pioggia.""" + fnc_name = self.get_driver_callback("rain_sensor_get", gpio_id) + vret = "" + + if fnc_name is None: + vret = self._gpio_read(gpio_id) + elif fnc_name == "drvnotfound": + self.log_write("drv", "error", f"Driver non trovato: {gpio_id}") + self.message_write("warning", f"Driver non trovato: {gpio_id}") + else: + if hasattr(self, fnc_name) and callable(getattr(self, fnc_name)): + self.drv_logger.info(f"{fnc_name} arg:{gpio_id}") + try: + vret = getattr(self, fnc_name)(gpio_id) + except Exception as e: + self.log_write("drv", "error", f"Errore in {fnc_name}: {e}") + else: + self.log_write("drv", "error", f"Funzione driver '{fnc_name}' non implementata.") + self.message_write("warning", f"Funzione driver '{fnc_name}' non implementata.") + return vret + + def drv_rain_online_get(self, driver_id): + """Legge lo stato delle condizioni meteo dal servizio online.""" + fnc_name = self.get_driver_callback("rain_online_get", driver_id) + vret = "" + + if fnc_name is None or fnc_name == "drvnotfound": + self.log_write("drv", "error", f"Driver non trovato o non specificato per il servizio meteo: {driver_id}") + self.message_write("warning", f"Driver non trovato per il servizio meteo: {driver_id}") + else: + if hasattr(self, fnc_name) and callable(getattr(self, fnc_name)): + self.drv_logger.info(f"{fnc_name} arg:{driver_id}") + try: + vret = getattr(self, fnc_name)(driver_id) + except Exception as e: + self.log_write("drv", "error", f"Errore in {fnc_name}: {e}") + else: + self.log_write("drv", "error", f"Funzione driver '{fnc_name}' non implementata.") + self.message_write("warning", f"Funzione driver '{fnc_name}' non implementata.") + return vret + +# --- Esempio di utilizzo --- +if __name__ == "__main__": + print("--- Test DriverManager ---") + + # Inizializza il DriverManager con la configurazione mock e le funzioni di log/messaggio + driver_manager = DriverManager(mock_config, log_write, message_write) + + print("\n--- Test setup_drv (eseguito all'inizializzazione) ---") + print(f"Driver rilevati: {driver_manager.list_drv}") + + print("\n--- Test drv_rele_init (GPIO diretto) ---") + driver_manager.drv_rele_init("17") # EV1_GPIO = 17 + + print("\n--- Test drv_rele_close (GPIO diretto) ---") + driver_manager.drv_rele_close("17") + + print("\n--- Test drv_rele_open (GPIO diretto) ---") + driver_manager.drv_rele_open("17") + + print("\n--- Test drv_rele_init (Custom Driver) ---") + driver_manager.drv_rele_init("drv:custom_rele") # EV2_GPIO = drv:custom_rele + + print("\n--- Test drv_supply_positive ---") + driver_manager.drv_supply_positive(mock_config["SUPPLY_GPIO_1"], mock_config["SUPPLY_GPIO_2"]) + + print("\n--- Test drv_rain_sensor_init ---") + driver_manager.drv_rain_sensor_init(mock_config["RAIN_GPIO"]) + + print("\n--- Test drv_rain_sensor_get ---") + rain_sensor_state = driver_manager.drv_rain_sensor_get(mock_config["RAIN_GPIO"]) + print(f"Stato sensore pioggia: {rain_sensor_state}") + + print("\n--- Test drv_rain_online_get (OpenWeatherMap Driver) ---") + online_rain_state = driver_manager.drv_rain_online_get(mock_config["WEATHER_SERVICE"]) + print(f"Stato pioggia online: {online_rain_state}") + + print("\n--- Test completato ---") + print(f"Controlla il file di log dei driver: {driver_manager.log_output_drv_file}") diff --git a/Python/event_include.py b/Python/event_include.py new file mode 100644 index 0000000..6e2a26f --- /dev/null +++ b/Python/event_include.py @@ -0,0 +1,255 @@ +import os +import subprocess +import time +import threading +import logging + +# --- Mock/Placeholder per le dipendenze esterne e configurazione --- +# In un'applicazione reale, queste verrebbero fornite dal tuo sistema piGarden principale. + +# Configura un logger di base per le funzioni di log +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + +def log_write(log_type, level, message): + """Simula la funzione log_write dal tuo script Bash.""" + if level == "info": + logging.info(f"[{log_type}] {message}") + elif level == "warning": + logging.warning(f"[{log_type}] {message}") + elif level == "error": + logging.error(f"[{log_type}] {message}") + else: + logging.debug(f"[{log_type}] {message}") + +def message_write(msg_type, message): + """Simula la funzione message_write dal tuo script Bash.""" + if msg_type == 'info': + logging.info(f"[message] INFO: {message}") + elif msg_type == 'warning': + logging.warning(f"[message] WARNING: {message}") + elif msg_type == 'success': + logging.info(f"[message] SUCCESS: {message}") + +def mqtt_status_mock(*args): + """Simula la funzione mqtt_status. In un'applicazione reale, chiamerebbe il tuo modulo MQTT.""" + log_write("mqtt", "info", f"Simulando mqtt_status con args: {args}") + +# Variabili di configurazione simulate +mock_config = { + "EVENT_DIR": "/tmp/piGarden_events", # Assicurati che questa directory esista per i test +} + +# --- Classe EventManager --- + +class EventManager: + def __init__(self, event_dir, log_writer, mqtt_status_func, message_writer_func=None): + self.event_dir = event_dir + self.log_write = log_writer + self.mqtt_status = mqtt_status_func + self.message_write = message_writer_func if message_writer_func else log_writer # Fallback if not provided + + self.CURRENT_EVENT = "" + self.CURRENT_EVENT_ALIAS = "" + + # Assicurati che la directory degli eventi esista per i test + os.makedirs(self.event_dir, exist_ok=True) + + def _build_script_args(self, event, *args): + """ + Costruisce la lista di argomenti per lo script esterno basandosi sul tipo di evento. + """ + script_args = [event] # Il primo argomento è sempre il nome dell'evento + + # Aggiungi il timestamp corrente come ultimo argomento per tutti i casi + current_timestamp = str(int(time.time())) + + if event in ["ev_open_before", "ev_open_after"]: + # ALIAS="$2", FORCE="$3" + alias = args[0] if len(args) > 0 else "" + force = args[1] if len(args) > 1 else "" + self.CURRENT_EVENT_ALIAS = alias + script_args.extend([alias, force, current_timestamp]) + elif event == "ev_open_in_before": + # ALIAS="$2", FORCE="$3", MINUTE_START="$4", MINUTE_STOP="$5" + alias = args[0] if len(args) > 0 else "" + force = args[1] if len(args) > 1 else "" + minute_start = args[2] if len(args) > 2 else "" + minute_stop = args[3] if len(args) > 3 else "" + self.CURRENT_EVENT_ALIAS = alias + script_args.extend([alias, force, minute_start, minute_stop, current_timestamp]) + elif event == "ev_open_in_after": + # ALIAS="$2", FORCE="$3", CRON_START="$4", CRON_STOP="$5" + alias = args[0] if len(args) > 0 else "" + force = args[1] if len(args) > 1 else "" + cron_start = args[2] if len(args) > 2 else "" + cron_stop = args[3] if len(args) > 3 else "" + self.CURRENT_EVENT_ALIAS = alias + script_args.extend([alias, force, cron_start, cron_stop, current_timestamp]) + elif event in ["ev_close_before", "ev_close_after"]: + # ALIAS="$2" + alias = args[0] if len(args) > 0 else "" + self.CURRENT_EVENT_ALIAS = alias + script_args.extend([alias, current_timestamp]) + elif event in ["check_rain_sensor_before", "check_rain_sensor_after", "check_rain_sensor_change"]: + # STATE="$2" + state = args[0] if len(args) > 0 else "" + script_args.extend([state, current_timestamp]) + elif event == "check_rain_online_before": + # STATE="$2" + state = args[0] if len(args) > 0 else "" + script_args.extend([state, current_timestamp]) + elif event in ["check_rain_online_after", "check_rain_online_change"]: + # STATE="$2", WEATHER="$3" + state = args[0] if len(args) > 0 else "" + weather = args[1] if len(args) > 1 else "" + script_args.extend([state, weather, current_timestamp]) + elif event in ["init_before", "init_after", "exec_poweroff_before", "exec_poweroff_after", + "exec_reboot_before", "exec_reboot_after"]: + # Nessun argomento specifico oltre l'evento, ma il Bash script passa $2 come CAUSE (spesso vuoto) + # e il timestamp. Qui passiamo solo il timestamp. + script_args.extend([current_timestamp]) + elif event in ["cron_add_before", "cron_add_after"]: + # CRON_TYPE="$2", CRON_ARG="$3", CRON_ELEMENT="$4" + cron_type = args[0] if len(args) > 0 else "" + cron_arg = args[1] if len(args) > 1 else "" + cron_element = args[2] if len(args) > 2 else "" + script_args.extend([cron_type, cron_arg, cron_element, current_timestamp]) + elif event in ["cron_del_before", "cron_del_after"]: + # CRON_TYPE="$2", CRON_ARG="$3" + cron_type = args[0] if len(args) > 0 else "" + cron_arg = args[1] if len(args) > 1 else "" + script_args.extend([cron_type, cron_arg, current_timestamp]) + else: # Caso generico + # EVENT="$1", CAUSE="$2" + cause = args[0] if len(args) > 0 else "" + script_args.extend([cause, current_timestamp]) + + return script_args + + def trigger_event(self, event, *args): + """ + Attiva un evento ed esegue gli script associati. + :param event: Nome dell'evento da attivare. + :param args: Argomenti aggiuntivi da passare agli script dell'evento. + :return: Codice di uscita dell'ultimo script eseguito, o 0 se tutto ok. + """ + self.log_write("event", "info", f"Triggering event: {event} with args: {args}") + + current_event_dir = os.path.join(self.event_dir, event) + return_code = 0 + + if os.path.isdir(current_event_dir): + # Ordina i file per garantire un ordine di esecuzione consistente + files_in_dir = sorted(os.listdir(current_event_dir)) + for f_name in files_in_dir: + script_path = os.path.join(current_event_dir, f_name) + + # Controlla se è un file eseguibile + if os.path.isfile(script_path) and os.access(script_path, os.X_OK): + script_args = self._build_script_args(event, *args) + + self.log_write("event", "info", f"Executing event script: {script_path} with args: {script_args}") + try: + # Esegue lo script esterno, reindirizzando stdout/stderr a DEVNULL per replicare &> /dev/null + result = subprocess.run( + [script_path] + script_args, + capture_output=True, + text=True, + check=True, # Solleva CalledProcessError per codici di uscita non zero + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + return_code = result.returncode + except subprocess.CalledProcessError as e: + return_code = e.returncode + self.log_write("event", "error", + f"Script evento '{script_path}' fallito con codice {return_code}. " + f"Output: {e.stdout.strip()} Errore: {e.stderr.strip()}") + + # Aggiorna CURRENT_EVENT e chiama mqtt_status in background + self.CURRENT_EVENT = event + threading.Thread(target=self.mqtt_status, args=(self.CURRENT_EVENT,)).start() + + self.log_write("event", "error", + f"Stop catena di eventi per codice di uscita {return_code} in {script_path}") + return return_code # Ferma l'esecuzione degli script successivi + + except FileNotFoundError: + self.log_write("event", "error", f"Script evento non trovato: {script_path}") + return_code = 127 # Codice di errore comune per "comando non trovato" + break # Ferma l'esecuzione degli script successivi + except Exception as e: + self.log_write("event", "error", f"Errore generico durante l'esecuzione di {script_path}: {e}") + return_code = 1 # Errore generico + break # Ferma l'esecuzione degli script successivi + else: + self.log_write("event", "info", f"Nessuna directory eventi trovata per: {event}") + + # Aggiorna CURRENT_EVENT e chiama mqtt_status in background, indipendentemente dal successo + self.CURRENT_EVENT = event + threading.Thread(target=self.mqtt_status, args=(self.CURRENT_EVENT,)).start() + + return return_code + +# --- Esempio di utilizzo --- +if __name__ == "__main__": + print("--- Test EventManager ---") + + # Crea una directory di test per gli eventi e alcuni script fittizi + test_event_dir = mock_config["EVENT_DIR"] + os.makedirs(os.path.join(test_event_dir, "test_event"), exist_ok=True) + os.makedirs(os.path.join(test_event_dir, "error_event"), exist_ok=True) + os.makedirs(os.path.join(test_event_dir, "ev_open_before"), exist_ok=True) + + # Script fittizio che ha successo + with open(os.path.join(test_event_dir, "test_event", "script_successo.sh"), "w") as f: + f.write("#!/bin/bash\n") + f.write("echo 'Script di successo eseguito per $1'\n") + f.write("exit 0\n") + os.chmod(os.path.join(test_event_dir, "test_event", "script_successo.sh"), 0o755) + + # Script fittizio che fallisce + with open(os.path.join(test_event_dir, "error_event", "script_fallimento.sh"), "w") as f: + f.write("#!/bin/bash\n") + f.write("echo 'Script di fallimento eseguito per $1'\n") + f.write("exit 10\n") # Codice di uscita non zero + os.chmod(os.path.join(test_event_dir, "error_event", "script_fallimento.sh"), 0o755) + + # Script fittizio per ev_open_before + with open(os.path.join(test_event_dir, "ev_open_before", "log_open_before.sh"), "w") as f: + f.write("#!/bin/bash\n") + f.write("echo 'ev_open_before: Evento=$1, Alias=$2, Force=$3, Timestamp=$4' >> /tmp/event_test.log\n") + f.write("exit 0\n") + os.chmod(os.path.join(test_event_dir, "ev_open_before", "log_open_before.sh"), 0o755) + + + event_manager = EventManager( + event_dir=mock_config["EVENT_DIR"], + log_writer=log_write, + mqtt_status_func=mqtt_status_mock, + message_writer_func=message_write + ) + + print("\n--- Esecuzione di un evento di successo ---") + result = event_manager.trigger_event("test_event", "some_cause") + print(f"Codice di ritorno: {result}") + print(f"CURRENT_EVENT: {event_manager.CURRENT_EVENT}") + print(f"CURRENT_EVENT_ALIAS: {event_manager.CURRENT_EVENT_ALIAS}") + + print("\n--- Esecuzione di un evento che fallisce ---") + result = event_manager.trigger_event("error_event", "another_cause") + print(f"Codice di ritorno: {result}") + print(f"CURRENT_EVENT: {event_manager.CURRENT_EVENT}") + print(f"CURRENT_EVENT_ALIAS: {event_manager.CURRENT_EVENT_ALIAS}") + + print("\n--- Esecuzione di un evento specifico (ev_open_before) ---") + result = event_manager.trigger_event("ev_open_before", "Zona_1", "true") + print(f"Codice di ritorno: {result}") + print(f"CURRENT_EVENT: {event_manager.CURRENT_EVENT}") + print(f"CURRENT_EVENT_ALIAS: {event_manager.CURRENT_EVENT_ALIAS}") + print("Controlla /tmp/event_test.log per l'output dello script.") + + print("\n--- Test completato ---") + # Pulisci le directory di test (opzionale) + # import shutil + # shutil.rmtree(test_event_dir) diff --git a/Python/piGarden.py b/Python/piGarden.py new file mode 100644 index 0000000..7e3924d --- /dev/null +++ b/Python/piGarden.py @@ -0,0 +1,1616 @@ +import os +import sys +import time +import subprocess +import json +import re +import hashlib +import datetime +import shutil # Per rm -f e rmdir ricorsivo +import socket # Per il client MQTT (se non usi paho-mqtt) + +# Per list_descendants +try: + import psutil +except ImportError: + print("Warning: psutil not found. Process descendant listing will be limited.", file=sys.stderr) + psutil = None + +# --- CONFIGURAZIONE --- +# Questa classe simula il caricamento da /etc/piGarden.conf +# Dovrai popolare questi valori con la tua configurazione reale. +class Config: + def __init__(self): + # Versione dello script + self.VERSION = 0 + self.SUB_VERSION = 6 + self.RELEASE_VERSION = 5 + + # Percorsi e file + self.DIR_SCRIPT = os.path.dirname(os.path.abspath(__file__)) + self.NAME_SCRIPT = os.path.basename(os.path.abspath(__file__)) + self.CONFIG_ETC = "/etc/piGarden.conf" # Il percorso del file di configurazione Bash originale + + # Percorso temporaneo (come in Bash, preferisce /run/shm) + self.TMP_PATH = "/run/shm" if os.path.isdir("/run/shm") else "/tmp" + + self.TCPSERVER_PID_FILE = os.path.join(self.TMP_PATH, "piGardenTcpServer.pid") + self.LOCK_FILE = os.path.join(self.TMP_PATH, "piGarden.dir.lock") + self.TMP_CRON_FILE = os.path.join(self.TMP_PATH, f"pigarden.user.cron.{os.getpid()}") # PID dinamico + + # Log + self.LOG_FILE = "/var/log/piGarden.log" # Esempio + self.LOG_OUTPUT_DRV_FILE = "/dev/null" # Default, come in Bash + self.LOG_FILE_MAX_SIZE = 10 * 1024 * 1024 # 10 MB, esempio + self.LOG_URL = "" # URL per inviare i log (se LOG_API_TOKEN è impostato) + self.LOG_API_TOKEN = "" + self.LOG_CURL_PARAM = "-s -k" # Parametri per curl (es. -s per silenzioso, -k per ignorare SSL) + + # Stato delle elettrovalvole + self.STATUS_DIR = os.path.join(self.TMP_PATH, "pigarden_status") # Directory per gli stati delle EV + os.makedirs(self.STATUS_DIR, exist_ok=True) # Assicurati che la directory esista + + # Elettrovalvole (esempi, devi popolarli con i tuoi dati reali) + # EV_TOTAL deve essere impostato in base a quante EV hai + self.EV_TOTAL = 2 # Esempio: 2 elettrovalvole + self.EV_MONOSTABLE = 0 # 0 per bistabile, 1 per monostabile (globale) + + # Dettagli per ogni elettrovalvola (simula EVx_ALIAS, EVx_GPIO, EVx_NORAIN, EVx_REMOTE, EVx_MONOSTABLE) + self.EV_CONFIG = { + 1: {"ALIAS": "Zona_1", "GPIO": 17, "NORAIN": 0, "REMOTE": 0, "MONOSTABLE": 0}, + 2: {"ALIAS": "Zona_2", "GPIO": 27, "NORAIN": 1, "REMOTE": 0, "MONOSTABLE": 0}, + # Aggiungi altre elettrovalvole qui + } + + # Sensori (esempi) + self.SENSOR_TOTAL = 0 # Esempio: 0 sensori + self.SENSOR_CONFIG = { + # 1: {"ALIAS": "Sensore_Umidita_1", "GPIO": 22, "TYPE": "moisture"}, + # Aggiungi altri sensori qui + } + self.SENSOR_STATE_TYPE = "moisture temperature fertility illuminance" # Tipi di sensori supportati + + self.RAIN_GPIO = None # GPIO per il sensore di pioggia (es. 23) + self.NOT_IRRIGATE_IF_RAIN_ONLINE = 0 # Secondi dopo l'ultima pioggia online per non irrigare + self.NOT_IRRIGATE_IF_RAIN_SENSOR = 0 # Secondi dopo l'ultima pioggia sensore per non irrigare + + # MQTT + self.MQTT_ENABLE = 0 # 1 per abilitare MQTT + self.MQTT_HOST = "localhost" + self.MQTT_PORT = 1883 + self.MQTT_USER = "" + self.MQTT_PWD = "" + self.MQTT_CLIENT_ID = "piGarden" + self.MQTT_TOPIC = "piGarden/status" + + # piGardenSched + self.PIGARDENSCHED_PATH = "/usr/local/bin/piGardenSched" # Percorso dell'eseguibile piGardenSched + self.PIGARDENSCHED = 0 # 1 se piGardenSched è configurato e eseguibile + if os.path.exists(self.PIGARDENSCHED_PATH) and os.access(self.PIGARDENSCHED_PATH, os.X_OK): + self.PIGARDENSCHED = 1 + + # Statistiche di utilizzo + self.NO_SEND_IDENTIFIER = 0 # 1 per disabilitare l'invio dell'identificativo + + # Variabili di stato interne (equivalenti a MESSAGE_INFO, CURRENT_EVENT, ecc. in Bash) + self.MESSAGE_INFO = "" + self.MESSAGE_WARNING = "" + self.MESSAGE_SUCCESS = "" + self.CURRENT_EVENT = "" + self.CURRENT_EVENT_ALIAS = "" + self.PARENT_PID = 0 # Usato per json_status quando chiamato da un processo figlio + + # Servizio meteo online + self.WEATHER_SERVICE = "drv:wunderground" # o "none" o "drv:your_service" + # self.WEATHER_API_KEY = "YOUR_API_KEY" # Chiave API per il servizio meteo + # self.WEATHER_LOCATION = "YOUR_LOCATION" # Località per il servizio meteo + +# Inizializza la configurazione globale +config = Config() + +# --- FUNZIONI UTILITY GLOBALI --- + +def log_write(source, level, message): + """ + Scrive un messaggio nel file di log e gestisce la rotazione. + Invia anche il log a piGardenWeb se configurato. + """ + if not os.path.exists(config.LOG_FILE): + try: + os.makedirs(os.path.dirname(config.LOG_FILE), exist_ok=True) + with open(config.LOG_FILE, 'a'): # Crea il file se non esiste + pass + except OSError as e: + print(f"Error creating log directory/file: {e}", file=sys.stderr) + return + + # Gestione rotazione log + try: + if os.path.exists(config.LOG_FILE): + actual_size = os.path.getsize(config.LOG_FILE) + if actual_size >= config.LOG_FILE_MAX_SIZE: + timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M") + shutil.move(config.LOG_FILE, f"{config.LOG_FILE}.{timestamp}") + # Non comprimiamo con gzip qui, lo script Bash lo faceva dopo lo spostamento + # Se vuoi gzip, puoi aggiungere: subprocess.run(["gzip", f"{config.LOG_FILE}.{timestamp}"]) + log_write("general", "info", f"Log file rotated to {config.LOG_FILE}.{timestamp}") + except Exception as e: + print(f"Error during log rotation: {e}", file=sys.stderr) + + # Scrivi sul file di log + timestamp_log = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + log_entry = f"{timestamp_log}\t\t{source}\t{level}\t{message}\n" + try: + with open(config.LOG_FILE, "a") as f: + f.write(log_entry) + except IOError as e: + print(f"Error writing to log file {config.LOG_FILE}: {e}", file=sys.stderr) + + # Invia a piGardenWeb (se configurato) + log_send(source, level, timestamp_log, message) + +def log_send(log_type, level, datetime_log, message): + """Invia un log verso piGardenWeb.""" + if config.LOG_URL and config.LOG_API_TOKEN: + try: + # Usiamo requests per un'interazione HTTP più robusta + # pip install requests + import requests + data = { + "api_token": config.LOG_API_TOKEN, + "type": log_type, + "level": level, + "datetime_log": datetime_log, + "message": message + } + # subprocess.Popen per non bloccare il processo principale + # In un'applicazione reale, useresti un thread separato o una coda di messaggi + # per inviare log in background per non bloccare l'esecuzione. + # Qui simulo il comportamento di `curl ... &` + + # Nota: requests.post blocca. Per non bloccare, dovresti usare un thread. + # Per emulare `&> /dev/null &` di Bash, useremmo subprocess.Popen + # con stdout/stderr a DEVNULL e un thread per la chiamata requests. + # Per semplicità, qui faremo una chiamata bloccante, ma con un timeout. + + # Esempio con subprocess.Popen per emulare curl in background + # Questo è più vicino al comportamento Bash originale + cmd = ["curl"] + config.LOG_CURL_PARAM.split() + [config.LOG_URL, "-d", + f"api_token={config.LOG_API_TOKEN}&type={log_type}&level={level}&datetime_log={datetime_log}&message={message}"] + subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + except ImportError: + log_write("log_send", "warning", "requests module not found, cannot send logs via HTTP.") + except Exception as e: + log_write("log_send", "error", f"Error sending log to piGardenWeb: {e}") + +def message_write(msg_type, message): + """Scrive una tipologia di messaggio da inviare via socket server (o memorizza in variabili globali).""" + if msg_type == 'info': + config.MESSAGE_INFO = message + elif msg_type == "warning": + config.MESSAGE_WARNING = message + elif msg_type == "success": + config.MESSAGE_SUCCESS = message + else: + return + log_write("message_write", msg_type, message) # Anche i messaggi vengono loggati + +def json_error(code, description): + """Mostra un json per una risposta di errore.""" + return json.dumps({"error": {"code": code, "description": description}}) + +# --- FUNZIONI DI GESTIONE DEL LOCK --- +def lock(current_time=0): + """Gestisce l'apertura di un lock (file-based).""" + max_time = 10 # Secondi + + if current_time > max_time: + log_write("general", "error", "Maximum locked time reached") + time.sleep(max_time) # Sleep prima di sbloccare e uscire + unlock() + sys.exit(1) + + try: + os.mkdir(config.LOCK_FILE) + # Lock acquisito + except FileExistsError: + log_write("general", "info", f"Sleep 1 second for locked state (attempt {current_time + 1})") + time.sleep(1) + lock(current_time + 1) # Riprova ricorsivamente + except OSError as e: + log_write("general", "error", f"Error creating lock directory {config.LOCK_FILE}: {e}") + sys.exit(1) + +def unlock(): + """Chiude un lock (file-based).""" + try: + os.rmdir(config.LOCK_FILE) + except OSError as e: + log_write("general", "error", f"Error removing lock directory {config.LOCK_FILE}: {e}") + +# --- FUNZIONI DI GESTIONE DELLE ELETTROVALVOLE (EV) --- + +def ev_set_state(ev_number, state): + """Imposta lo stato di una elettrovalvola.""" + state_file = os.path.join(config.STATUS_DIR, f"ev{ev_number}") + try: + with open(state_file, "w") as f: + f.write(str(state)) + except IOError as e: + log_write("ev_set_state", "error", f"Error writing EV state to {state_file}: {e}") + +def ev_get_state(ev_number): + """Legge lo stato di una elettrovalvola.""" + state_file = os.path.join(config.STATUS_DIR, f"ev{ev_number}") + if os.path.exists(state_file): + try: + with open(state_file, "r") as f: + return int(f.read().strip()) + except (IOError, ValueError) as e: + log_write("ev_get_state", "error", f"Error reading EV state from {state_file}: {e}") + return 0 # Default a chiuso in caso di errore + return 0 # Default a chiuso se il file non esiste + +def ev_alias2number(alias): + """Recupera il numero di un'elettrovalvola in base all'alias.""" + for num, ev_data in config.EV_CONFIG.items(): + if ev_data["ALIAS"] == alias: + return num + log_write("general", "error", f"ERROR solenoid alias not found: {alias}") + message_write("warning", "Solenoid alias not found") + json_status_wrapper() # Chiama la funzione per stampare JSON + sys.exit(1) # Esce come faceva lo script Bash + +def alias_exists(alias): + """Verifica se un alias di un'elettrovalvola esiste.""" + for ev_data in config.EV_CONFIG.values(): + if ev_data["ALIAS"] == alias: + return True + return False + +def ev_number2gpio(ev_number): + """Recupera il numero di gpio associato a un'elettrovalvola.""" + return config.EV_CONFIG.get(ev_number, {}).get("GPIO") + +def ev_number2norain(ev_number): + """Recupera il valore norain associato a un'elettrovalvola.""" + return config.EV_CONFIG.get(ev_number, {}).get("NORAIN", 0) # Default a 0 se non specificato + +def ev_number2remote(ev_number): + """Recupera il valore remote associato a un'elettrovalvola.""" + return config.EV_CONFIG.get(ev_number, {}).get("REMOTE", 0) + +def ev_number2monostable(ev_number): + """Recupera il valore monostable associato a un'elettrovalvola.""" + return config.EV_CONFIG.get(ev_number, {}).get("MONOSTABLE", 0) + +def ev_status_all(): + """Mostra lo stato di tutte le elettrovalvole.""" + for i in range(1, config.EV_TOTAL + 1): + alias = config.EV_CONFIG.get(i, {}).get("ALIAS", f"EV{i}") + state = ev_get_state(i) + print(f"{alias}: {state}") + +def ev_status(alias): + """Mostra lo stato di un'elettrovalvola.""" + ev_num = ev_alias2number(alias) + state = ev_get_state(ev_num) + print(f"{state}") # Stampa solo lo stato, come in Bash + return state + +def close_all(force=False): + """Chiude tutte le elettrovalvole.""" + for i in range(1, config.EV_TOTAL + 1): + alias = config.EV_CONFIG.get(i, {}).get("ALIAS", f"EV{i}") + state = ev_get_state(i) + if state > 0 or force: + ev_close(alias) + log_write("irrigate", "info", f"close_all - Close solenoid '{alias}' for rain") + +def list_alias(): + """Stampa la lista degli alias delle elettrovalvole.""" + for ev_data in config.EV_CONFIG.values(): + print(ev_data["ALIAS"]) + +# --- FUNZIONI DRIVER (PLACEHOLDERS) --- +# Queste funzioni simulano le interazioni hardware. +# DEVONO ESSERE IMPLEMENTATE CON LE LIBRERIE GPIO REALI (es. RPi.GPIO, gpiozero) + +def drv_init(): + """Inizializza i driver GPIO.""" + log_write("drv", "info", "Initializing GPIO drivers...") + # Esempio: + # import RPi.GPIO as GPIO + # GPIO.setmode(GPIO.BCM) + # GPIO.setwarnings(False) + +def drv_supply_bistable_init(gpio1, gpio2): + """Inizializza l'alimentazione per elettrovalvole bistabili.""" + log_write("drv", "info", f"Initializing bistable supply on GPIOs {gpio1}, {gpio2}") + # Imposta i GPIO come output e a stato iniziale (es. LOW) + +def drv_rele_init(gpio): + """Inizializza un GPIO per un relè (elettrovalvola).""" + log_write("drv", "info", f"Initializing relay on GPIO {gpio}") + # Imposta il GPIO come output + +def drv_rele_close(gpio): + """Attiva il relè per chiudere (o aprire, a seconda della logica del relè/valvola).""" + log_write("drv", "info", f"Activating relay (close) on GPIO {gpio}") + # Esempio: GPIO.output(gpio, GPIO.HIGH) # O LOW, a seconda del relè + return 0 # 0 per successo + +def drv_rele_open(gpio): + """Disattiva il relè per aprire (o chiudere, a seconda della logica).""" + log_write("drv", "info", f"Deactivating relay (open) on GPIO {gpio}") + # Esempio: GPIO.output(gpio, GPIO.LOW) # O HIGH + return 0 # 0 per successo + +def drv_supply_positive(gpio1, gpio2): + """Imposta l'alimentazione con voltaggio positivo per bistabili.""" + log_write("drv", "info", f"Setting positive supply on GPIOs {gpio1}, {gpio2}") + # Logica per invertire la polarità + +def drv_supply_negative(gpio1, gpio2): + """Imposta l'alimentazione con voltaggio negativo per bistabili.""" + log_write("drv", "info", f"Setting negative supply on GPIOs {gpio1}, {gpio2}") + # Logica per invertire la polarità + +def drv_rain_sensor_init(gpio): + """Inizializza il sensore di pioggia.""" + log_write("drv", "info", f"Initializing rain sensor on GPIO {gpio}") + # Imposta il GPIO come input e aggiungi un event listener + +# --- FUNZIONI CRON (PLACEHOLDERS) --- +# Queste funzioni simulano l'interazione con crontab. +# Dovrai implementarle usando una libreria come `python-crontab` o manipolando il crontab. + +def cron_del(cron_type, alias): + """Elimina le voci cron per un tipo e alias specifici.""" + log_write("cron", "info", f"Deleting cron entries for type: {cron_type}, alias: {alias}") + # Esempio: usa python-crontab per rimuovere le entry + return "" # Simula output vuoto per successo + +def cron_add(cron_type, cron_schedule, alias, force=""): + """Aggiunge una voce cron.""" + log_write("cron", "info", f"Adding cron entry for type: {cron_type}, schedule: {cron_schedule}, alias: {alias}, force: {force}") + # Esempio: usa python-crontab per aggiungere l'entry + return "" + +def cron_get(cron_type, alias): + """Recupera le voci cron per un tipo e alias specifici.""" + log_write("cron", "info", f"Getting cron entries for type: {cron_type}, alias: {alias}") + # Esempio: restituisce una stringa con le voci cron, separate da newline + return "" + +def cron_disable_all_open_close(): + """Disabilita tutte le schedulazioni cron di apertura/chiusura.""" + log_write("cron", "info", "Disabling all open/close cron schedules.") + return "" + +def cron_enable_all_open_close(): + """Abilita tutte le schedulazioni cron di apertura/chiusura.""" + log_write("cron", "info", "Enabling all open/close cron schedules.") + return "" + +def set_cron_init(): + """Imposta il crontab per l'inizializzazione dell'unità di controllo.""" + log_write("cron", "info", "Setting cron for init.") + return "" + +def del_cron_init(): + """Rimuove il crontab per l'inizializzazione dell'unità di controllo.""" + log_write("cron", "info", "Deleting cron for init.") + return "" + +def set_cron_start_socket_server(): + """Imposta il crontab per l'avvio del socket server.""" + log_write("cron", "info", "Setting cron for socket server start.") + return "" + +def del_cron_start_socket_server(): + """Rimuove il crontab per l'avvio del socket server.""" + log_write("cron", "info", "Deleting cron for socket server start.") + return "" + +def set_cron_check_rain_sensor(): + """Imposta il crontab per il controllo della pioggia dal sensore.""" + log_write("cron", "info", "Setting cron for rain sensor check.") + return "" + +def del_cron_check_rain_sensor(): + """Rimuove il crontab per il controllo della pioggia dal sensore.""" + log_write("cron", "info", "Deleting cron for rain sensor check.") + return "" + +def set_cron_check_rain_online(): + """Imposta il crontab per il controllo della pioggia dal servizio online.""" + log_write("cron", "info", "Setting cron for online rain check.") + return "" + +def del_cron_check_rain_online(): + """Rimuove il crontab per il controllo della pioggia dal servizio online.""" + log_write("cron", "info", "Deleting cron for online rain check.") + return "" + +def set_cron_close_all_for_rain(): + """Imposta il crontab per chiudere tutte le elettrovalvole quando piove.""" + log_write("cron", "info", "Setting cron for close all for rain.") + return "" + +def del_cron_close_all_for_rain(): + """Rimuove il crontab per chiudere tutte le elettrovalvole quando piove.""" + log_write("cron", "info", "Deleting cron for close all for rain.") + return "" + +# --- FUNZIONI SENSORI (PLACEHOLDERS) --- +def json_sensor_status_all(): + """Restituisce lo stato di tutti i sensori in formato JSON.""" + sensor_data = {} + for num, sensor_config in config.SENSOR_CONFIG.items(): + alias = sensor_config["ALIAS"] + # Qui dovresti leggere lo stato reale del sensore + # Per ora, simulo uno stato casuale o fisso + status_value = 0 # Placeholder + sensor_data[alias] = {"alias": alias, "status": status_value} + return json.dumps(sensor_data) + +def list_alias_sensor(): + """Stampa la lista degli alias dei sensori.""" + for sensor_data in config.SENSOR_CONFIG.values(): + print(sensor_data["ALIAS"]) + +def sensor_status(alias, sensor_type=None): + """Mostra lo stato di un sensore.""" + # Trova il sensore per alias + sensor_num = None + for num, s_config in config.SENSOR_CONFIG.items(): + if s_config["ALIAS"] == alias: + sensor_num = num + break + + if sensor_num is None: + log_write("sensor", "error", f"Sensor alias not found: {alias}") + message_write("warning", "Sensor alias not found") + json_status_wrapper() + return 1 # Errore + + # Qui dovresti leggere il valore reale dal sensore + # Per ora, restituisco un valore fisso o casuale + value = 0 # Placeholder + print(value) + return value + +def sensor_status_set(alias, sensor_type, value): + """Imposta lo stato di un sensore (se supportato, es. per sensori virtuali).""" + log_write("sensor", "info", f"Setting sensor {alias} type {sensor_type} to value {value}") + # Implementa la logica per impostare lo stato del sensore + return 0 # Successo + +def sensor_status_all(): + """Mostra lo stato di tutti i sensori.""" + for num, sensor_config in config.SENSOR_CONFIG.items(): + alias = sensor_config["ALIAS"] + # Qui dovresti leggere lo stato reale del sensore + status_value = 0 # Placeholder + print(f"{alias}: {status_value}") + + +# --- FUNZIONI PIOGGIA (PLACEHOLDERS) --- +def ev_check_moisture(ev_num): + """Verifica l'umidità del terreno per un'elettrovalvola.""" + log_write("irrigate", "info", f"Checking moisture for EV {ev_num}") + # Implementa la logica per leggere l'umidità del sensore associato all'EV + # Restituisce 0 se l'umidità è bassa (ok per irrigare), >0 altrimenti + return 0 # Placeholder: nessuna umidità alta + +def check_rain_sensor(): + """Controlla la pioggia dal sensore hardware.""" + if config.RAIN_GPIO: + log_write("rain", "info", f"Checking rain from sensor on GPIO {config.RAIN_GPIO}") + # Leggi lo stato del sensore GPIO + # Se piove, aggiorna il timestamp in config.STATUS_DIR/last_rain_sensor + # Esempio: + # if GPIO.input(config.RAIN_GPIO) == GPIO.LOW: # Assumendo LOW = pioggia + # with open(os.path.join(config.STATUS_DIR, "last_rain_sensor"), "w") as f: + # f.write(str(int(time.time()))) + # log_write("rain", "info", "Rain detected by sensor.") + pass + else: + log_write("rain", "info", "Rain sensor not configured.") + +def check_rain_online(): + """Controlla la pioggia da un servizio API online.""" + if config.WEATHER_SERVICE and config.WEATHER_SERVICE != "none": + log_write("rain", "info", f"Checking rain from online service: {config.WEATHER_SERVICE}") + # Implementa la chiamata all'API meteo (es. con 'requests') + # Se piove, aggiorna il timestamp in config.STATUS_DIR/last_rain_online + # Esempio: + # import requests + # try: + # response = requests.get(f"https://api.example.com/weather?location={config.WEATHER_LOCATION}&api_key={config.WEATHER_API_KEY}") + # data = response.json() + # if data.get("rain_detected"): # Logica specifica per il tuo servizio + # with open(os.path.join(config.STATUS_DIR, "last_rain_online"), "w") as f: + # f.write(str(int(time.time()))) + # log_write("rain", "info", "Rain detected by online service.") + # except Exception as e: + # log_write("rain", "error", f"Error checking online rain: {e}") + pass + else: + log_write("rain", "info", "Online rain service not configured.") + +def reset_last_rain_sensor_timestamp(): + """Resetta il timestamp dell'ultima pioggia dal sensore.""" + file_path = os.path.join(config.STATUS_DIR, "last_rain_sensor") + if os.path.exists(file_path): + os.remove(file_path) + log_write("rain", "info", "Timestamp of last sensor rain successfully reset.") + +def reset_last_rain_online_timestamp(): + """Resetta il timestamp dell'ultima pioggia online.""" + file_path = os.path.join(config.STATUS_DIR, "last_rain_online") + if os.path.exists(file_path): + os.remove(file_path) + log_write("rain", "info", "Timestamp of last online rain successfully reset.") + +def last_rain_sensor_timestamp(): + """Mostra il timestamp dell'ultima pioggia dal sensore.""" + file_path = os.path.join(config.STATUS_DIR, "last_rain_sensor") + if os.path.exists(file_path): + with open(file_path, "r") as f: + print(f.read().strip()) + else: + print("") + +def last_rain_online_timestamp(): + """Mostra il timestamp dell'ultima pioggia online.""" + file_path = os.path.join(config.STATUS_DIR, "last_rain_online") + if os.path.exists(file_path): + with open(file_path, "r") as f: + print(f.read().strip()) + else: + print("") + +# --- FUNZIONI EVENTI (PLACEHOLDERS) --- +# Queste funzioni simulano il sistema di eventi. +# Potresti voler implementare un sistema di eventi più robusto (es. con un dispatcher). +def trigger_event(event_name, alias="", param1="", param2=""): + """Triggera un evento.""" + config.CURRENT_EVENT = event_name + config.CURRENT_EVENT_ALIAS = alias + log_write("event", "info", f"Triggering event: {event_name} (alias: {alias}, params: {param1}, {param2})") + # Qui potresti chiamare script esterni o funzioni Python basate sull'evento + # Esempio: + # if event_name == "init_before": + # # Esegui codice specifico per init_before + # pass + return 0 # 0 per successo, diverso da 0 per errore (come in Bash) + +# --- FUNZIONI PRINCIPALI DEL SISTEMA --- + +def initialize(): + """Inizializza le elettrovalvole e l'alimentazione.""" + log_write("general", "info", "Run initialize") + unlock() # Assicurati che non ci siano lock pendenti + + trigger_event("init_before", "") + + # Inizializza i driver gpio + # list_drv non è definito nello script Bash fornito, presumo sia una lista di driver + # Per ora, useremo solo drv_init + log_write("drv", "info", "Initializing all drivers...") + drv_init() # Chiamata generica per inizializzare tutti i driver + + # Imposta l'alimentazione con voltaggio negativo e setta i gpio in scrittura per le elettrovalvole bistabili + if config.EV_MONOSTABLE != 1: + # SUPPLY_GPIO_1 e SUPPLY_GPIO_2 non sono definiti in config, dovresti aggiungerli + # Esempio: config.SUPPLY_GPIO_1 = 5, config.SUPPLY_GPIO_2 = 6 + drv_supply_bistable_init(getattr(config, 'SUPPLY_GPIO_1', None), getattr(config, 'SUPPLY_GPIO_2', None)) + + # Elimina tutti gli stati delle elettrovalvole preesistenti + try: + for f in os.listdir(config.STATUS_DIR): + if f.startswith("ev"): + os.remove(os.path.join(config.STATUS_DIR, f)) + log_write("general", "info", "Removed existing EV status files.") + except Exception as e: + log_write("general", "error", f"Error removing EV status files: {e}") + + # Inizializza i gpio delle elettrovalvole e ne chiude l'alimentazione + for i in range(1, config.EV_TOTAL + 1): + gpio = ev_number2gpio(i) + if gpio is not None: + drv_rele_init(gpio) + ev_set_state(i, 0) # Imposta lo stato iniziale a chiuso + + # Chiude tutte le elettrovalvole + for i in range(1, config.EV_TOTAL + 1): + alias = config.EV_CONFIG.get(i, {}).get("ALIAS") + if alias: + ev_close(alias) # Chiama ev_close per ogni valvola + + # Inizializza il sensore di rilevamento pioggia + if config.RAIN_GPIO: + drv_rain_sensor_init(config.RAIN_GPIO) + log_write("rain", "info", "Rain sensor initialized") + else: + log_write("rain", "info", "Rain sensor not present") + + trigger_event("init_after", "") + log_write("general", "info", "End initialize") + +def ev_open(alias, force=""): + """Commuta un'elettrovalvola nello stato aperto.""" + cron_del("open_in", alias) # Non catturiamo l'output, come &> /dev/null + + ev_num = ev_alias2number(alias) + gpio = ev_number2gpio(ev_num) + ev_norain = ev_number2norain(ev_num) + ev_is_remote = ev_number2remote(ev_num) + ev_is_monostable = ev_number2monostable(ev_num) + + if force != "force": + moisture = ev_check_moisture(ev_num) + if moisture > 0: + message_write("warning", "solenoid not open because maximum soil moisture has been reached") + trigger_event("ev_not_open_for_moisture", alias) + log_write("irrigate", "warning", f"Solenoid '{alias}' not open because maximum soil moisture has been reached") + return + + if ev_norain != 1: + now = int(time.time()) + + # Controllo pioggia online + last_rain_online_file = os.path.join(config.STATUS_DIR, "last_rain_online") + if config.NOT_IRRIGATE_IF_RAIN_ONLINE > 0 and os.path.exists(last_rain_online_file): + try: + with open(last_rain_online_file, "r") as f: + last_rain_online = int(f.read().strip()) + diff = now - last_rain_online + if diff < config.NOT_IRRIGATE_IF_RAIN_ONLINE and moisture == 0: # Bash aveva moisture -ne 0 + message_write("warning", "Solenoid not open for rain") + trigger_event("ev_not_open_for_rain_online", alias) + trigger_event("ev_not_open_for_rain", alias) + log_write("irrigate", "warning", f"Solenoid '{alias}' not open for rain (online check)") + return + except (IOError, ValueError): + pass # Ignora errori di lettura del file + + # Controllo pioggia sensore + check_rain_sensor() # Assicurati che aggiorni last_rain_sensor + last_rain_sensor_file = os.path.join(config.STATUS_DIR, "last_rain_sensor") + if config.NOT_IRRIGATE_IF_RAIN_SENSOR > 0 and os.path.exists(last_rain_sensor_file): + try: + with open(last_rain_sensor_file, "r") as f: + last_rain_sensor = int(f.read().strip()) + diff = now - last_rain_sensor + if diff < config.NOT_IRRIGATE_IF_RAIN_SENSOR and moisture == 0: # Bash aveva moisture -ne 0 + message_write("warning", "Solenoid not open for rain") + trigger_event("ev_not_open_for_rain_sensor", alias) + trigger_event("ev_not_open_for_rain", alias) + log_write("irrigate", "warning", f"Solenoid '{alias}' not open for rain (sensor check)") + return + except (IOError, ValueError): + pass # Ignora errori di lettura del file + + state = 1 + if force == "force": + state = 2 + + if trigger_event("ev_open_before", alias, force) != 0: + log_write("irrigate", "warning", f"Solenoid '{alias}' not open due to external event") + message_write('warning', "Solenoid not open due to external event") + mqtt_status() # Chiamata a mqtt_status (placeholder) + return + + lock() + + # Gestisce l'apertura dell'elettrovalvola in base alla tipologia (monostabile / bistabile) + if config.EV_MONOSTABLE == 1 or ev_is_remote == 1 or ev_is_monostable == 1: + if drv_rele_close(gpio) == 1: # Se drv_rele_close restituisce 1 per errore + unlock() + return + else: + supply_positive() # supply_positive(config.SUPPLY_GPIO_1, config.SUPPLY_GPIO_2) + drv_rele_close(gpio) + time.sleep(1) + drv_rele_open(gpio) + + ev_set_state(ev_num, state) + + log_write("irrigate", "info", f"Solenoid '{alias}' open") + message_write("success", "Solenoid open") + + trigger_event("ev_open_after", alias, force) + + unlock() + +def ev_open_in(minute_start_str, minute_stop_str, alias, force=""): + """Commuta un'elettrovalvola nello stato aperto dopo un intervallo.""" + try: + minute_start = int(minute_start_str) + minute_stop = int(minute_stop_str) + except ValueError: + print("Time start/stop of irrigation is wrong or not specified", file=sys.stderr) + message_write("warning", "Time start/stop of irrigation is wrong or not specified") + mqtt_status() + return 1 + + if minute_stop < 1: + print("Time stop of irrigation is wrong", file=sys.stderr) + message_write("warning", "Time stop of irrigation is wrong") + mqtt_status() + return 1 + + if not alias: + print("Alias solenoid not specified", file=sys.stderr) + message_write("warning", "Alias solenoid not specified") + mqtt_status() + return 1 + + trigger_event("ev_open_in_before", alias, force, minute_start_str, minute_stop_str) + + # gpio_alias2number $alias > /dev/null 2>&1 + # Questa riga in Bash sembra solo verificare l'esistenza dell'alias e uscire in caso di errore. + # La funzione ev_alias2number già fa questo. + ev_alias2number(alias) # Per verificare che l'alias esista + + minute_start_actual = minute_start + 1 + minute_stop_actual = minute_start_actual + minute_stop + + # Calcola le date/ore per cron + # In Python, non generiamo stringhe cron direttamente per `date -d`, + # ma calcoliamo i timestamp e li passiamo alle funzioni cron_add/del. + # Le funzioni cron_add/del dovranno poi convertire questi timestamp in stringhe cron se necessario. + + # Per semplicità, qui useremo un formato simile a quello di Bash per cron_start/stop + # che le funzioni cron_add/del dovranno interpretare. + # cron_start_dt = datetime.datetime.now() + datetime.timedelta(minutes=minute_start_actual) + # cron_stop_dt = datetime.datetime.now() + datetime.timedelta(minutes=minute_stop_actual) + # cron_start_str_for_cron = cron_start_dt.strftime("%M %H %d %m %w") # %w per giorno della settimana (0-6) + # cron_stop_str_for_cron = cron_stop_dt.strftime("%M %H %d %m %w") + + # Replicando il formato Bash per cron_add/del + # Il formato è "MM HH DD MM DW" (minuto, ora, giorno del mese, mese, giorno della settimana) + # dove DW è 0-6 (domenica-sabato) per `date +%u` in Bash. + # Python's datetime.strftime('%w') restituisce 0-6 (domenica-sabato) + + # Questo è un placeholder. Le funzioni cron_add/del dovrebbero gestire la logica cron reale. + # Qui passiamo solo i minuti relativi. + cron_del("open_in", alias) + cron_del("open_in_stop", alias) + + if minute_start_actual == 1: + ev_open(alias, force) + cron_start_info = "- - - - -" # Come in Bash + else: + # In un sistema reale, cron_add riceverebbe la data/ora esatta o i minuti di ritardo + # e si occuperebbe di schedulare il job. + cron_add("open_in", f"+{minute_start_actual}m", alias, force) + cron_start_info = f"Scheduled in {minute_start_actual} minutes" # Info per il log + + cron_add("open_in_stop", f"+{minute_stop_actual}m", alias) + cron_stop_info = f"Scheduled stop in {minute_stop_actual} minutes" # Info per il log + + message_write("success", "Scheduled start successfully performed") + + trigger_event("ev_open_in_after", alias, force, cron_start_info, cron_stop_info) + return 0 + + +def ev_close(alias): + """Commuta un'elettrovalvola nello stato chiuso.""" + ev_num = ev_alias2number(alias) + gpio = ev_number2gpio(ev_num) + ev_is_remote = ev_number2remote(ev_num) + + trigger_event("ev_close_before", alias) + + lock() + + if config.EV_MONOSTABLE == 1 or ev_is_remote == 1: + if drv_rele_open(gpio) == 1: # Se drv_rele_open restituisce 1 per errore + unlock() + return + else: + supply_negative() # supply_negative(config.SUPPLY_GPIO_1, config.SUPPLY_GPIO_2) + drv_rele_close(gpio) + time.sleep(1) + drv_rele_open(gpio) + + ev_set_state(ev_num, 0) # 0 = chiuso + + log_write("irrigate", "info", f"Solenoid '{alias}' close") + message_write("success", "Solenoid close") + + trigger_event("ev_close_after", alias) + + unlock() + + cron_del("open_in_stop", alias) # Non catturiamo l'output, come &> /dev/null + +def supply_positive(): + """Imposta l'alimentazione delle elettrovalvole con voltaggio positivo.""" + # SUPPLY_GPIO_1 e SUPPLY_GPIO_2 non sono definiti in config, dovresti aggiungerli + drv_supply_positive(getattr(config, 'SUPPLY_GPIO_1', None), getattr(config, 'SUPPLY_GPIO_2', None)) + +def supply_negative(): + """Imposta l'alimentazione delle elettrovalvole con voltaggio negativo.""" + # SUPPLY_GPIO_1 e SUPPLY_GPIO_2 non sono definiti in config, dovresti aggiungerli + drv_supply_negative(getattr(config, 'SUPPLY_GPIO_1', None), getattr(config, 'SUPPLY_GPIO_2', None)) + +def send_identifier(): + """Invia l'identificativo univoco ad uso statistico di utilizzo.""" + if config.NO_SEND_IDENTIFIER == 1: + return + + file_id_path = "/tmp/pigarden.id" + + if os.path.exists(file_id_path): + max_age_file = 86400 # 1 giorno in secondi + try: + time_file = os.path.getmtime(file_id_path) # Tempo dell'ultima modifica + age_file = int(time.time()) - int(time_file) + if age_file < max_age_file: + return # ID troppo giovane, non inviare + except OSError as e: + log_write("send_identifier", "error", f"Error getting file modification time for {file_id_path}: {e}") + return + + # Genera l'ID (come nello script Bash precedente) + try: + ifconfig_output = subprocess.check_output(['/sbin/ifconfig'], encoding='utf-8') + mac_address_pattern = re.compile(r'([0-9a-fA-F]{1,2}:){5}[0-9a-fA-F]{1,2}') + match = mac_address_pattern.search(ifconfig_output) + + if match: + mac_address = match.group(0) + id_hash = hashlib.md5(mac_address.encode('utf-8')).hexdigest() + else: + log_write("send_identifier", "warning", "No MAC address found for identifier.") + return + except (FileNotFoundError, subprocess.CalledProcessError) as e: + log_write("send_identifier", "error", f"Error getting MAC address for identifier: {e}") + return + + if not id_hash: + return + + try: + with open(file_id_path, "w") as f: + f.write(id_hash) + except IOError as e: + log_write("send_identifier", "error", f"Error writing identifier to {file_id_path}: {e}") + return + + log_write("general", "info", "Send installation identifier to collect usage") + + # Invio via CURL (subprocess.Popen per emulare nohup ... &) + url = f"https://www.lejubila.net/statistic/collect_usage/piGarden/{id_hash}/{config.VERSION}/{config.SUB_VERSION}/{config.RELEASE_VERSION}" + cmd = ["curl", url] + subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + +def exec_poweroff(): + """Spegne il sistema.""" + trigger_event("exec_poweroff_before", "") + log_write("system", "warning", "Executing system poweroff...") + # In un sistema reale, questo script potrebbe chiamare un altro script Python o un comando di sistema. + # Per replicare il comportamento Bash, potremmo chiamare un comando di sistema. + # Il sleep 15 è strano, forse per dare tempo al log di essere scritto. + time.sleep(15) # Simula il sleep + + # Esegui lo script poweroff.sh se esiste, altrimenti il comando di sistema + poweroff_script_path = os.path.join(config.DIR_SCRIPT, "scripts", "poweroff.sh") + if os.path.exists(poweroff_script_path) and os.access(poweroff_script_path, os.X_OK): + try: + subprocess.run([poweroff_script_path], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except subprocess.CalledProcessError as e: + log_write("system", "error", f"Error executing poweroff script: {e}") + else: + try: + # Assicurati che l'utente abbia i permessi sudo senza password per 'poweroff' + subprocess.run(["sudo", "poweroff"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except subprocess.CalledProcessError as e: + log_write("system", "error", f"Error executing sudo poweroff: {e}") + except FileNotFoundError: + log_write("system", "error", "Comando 'sudo' o 'poweroff' non trovato.") + + trigger_event("exec_poweroff_after", "") + +def exec_reboot(): + """Riavvia il sistema.""" + trigger_event("exec_reboot_before", "") + log_write("system", "warning", "Executing system reboot...") + time.sleep(15) # Simula il sleep + + reboot_script_path = os.path.join(config.DIR_SCRIPT, "scripts", "reboot.sh") + if os.path.exists(reboot_script_path) and os.access(reboot_script_path, os.X_OK): + try: + subprocess.run([reboot_script_path], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except subprocess.CalledProcessError as e: + log_write("system", "error", f"Error executing reboot script: {e}") + else: + try: + # Assicurati che l'utente abbia i permessi sudo senza password per 'reboot' + subprocess.run(["sudo", "reboot"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except subprocess.CalledProcessError as e: + log_write("system", "error", f"Error executing sudo reboot: {e}") + except FileNotFoundError: + log_write("system", "error", "Comando 'sudo' o 'reboot' non trovato.") + + trigger_event("exec_reboot_after", "") + +def cmd_pigardensched(*args): + """Esegue un comando con piGardenSched.""" + if config.PIGARDENSCHED == 0: + print("piGardenSched not configured in piGarden", file=sys.stderr) + log_write("piGardenSched", "error", "piGardenSched not configured in piGarden") + return 1 # Errore + + try: + # Passa tutti gli argomenti al sottoprocesso + result = subprocess.run([config.PIGARDENSCHED_PATH] + list(args), capture_output=True, text=True, check=True) + # print(result.stdout.strip()) # Stampa l'output di piGardenSched se necessario + return 0 # Successo + except subprocess.CalledProcessError as e: + print("piGardenSched command failed", file=sys.stderr) + log_write("piGardenSched", "error", f"piGardenSched command failed: {e.stderr.strip()}") + return 1 # Errore + except FileNotFoundError: + print(f"piGardenSched executable not found at {config.PIGARDENSCHED_PATH}", file=sys.stderr) + log_write("piGardenSched", "error", f"piGardenSched executable not found at {config.PIGARDENSCHED_PATH}") + return 1 + +def deg2dir(degrees): + """Converte da gradi a direzione.""" + try: + deg = int(float(degrees)) # Gestisce anche i decimali come in Bash (sed 's/\..*$//') + except (ValueError, TypeError): + return "" # Come in Bash per "null" o input non valido + + if deg <= 11: return "North" + elif deg <= 33: return "NNE" + elif deg <= 56: return "NE" + elif deg <= 78: return "ENE" + elif deg <= 101: return "East" + elif deg <= 123: return "ESE" + elif deg <= 146: return "SE" + elif deg <= 168: return "SSE" + elif deg <= 191: return "South" + elif deg <= 213: return "SSW" + elif deg <= 236: return "SW" + elif deg <= 258: return "WSW" + elif deg <= 281: return "West" + elif deg <= 303: return "WNW" + elif deg <= 326: return "NW" + elif deg <= 348: return "NNW" + else: return "North" + +def debug1(*args): + """Esegue il codice di debug 1.""" + log_write("debug", "info", f"Running debug1 with args: {args}") + # Qui potresti importare ed eseguire un modulo Python dedicato al debug + # Esempio: + # from debug import debug1_script + # debug1_script.run(*args) + print("Debug1 executed (placeholder).") + +def debug2(*args): + """Esegue il codice di debug 2.""" + log_write("debug", "info", f"Running debug2 with args: {args}") + # Esempio: + # from debug import debug2_script + # debug2_script.run(*args) + print("Debug2 executed (placeholder).") + +# --- FUNZIONE JSON STATUS --- +# Questa è una funzione complessa che richiede molte dipendenze. +# Ho cercato di replicare la sua logica il più fedelmente possibile. +def json_status_wrapper(*args_from_cli): + """ + Stampa un json contenente lo status della centralina. + I parametri opzionali controllano quali dati aggiuntivi includere. + """ + json_data = {} + + # Inizializza i messaggi (come le variabili globali in Bash) + json_data["info"] = config.MESSAGE_INFO + json_data["warning"] = config.MESSAGE_WARNING + json_data["success"] = config.MESSAGE_SUCCESS + + # Versione + json_data["version"] = { + "ver": config.VERSION, + "sub": config.SUB_VERSION, + "rel": config.RELEASE_VERSION + } + + # Timestamp + json_data["timestamp"] = int(time.time()) + + # Evento corrente + json_data["event"] = { + "event": config.CURRENT_EVENT, + "alias": config.CURRENT_EVENT_ALIAS + } + + # PID corrente (PARENT_PID se specificato) + current_pid = os.getpid() + if config.PARENT_PID > 0: + current_pid = config.PARENT_PID + json_data["pid"] = current_pid # Aggiunto per chiarezza, non esplicito in Bash ma utile + + # Parsing degli argomenti per includere dati aggiuntivi + with_get_cron = "0" + with_get_cron_open_in = "0" + with_get_schedule = "0" + + for arg in args_from_cli: + if arg == "get_cron": + with_get_cron = "1" + elif arg.startswith("get_cron:"): + with_get_cron = arg.split(":")[1] + elif arg == "get_cron_open_in": + with_get_cron_open_in = "1" + elif arg.startswith("get_cron_open_in:"): + with_get_cron_open_in = arg.split(":")[1] + elif arg == "get_schedule" and config.PIGARDENSCHED == 1: + with_get_schedule = "1" + + # Stato delle zone/elettrovalvole + zones_data = {} + ev_data_aliases = {} + for i in range(1, config.EV_TOTAL + 1): + alias = config.EV_CONFIG.get(i, {}).get("ALIAS", f"EV{i}") + state = ev_get_state(i) # ev_status $av > /dev/null; local sv=$? + zones_data[alias] = {"name": alias, "state": state} + ev_data_aliases[f"EV{i}"] = {"alias": alias} # EVx_ALIAS + json_data["zones"] = zones_data + json_data["ev"] = ev_data_aliases + + # Ultima pioggia (sensore e online) + last_rain_sensor_file = os.path.join(config.STATUS_DIR, "last_rain_sensor") + last_rain_online_file = os.path.join(config.STATUS_DIR, "last_rain_online") + last_weather_online_file = os.path.join(config.STATUS_DIR, "last_weather_online") + + json_data["last_rain_sensor"] = "" + if os.path.exists(last_rain_sensor_file): + try: + with open(last_rain_sensor_file, "r") as f: + json_data["last_rain_sensor"] = f.read().strip() + except IOError: + pass + + json_data["last_rain_online"] = "" + if os.path.exists(last_rain_online_file): + try: + with open(last_rain_online_file, "r") as f: + json_data["last_rain_online"] = f.read().strip() + except IOError: + pass + + json_data["last_weather_online"] = "" # In Bash era un JSON string, qui lo mettiamo come stringa vuota o il contenuto del file + if os.path.exists(last_weather_online_file): + try: + with open(last_weather_online_file, "r") as f: + content = f.read().strip() + # Se il contenuto è già un JSON valido, lo si può includere direttamente + # Altrimenti, lo si lascia come stringa + try: + json.loads(content) # Prova a parsare come JSON + json_data["last_weather_online"] = json.loads(content) + except json.JSONDecodeError: + json_data["last_weather_online"] = content # Lascia come stringa se non è JSON valido + except IOError: + pass + + # Errore (sempre presente nel JSON Bash) + json_data["error"] = {"code": 0, "description": ""} # Placeholder, dovrebbe essere aggiornato dalle funzioni di errore + + # Dati cron (open/close) + cron_open_data = {} + cron_close_data = {} + if with_get_cron != "0": + element_for_cron = [] + if with_get_cron == "1": + element_for_cron = range(1, config.EV_TOTAL + 1) + else: + try: + element_for_cron = [ev_alias2number(with_get_cron)] + except SystemExit: # ev_alias2number esce in caso di errore + pass # Non aggiungere cron data se l'alias non esiste + + for i in element_for_cron: + alias = config.EV_CONFIG.get(i, {}).get("ALIAS") + if alias: + crn_open = cron_get("open", alias).replace("\n", "%%") + cron_open_data[alias] = crn_open + crn_close = cron_get("close", alias).replace("\n", "%%") + cron_close_data[alias] = crn_close + json_data["cron"] = {"open": cron_open_data, "close": cron_close_data} + + # Dati schedule (piGardenSched) + schedule_data = {} + if with_get_schedule == "1": + try: + # Esegui piGardenSched e parsa l'output + result = subprocess.run([config.PIGARDENSCHED_PATH, "sched"], capture_output=True, text=True, check=True) + for line in result.stdout.strip().split('\n'): + if line: + parts = line.split(';') + if len(parts) >= 1: + ev_key = parts[0] + # Cerca l'alias corrispondente + alias_found = "" + for num, ev_conf in config.EV_CONFIG.items(): + if f"EV{num}" == ev_key: + alias_found = ev_conf["ALIAS"] + break + if alias_found: + schedule_data[ev_key] = {"alias": alias_found, "entry": line} + else: + log_write("json_status", "warning", f"Alias not found for scheduled EV key: {ev_key}") + json_data["schedule"] = schedule_data + except (subprocess.CalledProcessError, FileNotFoundError) as e: + log_write("json_status", "error", f"Error getting piGardenSched schedule: {e}") + json_data["schedule"] = {} # In caso di errore, restituisci un oggetto vuoto + else: + json_data["schedule"] = {} # Sempre includi la chiave, anche se vuota + + # Dati cron_open_in + cron_open_in_data = {} + cron_open_in_stop_data = {} + if with_get_cron_open_in != "0": + element_for_cron_open_in = [] + if with_get_cron_open_in == "1": + element_for_cron_open_in = range(1, config.EV_TOTAL + 1) + else: + try: + element_for_cron_open_in = [ev_alias2number(with_get_cron_open_in)] + except SystemExit: + pass + + for i in element_for_cron_open_in: + alias = config.EV_CONFIG.get(i, {}).get("ALIAS") + if alias: + crn_open_in = cron_get("open_in", alias).replace("\n", "%%") + cron_open_in_data[alias] = crn_open_in + crn_open_in_stop = cron_get("open_in_stop", alias).replace("\n", "%%") + cron_open_in_stop_data[alias] = crn_open_in_stop + json_data["cron_open_in"] = {"open_in": cron_open_in_data, "open_in_stop": cron_open_in_stop_data} + + # Stato dei sensori + json_data["sensors"] = json.loads(json_sensor_status_all()) # Assumendo che json_sensor_status_all restituisca JSON string + + # Stampa il JSON finale + print(json.dumps(json_data, indent=2)) # indent=2 per una stampa leggibile + +# --- MQTT STATUS --- +def mqtt_status(parent_pid=None): + """Invia al broker mqtt il json contenente lo stato del sistema.""" + if not config.MQTT_ENABLE == 1: + return + + if parent_pid is not None: + config.PARENT_PID = parent_pid + + # Genera il JSON di stato + # Cattura l'output di json_status_wrapper() reindirizzando stdout + old_stdout = sys.stdout + sys.stdout = captured_output = io.StringIO() + json_status_wrapper() + sys.stdout = old_stdout + js = captured_output.getvalue().strip() + + try: + # Usa paho-mqtt per inviare il messaggio + # pip install paho-mqtt + import paho.mqtt.client as mqtt + + client = mqtt.Client(client_id=config.MQTT_CLIENT_ID) + if config.MQTT_USER and config.MQTT_PWD: + client.username_pw_set(config.MQTT_USER, config.MQTT_PWD) + + client.connect(config.MQTT_HOST, config.MQTT_PORT, 60) # 60 secondi di keepalive + client.publish(config.MQTT_TOPIC, js, qos=1, retain=True) # retain=True come -r + client.disconnect() + log_write("mqtt", "info", "MQTT status published successfully.") + except ImportError: + log_write("mqtt", "error", "paho-mqtt module not found. Cannot publish MQTT status.") + except Exception as e: + log_write("mqtt", "error", f"Error publishing MQTT status: {e}") + + +# --- FUNZIONE PRINCIPALE (CLI DISPATCHER) --- + +def show_usage(): + """Mostra i parametri dello script.""" + print(f"piGarden v. {config.VERSION}.{config.SUB_VERSION}.{config.RELEASE_VERSION}") + print("\nUsage:") + print(f"\t{config.NAME_SCRIPT} init initialize supply and solenoid in closed state") + print(f"\t{config.NAME_SCRIPT} open alias [force] open a solenoid") + print(f"\t{config.NAME_SCRIPT} open_in minute_start minute_stop alias [force] open a solenoid in minute_start for minute_stop") + print(f"\t{config.NAME_SCRIPT} close alias close a solenoid") + print(f"\t{config.NAME_SCRIPT} list_alias view list of aliases solenoid") + print(f"\t{config.NAME_SCRIPT} ev_status alias show status solenoid") + print(f"\t{config.NAME_SCRIPT} ev_status_all show status solenoids") + print("\n") + print(f"\t{config.NAME_SCRIPT} list_alias_sensor view list of aliases sensor") + print(f"\t{config.NAME_SCRIPT} sensor_status alias [type] show status sensor (type: {config.SENSOR_STATE_TYPE})") + print(f"\t{config.NAME_SCRIPT} sensor_status_set alias type value set status of sensor (type: {config.SENSOR_STATE_TYPE})") + print(f"\t{config.NAME_SCRIPT} sensor_status_all show status of all sensors") + print("\n") + print(f"\t{config.NAME_SCRIPT} last_rain_sensor_timestamp show timestamp of last rain sensor") + print(f"\t{config.NAME_SCRIPT} last_rain_online_timestamp show timestamp of last rain online") + print(f"\t{config.NAME_SCRIPT} reset_last_rain_sensor_timestamp show timestamp of last rain sensor") + print(f"\t{config.NAME_SCRIPT} reset_last_rain_online_timestamp show timestamp of last rain online") + print("\n") + print(f"\t{config.NAME_SCRIPT} json_status [get_cron|get_cron_open_in|get_schedule] show status in json format") + print(f"\t{config.NAME_SCRIPT} mqtt_status send status in json format to mqtt broker") + print("\n") + print(f"\t{config.NAME_SCRIPT} check_rain_online check rain from online api service") + print(f"\t{config.NAME_SCRIPT} check_rain_sensor check rain from hardware sensor") + print("\n") + print(f"\t{config.NAME_SCRIPT} close_all_for_rain close all solenoid if it's raining") + print(f"\t{config.NAME_SCRIPT} close_all [force] close all solenoid") + print("\n") + print(f"\t{config.NAME_SCRIPT} start_socket_server [force] start socket server, with 'force' parameter force close socket server if already open") + print(f"\t{config.NAME_SCRIPT} stop_socket_server stop socket server") + print("\n") + print(f"\t{config.NAME_SCRIPT} reboot reboot system") + print(f"\t{config.NAME_SCRIPT} poweroff shutdown system") + print("\n") + print(f"\t{config.NAME_SCRIPT} set_cron_init set crontab for initialize control unit") + print(f"\t{config.NAME_SCRIPT} del_cron_init remove crontab for initialize control unit") + print(f"\t{config.NAME_SCRIPT} set_cron_start_socket_server set crontab for start socket server") + print(f"\t{config.NAME_SCRIPT} del_cron_start_socket_server remove crontab for start socket server") + print(f"\t{config.NAME_SCRIPT} set_cron_check_rain_sensor set crontab for check rein from sensor") + print(f"\t{config.NAME_SCRIPT} del_cron_check_rain_sensor remove crontab for check rein from sensor") + print(f"\t{config.NAME_SCRIPT} set_cron_check_rain_online set crontab for check rein from online service") + print(f"\t{config.NAME_SCRIPT} del_cron_check_rain_online remove crontab for check rein from online service") + print(f"\t{config.NAME_SCRIPT} set_cron_close_all_for_rain set crontab for close all solenoid when raining") + print(f"\t{config.NAME_SCRIPT} del_cron_close_all_for_rain remove crontab for close all solenoid when raining") + print("\n") + print(f"\t{config.NAME_SCRIPT} add_cron_open alias m h dom mon dow [disabled] add crontab for open a solenoid") + print(f"\t{config.NAME_SCRIPT} del_cron_open alias remove all crontab for open a solenoid") + print(f"\t{config.NAME_SCRIPT} get_cron_open alias get all crontab for open a solenoid") + print(f"\t{config.NAME_SCRIPT} del_cron_open_in alias remove all crontab for open_in a solenoid") + print(f"\t{config.NAME_SCRIPT} add_cron_close alias m h dom mon dow [disabled] add crontab for close a solenoid") + print(f"\t{config.NAME_SCRIPT} del_cron_close alias remove all crontab for close a solenoid") + print(f"\t{config.NAME_SCRIPT} get_cron_close alias get all crontab for close a solenoid") + print("\n") + print(f"\t{config.NAME_SCRIPT} cmd_pigardensched [prm1 [prm2 [prm3]...]] performs a pigardensched command") + print("\n") + print(f"\t{config.NAME_SCRIPT} debug1 [parameter]|[parameter]|..] Run debug code 1") + print(f"\t{config.NAME_SCRIPT} debug2 [parameter]|[parameter]|..] Run debug code 2") + + +# --- MAIN EXECUTION BLOCK --- +if __name__ == "__main__": + # Carica la configurazione dal file /etc/piGarden.conf (simulazione) + # In un'applicazione reale, useresti una libreria per parsare un file .conf o .ini + # Per ora, assumiamo che la classe Config sia già popolata con i valori di default + # o che tu la popoli manualmente qui. + # Esempio di caricamento (molto semplificato): + # try: + # with open(config.CONFIG_ETC, 'r') as f: + # # Parsa il file di configurazione e aggiorna gli attributi di 'config' + # # Questo richiede un parser INI/JSON/YAML a seconda del formato del tuo file + # pass + # except FileNotFoundError: + # print(f"Config file not found in {config.CONFIG_ETC}", file=sys.stderr) + # sys.exit(1) + + # Importa le funzioni del server socket (dal file precedente) + # Questo richiede che il file socket_server.py sia nello stesso PATH + # o che tu lo abbia copiato/incollato qui. + # Per evitare dipendenze circolari o problemi di import, + # ho copiato le funzioni start_socket_server e stop_socket_server direttamente qui. + + # Ho bisogno di importare io per catturare l'output di json_status_wrapper + import io + + # Funzioni del server socket (copiate dal file precedente per auto-contenimento) + # Rimuovi questa sezione se preferisci importare da un modulo separato. + + # Globals per il server socket (dall'altro script) + TCPSERVER_PID_FILE = config.TCPSERVER_PID_FILE + TCPSERVER_IP = "0.0.0.0" # Ascolta su tutte le interfacce disponibili + TCPSERVER_PORT = 12345 # Porta del server socket + TCPSERVER_USER = config.TCPSERVER_USER # Credenziali opzionali per l'autenticazione + TCPSERVER_PWD = config.TCPSERVER_PWD + + class MyTCPHandler(socketserver.BaseRequestHandler): + def handle(self): + global RUN_FROM_TCPSERVER + RUN_FROM_TCPSERVER = True + client_ip = self.client_address[0] + log_write("socket_server", "info", f"Nuova connessione da: {client_ip}") + response_to_client = "" + + try: + if TCPSERVER_USER and TCPSERVER_PWD: + self.request.settimeout(3) + try: + user_line = self.request.recv(1024).decode('utf-8').strip() + password_line = self.request.recv(1024).decode('utf-8').strip() + except socket.timeout: + log_write("socket_server", "warning", f"socket connection from: {client_ip} - Timeout during credentials read") + response_to_client = json_error(0, "Authentication timeout") + self.request.sendall(response_to_client.encode('utf-8') + b'\n') + return + + if user_line != TCPSERVER_USER or password_line != TCPSERVER_PWD: + log_write("socket_server", "warning", f"socket connection from: {client_ip} - Bad socket server credentials - user:{user_line}") + response_to_client = json_error(0, "Bad socket server credentials") + self.request.sendall(response_to_client.encode('utf-8') + b'\n') + return + else: + log_write("socket_server", "info", f"socket connection from: {client_ip} - Authentication successful") + + self.request.settimeout(None) + command_line = self.request.recv(4096).decode('utf-8').strip() + + args = command_line.split(' ') + # Mappa gli argomenti per facilitare il passaggio alle funzioni + cmd_args = [args[i] if len(args) > i else "" for i in range(8)] + + log_write("socket_server", "info", f"socket connection from: {client_ip} - command: {command_line}") + + # Cattura l'output delle funzioni che stampano su stdout + old_stdout = sys.stdout + sys.stdout = captured_output = io.StringIO() + + # Chiama la funzione di dispatch principale con gli argomenti + # Questo è il punto dove il server socket chiama le funzioni di piGarden + dispatch_command(*cmd_args) + + sys.stdout = old_stdout + response_to_client = captured_output.getvalue().strip() + + # Se non c'è una risposta esplicita (es. da json_status), invia un messaggio di successo generico + if not response_to_client: + response_to_client = json.dumps({"status": "success", "message": "Command executed."}) + + self.request.sendall(response_to_client.encode('utf-8') + b'\n') + + except socket.timeout: + log_write("socket_server", "warning", f"socket connection from: {client_ip} - Timeout waiting for command.") + response_to_client = json_error(0, "Timeout waiting for command") + self.request.sendall(response_to_client.encode('utf-8') + b'\n') + except Exception as e: + log_write("socket_server", "error", f"Errore durante la gestione della connessione da {client_ip}: {e}") + response_to_client = json_error(-1, f"Internal server error: {e}") + self.request.sendall(response_to_client.encode('utf-8') + b'\n') + finally: + self.request.close() + RUN_FROM_TCPSERVER = False + + def start_socket_server_internal(): + if os.path.exists(TCPSERVER_PID_FILE): + try: + os.remove(TCPSERVER_PID_FILE) + log_write("socket_server", "info", f"Rimosso file PID esistente: {TCPSERVER_PID_FILE}") + except OSError as e: + log_write("socket_server", "error", f"Errore nella rimozione del file PID: {e}") + sys.exit(1) + + current_pid = os.getpid() + try: + with open(TCPSERVER_PID_FILE, "w") as f: + f.write(str(current_pid)) + log_write("socket_server", "info", f"Server PID {current_pid} scritto in {TCPSERVER_PID_FILE}") + except IOError as e: + log_write("socket_server", "error", f"Errore nella scrittura del file PID: {e}") + sys.exit(1) + + try: + server = socketserver.ThreadingTCPServer((TCPSERVER_IP, TCPSERVER_PORT), MyTCPHandler) + log_write("socket_server", "info", f"Server socket avviato su {TCPSERVER_IP}:{TCPSERVER_PORT}") + server.serve_forever() + except Exception as e: + log_write("socket_server", "error", f"Errore all'avvio del server socket: {e}") + if os.path.exists(TCPSERVER_PID_FILE): + os.remove(TCPSERVER_PID_FILE) + sys.exit(1) + + def stop_socket_server_internal(): + if not os.path.exists(TCPSERVER_PID_FILE): + print("Daemon is not running") + sys.exit(1) + + log_write("socket_server", "info", "Richiesta di stop del socket server.") + + with open(TCPSERVER_PID_FILE, "r") as f: + try: + pid = int(f.read().strip()) + except ValueError: + print(f"Errore: Il file PID '{TCPSERVER_PID_FILE}' contiene un PID non valido.") + sys.exit(1) + + descendants = list_descendants(pid) + for d_pid in descendants: + try: + os.kill(d_pid, 9) + log_write("socket_server", "info", f"Terminato processo discendente {d_pid}") + except ProcessLookupError: + log_write("socket_server", "info", f"Processo discendente {d_pid} non trovato, probabilmente già terminato.") + except Exception as e: + log_write("socket_server", "error", f"Errore durante l'uccisione del processo discendente {d_pid}: {e}") + + try: + os.kill(pid, 9) + log_write("socket_server", "info", f"Terminato processo server {pid}") + except ProcessLookupError: + print(f"Processo con PID {pid} non trovato, probabilmente già terminato.") + except Exception as e: + log_write("socket_server", "error", f"Errore durante l'uccisione del processo server {pid}: {e}") + + if os.path.exists(TCPSERVER_PID_FILE): + try: + os.remove(TCPSERVER_PID_FILE) + log_write("socket_server", "info", "File PID rimosso.") + except OSError as e: + log_write("socket_server", "error", f"Errore nella rimozione del file PID: {e}") + + # Funzione di dispatch centrale per i comandi CLI e socket + def dispatch_command(cmd, *args): + if cmd == "init": + initialize() + elif cmd == "open": + ev_open(args[0], args[1] if len(args) > 1 else "") + elif cmd == "open_in": + ev_open_in(args[0], args[1], args[2], args[3] if len(args) > 3 else "") + elif cmd == "close": + ev_close(args[0]) + elif cmd == "list_alias": + list_alias() + elif cmd == "ev_status": + ev_status(args[0]) + elif cmd == "ev_status_all": + ev_status_all() + elif cmd == "list_alias_sensor": + list_alias_sensor() + elif cmd == "sensor_status": + sensor_status(args[0], args[1] if len(args) > 1 else None) + elif cmd == "sensor_status_set": + sensor_status_set(args[0], args[1], args[2]) + elif cmd == "sensor_status_all": + sensor_status_all() + elif cmd == "last_rain_sensor_timestamp": + last_rain_sensor_timestamp() + elif cmd == "last_rain_online_timestamp": + last_rain_online_timestamp() + elif cmd == "reset_last_rain_sensor_timestamp": + reset_last_rain_sensor_timestamp() + message_write("success", "Timestamp of last sensor rain successfully reset") + json_status_wrapper() + elif cmd == "reset_last_rain_online_timestamp": + reset_last_rain_online_timestamp() + message_write("success", "Timestamp of last online rain successfully reset") + json_status_wrapper() + elif cmd == "json_status": + json_status_wrapper(*args) + elif cmd == "mqtt_status": + mqtt_status() + elif cmd == "check_rain_online": + check_rain_online() + message_write("success", "Online rain check performed") + json_status_wrapper() + elif cmd == "check_rain_sensor": + check_rain_sensor() + message_write("success", "Sensor rain check performed") + json_status_wrapper() + elif cmd == "close_all_for_rain": + # La logica "for_rain" è implicita in close_all se non c'è "force" + close_all() + message_write("success", "All solenoid closed for rain") + json_status_wrapper() + elif cmd == "close_all": + close_all(force=(args[0] == "force" if len(args) > 0 else False)) + message_write("success", "All solenoid closed") + json_status_wrapper() + elif cmd == "start_socket_server": + # Il parametro force nello script Bash era per forzare la chiusura se già aperto + # Qui la logica di start_socket_server_internal già gestisce la rimozione del PID file + start_socket_server_internal() + elif cmd == "stop_socket_server": + stop_socket_server_internal() + elif cmd == "reboot": + exec_reboot() + elif cmd == "poweroff": + exec_poweroff() + elif cmd == "set_cron_init": + vret = set_cron_init() + if vret: json_error(0, "Cron set failed") + else: message_write("success", "Cron set successful"); json_status_wrapper() + elif cmd == "del_cron_init": + vret = del_cron_init() + if vret: json_error(0, "Cron del failed") + else: message_write("success", "Cron deleted successful"); json_status_wrapper() + elif cmd == "set_cron_start_socket_server": + vret = set_cron_start_socket_server() + if vret: json_error(0, "Cron set failed") + else: message_write("success", "Cron set successful"); json_status_wrapper() + elif cmd == "del_cron_start_socket_server": + vret = del_cron_start_socket_server() + if vret: json_error(0, "Cron del failed") + else: message_write("success", "Cron deleted successful"); json_status_wrapper() + elif cmd == "set_cron_check_rain_sensor": + vret = set_cron_check_rain_sensor() + if vret: json_error(0, "Cron set failed") + else: message_write("success", "Cron set successful"); json_status_wrapper() + elif cmd == "del_cron_check_rain_sensor": + vret = del_cron_check_rain_sensor() + if vret: json_error(0, "Cron del failed") + else: message_write("success", "Cron deleted successful"); json_status_wrapper() + elif cmd == "set_cron_check_rain_online": + vret = set_cron_check_rain_online() + if vret: json_error(0, "Cron set failed") + else: message_write("success", "Cron set successful"); json_status_wrapper() + elif cmd == "del_cron_check_rain_online": + vret = del_cron_check_rain_online() + if vret: json_error(0, "Cron del failed") + else: message_write("success", "Cron deleted successful"); json_status_wrapper() + elif cmd == "set_cron_close_all_for_rain": + vret = set_cron_close_all_for_rain() + if vret: json_error(0, "Cron set failed") + else: message_write("success", "Cron set successful"); json_status_wrapper() + elif cmd == "del_cron_close_all_for_rain": + vret = del_cron_close_all_for_rain() + if vret: json_error(0, "Cron del failed") + else: message_write("success", "Cron deleted successful"); json_status_wrapper() + elif cmd == "add_cron_open": + vret = add_cron_open(args[0], args[1], args[2], args[3], args[4], args[5], args[6] if len(args) > 6 else "") + if vret: json_error(0, "Cron set failed") + else: message_write("success", "Cron set successful"); json_status_wrapper() + elif cmd == "del_cron_open": + vret = del_cron_open(args[0]) + if vret: json_error(0, "Cron set failed") + else: message_write("success", "Cron set successful"); json_status_wrapper() + elif cmd == "get_cron_open": + print(cron_get("open", args[0])) # Stampa diretta + elif cmd == "del_cron_open_in": + vret = del_cron_open_in(args[0]) + if vret: json_error(0, "Cron del failed") + else: message_write("success", "Scheduled start successfully deleted"); json_status_wrapper(f"get_cron_open_in:{args[0]}") + elif cmd == "add_cron_close": + vret = add_cron_close(args[0], args[1], args[2], args[3], args[4], args[5], args[6] if len(args) > 6 else "") + if vret: json_error(0, "Cron set failed") + else: message_write("success", "Cron set successful"); json_status_wrapper() + elif cmd == "del_cron_close": + vret = del_cron_close(args[0]) + if vret: json_error(0, "Cron set failed") + else: message_write("success", "Cron set successful"); json_status_wrapper() + elif cmd == "get_cron_close": + print(cron_get("close", args[0])) # Stampa diretta + elif cmd == "cmd_pigardensched": + vret = cmd_pigardensched(*args) + if vret != 0: # cmd_pigardensched restituisce 0 per successo, 1 per errore + json_error(0, "piGardenSched command failed") + log_write("socket_server", "error", f"piGardenSched command failed: {vret}") + else: + message_write("success", "Schedule set successful") + json_status_wrapper() + elif cmd == "debug1": + debug1(*args) + elif cmd == "debug2": + debug2(*args) + else: + show_usage() + sys.exit(1) + + # Invio dell'identificativo all'avvio dello script (se non è un comando specifico) + if len(sys.argv) == 1 or sys.argv[1] not in ["start_socket_server", "stop_socket_server"]: + send_identifier() + + # Dispatcher per i comandi dalla riga di comando + if len(sys.argv) > 1: + dispatch_command(sys.argv[1], *sys.argv[2:]) + else: + show_usage() + sys.exit(1) + diff --git a/Python/rain_include.py b/Python/rain_include.py new file mode 100644 index 0000000..43e028f --- /dev/null +++ b/Python/rain_include.py @@ -0,0 +1,432 @@ +import os +import time +import json +import logging + +# --- Mock/Placeholder per le dipendenze esterne e configurazione --- +# In un'applicazione reale, queste verrebbero fornite dal tuo sistema piGarden principale. + +# Configura un logger di base per le funzioni di log +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + +def log_write(log_type, level, message): + """Simula la funzione log_write dal tuo script Bash.""" + if level == "info": + logging.info(f"[{log_type}] {message}") + elif level == "warning": + logging.warning(f"[{log_type}] {message}") + elif level == "error": + logging.error(f"[{log_type}] {message}") + else: + logging.debug(f"[{log_type}] {message}") + +# Mock per la classe EventManager (dal file event_manager_py) +class MockEventManager: + def __init__(self): + self.CURRENT_EVENT = "" + self.CURRENT_EVENT_ALIAS = "" + + def trigger_event(self, event, *args): + log_write("event", "info", f"Mock EventManager: Triggered event: {event} with args: {args}") + self.CURRENT_EVENT = event + # Simula un codice di ritorno di successo + return 0 + +# Mock per la classe DriverManager (dal file driver_manager_py) +class MockDriverManager: + def __init__(self, config): + self.config = config + + def drv_rain_online_get(self, service_id): + """Simula il recupero dello stato pioggia online.""" + # Restituisce un timestamp se piove, un valore negativo se non piove, o 0 in caso di errore + # Per i test, simuliamo che non piova (-1) o che piova (timestamp attuale) + if "openweathermap" in service_id: + # Simula pioggia 50% delle volte + if time.time() % 2 == 0: + # Scrivi un mock di dati meteo online per il test + mock_weather_data = { + "weather": [{"description": "light rain"}], + "main": {"temp": 280, "humidity": 90} + } + with open(os.path.join(self.config["STATUS_DIR"], "last_weather_online"), "w") as f: + json.dump(mock_weather_data, f) + return str(int(time.time())) # Simula pioggia (timestamp) + else: + return "-1" # Simula non pioggia + return "0" # Errore o servizio non riconosciuto + + def drv_rain_sensor_get(self, gpio_id): + """Simula il recupero dello stato dal sensore pioggia.""" + # Restituisce lo stato del GPIO (0 o 1) + # Per i test, simuliamo lo stato del sensore + if time.time() % 3 == 0: + return str(self.config.get("RAIN_GPIO_STATE", 0)) # Simula pioggia + return str(1 - self.config.get("RAIN_GPIO_STATE", 0)) # Simula non pioggia + +# Mock per la classe PiGarden (per le dipendenze ev_*) +class MockPiGarden: + def __init__(self, config): + self.config = config + self.solenoid_states = {} # Mappa alias a stato (0=chiuso, 1=aperto, 2=forzato) + # Inizializza stati fittizi + for i in range(1, self.config.get("EV_TOTAL", 0) + 1): + self.solenoid_states[str(i)] = 0 # Tutti chiusi di default + + def ev_status(self, alias): + """Simula ev_status, restituisce lo stato dell'elettrovalvola.""" + return self.solenoid_states.get(alias, 0) # 0 se non trovato o chiuso + + def ev_close(self, alias): + """Simula ev_close, imposta lo stato dell'elettrovalvola a chiuso.""" + self.solenoid_states[alias] = 0 + log_write("irrigate", "info", f"MockPiGarden: Elettrovalvola '{alias}' chiusa.") + + def ev_check_moisture(self, ev_num): + """ + Simula ev_check_moisture. + Ritorna 0 se l'umidità non è stata raggiunta (bisogno d'acqua), + >0 se l'umidità massima è stata raggiunta. + """ + # Per i test, simuliamo che l'umidità sia raggiunta per EV1, altrimenti no + if ev_num == 1: + return 1 # Umidità raggiunta + return 0 # Umidità non raggiunta + + def ev_check_moisture_autoclose(self, ev_num): + """ + Simula ev_check_moisture_autoclose. + Ritorna 0 se non deve chiudere automaticamente, >0 se sì. + """ + # Per i test, simuliamo che l'autochiusura sia attiva per EV1 e l'umidità sia raggiunta + if self.config.get(f"EV{ev_num}_SENSOR_MOISTURE_AUTOCLOSE", "0") == "1": + return self.ev_check_moisture(ev_num) # Usa la logica di check_moisture + return 0 + + def ev_number2norain(self, ev_num): + """Simula ev_number2norain.""" + return self.config.get(f"EV{ev_num}_NORAIN", "0") == "1" + +# Variabili di configurazione simulate (dal piGarden.conf) +mock_config_rain = { + "WEATHER_SERVICE": "drv:openweathermap", # Esempio di driver di servizio meteo + "RAIN_GPIO": 25, + "RAIN_GPIO_STATE": 0, # Stato del GPIO che indica pioggia + "NOT_IRRIGATE_IF_RAIN_ONLINE": 86400, # 24 ore in secondi + "NOT_IRRIGATE_IF_RAIN_SENSOR": 86400, # 24 ore in secondi + "EV_TOTAL": 6, + "STATUS_DIR": "/tmp/piGarden_status", # Directory per i file di stato + # Elettrovalvole di esempio per close_all_for_rain + "EV1_ALIAS": "Zona_1", "EV1_NORAIN": "0", "EV1_SENSOR_MOISTURE_AUTOCLOSE": "1", + "EV2_ALIAS": "Zona_2", "EV2_NORAIN": "1", # Questa zona non si chiude per pioggia + "EV3_ALIAS": "Zona_3", "EV3_NORAIN": "0", + "EV4_ALIAS": "Zona_4", "EV4_NORAIN": "0", + "EV5_ALIAS": "Zona_5", "EV5_NORAIN": "0", + "EV6_ALIAS": "Zona_6", "EV6_NORAIN": "0", +} + +# Assicurati che la directory di stato esista per i test +os.makedirs(mock_config_rain["STATUS_DIR"], exist_ok=True) + +# --- Classe RainManager --- + +class RainManager: + def __init__(self, config, log_writer, event_manager, driver_manager, pigarden_core): + self.config = config + self.log_write = log_writer + self.event_manager = event_manager + self.driver_manager = driver_manager + self.pigarden_core = pigarden_core # Istanza della classe PiGarden principale + + self.status_dir = self.config.get("STATUS_DIR") + self.weather_service = self.config.get("WEATHER_SERVICE") + self.rain_gpio = self.config.get("RAIN_GPIO") + self.rain_gpio_state = self.config.get("RAIN_GPIO_STATE") + self.not_irrigate_if_rain_online = self.config.get("NOT_IRRIGATE_IF_RAIN_ONLINE") + self.not_irrigate_if_rain_sensor = self.config.get("NOT_IRRIGATE_IF_RAIN_SENSOR") + self.ev_total = self.config.get("EV_TOTAL") + + # Inizializza i file di stato se non esistono + self._init_status_files() + + def _init_status_files(self): + """Assicura che i file di stato esistano per evitare errori FileNotFoundError.""" + for filename in ["last_state_rain_online", "last_rain_online", + "last_state_rain_sensor", "last_rain_sensor", + "last_weather_online"]: + file_path = os.path.join(self.status_dir, filename) + if not os.path.exists(file_path): + # Crea il file vuoto o con un valore di default appropriato + with open(file_path, 'w') as f: + if filename.startswith("last_rain_"): + f.write("0") # Timestamp 0 + elif filename.startswith("last_state_rain_"): + f.write("unknown") + elif filename == "last_weather_online": + f.write("{}") # JSON vuoto + + def _read_status_file(self, filename, default=""): + """Legge il contenuto di un file di stato.""" + file_path = os.path.join(self.status_dir, filename) + try: + with open(file_path, 'r') as f: + content = f.read().strip() + return content if content else default + except FileNotFoundError: + return default + except Exception as e: + self.log_write("rain", "error", f"Errore lettura {filename}: {e}") + return default + + def _write_status_file(self, filename, content): + """Scrive il contenuto in un file di stato.""" + file_path = os.path.join(self.status_dir, filename) + try: + with open(file_path, 'w') as f: + f.write(str(content)) + except Exception as e: + self.log_write("rain", "error", f"Errore scrittura {filename}: {e}") + + def _delete_status_file(self, filename): + """Elimina un file di stato.""" + file_path = os.path.join(self.status_dir, filename) + try: + if os.path.exists(file_path): + os.remove(file_path) + except Exception as e: + self.log_write("rain", "error", f"Errore eliminazione {filename}: {e}") + + def check_rain_online(self): + """ + Esegue il controllo meteo tramite il servizio online configurato. + """ + if self.weather_service == "none": + self.log_write("rain", "warning", "check_rain_online - servizio online disabilitato") + return + + self.event_manager.trigger_event("check_rain_online_before", "") + + local_epoch_str = self.driver_manager.drv_rain_online_get(self.weather_service) + current_state_rain_online = "" + last_state_rain_online = self._read_status_file("last_state_rain_online", default="norain") + weather_json = "{}" # Default a JSON vuoto + + if local_epoch_str and local_epoch_str.lstrip('-').isdigit(): # Controlla se è un numero (anche negativo) + local_epoch = int(local_epoch_str) + if local_epoch == 0: + self.log_write("rain", "error", "check_rain_online - fallita lettura dati online (valore 0)") + else: + if local_epoch > 0: + current_state_rain_online = 'rain' + self._write_status_file("last_rain_online", local_epoch) + else: + current_state_rain_online = 'norain' + + # Leggi il JSON meteo, se esiste e valido + weather_data_str = self._read_status_file("last_weather_online", default="{}") + try: + weather_json = json.loads(weather_data_str) + # Estrai solo la parte "weather" se presente, altrimenti l'intero JSON + weather_display = json.dumps(weather_json.get("weather", weather_json)) + except json.JSONDecodeError: + weather_display = "null" # Se il file non è un JSON valido + + self.log_write("rain", "info", f"check_rain_online - weather={weather_display}, local_epoch={local_epoch}") + + if current_state_rain_online != last_state_rain_online: + self._write_status_file("last_state_rain_online", current_state_rain_online) + self.event_manager.trigger_event("check_rain_online_change", current_state_rain_online, weather_display) + else: + self.log_write("rain", "error", "check_rain_online - fallita lettura dati online (non un numero)") + + self.event_manager.trigger_event("check_rain_online_after", current_state_rain_online, weather_json) + + def check_rain_sensor(self): + """ + Controlla se piove tramite sensore hardware. + """ + if not self.rain_gpio: + self.log_write("rain", "warning", "Sensore pioggia non presente") + return + + self.event_manager.trigger_event("check_rain_sensor_before", "") + + current_state_rain_sensor = "" + last_state_rain_sensor = self._read_status_file("last_state_rain_sensor", default="norain") + + s_str = self.driver_manager.drv_rain_sensor_get(self.rain_gpio) + s = int(s_str) if s_str and s_str.isdigit() else -1 # Converte in int, default a -1 se non valido + + if s == self.rain_gpio_state: # Confronta con lo stato configurato per la pioggia + current_state_rain_sensor = 'rain' + local_epoch = int(time.time()) + self._write_status_file("last_rain_sensor", local_epoch) + self.log_write("rain", "info", f"check_rain_sensor - ora sta piovendo ({local_epoch})") + else: + current_state_rain_sensor = 'norain' + self.log_write("rain", "info", "check_rain_sensor - ora non sta piovendo") + + if current_state_rain_sensor != last_state_rain_sensor: + self._write_status_file("last_state_rain_sensor", current_state_rain_sensor) + self.event_manager.trigger_event("check_rain_sensor_change", current_state_rain_sensor) + + self.event_manager.trigger_event("check_rain_sensor_after", current_state_rain_sensor) + + def close_all_for_rain(self): + """ + Chiude tutte le elettrovalvole se sta piovendo o se hanno raggiunto l'umidità massima. + """ + # Chiude le elettrovalvole che hanno raggiunto l'umidità del terreno impostata + for i in range(1, self.ev_total + 1): + alias = self.config.get(f"EV{i}_ALIAS") + if not alias: continue # Salta se l'alias non è definito + + state = self.pigarden_core.ev_status(alias) + moisture = self.pigarden_core.ev_check_moisture_autoclose(i) + + # Se l'elettrovalvola è aperta (stato 1) e l'umidità massima è stata raggiunta (moisture > 0) + if state == 1 and moisture > 0: + self.pigarden_core.ev_close(alias) + self.log_write("irrigate", "warning", f"close_all_for_rain - Chiusa elettrovalvola '{alias}' perché l'umidità massima del terreno è stata raggiunta") + + # Chiude le elettrovalvole in caso di pioggia (online o sensore) + close_all_flag = False + now = int(time.time()) + + # Controllo pioggia online + if self.not_irrigate_if_rain_online > 0: + last_rain_online_str = self._read_status_file("last_rain_online", default="0") + try: + last_rain_online = int(last_rain_online_str) + if now - last_rain_online < self.not_irrigate_if_rain_online: + close_all_flag = True + except ValueError: + pass # Ignora se il timestamp non è un numero + + # Controllo pioggia sensore + if self.not_irrigate_if_rain_sensor > 0: + last_rain_sensor_str = self._read_status_file("last_rain_sensor", default="0") + try: + last_rain_sensor = int(last_rain_sensor_str) + if now - last_rain_sensor < self.not_irrigate_if_rain_sensor: + close_all_flag = True + except ValueError: + pass # Ignora se il timestamp non è un numero + + if close_all_flag: + # Piove: valuta se chiudere le elettrovalvole + for i in range(1, self.ev_total + 1): + alias = self.config.get(f"EV{i}_ALIAS") + if not alias: continue + + state = self.pigarden_core.ev_status(alias) + ev_norain = self.pigarden_core.ev_number2norain(i) # True se non deve chiudere per pioggia + moisture = self.pigarden_core.ev_check_moisture(i) # 0 se non ha raggiunto l'umidità ottimale + + # Se l'elettrovalvola è aperta (stato 1), NON è impostata per ignorare la pioggia (ev_norain è False), + # E l'umidità non è ancora ottimale (moisture è 0 o non ha raggiunto il max) + # La logica Bash `[ "$moisture" -ne 0 ]` significa "se l'umidità NON è zero", + # che nel contesto di `ev_check_moisture` (che ritorna 0 per "non raggiunta", >0 per "raggiunta") + # significherebbe "se l'umidità è stata raggiunta". + # Tuttavia, il commento nello script Bash `if [ $moisture -gt 0 ]; then message_write "warning" "solenoid not open because maximum soil moisture has been reached"` + # suggerisce che >0 significa "umidità massima raggiunta". + # Quindi, per chiudere per pioggia, l'umidità NON deve essere già al massimo. + # Se `ev_check_moisture` ritorna 0 per "non raggiunto" e >0 per "raggiunto", + # allora `moisture == 0` significa "ha ancora bisogno d'acqua". + # Quindi, la condizione per chiudere per pioggia dovrebbe essere: + # `state == 1` (aperta) AND `not ev_norain` (non ignora pioggia) AND `moisture == 0` (ha ancora bisogno d'acqua) + # Il Bash `[ "$moisture" -ne 0 ]` nella seconda loop di close_all_for_rain è contro-intuitivo + # se `ev_check_moisture` ritorna >0 per "raggiunto". + # Assumo che `moisture -ne 0` in quel contesto significhi "se l'umidità non è perfetta, chiudi". + # Se `ev_check_moisture` ritorna 0 per "umidità OK/raggiunta" e 1 per "non OK", allora -ne 0 ha senso. + # Basandomi sulla funzione `ev_open` che usa `moisture -gt 0` per bloccare l'apertura (umidità già alta), + # `moisture -ne 0` qui dovrebbe significare "se l'umidità non è ancora ottimale/non è zero". + # Se 0 significa "umidità OK", allora -ne 0 significa "umidità NON OK". + # Adotterò la traduzione letterale di `moisture != 0` e lascerò al mock di `ev_check_moisture` di definire il comportamento. + + if state == 1 and not ev_norain and moisture != 0: + self.pigarden_core.ev_close(alias) + self.log_write("irrigate", "warning", f"close_all_for_rain - Chiusa elettrovalvola '{alias}' per pioggia") + + def last_rain_sensor_timestamp(self): + """Mostra il timestamp dell'ultima pioggia rilevato dal sensore.""" + return self._read_status_file("last_rain_sensor", default="0") + + def last_rain_online_timestamp(self): + """Mostra il timestamp dell'ultima pioggia rilevato dal servizio online.""" + return self._read_status_file("last_rain_online", default="0") + + def reset_last_rain_sensor_timestamp(self): + """Resetta il timestamp dell'ultima pioggia rilevato dal sensore.""" + self.event_manager.trigger_event("reset_last_rain_sensor_timestamp_before", "") + self._delete_status_file("last_rain_sensor") + self.event_manager.trigger_event("reset_last_rain_sensor_timestamp_after", "") + self.log_write("rain", "info", "reset_last_rain_sensor_timestamp") + + def reset_last_rain_online_timestamp(self): + """Resetta il timestamp dell'ultima pioggia rilevato dal servizio online.""" + self.event_manager.trigger_event("reset_last_rain_online_timestamp_before", "") + self._delete_status_file("last_rain_online") + # Il Bash script chiama trigger_event due volte con _before, correggo a _after + self.event_manager.trigger_event("reset_last_rain_online_timestamp_after", "") + self.log_write("rain", "info", "reset_last_rain_online_timestamp") + +# --- Esempio di utilizzo --- +if __name__ == "__main__": + print("--- Test RainManager ---") + + # Inizializza le dipendenze mock + mock_event_manager = MockEventManager() + mock_driver_manager = MockDriverManager(mock_config_rain) + mock_pigarden_core = MockPiGarden(mock_config_rain) + + # Inizializza RainManager + rain_manager = RainManager( + config=mock_config_rain, + log_writer=log_write, + event_manager=mock_event_manager, + driver_manager=mock_driver_manager, + pigarden_core=mock_pigarden_core + ) + + # --- Test check_rain_online --- + print("\n--- Test: check_rain_online (potrebbe simulare pioggia o no) ---") + rain_manager.check_rain_online() + print(f"Stato pioggia online (file): {rain_manager.last_rain_online_timestamp()}") + print(f"Stato ultimo rilevamento online (file): {rain_manager._read_status_file('last_state_rain_online')}") + + # --- Test check_rain_sensor --- + print("\n--- Test: check_rain_sensor (potrebbe simulare pioggia o no) ---") + rain_manager.check_rain_sensor() + print(f"Stato pioggia sensore (file): {rain_manager.last_rain_sensor_timestamp()}") + print(f"Stato ultimo rilevamento sensore (file): {rain_manager._read_status_file('last_state_rain_sensor')}") + + # --- Test close_all_for_rain --- + print("\n--- Test: close_all_for_rain ---") + # Imposta un'elettrovalvola aperta per il test + mock_pigarden_core.solenoid_states["Zona_1"] = 1 + mock_pigarden_core.solenoid_states["Zona_3"] = 1 + print(f"Stato iniziale Zona_1: {mock_pigarden_core.ev_status('Zona_1')}") + print(f"Stato iniziale Zona_3: {mock_pigarden_core.ev_status('Zona_3')}") + + # Simula una pioggia recente per attivare la chiusura + rain_manager._write_status_file("last_rain_online", int(time.time()) - 100) # 100 secondi fa + rain_manager._write_status_file("last_rain_sensor", int(time.time()) - 50) # 50 secondi fa + + rain_manager.close_all_for_rain() + print(f"Stato finale Zona_1: {mock_pigarden_core.ev_status('Zona_1')} (dovrebbe essere 0 se autoclose è attivo o piove)") + print(f"Stato finale Zona_3: {mock_pigarden_core.ev_status('Zona_3')} (dovrebbe essere 0 se piove)") + + # --- Test reset timestamp --- + print("\n--- Test: reset_last_rain_sensor_timestamp ---") + rain_manager.reset_last_rain_sensor_timestamp() + print(f"Timestamp sensore dopo reset: {rain_manager.last_rain_sensor_timestamp()}") + + print("\n--- Test: reset_last_rain_online_timestamp ---") + rain_manager.reset_last_rain_online_timestamp() + print(f"Timestamp online dopo reset: {rain_manager.last_rain_online_timestamp()}") + + print("\n--- Test completato ---") + # Pulisci le directory di test (opzionale) + # import shutil + # shutil.rmtree(mock_config_rain["STATUS_DIR"]) diff --git a/Python/sensor_include.py b/Python/sensor_include.py new file mode 100644 index 0000000..43e028f --- /dev/null +++ b/Python/sensor_include.py @@ -0,0 +1,432 @@ +import os +import time +import json +import logging + +# --- Mock/Placeholder per le dipendenze esterne e configurazione --- +# In un'applicazione reale, queste verrebbero fornite dal tuo sistema piGarden principale. + +# Configura un logger di base per le funzioni di log +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + +def log_write(log_type, level, message): + """Simula la funzione log_write dal tuo script Bash.""" + if level == "info": + logging.info(f"[{log_type}] {message}") + elif level == "warning": + logging.warning(f"[{log_type}] {message}") + elif level == "error": + logging.error(f"[{log_type}] {message}") + else: + logging.debug(f"[{log_type}] {message}") + +# Mock per la classe EventManager (dal file event_manager_py) +class MockEventManager: + def __init__(self): + self.CURRENT_EVENT = "" + self.CURRENT_EVENT_ALIAS = "" + + def trigger_event(self, event, *args): + log_write("event", "info", f"Mock EventManager: Triggered event: {event} with args: {args}") + self.CURRENT_EVENT = event + # Simula un codice di ritorno di successo + return 0 + +# Mock per la classe DriverManager (dal file driver_manager_py) +class MockDriverManager: + def __init__(self, config): + self.config = config + + def drv_rain_online_get(self, service_id): + """Simula il recupero dello stato pioggia online.""" + # Restituisce un timestamp se piove, un valore negativo se non piove, o 0 in caso di errore + # Per i test, simuliamo che non piova (-1) o che piova (timestamp attuale) + if "openweathermap" in service_id: + # Simula pioggia 50% delle volte + if time.time() % 2 == 0: + # Scrivi un mock di dati meteo online per il test + mock_weather_data = { + "weather": [{"description": "light rain"}], + "main": {"temp": 280, "humidity": 90} + } + with open(os.path.join(self.config["STATUS_DIR"], "last_weather_online"), "w") as f: + json.dump(mock_weather_data, f) + return str(int(time.time())) # Simula pioggia (timestamp) + else: + return "-1" # Simula non pioggia + return "0" # Errore o servizio non riconosciuto + + def drv_rain_sensor_get(self, gpio_id): + """Simula il recupero dello stato dal sensore pioggia.""" + # Restituisce lo stato del GPIO (0 o 1) + # Per i test, simuliamo lo stato del sensore + if time.time() % 3 == 0: + return str(self.config.get("RAIN_GPIO_STATE", 0)) # Simula pioggia + return str(1 - self.config.get("RAIN_GPIO_STATE", 0)) # Simula non pioggia + +# Mock per la classe PiGarden (per le dipendenze ev_*) +class MockPiGarden: + def __init__(self, config): + self.config = config + self.solenoid_states = {} # Mappa alias a stato (0=chiuso, 1=aperto, 2=forzato) + # Inizializza stati fittizi + for i in range(1, self.config.get("EV_TOTAL", 0) + 1): + self.solenoid_states[str(i)] = 0 # Tutti chiusi di default + + def ev_status(self, alias): + """Simula ev_status, restituisce lo stato dell'elettrovalvola.""" + return self.solenoid_states.get(alias, 0) # 0 se non trovato o chiuso + + def ev_close(self, alias): + """Simula ev_close, imposta lo stato dell'elettrovalvola a chiuso.""" + self.solenoid_states[alias] = 0 + log_write("irrigate", "info", f"MockPiGarden: Elettrovalvola '{alias}' chiusa.") + + def ev_check_moisture(self, ev_num): + """ + Simula ev_check_moisture. + Ritorna 0 se l'umidità non è stata raggiunta (bisogno d'acqua), + >0 se l'umidità massima è stata raggiunta. + """ + # Per i test, simuliamo che l'umidità sia raggiunta per EV1, altrimenti no + if ev_num == 1: + return 1 # Umidità raggiunta + return 0 # Umidità non raggiunta + + def ev_check_moisture_autoclose(self, ev_num): + """ + Simula ev_check_moisture_autoclose. + Ritorna 0 se non deve chiudere automaticamente, >0 se sì. + """ + # Per i test, simuliamo che l'autochiusura sia attiva per EV1 e l'umidità sia raggiunta + if self.config.get(f"EV{ev_num}_SENSOR_MOISTURE_AUTOCLOSE", "0") == "1": + return self.ev_check_moisture(ev_num) # Usa la logica di check_moisture + return 0 + + def ev_number2norain(self, ev_num): + """Simula ev_number2norain.""" + return self.config.get(f"EV{ev_num}_NORAIN", "0") == "1" + +# Variabili di configurazione simulate (dal piGarden.conf) +mock_config_rain = { + "WEATHER_SERVICE": "drv:openweathermap", # Esempio di driver di servizio meteo + "RAIN_GPIO": 25, + "RAIN_GPIO_STATE": 0, # Stato del GPIO che indica pioggia + "NOT_IRRIGATE_IF_RAIN_ONLINE": 86400, # 24 ore in secondi + "NOT_IRRIGATE_IF_RAIN_SENSOR": 86400, # 24 ore in secondi + "EV_TOTAL": 6, + "STATUS_DIR": "/tmp/piGarden_status", # Directory per i file di stato + # Elettrovalvole di esempio per close_all_for_rain + "EV1_ALIAS": "Zona_1", "EV1_NORAIN": "0", "EV1_SENSOR_MOISTURE_AUTOCLOSE": "1", + "EV2_ALIAS": "Zona_2", "EV2_NORAIN": "1", # Questa zona non si chiude per pioggia + "EV3_ALIAS": "Zona_3", "EV3_NORAIN": "0", + "EV4_ALIAS": "Zona_4", "EV4_NORAIN": "0", + "EV5_ALIAS": "Zona_5", "EV5_NORAIN": "0", + "EV6_ALIAS": "Zona_6", "EV6_NORAIN": "0", +} + +# Assicurati che la directory di stato esista per i test +os.makedirs(mock_config_rain["STATUS_DIR"], exist_ok=True) + +# --- Classe RainManager --- + +class RainManager: + def __init__(self, config, log_writer, event_manager, driver_manager, pigarden_core): + self.config = config + self.log_write = log_writer + self.event_manager = event_manager + self.driver_manager = driver_manager + self.pigarden_core = pigarden_core # Istanza della classe PiGarden principale + + self.status_dir = self.config.get("STATUS_DIR") + self.weather_service = self.config.get("WEATHER_SERVICE") + self.rain_gpio = self.config.get("RAIN_GPIO") + self.rain_gpio_state = self.config.get("RAIN_GPIO_STATE") + self.not_irrigate_if_rain_online = self.config.get("NOT_IRRIGATE_IF_RAIN_ONLINE") + self.not_irrigate_if_rain_sensor = self.config.get("NOT_IRRIGATE_IF_RAIN_SENSOR") + self.ev_total = self.config.get("EV_TOTAL") + + # Inizializza i file di stato se non esistono + self._init_status_files() + + def _init_status_files(self): + """Assicura che i file di stato esistano per evitare errori FileNotFoundError.""" + for filename in ["last_state_rain_online", "last_rain_online", + "last_state_rain_sensor", "last_rain_sensor", + "last_weather_online"]: + file_path = os.path.join(self.status_dir, filename) + if not os.path.exists(file_path): + # Crea il file vuoto o con un valore di default appropriato + with open(file_path, 'w') as f: + if filename.startswith("last_rain_"): + f.write("0") # Timestamp 0 + elif filename.startswith("last_state_rain_"): + f.write("unknown") + elif filename == "last_weather_online": + f.write("{}") # JSON vuoto + + def _read_status_file(self, filename, default=""): + """Legge il contenuto di un file di stato.""" + file_path = os.path.join(self.status_dir, filename) + try: + with open(file_path, 'r') as f: + content = f.read().strip() + return content if content else default + except FileNotFoundError: + return default + except Exception as e: + self.log_write("rain", "error", f"Errore lettura {filename}: {e}") + return default + + def _write_status_file(self, filename, content): + """Scrive il contenuto in un file di stato.""" + file_path = os.path.join(self.status_dir, filename) + try: + with open(file_path, 'w') as f: + f.write(str(content)) + except Exception as e: + self.log_write("rain", "error", f"Errore scrittura {filename}: {e}") + + def _delete_status_file(self, filename): + """Elimina un file di stato.""" + file_path = os.path.join(self.status_dir, filename) + try: + if os.path.exists(file_path): + os.remove(file_path) + except Exception as e: + self.log_write("rain", "error", f"Errore eliminazione {filename}: {e}") + + def check_rain_online(self): + """ + Esegue il controllo meteo tramite il servizio online configurato. + """ + if self.weather_service == "none": + self.log_write("rain", "warning", "check_rain_online - servizio online disabilitato") + return + + self.event_manager.trigger_event("check_rain_online_before", "") + + local_epoch_str = self.driver_manager.drv_rain_online_get(self.weather_service) + current_state_rain_online = "" + last_state_rain_online = self._read_status_file("last_state_rain_online", default="norain") + weather_json = "{}" # Default a JSON vuoto + + if local_epoch_str and local_epoch_str.lstrip('-').isdigit(): # Controlla se è un numero (anche negativo) + local_epoch = int(local_epoch_str) + if local_epoch == 0: + self.log_write("rain", "error", "check_rain_online - fallita lettura dati online (valore 0)") + else: + if local_epoch > 0: + current_state_rain_online = 'rain' + self._write_status_file("last_rain_online", local_epoch) + else: + current_state_rain_online = 'norain' + + # Leggi il JSON meteo, se esiste e valido + weather_data_str = self._read_status_file("last_weather_online", default="{}") + try: + weather_json = json.loads(weather_data_str) + # Estrai solo la parte "weather" se presente, altrimenti l'intero JSON + weather_display = json.dumps(weather_json.get("weather", weather_json)) + except json.JSONDecodeError: + weather_display = "null" # Se il file non è un JSON valido + + self.log_write("rain", "info", f"check_rain_online - weather={weather_display}, local_epoch={local_epoch}") + + if current_state_rain_online != last_state_rain_online: + self._write_status_file("last_state_rain_online", current_state_rain_online) + self.event_manager.trigger_event("check_rain_online_change", current_state_rain_online, weather_display) + else: + self.log_write("rain", "error", "check_rain_online - fallita lettura dati online (non un numero)") + + self.event_manager.trigger_event("check_rain_online_after", current_state_rain_online, weather_json) + + def check_rain_sensor(self): + """ + Controlla se piove tramite sensore hardware. + """ + if not self.rain_gpio: + self.log_write("rain", "warning", "Sensore pioggia non presente") + return + + self.event_manager.trigger_event("check_rain_sensor_before", "") + + current_state_rain_sensor = "" + last_state_rain_sensor = self._read_status_file("last_state_rain_sensor", default="norain") + + s_str = self.driver_manager.drv_rain_sensor_get(self.rain_gpio) + s = int(s_str) if s_str and s_str.isdigit() else -1 # Converte in int, default a -1 se non valido + + if s == self.rain_gpio_state: # Confronta con lo stato configurato per la pioggia + current_state_rain_sensor = 'rain' + local_epoch = int(time.time()) + self._write_status_file("last_rain_sensor", local_epoch) + self.log_write("rain", "info", f"check_rain_sensor - ora sta piovendo ({local_epoch})") + else: + current_state_rain_sensor = 'norain' + self.log_write("rain", "info", "check_rain_sensor - ora non sta piovendo") + + if current_state_rain_sensor != last_state_rain_sensor: + self._write_status_file("last_state_rain_sensor", current_state_rain_sensor) + self.event_manager.trigger_event("check_rain_sensor_change", current_state_rain_sensor) + + self.event_manager.trigger_event("check_rain_sensor_after", current_state_rain_sensor) + + def close_all_for_rain(self): + """ + Chiude tutte le elettrovalvole se sta piovendo o se hanno raggiunto l'umidità massima. + """ + # Chiude le elettrovalvole che hanno raggiunto l'umidità del terreno impostata + for i in range(1, self.ev_total + 1): + alias = self.config.get(f"EV{i}_ALIAS") + if not alias: continue # Salta se l'alias non è definito + + state = self.pigarden_core.ev_status(alias) + moisture = self.pigarden_core.ev_check_moisture_autoclose(i) + + # Se l'elettrovalvola è aperta (stato 1) e l'umidità massima è stata raggiunta (moisture > 0) + if state == 1 and moisture > 0: + self.pigarden_core.ev_close(alias) + self.log_write("irrigate", "warning", f"close_all_for_rain - Chiusa elettrovalvola '{alias}' perché l'umidità massima del terreno è stata raggiunta") + + # Chiude le elettrovalvole in caso di pioggia (online o sensore) + close_all_flag = False + now = int(time.time()) + + # Controllo pioggia online + if self.not_irrigate_if_rain_online > 0: + last_rain_online_str = self._read_status_file("last_rain_online", default="0") + try: + last_rain_online = int(last_rain_online_str) + if now - last_rain_online < self.not_irrigate_if_rain_online: + close_all_flag = True + except ValueError: + pass # Ignora se il timestamp non è un numero + + # Controllo pioggia sensore + if self.not_irrigate_if_rain_sensor > 0: + last_rain_sensor_str = self._read_status_file("last_rain_sensor", default="0") + try: + last_rain_sensor = int(last_rain_sensor_str) + if now - last_rain_sensor < self.not_irrigate_if_rain_sensor: + close_all_flag = True + except ValueError: + pass # Ignora se il timestamp non è un numero + + if close_all_flag: + # Piove: valuta se chiudere le elettrovalvole + for i in range(1, self.ev_total + 1): + alias = self.config.get(f"EV{i}_ALIAS") + if not alias: continue + + state = self.pigarden_core.ev_status(alias) + ev_norain = self.pigarden_core.ev_number2norain(i) # True se non deve chiudere per pioggia + moisture = self.pigarden_core.ev_check_moisture(i) # 0 se non ha raggiunto l'umidità ottimale + + # Se l'elettrovalvola è aperta (stato 1), NON è impostata per ignorare la pioggia (ev_norain è False), + # E l'umidità non è ancora ottimale (moisture è 0 o non ha raggiunto il max) + # La logica Bash `[ "$moisture" -ne 0 ]` significa "se l'umidità NON è zero", + # che nel contesto di `ev_check_moisture` (che ritorna 0 per "non raggiunta", >0 per "raggiunta") + # significherebbe "se l'umidità è stata raggiunta". + # Tuttavia, il commento nello script Bash `if [ $moisture -gt 0 ]; then message_write "warning" "solenoid not open because maximum soil moisture has been reached"` + # suggerisce che >0 significa "umidità massima raggiunta". + # Quindi, per chiudere per pioggia, l'umidità NON deve essere già al massimo. + # Se `ev_check_moisture` ritorna 0 per "non raggiunto" e >0 per "raggiunto", + # allora `moisture == 0` significa "ha ancora bisogno d'acqua". + # Quindi, la condizione per chiudere per pioggia dovrebbe essere: + # `state == 1` (aperta) AND `not ev_norain` (non ignora pioggia) AND `moisture == 0` (ha ancora bisogno d'acqua) + # Il Bash `[ "$moisture" -ne 0 ]` nella seconda loop di close_all_for_rain è contro-intuitivo + # se `ev_check_moisture` ritorna >0 per "raggiunto". + # Assumo che `moisture -ne 0` in quel contesto significhi "se l'umidità non è perfetta, chiudi". + # Se `ev_check_moisture` ritorna 0 per "umidità OK/raggiunta" e 1 per "non OK", allora -ne 0 ha senso. + # Basandomi sulla funzione `ev_open` che usa `moisture -gt 0` per bloccare l'apertura (umidità già alta), + # `moisture -ne 0` qui dovrebbe significare "se l'umidità non è ancora ottimale/non è zero". + # Se 0 significa "umidità OK", allora -ne 0 significa "umidità NON OK". + # Adotterò la traduzione letterale di `moisture != 0` e lascerò al mock di `ev_check_moisture` di definire il comportamento. + + if state == 1 and not ev_norain and moisture != 0: + self.pigarden_core.ev_close(alias) + self.log_write("irrigate", "warning", f"close_all_for_rain - Chiusa elettrovalvola '{alias}' per pioggia") + + def last_rain_sensor_timestamp(self): + """Mostra il timestamp dell'ultima pioggia rilevato dal sensore.""" + return self._read_status_file("last_rain_sensor", default="0") + + def last_rain_online_timestamp(self): + """Mostra il timestamp dell'ultima pioggia rilevato dal servizio online.""" + return self._read_status_file("last_rain_online", default="0") + + def reset_last_rain_sensor_timestamp(self): + """Resetta il timestamp dell'ultima pioggia rilevato dal sensore.""" + self.event_manager.trigger_event("reset_last_rain_sensor_timestamp_before", "") + self._delete_status_file("last_rain_sensor") + self.event_manager.trigger_event("reset_last_rain_sensor_timestamp_after", "") + self.log_write("rain", "info", "reset_last_rain_sensor_timestamp") + + def reset_last_rain_online_timestamp(self): + """Resetta il timestamp dell'ultima pioggia rilevato dal servizio online.""" + self.event_manager.trigger_event("reset_last_rain_online_timestamp_before", "") + self._delete_status_file("last_rain_online") + # Il Bash script chiama trigger_event due volte con _before, correggo a _after + self.event_manager.trigger_event("reset_last_rain_online_timestamp_after", "") + self.log_write("rain", "info", "reset_last_rain_online_timestamp") + +# --- Esempio di utilizzo --- +if __name__ == "__main__": + print("--- Test RainManager ---") + + # Inizializza le dipendenze mock + mock_event_manager = MockEventManager() + mock_driver_manager = MockDriverManager(mock_config_rain) + mock_pigarden_core = MockPiGarden(mock_config_rain) + + # Inizializza RainManager + rain_manager = RainManager( + config=mock_config_rain, + log_writer=log_write, + event_manager=mock_event_manager, + driver_manager=mock_driver_manager, + pigarden_core=mock_pigarden_core + ) + + # --- Test check_rain_online --- + print("\n--- Test: check_rain_online (potrebbe simulare pioggia o no) ---") + rain_manager.check_rain_online() + print(f"Stato pioggia online (file): {rain_manager.last_rain_online_timestamp()}") + print(f"Stato ultimo rilevamento online (file): {rain_manager._read_status_file('last_state_rain_online')}") + + # --- Test check_rain_sensor --- + print("\n--- Test: check_rain_sensor (potrebbe simulare pioggia o no) ---") + rain_manager.check_rain_sensor() + print(f"Stato pioggia sensore (file): {rain_manager.last_rain_sensor_timestamp()}") + print(f"Stato ultimo rilevamento sensore (file): {rain_manager._read_status_file('last_state_rain_sensor')}") + + # --- Test close_all_for_rain --- + print("\n--- Test: close_all_for_rain ---") + # Imposta un'elettrovalvola aperta per il test + mock_pigarden_core.solenoid_states["Zona_1"] = 1 + mock_pigarden_core.solenoid_states["Zona_3"] = 1 + print(f"Stato iniziale Zona_1: {mock_pigarden_core.ev_status('Zona_1')}") + print(f"Stato iniziale Zona_3: {mock_pigarden_core.ev_status('Zona_3')}") + + # Simula una pioggia recente per attivare la chiusura + rain_manager._write_status_file("last_rain_online", int(time.time()) - 100) # 100 secondi fa + rain_manager._write_status_file("last_rain_sensor", int(time.time()) - 50) # 50 secondi fa + + rain_manager.close_all_for_rain() + print(f"Stato finale Zona_1: {mock_pigarden_core.ev_status('Zona_1')} (dovrebbe essere 0 se autoclose è attivo o piove)") + print(f"Stato finale Zona_3: {mock_pigarden_core.ev_status('Zona_3')} (dovrebbe essere 0 se piove)") + + # --- Test reset timestamp --- + print("\n--- Test: reset_last_rain_sensor_timestamp ---") + rain_manager.reset_last_rain_sensor_timestamp() + print(f"Timestamp sensore dopo reset: {rain_manager.last_rain_sensor_timestamp()}") + + print("\n--- Test: reset_last_rain_online_timestamp ---") + rain_manager.reset_last_rain_online_timestamp() + print(f"Timestamp online dopo reset: {rain_manager.last_rain_online_timestamp()}") + + print("\n--- Test completato ---") + # Pulisci le directory di test (opzionale) + # import shutil + # shutil.rmtree(mock_config_rain["STATUS_DIR"]) diff --git a/Python/socket.include.py b/Python/socket.include.py new file mode 100644 index 0000000..a1170fc --- /dev/null +++ b/Python/socket.include.py @@ -0,0 +1,525 @@ +import os +import subprocess +import threading +import socket +import socketserver +import time +import sys + +# --- CONFIGURAZIONE (DA ADATTARE ALLE TUE ESIGENZE) --- +# Queste variabili dovrebbero essere configurate in un file di configurazione separato +# o passate come argomenti al tuo script Python. + +TCPSERVER_PID_FILE = "/var/run/my_socket_server.pid" +TCPSERVER_IP = "0.0.0.0" # Ascolta su tutte le interfacce +TCPSERVER_PORT = 12345 # Porta del server socket + +TCPSERVER_USER = "admin" # Credenziali opzionali per l'autenticazione +TCPSERVER_PWD = "password123" + +# Variabili usate internamente dallo script Bash, che in Python diventano parametri o logica +RUN_FROM_TCPSERVER = False # Sarà True quando il comando è eseguito dal server socket + +# Definisci i percorsi per i comandi esterni (se non sono nel PATH di sistema) +# In un ambiente di produzione, è meglio specificare percorsi assoluti. +# Ad esempio, TR_COMMAND = "/usr/bin/tr" +TR_COMMAND = "tr" +CUT_COMMAND = "cut" +READLINK_COMMAND = "readlink" + + +# --- FUNZIONI UTILITY (PLACEHOLDERS) --- +# Queste sono le funzioni che erano richiamate dallo script Bash +# Dovrai implementarle in Python o collegarle alle tue librerie esistenti. + +def log_write(source, level, message): + """Placeholder per la funzione di logging.""" + timestamp = time.strftime("%Y-%m-%d %H:%M:%S") + print(f"[{timestamp}] [{source}] [{level.upper()}] {message}") + +def json_error(code, message): + """Placeholder per la funzione che genera un JSON di errore.""" + print(f'{{"status": "error", "code": {code}, "message": "{message}"}}') + +def json_status(*args): + """Placeholder per la funzione che genera un JSON di stato.""" + if args: + print(f'{{"status": "success", "data": "{", ".join(args)}"}}') + else: + print('{"status": "success"}') + +def message_write(level, message): + """Placeholder per la funzione che scrive un messaggio.""" + print(f"[{level.upper()}] {message}") + +def ev_open(alias, param=None): + """Placeholder per la funzione di apertura valvola.""" + log_write("socket_server", "info", f"Executing ev_open for alias: {alias}, param: {param}") + # Qui andrebbe la logica per aprire la valvola + pass + +def ev_open_in(arg2, arg3, arg4, arg5): + """Placeholder per la funzione di apertura valvola in un intervallo.""" + log_write("socket_server", "info", f"Executing ev_open_in with args: {arg2}, {arg3}, {arg4}, {arg5}") + # Qui andrebbe la logica per aprire la valvola in un intervallo + pass + +def ev_close(alias): + """Placeholder per la funzione di chiusura valvola.""" + log_write("socket_server", "info", f"Executing ev_close for alias: {alias}") + # Qui andrebbe la logica per chiudere la valvola + pass + +def close_all(): + """Placeholder per la funzione di chiusura di tutte le valvole.""" + log_write("socket_server", "info", "Executing close_all") + # Qui andrebbe la logica per chiudere tutte le valvole + pass + +def cron_disable_all_open_close(): + """Placeholder per la funzione di disabilitazione scheduling cron.""" + log_write("socket_server", "info", "Executing cron_disable_all_open_close") + # Qui andrebbe la logica per disabilitare lo scheduling + pass + +def cron_enable_all_open_close(): + """Placeholder per la funzione di abilitazione scheduling cron.""" + log_write("socket_server", "info", "Executing cron_enable_all_open_close") + # Qui andrebbe la logica per abilitare lo scheduling + pass + +def set_cron_init(): + """Placeholder per la funzione set_cron_init.""" + log_write("socket_server", "info", "Executing set_cron_init") + return "" # Simula output vuoto per successo + +def set_cron_start_socket_server(): + """Placeholder per la funzione set_cron_start_socket_server.""" + log_write("socket_server", "info", "Executing set_cron_start_socket_server") + return "" + +def set_cron_check_rain_sensor(): + """Placeholder per la funzione set_cron_check_rain_sensor.""" + log_write("socket_server", "info", "Executing set_cron_check_rain_sensor") + return "" + +def set_cron_check_rain_online(): + """Placeholder per la funzione set_cron_check_rain_online.""" + log_write("socket_server", "info", "Executing set_cron_check_rain_online") + return "" + +def set_cron_close_all_for_rain(): + """Placeholder per la funzione set_cron_close_all_for_rain.""" + log_write("socket_server", "info", "Executing set_cron_close_all_for_rain") + return "" + +def del_cron_open(arg): + """Placeholder per la funzione del_cron_open.""" + log_write("socket_server", "info", f"Executing del_cron_open with arg: {arg}") + return "" + +def del_cron_open_in(arg): + """Placeholder per la funzione del_cron_open_in.""" + log_write("socket_server", "info", f"Executing del_cron_open_in with arg: {arg}") + return "" + +def del_cron_close(arg): + """Placeholder per la funzione del_cron_close.""" + log_write("socket_server", "info", f"Executing del_cron_close with arg: {arg}") + return "" + +def add_cron_open(arg2, arg3, arg4, arg5, arg6, arg7, arg8): + """Placeholder per la funzione add_cron_open.""" + log_write("socket_server", "info", f"Executing add_cron_open with args: {arg2}, {arg3}, {arg4}, {arg5}, {arg6}, {arg7}, {arg8}") + return "" + +def add_cron_close(arg2, arg3, arg4, arg5, arg6, arg7, arg8): + """Placeholder per la funzione add_cron_close.""" + log_write("socket_server", "info", f"Executing add_cron_close with args: {arg2}, {arg3}, {arg4}, {arg5}, {arg6}, {arg7}, {arg8}") + return "" + +def cmd_pigardensched(arg2, arg3, arg4, arg5, arg6): + """Placeholder per la funzione cmd_pigardensched.""" + log_write("socket_server", "info", f"Executing cmd_pigardensched with args: {arg2}, {arg3}, {arg4}, {arg5}, {arg6}") + return "" + +def reset_last_rain_sensor_timestamp(): + """Placeholder per la funzione reset_last_rain_sensor_timestamp.""" + log_write("socket_server", "info", "Executing reset_last_rain_sensor_timestamp") + pass + +def reset_last_rain_online_timestamp(): + """Placeholder per la funzione reset_last_rain_online_timestamp.""" + log_write("socket_server", "info", "Executing reset_last_rain_online_timestamp") + pass + +def sensor_status_set(arg2, arg3, arg4): + """Placeholder per la funzione sensor_status_set.""" + log_write("socket_server", "info", f"Executing sensor_status_set with args: {arg2}, {arg3}, {arg4}") + pass + +# --- FUNZIONI DI GESTIONE DEI PROCESSI --- +def list_descendants(pid): + """ + Simula la logica di 'list_descendants' Bash. + In un sistema reale, dovresti usare librerie come 'psutil' + per ottenere i processi figli. Qui è solo una simulazione. + """ + try: + # psutil è l'opzione migliore, se disponibile + # import psutil + # parent = psutil.Process(pid) + # return [child.pid for child in parent.children(recursive=True)] + + # Fallback semplice per simulazione (non accurato per processi reali) + # Se non hai psutil, questa funzione è difficile da replicare accuratamente in modo generico. + # Potrebbe essere necessario un approccio specifico per il tuo sistema operativo. + return [] # Nessun discendente noto senza psutil + except Exception as e: + log_write("process_manager", "error", f"Errore nel listing dei discendenti di {pid}: {e}") + return [] + +def get_script_path(): + """Restituisce il percorso assoluto dello script corrente.""" + return os.path.abspath(sys.argv[0]) + +# --- CLASSE GESTORE RICHIESTE SOCKET --- +class MyTCPHandler(socketserver.BaseRequestHandler): + """ + La classe MyTCPHandler gestirà ogni nuova connessione client. + Sovrascrive il metodo handle() per implementare la logica del server. + """ + def handle(self): + global RUN_FROM_TCPSERVER + RUN_FROM_TCPSERVER = True + + # TCPREMOTEIP in Python è self.client_address[0] + client_ip = self.client_address[0] + log_write("socket_server", "info", f"Nuova connessione da: {client_ip}") + + try: + # Autenticazione (se configurata) + if TCPSERVER_USER and TCPSERVER_PWD: + # Leggi utente (timeout 3 secondi) + self.request.settimeout(3) + try: + user_line = self.request.recv(1024).decode('utf-8').strip() + password_line = self.request.recv(1024).decode('utf-8').strip() + except socket.timeout: + log_write("socket_server", "warning", f"socket connection from: {client_ip} - Timeout during credentials read") + json_error(0, "Authentication timeout") + return + + if user_line != TCPSERVER_USER or password_line != TCPSERVER_PWD: + log_write("socket_server", "warning", f"socket connection from: {client_ip} - Bad socket server credentials - user:{user_line}") + self.request.sendall(f'{{"status": "error", "code": 0, "message": "Bad socket server credentials"}}\n'.encode('utf-8')) + return + else: + log_write("socket_server", "info", f"socket connection from: {client_ip} - Authentication successful") + + # Leggi il comando + command_line = self.request.recv(4096).decode('utf-8').strip() + + # Parsing del comando + args = command_line.split(' ') + arg1 = args[0] if len(args) > 0 else "" + arg2 = args[1] if len(args) > 1 else "" + arg3 = args[2] if len(args) > 2 else "" + arg4 = args[3] if len(args) > 3 else "" + arg5 = args[4] if len(args) > 4 else "" + arg6 = args[5] if len(args) > 5 else "" + arg7 = args[6] if len(args) > 6 else "" + arg8 = args[7] if len(args) > 7 else "" + + log_write("socket_server", "info", f"socket connection from: {client_ip} - command: {command_line}") + + # Esegui il comando + response = self.execute_command(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8) + + # Invia la risposta al client (assumendo che le funzioni json_error/json_status stampino su stdout) + # Potresti voler catturare l'output di quelle funzioni e inviarlo qui. + # Per semplicità, qui si presuppone che inviino direttamente al client, + # ma in un sistema reale dovresti costruire la risposta JSON e inviarla. + self.request.sendall(response.encode('utf-8') + b'\n') + + except socket.timeout: + log_write("socket_server", "warning", f"socket connection from: {client_ip} - Timeout waiting for command.") + self.request.sendall(f'{{"status": "error", "code": 0, "message": "Timeout waiting for command"}}\n'.encode('utf-8')) + except Exception as e: + log_write("socket_server", "error", f"Errore durante la gestione della connessione da {client_ip}: {e}") + self.request.sendall(f'{{"status": "error", "code": -1, "message": "Internal server error"}}\n'.encode('utf-8')) + finally: + self.request.close() # Chiudi la connessione + RUN_FROM_TCPSERVER = False # Reset per la prossima connessione o per operazioni interne + + def execute_command(self, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8): + """Esegue il comando ricevuto dal socket server.""" + # Questo metodo replicherà la logica della case statement Bash + vret = "" # Per catturare i risultati delle funzioni + + if arg1 == "status": + json_status(arg2, arg3, arg4, arg5, arg6, arg7) + elif arg1 == "open": + if not arg2: # "empty$arg2" == "empty" + json_error(0, "Alias solenoid not specified") + else: + ev_open(arg2, arg3) # &> /dev/null è gestito non catturando l'output + json_status(f"get_cron_open_in:{arg2}") + elif arg1 == "open_in": + ev_open_in(arg2, arg3, arg4, arg5) + json_status(f"get_cron_open_in:{arg4}") + elif arg1 == "close": + if not arg2: + json_error(0, "Alias solenoid not specified") + else: + ev_close(arg2) + json_status(f"get_cron_open_in:{arg2}") + elif arg1 == "close_all": + if arg2 == "disable_scheduling": + cron_disable_all_open_close() + close_all() + message_write("success", "All solenoid closed") + json_status() + elif arg1 == "cron_enable_all_open_close": + cron_enable_all_open_close() + message_write("success", "All solenoid enabled") + json_status() + elif arg1 == "set_general_cron": + # Per i comandi di set_general_cron, chiami le funzioni e concateni i risultati + # Ho ipotizzato che i risultati vuoti indichino successo e stringhe non vuote errori + funcs_to_call = { + "set_cron_init": set_cron_init, + "set_cron_start_socket_server": set_cron_start_socket_server, + "set_cron_check_rain_sensor": set_cron_check_rain_sensor, + "set_cron_check_rain_online": set_cron_check_rain_online, + "set_cron_close_all_for_rain": set_cron_close_all_for_rain, + } + + # Qui si itera sugli argomenti come nello script Bash + for i_arg in [arg for arg in [arg2, arg3, arg4, arg5, arg6, arg7] if arg]: + if i_arg in funcs_to_call: + ret_val = funcs_to_call[i_arg]() + if ret_val: # Se la funzione restituisce qualcosa (un errore in Bash) + vret += ret_val + + if vret: # Se c'è stato qualche errore in una delle chiamate + json_error(0, "Cron set failed") + log_write("socket_server", "error", f"Cron set failed: {vret}") + else: + message_write("success", "Cron set successful") + json_status() + + elif arg1 == "del_cron_open": + vret = del_cron_open(arg2) + if vret: + json_error(0, "Cron set failed") + log_write("socket_server", "error", f"Cron del failed: {vret}") + else: + message_write("success", "Cron set successful") + json_status() + + elif arg1 == "del_cron_open_in": + vret = del_cron_open_in(arg2) + if vret: + json_error(0, "Cron del failed") + log_write("socket_server", "error", f"Cron del failed: {vret}") + else: + message_write("success", "Scheduled start successfully deleted") + json_status(f"get_cron_open_in:{arg2}") + + elif arg1 == "del_cron_close": + vret = del_cron_close(arg2) + if vret: + json_error(0, "Cron set failed") + log_write("socket_server", "error", f"Cron set failed: {vret}") + else: + message_write("success", "Cron set successful") + json_status() + + elif arg1 == "add_cron_open": + vret = add_cron_open(arg2, arg3, arg4, arg5, arg6, arg7, arg8) + if vret: + json_error(0, "Cron set failed") + log_write("socket_server", "error", f"Cron set failed: {vret}") + else: + message_write("success", "Cron set successful") + json_status() + + elif arg1 == "add_cron_close": + vret = add_cron_close(arg2, arg3, arg4, arg5, arg6, arg7, arg8) + if vret: + json_error(0, "Cron set failed") + log_write("socket_server", "error", f"Cron set failed: {vret}") + else: + message_write("success", "Cron set successful") + json_status() + + elif arg1 == "cmd_pigardensched": + vret = cmd_pigardensched(arg2, arg3, arg4, arg5, arg6) + if vret: + json_error(0, "piGardenSched command failed") + log_write("socket_server", "error", f"piGardenSched command failed: {vret}") + else: + message_write("success", "Schedule set successful") + json_status() + + elif arg1 == "reboot": + message_write("warning", "System reboot is started") + json_status() + current_script_path = get_script_path() + # Esegui il reboot in un sottoprocesso separato per non bloccare il server + # e con nohup-like behavior. + # Questo è un esempio, la gestione di reboot/poweroff in Python è delicata. + # Potresti voler chiamare un comando di sistema come `sudo reboot`. + subprocess.Popen([current_script_path, "reboot_system_internal_cmd"], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + preexec_fn=os.setsid) # setsid per disassociare dal gruppo di processi + + elif arg1 == "poweroff": + message_write("warning", "System shutdown is started") + json_status() + current_script_path = get_script_path() + subprocess.Popen([current_script_path, "poweroff_system_internal_cmd"], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + preexec_fn=os.setsid) # setsid per disassociare dal gruppo di processi + + elif arg1 == "reset_last_rain_sensor_timestamp": + reset_last_rain_sensor_timestamp() + message_write("success", "Timestamp of last sensor rain successful reset") + json_status() + + elif arg1 == "reset_last_rain_online_timestamp": + reset_last_rain_online_timestamp() + message_write("success", "Timestamp of last online rain successful reset") + json_status() + + elif arg1 == "sensor_status_set": + if not arg2: + json_error(0, "Alias sensor not specified") + else: + sensor_status_set(arg2, arg3, arg4) + json_status() + + else: + json_error(0, "invalid command") + + # Le funzioni json_error, json_status, message_write stampano direttamente su stdout + # In un contesto di server reale, dovresti catturare il loro output e inviarlo al client. + # Per questa conversione, sto assumendo che tu voglia replicare il comportamento Bash + # di "stampa e poi la pipeline gestisce l'invio". Potrebbe richiedere un refactoring + # delle funzioni json_error/status per restituire stringhe invece di stampare. + # Per ora, restituisco una stringa vuota o un placeholder. + return "" # Potresti voler restituire il JSON o il messaggio qui + +# --- FUNZIONI PRINCIPALI DEL SERVER --- + +def start_socket_server(): + """Avvia il server socket.""" + # Rimuovi il file PID esistente + if os.path.exists(TCPSERVER_PID_FILE): + os.remove(TCPSERVER_PID_FILE) + + # Scrivi il PID dello script principale nel file PID + # In un sistema reale, dovresti scrivere il PID del processo del server, + # che potrebbe essere diverso se usi un manager di processi come systemd. + current_pid = os.getpid() + with open(TCPSERVER_PID_FILE, "w") as f: + f.write(str(current_pid)) + + log_write("socket_server", "info", f"Server PID {current_pid} scritto in {TCPSERVER_PID_FILE}") + + try: + # Crea il server TCP + # ThreadingTCPServer gestisce ogni richiesta in un thread separato + with socketserver.ThreadingTCPServer((TCPSERVER_IP, TCPSERVER_PORT), MyTCPHandler) as server: + log_write("socket_server", "info", f"Server socket avviato su {TCPSERVER_IP}:{TCPSERVER_PORT}") + # Avvia il server, rimarrà in ascolto indefinitamente + server.serve_forever() + except Exception as e: + log_write("socket_server", "error", f"Errore all'avvio del server socket: {e}") + if os.path.exists(TCPSERVER_PID_FILE): + os.remove(TCPSERVER_PID_FILE) # Pulisci il PID file in caso di errore + sys.exit(1) + + +def stop_socket_server(): + """Ferma il server socket.""" + if not os.path.exists(TCPSERVER_PID_FILE): + print("Daemon is not running") + sys.exit(1) + + log_write("socket_server", "info", "stop socket server") + + with open(TCPSERVER_PID_FILE, "r") as f: + try: + pid = int(f.read().strip()) + except ValueError: + print(f"Errore: Il file PID '{TCPSERVER_PID_FILE}' contiene un PID non valido.") + sys.exit(1) + + # Tentativo di killare i processi discendenti (se list_descendants è implementato) + descendants = list_descendants(pid) + for d_pid in descendants: + try: + os.kill(d_pid, 9) # SIGKILL + log_write("socket_server", "info", f"Terminato processo discendente {d_pid}") + except ProcessLookupError: + pass # Il processo non esiste + + # Tenta di killare il processo principale del server + try: + os.kill(pid, 9) # SIGKILL + log_write("socket_server", "info", f"Terminato processo server {pid}") + except ProcessLookupError: + print(f"Processo con PID {pid} non trovato.") + except Exception as e: + log_write("socket_server", "error", f"Errore durante l'uccisione del processo server {pid}: {e}") + + # Rimuovi il file PID + if os.path.exists(TCPSERVER_PID_FILE): + os.remove(TCPSERVER_PID_FILE) + log_write("socket_server", "info", "File PID rimosso.") + + +# --- FUNZIONE PRINCIPALE PER LA GESTIONE DEGLI ARGOMENTI DELLA CLI --- +if __name__ == "__main__": + if len(sys.argv) > 1: + command = sys.argv[1] + + if command == "start_socket_server": + # Per avviare in background come un vero daemon, dovresti implementare + # la demonizzazione (forking) qui o usare una libreria come `python-daemon`. + # Per ora, questo lo avvia nel terminale corrente. + log_write("main", "info", "Richiesta di avvio del socket server.") + start_socket_server() + elif command == "stop_socket_server": + log_write("main", "info", "Richiesta di stop del socket server.") + stop_socket_server() + elif command == "socket_server_command": + # Questa parte dovrebbe essere gestita dal server TCP stesso. + # Non viene chiamata direttamente dalla riga di comando in Python in questo modo. + # Se hai bisogno di testare la logica 'socket_server_command' isolatamente, + # dovresti chiamare MyTCPHandler.execute_command() con argomenti di test. + print("Errore: 'socket_server_command' non può essere chiamato direttamente come script.") + print("Questa logica è gestita internamente dal server TCP.") + sys.exit(1) + elif command == "reboot_system_internal_cmd": + # Comando interno per il riavvio effettivo + log_write("main", "warning", "Eseguo il riavvio del sistema...") + # In un sistema Linux, questo è il modo per riavviare + subprocess.run(["sudo", "reboot"]) + # Assicurati che l'utente che esegue lo script abbia i permessi sudo senza password per 'reboot' + elif command == "poweroff_system_internal_cmd": + # Comando interno per lo spegnimento effettivo + log_write("main", "warning", "Eseguo lo spegnimento del sistema...") + # In un sistema Linux, questo è il modo per spegnere + subprocess.run(["sudo", "poweroff"]) + # Assicurati che l'utente che esegue lo script abbia i permessi sudo senza password per 'poweroff' + else: + print(f"Comando non riconosciuto: {command}") + print("Utilizzo: python your_script_name.py [start_socket_server|stop_socket_server]") + sys.exit(1) + else: + print("Nessun comando specificato.") + print("Utilizzo: python your_script_name.py [start_socket_server|stop_socket_server]") + sys.exit(1) \ No newline at end of file