Saltar al contenido
Todo Automatizado
Python Industrial

Cómo Crear un Dashboard Industrial con Python y Flask para Visualizar Datos de Producción

Aprende a construir un dashboard industrial con Python y Flask paso a paso. Visualiza datos de producción en tiempo real conectando sensores, PLCs y bases de datos.

Carlos 16 min de lectura

Cómo Crear un Dashboard Industrial con Python y Flask para Visualizar Datos de Producción

Keywords secundarias: dashboard producción python, flask aplicación industrial, visualizar datos planta, monitorización producción web, python manufactura datos


Si trabajas en una planta industrial y te has cansado de abrir Excel cada mañana para revisar los datos de producción del turno anterior, este tutorial es para ti. Vamos a construir un dashboard industrial con Python y Flask que muestra datos de producción en tiempo real desde un navegador — sin licencias caras, sin software propietario y con control total sobre tu código.

La ventaja de usar Python y Flask frente a soluciones como Grafana o Power BI es clara: flexibilidad absoluta. Tú decides qué datos mostrar, cómo calcularlos y cómo presentarlos. Y si mañana necesitas añadir un algoritmo de detección de anomalías o un modelo predictivo, ya tienes la base en Python para hacerlo sin cambiar de herramienta.

Por Qué Flask y No Otra Herramienta

Hay decenas de frameworks web en Python. ¿Por qué Flask para un dashboard industrial?

Flask es minimalista por diseño. No te obliga a usar un ORM concreto, una estructura de carpetas rígida ni un motor de plantillas específico. En entornos industriales, donde cada planta tiene su propia base de datos, sus propios protocolos y sus propias necesidades, esa flexibilidad vale oro.

Comparado con Django, Flask arranca más rápido y consume menos recursos — algo relevante si vas a desplegarlo en un servidor local de planta con hardware limitado. Comparado con Dash o Streamlit, Flask te da control total sobre el frontend: puedes usar Chart.js, Plotly, D3.js o lo que prefieras sin estar atado a componentes predefinidos.

Además, Flask tiene un ecosistema maduro con extensiones para todo lo que necesitas:

  • Flask-SQLAlchemy para conectar con PostgreSQL, MySQL o SQLite
  • Flask-SocketIO para actualizaciones en tiempo real vía WebSockets
  • Flask-Login para autenticación (porque no quieres que cualquiera vea tus datos de producción)
  • Flask-CORS si necesitas que otros sistemas consuman tu API

Arquitectura del Dashboard: Qué Vamos a Construir

Antes de tocar código, veamos la arquitectura completa. Nuestro dashboard tiene tres capas:

Capa de datos (backend):

  • Base de datos PostgreSQL donde se almacenan los registros de producción
  • Modelo de datos con tablas para líneas de producción, turnos, métricas y alarmas
  • API REST en Flask que expone los datos como JSON

Capa de presentación (frontend):

  • Plantillas HTML con Jinja2
  • Gráficos interactivos con Chart.js
  • Actualización automática cada 30 segundos (configurable)
  • Diseño responsive para ver el dashboard desde un móvil o una tablet en planta

Capa de integración (opcional pero recomendada):

  • Script Python que lee datos del PLC vía Modbus TCP o OPC-UA
  • Inserción periódica en PostgreSQL mediante un cron job o un servicio systemd

Esta separación en capas permite que cada parte evolucione de forma independiente. Puedes cambiar la base de datos sin tocar el frontend, o rediseñar los gráficos sin modificar la API.

Preparar el Entorno de Desarrollo

Empezamos por lo básico. Necesitas Python 3.10 o superior y PostgreSQL instalado (o acceso a una instancia remota).

# Crear directorio del proyecto
mkdir dashboard-industrial && cd dashboard-industrial

# Entorno virtual (siempre, por favor)
python3 -m venv .venv
source .venv/bin/activate

# Dependencias
pip install flask flask-sqlalchemy psycopg2-binary chart-js-helper

Crea esta estructura de carpetas:

