Project

General

Profile

Servidor » History » Version 2

cristobal hernandez, 12/18/2025 01:17 AM

1 1 cristobal hernandez
h1. Servidor
2 1 cristobal hernandez
<pre><code class="python">
3 2 cristobal hernandez
#!/usr/bin/env python3
4 2 cristobal hernandez
# -*- coding: utf-8 -*-
5 1 cristobal hernandez
6 2 cristobal hernandez
"""
7 2 cristobal hernandez
-----------------------------------------------------------------------
8 2 cristobal hernandez
  SISTEMA SAVENEMO - CONTROL DE ACUARIO
9 2 cristobal hernandez
  Plataforma: Raspberry Pi + GrovePi
10 2 cristobal hernandez
-----------------------------------------------------------------------
11 2 cristobal hernandez
"""
12 1 cristobal hernandez
13 2 cristobal hernandez
import time
14 2 cristobal hernandez
import glob
15 2 cristobal hernandez
import json
16 2 cristobal hernandez
import requests
17 2 cristobal hernandez
import os
18 2 cristobal hernandez
import random
19 2 cristobal hernandez
from datetime import datetime
20 1 cristobal hernandez
21 2 cristobal hernandez
import grovepi
22 2 cristobal hernandez
from grovepi import *
23 2 cristobal hernandez
from grove_rgb_lcd import *
24 2 cristobal hernandez
from gpiozero import AngularServo
25 1 cristobal hernandez
26 2 cristobal hernandez
import firebase_admin
27 2 cristobal hernandez
from firebase_admin import credentials, firestore, messaging
28 2 cristobal hernandez
29 2 cristobal hernandez
30 2 cristobal hernandez
# Archivos Locales
31 2 cristobal hernandez
FILE_ID = "savenemo_id.txt"
32 2 cristobal hernandez
FILE_CONFIG_LOCAL = "config.json"
33 2 cristobal hernandez
FILE_CREDENTIALS = "serviceAccountKey.json"
34 2 cristobal hernandez
35 2 cristobal hernandez
# Configuracion Sensor Temperatura
36 2 cristobal hernandez
SENSOR_TEMP_ID = "28-00000053d2ea"
37 2 cristobal hernandez
SENSOR_TEMP_PATH = f"/sys/bus/w1/devices/{SENSOR_TEMP_ID}/w1_slave"
38 2 cristobal hernandez
39 2 cristobal hernandez
# Puertos GrovePi
40 2 cristobal hernandez
PORT_ULTRASONIC  = 8   # D8
41 2 cristobal hernandez
PORT_RELAY_CAL   = 3   # D3 (Calefactor)
42 2 cristobal hernandez
PORT_PH          = 0   # A0
43 2 cristobal hernandez
PORT_SENSOR_LUZ  = 1   # A1
44 2 cristobal hernandez
SERVO_PIN = 18         # Pin GPIO (Servomotor)
45 2 cristobal hernandez
46 2 cristobal hernandez
# Variables Globales de Hardware
47 2 cristobal hernandez
alimentador_servo = None 
48 2 cristobal hernandez
49 2 cristobal hernandez
# Configuracion Inicial por Defecto
50 2 cristobal hernandez
DEFAULT_CONFIG = {
51 2 cristobal hernandez
    "nombre_pez": "Esperando App...",
52 2 cristobal hernandez
    "temp_min": 24.0, "temp_max": 28.0,
53 2 cristobal hernandez
    "ph_min": 6.5, "ph_max": 7.5,
54 2 cristobal hernandez
    "nivel_luz": 50, "nivel_agua": 80,
55 2 cristobal hernandez
    "horarios_comida": [],
56 2 cristobal hernandez
    "sistema_alimentacion_on": True 
57 2 cristobal hernandez
}
58 2 cristobal hernandez
59 2 cristobal hernandez
# Estado del Sistema
60 2 cristobal hernandez
config_actual = DEFAULT_CONFIG.copy()
61 2 cristobal hernandez
nemo_id = ""
62 2 cristobal hernandez
db = None
63 2 cristobal hernandez
firebase_activo = False
64 2 cristobal hernandez
ultimo_minuto_alimentacion = "" 
65 2 cristobal hernandez
ya_se_envio_notificacion = False
66 2 cristobal hernandez
67 2 cristobal hernandez
# Funciones BD
68 2 cristobal hernandez
69 2 cristobal hernandez
def obtener_id_unico():
70 2 cristobal hernandez
    """Lee o genera un ID unico para identificar esta pecera."""
