from flask import Blueprint, render_template, request, redirect, url_for, flash, session from models import db, CatalogoPaneles, DatosSolaresCiudad, ProyectosUsuario, Casa, Configuracion from datetime import datetime from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user from extensions import socketio, login_manager import paramiko import threading import time import os main = Blueprint('main', __name__) # --- Auth Setup --- class User(UserMixin): def __init__(self, id): self.id = id @login_manager.user_loader def load_user(user_id): if user_id == '1': return User(id='1') return None @main.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': username = request.form.get('username') password = request.form.get('password') if username == 'admin' and password == 'solaradmin': user = User(id='1') login_user(user) return redirect(url_for('main.admin')) else: flash('Credenciales inválidas', 'danger') return render_template('login.html') @main.route('/logout') @login_required def logout(): logout_user() return redirect(url_for('main.index')) @main.route('/admin') @login_required def admin(): return render_template('admin.html') # --- SSH Logic --- ssh_sessions = {} @socketio.on('connect', namespace='/ssh') def connect_ssh(): if not current_user.is_authenticated: return False # Start a new SSH session # WARNING: Hardcoded credentials for demo purposes. EXTREMELY INSECURE. # In a real app, use keys or prompt for credentials. # Using the server's own credentials or a specific user. # For this demo, we'll try to connect to localhost if possible, or just echo. # But the user asked for "SSH console". # Let's assume we want to connect to the machine running this app. # We need credentials. The user asked for "dialogo que indique credenciales". # So we will expect the user to provide them via the UI, but for the initial connection # we might need to wait for a 'start_ssh' event with credentials. pass @socketio.on('start_ssh', namespace='/ssh') def start_ssh(data): if not current_user.is_authenticated: return host = data.get('host', 'localhost') port = int(data.get('port', 22)) username = data.get('username') password = data.get('password') sid = request.sid try: client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect(host, port=port, username=username, password=password) channel = client.invoke_shell() ssh_sessions[sid] = {'client': client, 'channel': channel} def listen_to_ssh(): while True: if channel.recv_ready(): data = channel.recv(1024).decode('utf-8') socketio.emit('ssh_output', {'data': data}, room=sid, namespace='/ssh') socketio.sleep(0.1) if not channel.active: break socketio.start_background_task(listen_to_ssh) socketio.emit('ssh_status', {'status': 'connected'}, room=sid, namespace='/ssh') except Exception as e: socketio.emit('ssh_status', {'status': 'error', 'message': str(e)}, room=sid, namespace='/ssh') @socketio.on('ssh_input', namespace='/ssh') def ssh_input(data): sid = request.sid if sid in ssh_sessions: channel = ssh_sessions[sid]['channel'] channel.send(data['data']) @socketio.on('disconnect', namespace='/ssh') def disconnect_ssh(): sid = request.sid if sid in ssh_sessions: ssh_sessions[sid]['client'].close() del ssh_sessions[sid] @main.route('/') def index(): return render_template('index.html') @main.route('/metodologia') def metodologia(): return render_template('methodology.html') @main.route('/configuracion', methods=['GET', 'POST']) def configuracion(): config_precio = Configuracion.query.filter_by(clave='precio_kwh').first() ciudades = DatosSolaresCiudad.query.all() casas = Casa.query.all() paneles = CatalogoPaneles.query.all() edit_id = request.args.get('edit_id') casa_editar = None if edit_id: casa_editar = Casa.query.get(edit_id) if request.method == 'POST': nuevo_precio = request.form.get('precio_kwh') if config_precio: config_precio.valor = nuevo_precio else: config_precio = Configuracion(clave='precio_kwh', valor=nuevo_precio) db.session.add(config_precio) db.session.commit() return redirect(url_for('main.configuracion')) return render_template('configuracion.html', precio_kwh=config_precio.valor if config_precio else '0.15', ciudades=ciudades, casas=casas, paneles=paneles, casa_editar=casa_editar) @main.route('/casa/editar/', methods=['POST']) def editar_casa(id_casa): casa = Casa.query.get_or_404(id_casa) casa.nombre = request.form.get('nombre') casa.area_techo = float(request.form.get('area_techo')) casa.orientacion = request.form.get('orientacion') inclinacion_str = request.form.get('inclinacion') casa.inclinacion = float(inclinacion_str) if inclinacion_str else 0.0 casa.id_ciudad = request.form.get('id_ciudad') db.session.commit() flash('Casa actualizada correctamente.', 'success') return redirect(url_for('main.configuracion')) @main.route('/casa/eliminar/', methods=['POST']) def eliminar_casa(id_casa): casa = Casa.query.get_or_404(id_casa) # Desvincular proyectos antes de eliminar proyectos = ProyectosUsuario.query.filter_by(id_casa=id_casa).all() for p in proyectos: p.id_casa = None db.session.delete(casa) db.session.commit() flash('Casa eliminada correctamente.', 'success') return redirect(url_for('main.configuracion')) @main.route('/panel/nuevo', methods=['POST']) def nuevo_panel(): marca = request.form.get('marca') modelo = request.form.get('modelo') eficiencia_r = float(request.form.get('eficiencia_r')) area_m2 = float(request.form.get('area_m2')) precio_unitario = float(request.form.get('precio_unitario')) panel = CatalogoPaneles(marca=marca, modelo=modelo, eficiencia_r=eficiencia_r, area_m2=area_m2, precio_unitario=precio_unitario) db.session.add(panel) db.session.commit() flash('Panel agregado correctamente.', 'success') return redirect(url_for('main.configuracion')) @main.route('/panel/editar/', methods=['POST']) def editar_panel(id_panel): panel = CatalogoPaneles.query.get_or_404(id_panel) panel.marca = request.form.get('marca') panel.modelo = request.form.get('modelo') panel.eficiencia_r = float(request.form.get('eficiencia_r')) panel.area_m2 = float(request.form.get('area_m2')) panel.precio_unitario = float(request.form.get('precio_unitario')) db.session.commit() flash('Panel actualizado correctamente.', 'success') return redirect(url_for('main.configuracion')) @main.route('/panel/eliminar/', methods=['POST']) def eliminar_panel(id_panel): panel = CatalogoPaneles.query.get_or_404(id_panel) # Verificar si hay proyectos usando este panel proyectos = ProyectosUsuario.query.filter_by(id_panel=id_panel).first() if proyectos: flash('No se puede eliminar este panel porque hay proyectos asociados a él.', 'danger') return redirect(url_for('main.configuracion')) db.session.delete(panel) db.session.commit() flash('Panel eliminado correctamente.', 'success') return redirect(url_for('main.configuracion')) @main.route('/casas') def casas(): casas = Casa.query.all() ciudades = DatosSolaresCiudad.query.all() return render_template('casas.html', casas=casas, ciudades=ciudades) @main.route('/proyectos') def proyectos(): id_casa_filter = request.args.get('id_casa', type=int) query = ProyectosUsuario.query if id_casa_filter: query = query.filter_by(id_casa=id_casa_filter) proyectos = query.order_by(ProyectosUsuario.fecha_referencia.desc()).all() casas = Casa.query.all() # Prepare chart data (reversed for chronological order in chart) chart_proyectos = proyectos[::-1] labels = [p.fecha_referencia.strftime('%Y-%m-%d') for p in chart_proyectos] costo_sin_solar = [] costo_con_solar = [] for p in chart_proyectos: costo_actual = p.costo_actual_mensual if p.costo_actual_mensual else 0 ahorro = p.ahorro_estimado if p.ahorro_estimado else 0 nuevo_costo = max(0, costo_actual - ahorro) costo_sin_solar.append(costo_actual) costo_con_solar.append(nuevo_costo) return render_template('proyectos.html', proyectos=proyectos, casas=casas, selected_casa=id_casa_filter, labels=labels, costo_sin_solar=costo_sin_solar, costo_con_solar=costo_con_solar) @main.route('/casa/nueva', methods=['POST']) def nueva_casa(): nombre = request.form.get('nombre') area_techo = float(request.form.get('area_techo')) orientacion = request.form.get('orientacion', 'Norte') inclinacion_str = request.form.get('inclinacion') inclinacion = float(inclinacion_str) if inclinacion_str else 0.0 id_ciudad = request.form.get('id_ciudad') casa = Casa(nombre=nombre, area_techo=area_techo, orientacion=orientacion, inclinacion=inclinacion, id_ciudad=id_ciudad) db.session.add(casa) db.session.commit() return redirect(url_for('main.configuracion')) @main.route('/simular/') def simular(id_casa): casa = Casa.query.get_or_404(id_casa) paneles = CatalogoPaneles.query.all() config_precio = Configuracion.query.filter_by(clave='precio_kwh').first() precio_kwh = float(config_precio.valor) if config_precio else 0.15 return render_template('simulation.html', casa=casa, paneles=paneles, precio_kwh=precio_kwh) @main.route('/calculate', methods=['POST']) def calculate(): id_casa = request.form.get('id_casa') id_panel = request.form.get('id_panel') cantidad = int(request.form.get('cantidad')) # Nuevos campos opcionales consumo_kwh = request.form.get('consumo_kwh') consumo_kwh = float(consumo_kwh) if consumo_kwh else 0 # Obtenemos el precio global configurado config_precio = Configuracion.query.filter_by(clave='precio_kwh').first() precio_kwh = float(config_precio.valor) if config_precio else 0.15 # Calculamos el costo mensual basado en el consumo y el precio global costo_mensual = consumo_kwh * precio_kwh casa = Casa.query.get(id_casa) ciudad = casa.ciudad panel = CatalogoPaneles.query.get(id_panel) # E = A * r * H * PR # Monthly E = (A_panel * quantity) * r * H * 30 * 0.75 area_total = panel.area_m2 * cantidad pr = 0.75 # Factor de corrección por orientación e inclinación # Ecuador está en latitud 0, por lo que la inclinación ideal es casi plana (0-10 grados). # Si la inclinación es mayor, la orientación empieza a importar más. factor_orientacion = 1.0 if casa.inclinacion > 10: if casa.orientacion in ['Este', 'Oeste']: factor_orientacion = 0.90 # Pérdida por menos horas de sol pico elif casa.orientacion in ['Norte', 'Sur']: factor_orientacion = 0.95 # Pérdida leve por ángulo solar elif casa.orientacion == 'Plano': factor_orientacion = 1.0 else: factor_orientacion = 0.92 # Noreste, Sureste, etc. days = 30 energia_mensual = area_total * panel.eficiencia_r * ciudad.irradiacion_h_promedio * days * pr * factor_orientacion # Bonus: ROI ahorro_mensual = energia_mensual * precio_kwh # Si el ahorro es mayor que el costo actual (si existe), lo limitamos al costo actual (no puedes ahorrar más de lo que pagas) # Aunque técnicamente podrías vender excedentes, para simplificar asumimos net metering o autoconsumo puro. if costo_mensual > 0 and ahorro_mensual > costo_mensual: ahorro_mensual = costo_mensual costo_total = panel.precio_unitario * cantidad tiempo_recuperacion = (costo_total / ahorro_mensual / 12) if ahorro_mensual > 0 else 0 # Years # Bonus: CO2 co2_evitado = energia_mensual * 0.4 # Nuevo costo estimado nuevo_costo_mensual = max(0, costo_mensual - ahorro_mensual) if costo_mensual > 0 else 0 porcentaje_cobertura = (energia_mensual / consumo_kwh * 100) if consumo_kwh > 0 else 0 return render_template('results.html', energia=round(energia_mensual, 2), ahorro=round(ahorro_mensual, 2), recuperacion=round(tiempo_recuperacion, 1), co2=round(co2_evitado, 2), ciudad=ciudad, panel=panel, cantidad=cantidad, costo_total=costo_total, casa=casa, consumo_kwh=consumo_kwh, costo_mensual=round(costo_mensual, 2), nuevo_costo_mensual=round(nuevo_costo_mensual, 2), porcentaje_cobertura=round(porcentaje_cobertura, 1), today=datetime.now().strftime('%Y-%m-%d')) @main.route('/save', methods=['POST']) def save(): nombre_cliente = request.form.get('nombre_cliente') id_casa = request.form.get('id_casa') id_panel = request.form.get('id_panel') cantidad = request.form.get('cantidad') energia = request.form.get('energia') ahorro = request.form.get('ahorro') consumo_kwh = request.form.get('consumo_kwh') costo_mensual = request.form.get('costo_mensual') fecha_str = request.form.get('fecha_referencia') casa = Casa.query.get(id_casa) if fecha_str: try: fecha_referencia = datetime.strptime(fecha_str, '%Y-%m-%d') except ValueError: fecha_referencia = datetime.now() else: fecha_referencia = datetime.now() proyecto = ProyectosUsuario( nombre_cliente=nombre_cliente, id_casa=id_casa, id_ciudad=casa.id_ciudad, id_panel=id_panel, cantidad_paneles=cantidad, energia_estimada_mensual=energia, ahorro_estimado=ahorro, consumo_actual_kwh=float(consumo_kwh) if consumo_kwh and float(consumo_kwh) > 0 else None, costo_actual_mensual=float(costo_mensual) if costo_mensual and float(costo_mensual) > 0 else None, fecha_referencia=fecha_referencia ) db.session.add(proyecto) db.session.commit() return redirect(url_for('main.proyectos'))