dashboard-industrial/
├── app.py                  # Aplicación Flask principal
├── config.py               # Configuración (DB, secretos)
├── models.py               # Modelos SQLAlchemy
├── seed_data.py            # Datos de ejemplo para pruebas
├── templates/
│   ├── base.html           # Plantilla base
│   ├── dashboard.html      # Dashboard principal
│   └── linea_detalle.html  # Detalle por línea de producción
└── static/
    ├── css/
    │   └── style.css
    └── js/
        └── charts.js       # Lógica de gráficos

Configurar la Base de Datos y los Modelos

El modelo de datos es la columna vertebral de cualquier dashboard de producción. Necesitamos representar líneas de producción, turnos y las métricas que queremos visualizar.

config.py:

import os

class Config:
    SQLALCHEMY_DATABASE_URI = os.environ.get(
        'DATABASE_URL',
        'postgresql://dashboard:secreto@localhost:5432/produccion'
    )
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    SECRET_KEY = os.environ.get('SECRET_KEY', 'cambia-esto-en-produccion')

models.py:

from flask_sqlalchemy import SQLAlchemy
from datetime import datetime

db = SQLAlchemy()

class LineaProduccion(db.Model):
    __tablename__ = 'lineas_produccion'

    id = db.Column(db.Integer, primary_key=True)
    nombre = db.Column(db.String(100), nullable=False)
    ubicacion = db.Column(db.String(200))
    activa = db.Column(db.Boolean, default=True)
    registros = db.relationship('RegistroProduccion', backref='linea',
                                 lazy='dynamic')

class RegistroProduccion(db.Model):
    __tablename__ = 'registros_produccion'

    id = db.Column(db.Integer, primary_key=True)
    linea_id = db.Column(db.Integer, db.ForeignKey('lineas_produccion.id'),
                         nullable=False)
    timestamp = db.Column(db.DateTime, default=datetime.utcnow, index=True)
    unidades_producidas = db.Column(db.Integer, default=0)
    unidades_defectuosas = db.Column(db.Integer, default=0)
    tiempo_parada_min = db.Column(db.Float, default=0.0)
    velocidad_linea = db.Column(db.Float)  # unidades/hora
    temperatura_proceso = db.Column(db.Float)
    turno = db.Column(db.String(20))  # 'mañana', 'tarde', 'noche'

    @property
    def oee(self):
        """Calcula OEE simplificado para este registro."""
        if not self.velocidad_linea or self.velocidad_linea == 0:
            return 0.0
        disponibilidad = max(0, (480 - self.tiempo_parada_min)) / 480
        rendimiento = min(1.0, self.unidades_producidas
                          / (self.velocidad_linea * 8))
        calidad = (1 - self.unidades_defectuosas
                   / max(1, self.unidades_producidas))
        return round(disponibilidad * rendimiento * calidad * 100, 1)

Un detalle que merece atención: el cálculo de OEE (Overall Equipment Effectiveness) directamente en el modelo. El OEE es la métrica reina en manufactura — combina disponibilidad, rendimiento y calidad en un solo porcentaje. Tenerlo como propiedad calculada significa que cualquier vista puede acceder a él sin duplicar lógica.

Crear la Aplicación Flask y las Rutas

Ahora viene el núcleo de la aplicación. Flask necesita pocas líneas para funcionar, pero las rutas que definas determinan qué información expone tu dashboard.

app.py:

from flask import Flask, render_template, jsonify, request
from models import db, LineaProduccion, RegistroProduccion
from config import Config
from datetime import datetime, timedelta
from sqlalchemy import func

app = Flask(__name__)
app.config.from_object(Config)
db.init_app(app)

@app.route('/')
def dashboard():
    """Vista principal del dashboard."""
    lineas = LineaProduccion.query.filter_by(activa=True).all()
    resumen = []

    for linea in lineas:
        ultimo_registro = (RegistroProduccion.query
                           .filter_by(linea_id=linea.id)
                           .order_by(RegistroProduccion.timestamp.desc())
                           .first())
        # Producción últimas 24h
        hace_24h = datetime.utcnow() - timedelta(hours=24)
        produccion_24h = (db.session.query(
                              func.sum(RegistroProduccion.unidades_producidas))
                          .filter(RegistroProduccion.linea_id == linea.id,
                                  RegistroProduccion.timestamp >= hace_24h)
                          .scalar() or 0)
        resumen.append({
            'linea': linea,
            'ultimo_registro': ultimo_registro,
            'produccion_24h': produccion_24h,
            'oee': ultimo_registro.oee if ultimo_registro else 0
        })

    return render_template('dashboard.html', resumen=resumen)