71 2 cristobal hernandez
    if os.path.exists(FILE_ID):
72 2 cristobal hernandez
        with open(FILE_ID, 'r') as f:
73 2 cristobal hernandez
            return f.read().strip()
74 2 cristobal hernandez
    else:
75 2 cristobal hernandez
        nuevo_id = f"NEMO-{random.randint(1000, 9999)}"
76 2 cristobal hernandez
        with open(FILE_ID, 'w') as f:
77 2 cristobal hernandez
            f.write(nuevo_id)
78 2 cristobal hernandez
        return nuevo_id
79 2 cristobal hernandez
80 2 cristobal hernandez
def verificar_internet():
81 2 cristobal hernandez
    try:
82 2 cristobal hernandez
        requests.get('https://www.google.com', timeout=3)
83 2 cristobal hernandez
        return True
84 2 cristobal hernandez
    except: return False
85 2 cristobal hernandez
86 2 cristobal hernandez
def esperar_internet():
87 2 cristobal hernandez
    print("Buscando conexion a internet...")
88 2 cristobal hernandez
    while not verificar_internet():
89 2 cristobal hernandez
        time.sleep(3)
90 2 cristobal hernandez
    print("Conexion establecida.")
91 2 cristobal hernandez
92 2 cristobal hernandez
def iniciar_firebase():
93 2 cristobal hernandez
    global db, firebase_activo
94 2 cristobal hernandez
    try:
95 2 cristobal hernandez
        if os.path.exists(FILE_CREDENTIALS):
96 2 cristobal hernandez
            cred = credentials.Certificate(FILE_CREDENTIALS)
97 2 cristobal hernandez
            if not firebase_admin._apps:
98 2 cristobal hernandez
                firebase_admin.initialize_app(cred)
99 2 cristobal hernandez
            db = firestore.client()
100 2 cristobal hernandez
            firebase_activo = True
101 2 cristobal hernandez
            print("Firebase conectado exitosamente.")
102 2 cristobal hernandez
        else:
103 2 cristobal hernandez
            print("Falta archivo serviceAccountKey.json")
104 2 cristobal hernandez
    except Exception as e:
105 2 cristobal hernandez
        print(f"Error Firebase: {e}")
106 2 cristobal hernandez
        firebase_activo = False
107 2 cristobal hernandez
108 2 cristobal hernandez
def sincronizar_configuracion():
109 2 cristobal hernandez
    global config_actual
110 2 cristobal hernandez
    if not firebase_activo: return
111 2 cristobal hernandez
    try:
112 2 cristobal hernandez
        doc = db.collection('acuarios').document(nemo_id).collection('data').document('config').get()
113 2 cristobal hernandez
        if doc.exists:
114 2 cristobal hernandez
            datos = doc.to_dict()
115 2 cristobal hernandez
            if datos != config_actual: # Solo actualizar si hay cambios
116 2 cristobal hernandez
                print("Configuracion actualizada desde la nube.")
117 2 cristobal hernandez
                config_actual.update(datos)
118 2 cristobal hernandez
                with open(FILE_CONFIG_LOCAL, 'w') as f: json.dump(config_actual, f)
119 2 cristobal hernandez
    except: pass
120 2 cristobal hernandez
121 2 cristobal hernandez
def subir_estado(temp, ph, luz, agua, alertas):
122 2 cristobal hernandez
    """Sube los datos de sensores a Firestore."""
123 2 cristobal hernandez
    if not firebase_activo: return
124 2 cristobal hernandez
    try:
