routes.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. from flask import Blueprint, render_template, request, redirect, url_for, flash, session
  2. from models import db, CatalogoPaneles, DatosSolaresCiudad, ProyectosUsuario, Casa, Configuracion
  3. from datetime import datetime
  4. from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
  5. from extensions import socketio, login_manager
  6. import paramiko
  7. import threading
  8. import time
  9. import os
  10. main = Blueprint('main', __name__)
  11. # --- Auth Setup ---
  12. class User(UserMixin):
  13. def __init__(self, id):
  14. self.id = id
  15. @login_manager.user_loader
  16. def load_user(user_id):
  17. if user_id == '1':
  18. return User(id='1')
  19. return None
  20. @main.route('/login', methods=['GET', 'POST'])
  21. def login():
  22. if request.method == 'POST':
  23. username = request.form.get('username')
  24. password = request.form.get('password')
  25. if username == 'admin' and password == 'solaradmin':
  26. user = User(id='1')
  27. login_user(user)
  28. return redirect(url_for('main.admin'))
  29. else:
  30. flash('Credenciales inválidas', 'danger')
  31. return render_template('login.html')
  32. @main.route('/logout')
  33. @login_required
  34. def logout():
  35. logout_user()
  36. return redirect(url_for('main.index'))
  37. @main.route('/admin')
  38. @login_required
  39. def admin():
  40. return render_template('admin.html')
  41. # --- SSH Logic ---
  42. ssh_sessions = {}
  43. @socketio.on('connect', namespace='/ssh')
  44. def connect_ssh():
  45. if not current_user.is_authenticated:
  46. return False
  47. # Start a new SSH session
  48. # WARNING: Hardcoded credentials for demo purposes. EXTREMELY INSECURE.
  49. # In a real app, use keys or prompt for credentials.
  50. # Using the server's own credentials or a specific user.
  51. # For this demo, we'll try to connect to localhost if possible, or just echo.
  52. # But the user asked for "SSH console".
  53. # Let's assume we want to connect to the machine running this app.
  54. # We need credentials. The user asked for "dialogo que indique credenciales".
  55. # So we will expect the user to provide them via the UI, but for the initial connection
  56. # we might need to wait for a 'start_ssh' event with credentials.
  57. pass
  58. @socketio.on('start_ssh', namespace='/ssh')
  59. def start_ssh(data):
  60. if not current_user.is_authenticated:
  61. return
  62. host = data.get('host', 'localhost')
  63. port = int(data.get('port', 22))
  64. username = data.get('username')
  65. password = data.get('password')
  66. sid = request.sid
  67. try:
  68. client = paramiko.SSHClient()
  69. client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
  70. client.connect(host, port=port, username=username, password=password)
  71. channel = client.invoke_shell()
  72. ssh_sessions[sid] = {'client': client, 'channel': channel}
  73. def listen_to_ssh():
  74. while True:
  75. if channel.recv_ready():
  76. data = channel.recv(1024).decode('utf-8')
  77. socketio.emit('ssh_output', {'data': data}, room=sid, namespace='/ssh')
  78. socketio.sleep(0.1)
  79. if not channel.active:
  80. break
  81. socketio.start_background_task(listen_to_ssh)
  82. socketio.emit('ssh_status', {'status': 'connected'}, room=sid, namespace='/ssh')
  83. except Exception as e:
  84. socketio.emit('ssh_status', {'status': 'error', 'message': str(e)}, room=sid, namespace='/ssh')
  85. @socketio.on('ssh_input', namespace='/ssh')
  86. def ssh_input(data):
  87. sid = request.sid
  88. if sid in ssh_sessions:
  89. channel = ssh_sessions[sid]['channel']
  90. channel.send(data['data'])
  91. @socketio.on('disconnect', namespace='/ssh')
  92. def disconnect_ssh():
  93. sid = request.sid
  94. if sid in ssh_sessions:
  95. ssh_sessions[sid]['client'].close()
  96. del ssh_sessions[sid]
  97. @main.route('/')
  98. def index():
  99. return render_template('index.html')
  100. @main.route('/metodologia')
  101. def metodologia():
  102. return render_template('methodology.html')
  103. @main.route('/configuracion', methods=['GET', 'POST'])
  104. def configuracion():
  105. config_precio = Configuracion.query.filter_by(clave='precio_kwh').first()
  106. ciudades = DatosSolaresCiudad.query.all()
  107. casas = Casa.query.all()
  108. paneles = CatalogoPaneles.query.all()
  109. edit_id = request.args.get('edit_id')
  110. casa_editar = None
  111. if edit_id:
  112. casa_editar = Casa.query.get(edit_id)
  113. if request.method == 'POST':
  114. nuevo_precio = request.form.get('precio_kwh')
  115. if config_precio:
  116. config_precio.valor = nuevo_precio
  117. else:
  118. config_precio = Configuracion(clave='precio_kwh', valor=nuevo_precio)
  119. db.session.add(config_precio)
  120. db.session.commit()
  121. return redirect(url_for('main.configuracion'))
  122. return render_template('configuracion.html',
  123. precio_kwh=config_precio.valor if config_precio else '0.15',
  124. ciudades=ciudades,
  125. casas=casas,
  126. paneles=paneles,
  127. casa_editar=casa_editar)
  128. @main.route('/casa/editar/<int:id_casa>', methods=['POST'])
  129. def editar_casa(id_casa):
  130. casa = Casa.query.get_or_404(id_casa)
  131. casa.nombre = request.form.get('nombre')
  132. casa.area_techo = float(request.form.get('area_techo'))
  133. casa.orientacion = request.form.get('orientacion')
  134. inclinacion_str = request.form.get('inclinacion')
  135. casa.inclinacion = float(inclinacion_str) if inclinacion_str else 0.0
  136. casa.id_ciudad = request.form.get('id_ciudad')
  137. db.session.commit()
  138. flash('Casa actualizada correctamente.', 'success')
  139. return redirect(url_for('main.configuracion'))
  140. @main.route('/casa/eliminar/<int:id_casa>', methods=['POST'])
  141. def eliminar_casa(id_casa):
  142. casa = Casa.query.get_or_404(id_casa)
  143. # Desvincular proyectos antes de eliminar
  144. proyectos = ProyectosUsuario.query.filter_by(id_casa=id_casa).all()
  145. for p in proyectos:
  146. p.id_casa = None
  147. db.session.delete(casa)
  148. db.session.commit()
  149. flash('Casa eliminada correctamente.', 'success')
  150. return redirect(url_for('main.configuracion'))
  151. @main.route('/panel/nuevo', methods=['POST'])
  152. def nuevo_panel():
  153. marca = request.form.get('marca')
  154. modelo = request.form.get('modelo')
  155. eficiencia_r = float(request.form.get('eficiencia_r'))
  156. area_m2 = float(request.form.get('area_m2'))
  157. precio_unitario = float(request.form.get('precio_unitario'))
  158. panel = CatalogoPaneles(marca=marca, modelo=modelo, eficiencia_r=eficiencia_r, area_m2=area_m2, precio_unitario=precio_unitario)
  159. db.session.add(panel)
  160. db.session.commit()
  161. flash('Panel agregado correctamente.', 'success')
  162. return redirect(url_for('main.configuracion'))
  163. @main.route('/panel/editar/<int:id_panel>', methods=['POST'])
  164. def editar_panel(id_panel):
  165. panel = CatalogoPaneles.query.get_or_404(id_panel)
  166. panel.marca = request.form.get('marca')
  167. panel.modelo = request.form.get('modelo')
  168. panel.eficiencia_r = float(request.form.get('eficiencia_r'))
  169. panel.area_m2 = float(request.form.get('area_m2'))
  170. panel.precio_unitario = float(request.form.get('precio_unitario'))
  171. db.session.commit()
  172. flash('Panel actualizado correctamente.', 'success')
  173. return redirect(url_for('main.configuracion'))
  174. @main.route('/panel/eliminar/<int:id_panel>', methods=['POST'])
  175. def eliminar_panel(id_panel):
  176. panel = CatalogoPaneles.query.get_or_404(id_panel)
  177. # Verificar si hay proyectos usando este panel
  178. proyectos = ProyectosUsuario.query.filter_by(id_panel=id_panel).first()
  179. if proyectos:
  180. flash('No se puede eliminar este panel porque hay proyectos asociados a él.', 'danger')
  181. return redirect(url_for('main.configuracion'))
  182. db.session.delete(panel)
  183. db.session.commit()
  184. flash('Panel eliminado correctamente.', 'success')
  185. return redirect(url_for('main.configuracion'))
  186. @main.route('/casas')
  187. def casas():
  188. casas = Casa.query.all()
  189. ciudades = DatosSolaresCiudad.query.all()
  190. return render_template('casas.html', casas=casas, ciudades=ciudades)
  191. @main.route('/proyectos')
  192. def proyectos():
  193. id_casa_filter = request.args.get('id_casa', type=int)
  194. query = ProyectosUsuario.query
  195. if id_casa_filter:
  196. query = query.filter_by(id_casa=id_casa_filter)
  197. proyectos = query.order_by(ProyectosUsuario.fecha_referencia.desc()).all()
  198. casas = Casa.query.all()
  199. # Prepare chart data (reversed for chronological order in chart)
  200. chart_proyectos = proyectos[::-1]
  201. labels = [p.fecha_referencia.strftime('%Y-%m-%d') for p in chart_proyectos]
  202. costo_sin_solar = []
  203. costo_con_solar = []
  204. for p in chart_proyectos:
  205. costo_actual = p.costo_actual_mensual if p.costo_actual_mensual else 0
  206. ahorro = p.ahorro_estimado if p.ahorro_estimado else 0
  207. nuevo_costo = max(0, costo_actual - ahorro)
  208. costo_sin_solar.append(costo_actual)
  209. costo_con_solar.append(nuevo_costo)
  210. return render_template('proyectos.html',
  211. proyectos=proyectos,
  212. casas=casas,
  213. selected_casa=id_casa_filter,
  214. labels=labels,
  215. costo_sin_solar=costo_sin_solar,
  216. costo_con_solar=costo_con_solar)
  217. @main.route('/casa/nueva', methods=['POST'])
  218. def nueva_casa():
  219. nombre = request.form.get('nombre')
  220. area_techo = float(request.form.get('area_techo'))
  221. orientacion = request.form.get('orientacion', 'Norte')
  222. inclinacion_str = request.form.get('inclinacion')
  223. inclinacion = float(inclinacion_str) if inclinacion_str else 0.0
  224. id_ciudad = request.form.get('id_ciudad')
  225. casa = Casa(nombre=nombre, area_techo=area_techo, orientacion=orientacion, inclinacion=inclinacion, id_ciudad=id_ciudad)
  226. db.session.add(casa)
  227. db.session.commit()
  228. return redirect(url_for('main.configuracion'))
  229. @main.route('/simular/<int:id_casa>')
  230. def simular(id_casa):
  231. casa = Casa.query.get_or_404(id_casa)
  232. paneles = CatalogoPaneles.query.all()
  233. config_precio = Configuracion.query.filter_by(clave='precio_kwh').first()
  234. precio_kwh = float(config_precio.valor) if config_precio else 0.15
  235. return render_template('simulation.html', casa=casa, paneles=paneles, precio_kwh=precio_kwh)
  236. @main.route('/calculate', methods=['POST'])
  237. def calculate():
  238. id_casa = request.form.get('id_casa')
  239. id_panel = request.form.get('id_panel')
  240. cantidad = int(request.form.get('cantidad'))
  241. # Nuevos campos opcionales
  242. consumo_kwh = request.form.get('consumo_kwh')
  243. consumo_kwh = float(consumo_kwh) if consumo_kwh else 0
  244. # Obtenemos el precio global configurado
  245. config_precio = Configuracion.query.filter_by(clave='precio_kwh').first()
  246. precio_kwh = float(config_precio.valor) if config_precio else 0.15
  247. # Calculamos el costo mensual basado en el consumo y el precio global
  248. costo_mensual = consumo_kwh * precio_kwh
  249. casa = Casa.query.get(id_casa)
  250. ciudad = casa.ciudad
  251. panel = CatalogoPaneles.query.get(id_panel)
  252. # E = A * r * H * PR
  253. # Monthly E = (A_panel * quantity) * r * H * 30 * 0.75
  254. area_total = panel.area_m2 * cantidad
  255. pr = 0.75
  256. # Factor de corrección por orientación e inclinación
  257. # Ecuador está en latitud 0, por lo que la inclinación ideal es casi plana (0-10 grados).
  258. # Si la inclinación es mayor, la orientación empieza a importar más.
  259. factor_orientacion = 1.0
  260. if casa.inclinacion > 10:
  261. if casa.orientacion in ['Este', 'Oeste']:
  262. factor_orientacion = 0.90 # Pérdida por menos horas de sol pico
  263. elif casa.orientacion in ['Norte', 'Sur']:
  264. factor_orientacion = 0.95 # Pérdida leve por ángulo solar
  265. elif casa.orientacion == 'Plano':
  266. factor_orientacion = 1.0
  267. else:
  268. factor_orientacion = 0.92 # Noreste, Sureste, etc.
  269. days = 30
  270. energia_mensual = area_total * panel.eficiencia_r * ciudad.irradiacion_h_promedio * days * pr * factor_orientacion
  271. # Bonus: ROI
  272. ahorro_mensual = energia_mensual * precio_kwh
  273. # 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)
  274. # Aunque técnicamente podrías vender excedentes, para simplificar asumimos net metering o autoconsumo puro.
  275. if costo_mensual > 0 and ahorro_mensual > costo_mensual:
  276. ahorro_mensual = costo_mensual
  277. costo_total = panel.precio_unitario * cantidad
  278. tiempo_recuperacion = (costo_total / ahorro_mensual / 12) if ahorro_mensual > 0 else 0 # Years
  279. # Bonus: CO2
  280. co2_evitado = energia_mensual * 0.4
  281. # Nuevo costo estimado
  282. nuevo_costo_mensual = max(0, costo_mensual - ahorro_mensual) if costo_mensual > 0 else 0
  283. porcentaje_cobertura = (energia_mensual / consumo_kwh * 100) if consumo_kwh > 0 else 0
  284. return render_template('results.html',
  285. energia=round(energia_mensual, 2),
  286. ahorro=round(ahorro_mensual, 2),
  287. recuperacion=round(tiempo_recuperacion, 1),
  288. co2=round(co2_evitado, 2),
  289. ciudad=ciudad,
  290. panel=panel,
  291. cantidad=cantidad,
  292. costo_total=costo_total,
  293. casa=casa,
  294. consumo_kwh=consumo_kwh,
  295. costo_mensual=round(costo_mensual, 2),
  296. nuevo_costo_mensual=round(nuevo_costo_mensual, 2),
  297. porcentaje_cobertura=round(porcentaje_cobertura, 1),
  298. today=datetime.now().strftime('%Y-%m-%d'))
  299. @main.route('/save', methods=['POST'])
  300. def save():
  301. nombre_cliente = request.form.get('nombre_cliente')
  302. id_casa = request.form.get('id_casa')
  303. id_panel = request.form.get('id_panel')
  304. cantidad = request.form.get('cantidad')
  305. energia = request.form.get('energia')
  306. ahorro = request.form.get('ahorro')
  307. consumo_kwh = request.form.get('consumo_kwh')
  308. costo_mensual = request.form.get('costo_mensual')
  309. fecha_str = request.form.get('fecha_referencia')
  310. casa = Casa.query.get(id_casa)
  311. if fecha_str:
  312. try:
  313. fecha_referencia = datetime.strptime(fecha_str, '%Y-%m-%d')
  314. except ValueError:
  315. fecha_referencia = datetime.now()
  316. else:
  317. fecha_referencia = datetime.now()
  318. proyecto = ProyectosUsuario(
  319. nombre_cliente=nombre_cliente,
  320. id_casa=id_casa,
  321. id_ciudad=casa.id_ciudad,
  322. id_panel=id_panel,
  323. cantidad_paneles=cantidad,
  324. energia_estimada_mensual=energia,
  325. ahorro_estimado=ahorro,
  326. consumo_actual_kwh=float(consumo_kwh) if consumo_kwh and float(consumo_kwh) > 0 else None,
  327. costo_actual_mensual=float(costo_mensual) if costo_mensual and float(costo_mensual) > 0 else None,
  328. fecha_referencia=fecha_referencia
  329. )
  330. db.session.add(proyecto)
  331. db.session.commit()
  332. return redirect(url_for('main.proyectos'))