@app.route('/api/produccion/<int:linea_id>')
def api_produccion(linea_id):
    """API REST: datos de producción por línea (últimas 24h)."""
    horas = request.args.get('horas', 24, type=int)
    desde = datetime.utcnow() - timedelta(hours=horas)

    registros = (RegistroProduccion.query
                 .filter(RegistroProduccion.linea_id == linea_id,
                         RegistroProduccion.timestamp >= desde)
                 .order_by(RegistroProduccion.timestamp.asc())
                 .all())

    return jsonify({
        'linea_id': linea_id,
        'registros': [{
            'timestamp': r.timestamp.isoformat(),
            'unidades': r.unidades_producidas,
            'defectos': r.unidades_defectuosas,
            'velocidad': r.velocidad_linea,
            'temperatura': r.temperatura_proceso,
            'oee': r.oee,
            'turno': r.turno
        } for r in registros]
    })

@app.route('/api/kpis')
def api_kpis():
    """KPIs globales de planta."""
    hace_24h = datetime.utcnow() - timedelta(hours=24)

    total_producido = (db.session.query(
                           func.sum(RegistroProduccion.unidades_producidas))
                       .filter(RegistroProduccion.timestamp >= hace_24h)
                       .scalar() or 0)
    total_defectos = (db.session.query(
                          func.sum(RegistroProduccion.unidades_defectuosas))
                      .filter(RegistroProduccion.timestamp >= hace_24h)
                      .scalar() or 0)
    lineas_activas = LineaProduccion.query.filter_by(activa=True).count()

    tasa_calidad = (round((1 - total_defectos / max(1, total_producido))
                          * 100, 1))

    return jsonify({
        'total_producido_24h': total_producido,
        'total_defectos_24h': total_defectos,
        'tasa_calidad': tasa_calidad,
        'lineas_activas': lineas_activas
    })

@app.route('/linea/<int:linea_id>')
def detalle_linea(linea_id):
    """Vista detallada de una línea de producción."""
    linea = LineaProduccion.query.get_or_404(linea_id)
    return render_template('linea_detalle.html', linea=linea)

if __name__ == '__main__':
    with app.app_context():
        db.create_all()
    app.run(host='0.0.0.0', port=5000, debug=True)

Fíjate en que la aplicación expone tanto vistas HTML (para humanos) como endpoints API en JSON (para integraciones). Esto te permite, por ejemplo, que un sistema SCADA o un script de n8n consuma los mismos datos que ves en el navegador.

Diseñar el Frontend con Plantillas y Gráficos

El frontend es lo que va a ver el operario de planta o el jefe de turno en la pantalla. Tiene que ser claro, rápido y funcional — nada de animaciones innecesarias.

templates/base.html:

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}Dashboard Producción{% endblock %}</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
    <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4/dist/chart.umd.min.js"></script>
</head>
<body>
    <nav class="navbar">
        <h1>🏭 Dashboard de Producción</h1>
        <span id="reloj"></span>
    </nav>
    <main class="container">
        {% block content %}{% endblock %}
    </main>
    <script>
        // Reloj en tiempo real
        setInterval(() => {
            document.getElementById('reloj').textContent =
                new Date().toLocaleString('es-ES');
        }, 1000);
    </script>
    {% block scripts %}{% endblock %}
</body>
</html>

templates/dashboard.html:

{% extends "base.html" %}