125 2 cristobal hernandez
        db.collection('acuarios').document(nemo_id).collection('data').document('estado').set({
126 2 cristobal hernandez
            'temp_actual': temp, 'ph_actual': ph, 'luz_actual': luz, 'agua_nivel': agua,
127 2 cristobal hernandez
            'alertas': alertas, 'ultima_actualizacion': datetime.now().isoformat()
128 2 cristobal hernandez
        })
129 2 cristobal hernandez
    except: pass
130 2 cristobal hernandez
131 2 cristobal hernandez
def enviar_notificacion_push(titulo, mensaje):
132 2 cristobal hernandez
    if not firebase_activo: return
133 2 cristobal hernandez
    try:
134 2 cristobal hernandez
        tokens_ref = db.collection('acuarios').document(nemo_id).collection('data').document('tokens')
135 2 cristobal hernandez
        doc = tokens_ref.get()
136 2 cristobal hernandez
        token = doc.to_dict().get('token_celular') if doc.exists else None
137 2 cristobal hernandez
        
138 2 cristobal hernandez
        if token:
139 2 cristobal hernandez
            msg = messaging.Message(
140 2 cristobal hernandez
                notification=messaging.Notification(title=titulo, body=mensaje),
141 2 cristobal hernandez
                token=token,
142 2 cristobal hernandez
            )
143 2 cristobal hernandez
            messaging.send(msg)
144 2 cristobal hernandez
            print(f"Push enviado: {titulo}")
145 2 cristobal hernandez
    except Exception as e: 
146 2 cristobal hernandez
        print(f"Error push: {e}")
147 2 cristobal hernandez
148 2 cristobal hernandez
def guardar_historial_evento(titulo, mensaje):
149 2 cristobal hernandez
    if not firebase_activo: return
150 2 cristobal hernandez
    try:
151 2 cristobal hernandez
        db.collection('acuarios').document(nemo_id).collection('historial').add({
152 2 cristobal hernandez
            'titulo': titulo,
153 2 cristobal hernandez
            'mensaje': mensaje,
154 2 cristobal hernandez
            'fecha': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
155 2 cristobal hernandez
            'timestamp': datetime.now().timestamp()
156 2 cristobal hernandez
        })
157 2 cristobal hernandez
    except: pass
158 2 cristobal hernandez
159 2 cristobal hernandez
# Hardware
160 2 cristobal hernandez
161 2 cristobal hernandez
def inicializar_hardware():
162 2 cristobal hernandez
    global alimentador_servo
163 2 cristobal hernandez
    try:
164 2 cristobal hernandez
        # Relay
165 2 cristobal hernandez
        pinMode(PORT_RELAY_CAL, "OUTPUT")
166 2 cristobal hernandez
        digitalWrite(PORT_RELAY_CAL, 0)
167 2 cristobal hernandez
        
168 2 cristobal hernandez
        # LCD
169 2 cristobal hernandez
        try:
170 2 cristobal hernandez
            setText("INICIANDO\nSISTEMA...")
171 2 cristobal hernandez
            setRGB(0, 0, 255) # Azul
172 2 cristobal hernandez
        except: print("LCD no detectado (continuando...)")
173 2 cristobal hernandez
        
174 2 cristobal hernandez
        # Servo Motor
175 2 cristobal hernandez
        alimentador_servo = AngularServo(SERVO_PIN, min_angle=0, max_angle=180, 
176 2 cristobal hernandez
                                         min_pulse_width=0.0006, max_pulse_width=0.0024)
177 2 cristobal hernandez
        print("Hardware inicializado correctamente.")
178 2 cristobal hernandez
    except Exception as e:
179 2 cristobal hernandez
        print(f"Error Hardware: {e}")
180 2 cristobal hernandez
        alimentador_servo = None
181 2 cristobal hernandez
182 2 cristobal hernandez
# Sensores
183 2 cristobal hernandez
def leer_temperatura():
184 2 cristobal hernandez
    try:
185 2 cristobal hernandez
        if not os.path.exists(SENSOR_TEMP_PATH):