{% block content %}
<!-- KPIs principales -->
<section class="kpis" id="kpis-container">
    <div class="kpi-card">
        <span class="kpi-valor" id="kpi-produccion">--</span>
        <span class="kpi-label">Unidades (24h)</span>
    </div>
    <div class="kpi-card">
        <span class="kpi-valor" id="kpi-calidad">--</span>
        <span class="kpi-label">Tasa de Calidad</span>
    </div>
    <div class="kpi-card">
        <span class="kpi-valor" id="kpi-lineas">--</span>
        <span class="kpi-label">Líneas Activas</span>
    </div>
</section>

<!-- Tarjetas por línea -->
<section class="lineas-grid">
    {% for item in resumen %}
    <div class="linea-card {% if item.oee < 60 %}alerta{% elif item.oee < 80 %}aviso{% endif %}">
        <h3>{{ item.linea.nombre }}</h3>
        <p class="oee-valor">OEE: {{ item.oee }}%</p>
        <p>Producción 24h: {{ item.produccion_24h }} uds</p>
        <canvas id="chart-linea-{{ item.linea.id }}" height="150"></canvas>
        <a href="/linea/{{ item.linea.id }}" class="btn-detalle">Ver detalle →</a>
    </div>
    {% endfor %}
</section>
{% endblock %}

{% block scripts %}
<script src="{{ url_for('static', filename='js/charts.js') }}"></script>
<script>
    // Cargar KPIs
    async function cargarKPIs() {
        const resp = await fetch('/api/kpis');
        const data = await resp.json();
        document.getElementById('kpi-produccion').textContent =
            data.total_producido_24h.toLocaleString('es-ES');
        document.getElementById('kpi-calidad').textContent =
            data.tasa_calidad + '%';
        document.getElementById('kpi-lineas').textContent =
            data.lineas_activas;
    }

    // Cargar gráficos por línea
    {% for item in resumen %}
    cargarGraficoLinea({{ item.linea.id }});
    {% endfor %}

    // Actualizar cada 30 segundos
    cargarKPIs();
    setInterval(cargarKPIs, 30000);
</script>
{% endblock %}

static/js/charts.js:

async function cargarGraficoLinea(lineaId) {
    const resp = await fetch(`/api/produccion/${lineaId}?horas=24`);
    const data = await resp.json();

    const labels = data.registros.map(r =>
        new Date(r.timestamp).toLocaleTimeString('es-ES', {
            hour: '2-digit', minute: '2-digit'
        })
    );
    const produccion = data.registros.map(r => r.unidades);
    const oee = data.registros.map(r => r.oee);

    const ctx = document.getElementById(`chart-linea-${lineaId}`);
    if (!ctx) return;

    new Chart(ctx, {
        type: 'line',
        data: {
            labels: labels,
            datasets: [
                {
                    label: 'Producción (uds)',
                    data: produccion,
                    borderColor: '#2563eb',
                    backgroundColor: 'rgba(37, 99, 235, 0.1)',
                    fill: true,
                    yAxisID: 'y'
                },
                {
                    label: 'OEE (%)',
                    data: oee,
                    borderColor: '#16a34a',
                    borderDash: [5, 5],
                    fill: false,
                    yAxisID: 'y1'
                }
            ]
        },
        options: {
            responsive: true,
            interaction: { mode: 'index', intersect: false },
            scales: {
                y: { position: 'left', title: { display: true, text: 'Unidades' } },
                y1: { position: 'right', min: 0, max: 100,
                       title: { display: true, text: 'OEE %' },
                       grid: { drawOnChartArea: false } }
            },
            plugins: {
                legend: { position: 'bottom' }
            }
        }
    });
}

El CSS lo dejo como ejercicio — lo importante es que uses display: grid para la cuadrícula de líneas y colores semafóricos para el OEE (verde > 80%, amarillo 60-80%, rojo < 60%). Un ejemplo mínimo:

.kpis {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 1rem;
    margin-bottom: 2rem;
}
.kpi-card {
    background: #1e293b;
    color: white;
    padding: 1.5rem;
    border-radius: 8px;
    text-align: center;
}
.kpi-valor { font-size: 2.5rem; font-weight: bold; display: block; }
.linea-card { background: white; padding: 1.5rem; border-radius: 8px;
              border-left: 4px solid #16a34a; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.linea-card.aviso { border-left-color: #eab308; }
.linea-card.alerta { border-left-color: #dc2626; }
.lineas-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
               gap: 1.5rem; }

Generar Datos de Prueba Realistas

Para probar el dashboard necesitas datos que se parezcan a los de una planta real. Este script genera registros simulados con variaciones realistas por turno:

seed_data.py:

from app import app, db
from models import LineaProduccion, RegistroProduccion
from datetime import datetime, timedelta
import random

def seed():
    with app.app_context():
        db.create_all()

        # Crear líneas de producción
        lineas = [
            LineaProduccion(nombre='Línea A - Envasado',
                            ubicacion='Nave 1'),
            LineaProduccion(nombre='Línea B - Montaje',
                            ubicacion='Nave 2'),
            LineaProduccion(nombre='Línea C - Embalaje',
                            ubicacion='Nave 1'),
        ]
        db.session.add_all(lineas)
        db.session.commit()

        # Generar 7 días de datos, un registro cada hora
        turnos = {
            range(6, 14): 'mañana',
            range(14, 22): 'tarde',
        }
        # turno noche: 22-6
        ahora = datetime.utcnow()

        for linea in lineas:
            for dia in range(7):
                for hora in range(24):
                    ts = ahora - timedelta(days=dia, hours=hora)
                    h = ts.hour

                    # Determinar turno
                    if 6 <= h < 14:
                        turno = 'mañana'
                        factor_rendimiento = 0.90
                    elif 14 <= h < 22:
                        turno = 'tarde'
                        factor_rendimiento = 0.85
                    else:
                        turno = 'noche'
                        factor_rendimiento = 0.75

                    velocidad = random.uniform(80, 120)
                    producidas = int(velocidad * factor_rendimiento
                                     * random.uniform(0.8, 1.1))
                    defectos = int(producidas * random.uniform(0.01, 0.06))
                    parada = random.uniform(0, 45) if random.random() < 0.3 else 0
                    temp = random.uniform(18.0, 32.0)

                    registro = RegistroProduccion(
                        linea_id=linea.id,
                        timestamp=ts,
                        unidades_producidas=producidas,
                        unidades_defectuosas=defectos,
                        tiempo_parada_min=round(parada, 1),
                        velocidad_linea=round(velocidad, 1),
                        temperatura_proceso=round(temp, 1),
                        turno=turno
                    )
                    db.session.add(registro)

        db.session.commit()
        print(f'Seed completado: {len(lineas)} líneas, '
              f'{7 * 24 * len(lineas)} registros')

if __name__ == '__main__':
    seed()

Ejecuta python seed_data.py y tendrás una semana completa de datos realistas para tres líneas de producción.

Conectar Datos Reales desde un PLC

Hasta ahora hemos trabajado con datos simulados. En un entorno real, necesitas un script que lea datos del PLC y los inserte en PostgreSQL. Aquí tienes dos opciones según tu protocolo.

Opción 1: Modbus TCP (para PLCs Schneider, ABB, etc.)

from pymodbus.client import ModbusTcpClient
from models import db, RegistroProduccion
from app import app
from datetime import datetime

def leer_plc_modbus(host='192.168.1.10', port=502, linea_id=1):
    client = ModbusTcpClient(host, port=port)
    client.connect()

    # Leer registros holding (adaptar direcciones a tu PLC)
    resultado = client.read_holding_registers(address=0, count=5, slave=1)
    if resultado.isError():
        print(f'Error leyendo PLC: {resultado}')
        client.close()
        return

    unidades = resultado.registers[0]
    defectos = resultado.registers[1]
    velocidad = resultado.registers[2] / 10.0  # escalado
    temperatura = resultado.registers[3] / 10.0
    parada_min = resultado.registers[4] / 10.0

    client.close()

    with app.app_context():
        registro = RegistroProduccion(
            linea_id=linea_id,
            unidades_producidas=unidades,
            unidades_defectuosas=defectos,
            velocidad_linea=velocidad,
            temperatura_proceso=temperatura,
            tiempo_parada_min=parada_min,
            turno=determinar_turno(datetime.now().hour)
        )
        db.session.add(registro)
        db.session.commit()

def determinar_turno(hora):
    if 6 <= hora < 14:
        return 'mañana'
    elif 14 <= hora < 22:
        return 'tarde'
    return 'noche'

Opción 2: OPC-UA (para PLCs Siemens, Beckhoff, etc.)

from opcua import Client as OPCClient

def leer_plc_opcua(endpoint='opc.tcp://192.168.1.20:4840'):
    client = OPCClient(endpoint)
    client.connect()

    try:
        # Nodos OPC-UA (adaptar a tu servidor)
        unidades = client.get_node('ns=2;s=Linea1.Produccion').get_value()
        defectos = client.get_node('ns=2;s=Linea1.Defectos').get_value()
        velocidad = client.get_node('ns=2;s=Linea1.Velocidad').get_value()
        temperatura = client.get_node('ns=2;s=Linea1.Temperatura').get_value()
        # ... insertar en BD igual que el ejemplo Modbus
    finally:
        client.disconnect()

Para que la lectura sea periódica, configura un cron job que ejecute el script cada minuto (o cada 5 minutos, según la granularidad que necesites):

# Cada 5 minutos, leer PLC e insertar en BD
*/5 * * * * cd /opt/dashboard-industrial && .venv/bin/python leer_plc.py

Si quieres más detalle sobre cómo conectar un PLC con una base de datos SQL, en nuestro artículo sobre conexión PLC-base de datos cubrimos el tema en profundidad.

Añadir Actualización en Tiempo Real con WebSockets

La actualización cada 30 segundos funciona, pero si quieres que el dashboard refleje cambios al instante — por ejemplo, una alarma de parada — necesitas WebSockets.

pip install flask-socketio eventlet

Añade esto a tu app.py:

from flask_socketio import SocketIO, emit

socketio = SocketIO(app, cors_allowed_origins='*')

@socketio.on('connect')
def handle_connect():
    print('Cliente conectado al dashboard')

def notificar_nuevo_registro(registro):
    """Llamar después de insertar un registro nuevo."""
    socketio.emit('nuevo_registro', {
        'linea_id': registro.linea_id,
        'unidades': registro.unidades_producidas,
        'oee': registro.oee,
        'timestamp': registro.timestamp.isoformat()
    })

# Cambiar app.run() por:
if __name__ == '__main__':
    with app.app_context():
        db.create_all()
    socketio.run(app, host='0.0.0.0', port=5000)

Y en el frontend, añade el cliente Socket.IO:

<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
<script>
    const socket = io();
    socket.on('nuevo_registro', function(data) {
        console.log('Nuevo dato:', data);
        cargarKPIs();  // Refrescar KPIs
        cargarGraficoLinea(data.linea_id);  // Refrescar gráfico
    });
</script>

Ahora cada vez que el script de lectura del PLC inserte un registro y llame a notificar_nuevo_registro(), todos los navegadores conectados se actualizan al instante.

Seguridad: Proteger el Dashboard en Planta

Un dashboard con datos de producción no puede estar abierto a cualquiera. Estas son las medidas mínimas que deberías implementar:

Autenticación básica con Flask-Login:

from flask_login import LoginManager, login_required

login_manager = LoginManager(app)

@app.route('/')
@login_required
def dashboard():
    # ... tu código existente

HTTPS obligatorio. Si despliegas en la red local de planta, usa un certificado autofirmado o, mejor, configura un reverse proxy con Nginx y Let’s Encrypt si hay acceso desde fuera.

Separación de redes. El servidor del dashboard debería estar en la red IT, no directamente en la red OT. El script de lectura del PLC actúa como puente entre ambas redes — es el único punto de contacto.

Registro de accesos. Guarda quién accede y cuándo. En entornos regulados (farmacéutica, alimentación), esto no es opcional — es un requisito de auditoría.

Para profundizar en ciberseguridad en entornos industriales, puedes leer nuestra guía de ciberseguridad industrial OT.

Desplegar en Producción

Para pasar de desarrollo a producción, necesitas tres cosas:

1. Servidor WSGI (Gunicorn):

pip install gunicorn
gunicorn -w 4 -b 0.0.0.0:5000 app:app

Con 4 workers puedes servir varios usuarios simultáneamente. Si usas WebSockets, necesitas el worker de eventlet:

gunicorn -k eventlet -w 1 -b 0.0.0.0:5000 app:app

2. Reverse proxy (Nginx):

server {
    listen 80;
    server_name dashboard.planta.local;

    location / {
        proxy_pass http://127.0.0.1:5000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location /socket.io/ {
        proxy_pass http://127.0.0.1:5000/socket.io/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

3. Servicio systemd:

[Unit]
Description=Dashboard Industrial Flask
After=postgresql.service

[Service]
User=dashboard
WorkingDirectory=/opt/dashboard-industrial
Environment=DATABASE_URL=postgresql://dashboard:secreto@localhost/produccion
ExecStart=/opt/dashboard-industrial/.venv/bin/gunicorn -k eventlet -w 1 -b 127.0.0.1:5000 app:app
Restart=always

[Install]
WantedBy=multi-user.target

Con esta configuración, el dashboard se inicia automáticamente con el servidor y se reinicia si se cae.

Flask vs Grafana vs Power BI: Cuándo Usar Cada Uno

Una pregunta legítima: ¿por qué molestarse con Flask cuando existen Grafana o Power BI?

CriterioFlask (Python)GrafanaPower BI
CosteGratisGratis (OSS)Licencia mensual
PersonalizaciónTotalMedia (plugins)Baja
Integración con ML/IANativa (Python)LimitadaLimitada
Curva de aprendizajeMedia-altaBajaBaja
Ideal paraDashboards a medida con lógica complejaMonitorización estándarReportes ejecutivos

Usa Flask cuando necesitas lógica de negocio compleja, algoritmos propios o integración directa con modelos de machine learning. Usa Grafana cuando quieres monitorización estándar sin escribir mucho código — si te interesa esa opción, cubrimos la integración n8n + Grafana para dashboards de producción en otro artículo. Usa Power BI cuando los destinatarios son directivos que quieren arrastrar y soltar.

En la práctica, muchas plantas usan una combinación: Flask para el dashboard operativo en planta (con lógica customizada) y Grafana o Power BI para los informes de dirección.

Próximos Pasos y Mejoras

Una vez que tengas el dashboard funcionando, estas son las mejoras que más impacto generan:

  • Alertas automáticas. Envía un email o un mensaje de Telegram cuando el OEE baje del 70% o cuando una línea lleve más de 30 minutos parada.
  • Histórico y tendencias. Añade vistas semanales y mensuales para detectar patrones de degradación.
  • Exportar a PDF. Los jefes de turno agradecen poder imprimir un resumen del día.
  • Modelo predictivo. Con los datos históricos en PostgreSQL, puedes entrenar un modelo de mantenimiento predictivo directamente en Python — sin exportar datos a otra herramienta.
  • Dockerizar. Empaqueta Flask + PostgreSQL en un docker-compose.yml para desplegar en cualquier servidor con un solo comando.

Conclusión

Construir un dashboard industrial con Python y Flask para visualizar datos de producción no requiere un equipo de desarrolladores ni software de miles de euros. Con Python, Flask, PostgreSQL y un par de librerías JavaScript, tienes un sistema completo que lee datos de tus PLCs, los almacena y los presenta en un navegador — accesible desde cualquier dispositivo de la planta.

La clave está en empezar simple: tres líneas, cuatro métricas, un gráfico por línea. Cuando funcione, iteras. Añades alertas, WebSockets, autenticación, exportación. Pero la base ya está ahí, y es tuya al 100%.

¿Necesitas ayuda para montar un dashboard de producción adaptado a tu planta? Contacta con nosotros y lo diseñamos juntos.


¿Te ha resultado útil? Comparte este artículo con tu equipo de ingeniería. Y si quieres recibir tutoriales como este cada semana, suscríbete a nuestra newsletter.

#n8n #python #plc #scada #opc-ua #ia #ciberseguridad #docker

Sigue leyendo