186 2 cristobal hernandez
            print("Sensor de temperatura no encontrado (Cable desconectado?)")
187 2 cristobal hernandez
            return 25.0
188 2 cristobal hernandez
189 2 cristobal hernandez
        with open(SENSOR_TEMP_PATH, 'r') as f:
190 2 cristobal hernandez
            lineas = f.readlines()
191 2 cristobal hernandez
        
192 2 cristobal hernandez
        if lineas[0].strip()[-3:] == 'YES':
193 2 cristobal hernandez
            posicion_t = lineas[1].find('t=')
194 2 cristobal hernandez
            if posicion_t != -1:
195 2 cristobal hernandez
                temp_string = lineas[1][posicion_t+2:]
196 2 cristobal hernandez
                temp_c = float(temp_string) / 1000.0
197 2 cristobal hernandez
                return round(temp_c, 2)
198 2 cristobal hernandez
    except Exception as e:
199 2 cristobal hernandez
        print(f"Error leyendo temp: {e}")
200 1 cristobal hernandez
    
201 2 cristobal hernandez
    return 25.0
202 1 cristobal hernandez
203 2 cristobal hernandez
def leer_ph():
204 2 cristobal hernandez
    VOLTAJE_NEUTRO = 2.50   
205 2 cristobal hernandez
    FACTOR_CONVERSION = 4.17 
206 1 cristobal hernandez
207 2 cristobal hernandez
    try:
208 2 cristobal hernandez
        valor_raw = grovepi.analogRead(PORT_PH)
209 2 cristobal hernandez
        voltaje = float(valor_raw) * 5.0 / 1023
210 2 cristobal hernandez
        ph_actual = 7.0 + ((voltaje - VOLTAJE_NEUTRO) * FACTOR_CONVERSION)
211 2 cristobal hernandez
        return round(ph_actual, 1)
212 2 cristobal hernandez
    except: return 7.0
213 1 cristobal hernandez
214 2 cristobal hernandez
def leer_nivel_agua():
215 2 cristobal hernandez
    try:
216 2 cristobal hernandez
        altura_total = 40
217 2 cristobal hernandez
        dist = ultrasonicRead(PORT_ULTRASONIC)
218 2 cristobal hernandez
        if dist > altura_total: dist = 40
219 2 cristobal hernandez
        porc = int(100 - ((dist / altura_total) * 100))
220 2 cristobal hernandez
        return max(0, min(100, porc))
221 2 cristobal hernandez
    except: return 80
222 1 cristobal hernandez
223 2 cristobal hernandez
def leer_luz():
224 2 cristobal hernandez
    try:
225 2 cristobal hernandez
        lectura = analogRead(PORT_SENSOR_LUZ)
226 2 cristobal hernandez
        return int((lectura / 800.0) * 100)
227 2 cristobal hernandez
    except: return 50
228 1 cristobal hernandez
229 2 cristobal hernandez
# Actuadores
230 1 cristobal hernandez
231 2 cristobal hernandez
def gestionar_calefactor(temp_actual):
232 2 cristobal hernandez
    target = float(config_actual.get('temp_min', 24.0))
233 2 cristobal hernandez
    if temp_actual < target:
234 2 cristobal hernandez
        digitalWrite(PORT_RELAY_CAL, 1) # Encender
235 2 cristobal hernandez
        return True
236 2 cristobal hernandez
    elif temp_actual > target + 0.5:
237 2 cristobal hernandez
        digitalWrite(PORT_RELAY_CAL, 0) # Apagar
238 2 cristobal hernandez
        return False
239 2 cristobal hernandez
    return False
240 1 cristobal hernandez
241 2 cristobal hernandez
def mover_servo_alimentar():
242 2 cristobal hernandez
    if alimentador_servo is None: return False
243 2 cristobal hernandez
    try:
244 2 cristobal hernandez
        print("Dispensando comida...")
245 2 cristobal hernandez
        alimentador_servo.angle = 90
246 2 cristobal hernandez
        time.sleep(0.8) 
247 2 cristobal hernandez
        alimentador_servo.angle = 110
248 2 cristobal hernandez
        time.sleep(0.3)
249 2 cristobal hernandez
        alimentador_servo.angle = 90
250 2 cristobal hernandez
        time.sleep(0.3)
251 2 cristobal hernandez
        alimentador_servo.angle = 0
252 2 cristobal hernandez
        time.sleep(1)
253 2 cristobal hernandez
        alimentador_servo.value = None 
254 2 cristobal hernandez
        return True
255 2 cristobal hernandez
    except Exception as e:
256 2 cristobal hernandez
        print(f"Error Servo: {e}")
257 2 cristobal hernandez
        return False
258 1 cristobal hernandez
259 2 cristobal hernandez
def gestionar_comida():
260 2 cristobal hernandez
    global ultimo_minuto_alimentacion
261 2 cristobal hernandez
    if not config_actual.get('sistema_alimentacion_on', True): 
262 2 cristobal hernandez
        return False
263 2 cristobal hernandez
264 2 cristobal hernandez
    hora_actual = datetime.now().strftime("%H:%M")
265 2 cristobal hernandez
    horarios = config_actual.get("horarios_comida", [])
266 2 cristobal hernandez
267 2 cristobal hernandez
    if (hora_actual in horarios) and (hora_actual != ultimo_minuto_alimentacion):
268 2 cristobal hernandez
        print(f"Intentando alimentar ({hora_actual})...")
269 2 cristobal hernandez
        exito = mover_servo_alimentar()
270 2 cristobal hernandez
        if exito:
271 2 cristobal hernandez
            ultimo_minuto_alimentacion = hora_actual
272 2 cristobal hernandez
            # EMOJI REMOVIDO AQUI
273 2 cristobal hernandez
            enviar_notificacion_push("Hora de comer!", f"Tu pez ha sido alimentado a las {hora_actual}")
274 2 cristobal hernandez
            guardar_historial_evento("Comida Servida", "Dispensador automatico activado con exito.")
275 2 cristobal hernandez
            return True
276 2 cristobal hernandez
    return False
277 2 cristobal hernandez
278 2 cristobal hernandez
def actualizar_lcd(nemo_id, temp, ph, luz, agua, calefactor_on, comida_servida):
279 2 cristobal hernandez
    try:
280 2 cristobal hernandez
        if comida_servida:
281 2 cristobal hernandez
            setText("   DISPENSANDO\n    COMIDA...")
282 2 cristobal hernandez
            time.sleep(3)
283 2 cristobal hernandez
            return
284 2 cristobal hernandez
285 2 cristobal hernandez
        if calefactor_on: setRGB(255, 60, 0) # Naranja
286 2 cristobal hernandez
        else: setRGB(0, 255, 0) # Verde
287 2 cristobal hernandez
288 2 cristobal hernandez
        setText(f"ID:{nemo_id}\nT:{temp}C  pH:{ph}")
289 2 cristobal hernandez
        time.sleep(4)
290 2 cristobal hernandez
        setText(f"ID:{nemo_id}\nLuz:{luz}% Agua:{agua}%")
291 2 cristobal hernandez
        time.sleep(4)
292 2 cristobal hernandez
    except: pass
293 2 cristobal hernandez
294 2 cristobal hernandez
def main():
295 2 cristobal hernandez
    global nemo_id, ya_se_envio_notificacion
296 2 cristobal hernandez
    
297 2 cristobal hernandez
    print("\n--- INICIANDO SAVENEMO ---")
298 2 cristobal hernandez
    
299 2 cristobal hernandez
    inicializar_hardware()
300 2 cristobal hernandez
    nemo_id = obtener_id_unico()
301 2 cristobal hernandez
    print(f"ID DISPOSITIVO: {nemo_id}")
302 2 cristobal hernandez
    
303 2 cristobal hernandez
    esperar_internet()
304 2 cristobal hernandez
    iniciar_firebase()
305 2 cristobal hernandez
    
306 2 cristobal hernandez
    print("Loop principal iniciado.\n")
307 2 cristobal hernandez
308 2 cristobal hernandez
    while True:
309 2 cristobal hernandez
        try:
310 2 cristobal hernandez
            sincronizar_configuracion()
311 2 cristobal hernandez
            
312 2 cristobal hernandez
            temp = leer_temperatura()
313 2 cristobal hernandez
            ph = leer_ph()
314 2 cristobal hernandez
            agua = leer_nivel_agua()
315 2 cristobal hernandez
            luz = leer_luz()
316 2 cristobal hernandez
            
317 2 cristobal hernandez
            alertas = []
318 2 cristobal hernandez
            cfg = config_actual
319 2 cristobal hernandez
            if temp < float(cfg.get('temp_min', 20)): alertas.append("Temp Baja")
320 2 cristobal hernandez
            if temp > float(cfg.get('temp_max', 35)): alertas.append("Temp Alta")
321 2 cristobal hernandez
            if agua < int(cfg.get('nivel_agua', 80)): alertas.append("Nivel Bajo")
322 2 cristobal hernandez
            if ph < int(cfg.get('ph_min', 80)): alertas.append("PH Bajo")
323 2 cristobal hernandez
            if ph > int(cfg.get('ph_max', 80)): alertas.append("PH Alto")
324 2 cristobal hernandez
325 2 cristobal hernandez
            calefactor_on = gestionar_calefactor(temp)
326 2 cristobal hernandez
            comida_servida = gestionar_comida()
327 2 cristobal hernandez
328 2 cristobal hernandez
            if alertas:
329 2 cristobal hernandez
                if not ya_se_envio_notificacion:
330 2 cristobal hernandez
                    print(f"Alertas: {alertas}")
331 2 cristobal hernandez
                    # EMOJI REMOVIDO AQUI
332 2 cristobal hernandez
                    enviar_notificacion_push("ALERTA ACUARIO ", f"Atencion: {', '.join(alertas)}")
333 2 cristobal hernandez
                    guardar_historial_evento("Alerta Detectada", ', '.join(alertas))
334 2 cristobal hernandez
                    ya_se_envio_notificacion = True
335 2 cristobal hernandez
            else:
336 2 cristobal hernandez
                if ya_se_envio_notificacion: ya_se_envio_notificacion = False
337 2 cristobal hernandez
338 2 cristobal hernandez
            subir_estado(temp, ph, luz, agua, alertas)
339 2 cristobal hernandez
            
340 2 cristobal hernandez
            cal_status = 'ON' if calefactor_on else 'OFF'
341 2 cristobal hernandez
342 2 cristobal hernandez
            # Solo para ver por terminal
343 2 cristobal hernandez
            print(f"[{datetime.now().strftime('%H:%M')}] T:{temp}C pH:{ph} L:{luz}% A:{agua}% Cal:{cal_status}")
344 2 cristobal hernandez
            
345 2 cristobal hernandez
            actualizar_lcd(nemo_id, temp, ph, luz, agua, calefactor_on, comida_servida)
346 2 cristobal hernandez
347 2 cristobal hernandez
        except KeyboardInterrupt:
348 2 cristobal hernandez
            print("\nApagando sistema...")
349 2 cristobal hernandez
            try:
350 2 cristobal hernandez
                digitalWrite(PORT_RELAY_CAL, 0)
351 2 cristobal hernandez
                setRGB(0, 0, 0)
352 2 cristobal hernandez
                setText("")
353 2 cristobal hernandez
                if alimentador_servo: alimentador_servo.detach()
354 2 cristobal hernandez
            except: pass
355 2 cristobal hernandez
            break
356 2 cristobal hernandez
        except Exception as e:
357 2 cristobal hernandez
            print(f"Error en Loop: {e}")
358 2 cristobal hernandez
            time.sleep(5)
359 2 cristobal hernandez
360 2 cristobal hernandez
if __name__ == "__main__":
361 2 cristobal hernandez
    main()
362 1 cristobal hernandez
        
363 1 cristobal hernandez
</code></pre>