El objetivo de este artículo es proveer de una alternativa para poder enviar alarmas de WinCC mediante Whatsapp, de forma que cuando en nuestro sistema de control se dispare dicha alarma, se envíe el texto de la alarma a un grupo de Whatsapp.

La utilidad de esta solución, es poder alertar a los usuarios del sistema (mantenimiento, producción, etc.) a tiempo y mediante un canal muy utilizado. En este ejemplo lo mandaremos a un grupo, aunque es igual de aplicable a personas físicas.

Existen diversas formas de hacerlo, en mi caso he optado por NO usar la API de Whatsapp business, ya que presenta mayor complejidad y costes añadidos. Se ha utilizado el manejo del navegador y la librería pywhatkit.

Índice


Arquitectura


Elementos:

  • WinCC V8 Runtime corriendo en una maquina virtual (VM 1) con IP 192.168.18.88
  • Servidor Flask, en otra máquina (VM 2), para escucha de mensajes y envío por Whatsap, con IP 192.168.18.77 y puerto 5000
  • Teléfono emisor: necesario asociar un teléfono a la cuenta del servidor Flask, ya que será quien envíe los mensajes. Solo necesario escanear QR primera vez.
  • Grupo de Whatsapp: será al grupo al que enviaremos el mensaje de alarma de Wincc.

Notas: como se observa en la imagen, VM1 no tiene conexión a internet y queda aislado, simulando una red OT protegida. Comunica con VM2 por red local, emulando la DMZ, y es esta quien sí puede comunicarse con el exterior.


Configuración Servidor Flask

Un servidor Flask es una aplicación web ligera escrita en Python que permite crear y servir páginas web o APIs de forma sencilla y rápida.

En este ejemplo se usa para estar a la escucha de solicitudes POST desde el SCADA WinCC. Una vez que recibe dicha solicitud, con el mensaje de la alarma en un JSON, ejecuta mediante Pywhatkit un manejo del navegador que abre la aplicación de Whatsapp (previamente autenticada, con un teléfono de emisor).

Pywhatkit se encarga de abrir sesión, localizar el grupo de Whatsapp (con su ID; ver en enlace de invitación), y escribir el mensaje que ha recibido del sistema SCADA.

from flask import Flask, request, jsonify
import pywhatkit  # type: ignore
import time
from datetime import datetime
from webdriver_manager.chrome import ChromeDriverManager  # type: ignore

app = Flask(__name__)

@app.route('/wincc', methods=['POST'])
def enviar_mensaje_grupo():
    print("🔹 Solicitud recibida desde WinCC.")
    start_time = datetime.now()  # Capturar el tiempo de inicio

    # 1) Leer el JSON con el texto a enviar
    data = request.get_json(silent=True)
    if not data or "mensaje" not in data:
        return jsonify({
            "status": "error",
            "message": "No se encontró la clave 'mensaje' en el JSON."
        }), 400

    mensaje = data["mensaje"]
    
    # ID del grupo (desde el enlace de invitación)
    group_id = "XXXXXXXXXXXXX"

    try:
        # 2) Usar PyWhatKit para abrir WhatsApp Web y escribir el mensaje
        print("🔹 PyWhatKit abriendo WhatsApp Web y escribiendo mensaje...")
        pywhatkit.sendwhatmsg_to_group_instantly(group_id, mensaje, wait_time=7, tab_close=False)

        # 3) Esperar a que el mensaje esté escrito en el cuadro
        time.sleep(3)

        end_time = datetime.now()  # Capturar el tiempo de finalización
        elapsed_time = (end_time - start_time).total_seconds()  # Calcular tiempo transcurrido

        print(f"✅ Mensaje enviado al grupo con ID {group_id}: {mensaje}")
        print(f"⏳ Tiempo transcurrido: {elapsed_time:.2f} segundos")

        return jsonify({
            "status": "success",
            "message": f"Mensaje enviado correctamente al grupo (ID {group_id}).",
            "elapsed_time": f"{elapsed_time:.2f} segundos"
        }), 200

    except Exception as e:
        print(f"❌ Error: {e}")
        return jsonify({"status": "error", "message": str(e)}), 500

if __name__ == '__main__':
    app.run(host="192.168.18.77", port=5000)

Los pasos para dejar este servicio escuchando, son como en cualquier otra aplicación Flask. Primero un entorno VENV con las librerías necesarias, activarlo y ejecutarlo con “python app.py”

Este servicio, debe estar siempre disponible y activo para recibir y procesar las solicitudes de WinCC. En artículos posteriores veremos como podemos embeberlo en un Docker.


Configuración WinCC

Una vez tenemos el servidor preparado a la escucha, y con Whatsapp autenticado, pasamos a preparar nuestro SCADA para que sea capaz de extraer el texto de la alarma y luego enviarla cuando esta sea disparada.

Extracción texto alarma con GMsgFunction y MSRTGetMsgText

Aunque parece trivial, no es sencillo el poder extraer el texto de una alarma cuando esta se dispara, ya que hay que usar varias funciones para obtenerlo y realizar configuraciones, veámoslo:

Paso 1: Configurar la acción de la alarma. Hay que marcar la opción de “Activar una acción“, “Loop in alarm” y asignar la función “GMsgFunction“, que es la que se ejecuta cuando la alarma se dispara.

Paso 2: Modificar GMsgFunction para que al dispararse la alarma, recoja sus parámetros incluído el texto de la alarma. La función esta en C, y viene ya preparada para traer algunos datos de la alarma como Timestamp, ID, y otros, pero NO EL TEXTO.

Para obtener el texto de la alarma, hay que usar otra función que pasándole como parámetro el objeto alarma, y apuntando al campo correcto, se trae el texto del mensaje. Os dejo aquí el código:

BOOL GMsgFunction( char* pszMsgData)
{
BOOL bOK;
WORD wTextBlock=0;
DWORD dwTextNr =34 ;
MSG_TEXT_STRUCT scMsgText;
CMN_ERROR scError;
MSG_CSDATA_STRUCT sM; // holds alarm info
CMN_ERROR pError;


  MSG_RTDATA_STRUCT mRT;
  memset( &mRT, 0, sizeof( MSG_RTDATA_STRUCT ) );


  if( pszMsgData != NULL )
  {
     printf( "Meldung : %s \r\n", pszMsgData );

    // Meldungsdaten einlesen
     sscanf( pszMsgData,  "%ld,%ld,%04d.%02d.%02d,%02d:%02d:%02d:%03d,%ld, %ld, %ld, %d,%d",
	&mRT.dwMsgNr, 			// Meldungsnummer
	&mRT.dwMsgState,  			// Status MSG_STATE_COME, .._GO, .._QUIT, .._QUIT_SYSTEM
	&mRT.stMsgTime.wYear, 		// Jahr
	&mRT.stMsgTime.wMonth, 		// Monat
	&mRT.stMsgTime.wDay,		// Tag
	&mRT.stMsgTime.wHour, 		// Stunde
	&mRT.stMsgTime.wMinute,		// Minute
	&mRT.stMsgTime.wSecond, 		// Sekunde
	&mRT.stMsgTime.wMilliseconds,	// Millisekunde
	&mRT.dwTimeDiff,			// Zeitdauer der anstehenden Meldung
	&mRT.dwCounter,			// Interner Meldungszähler
	&mRT.dwFlags,			// Flags( intern )
	&mRT.wPValueUsed,
	&mRT.wTextValueUsed );
 
      // Prozesswerte lesen, falls gewünscht
    } 

  printf("Nr : %d, St: %x, %d-%d-%d %d:%d:%d.%d, Dur: %d, Cnt %d, Fl %d\r\n" , 
  mRT.dwMsgNr, mRT.dwMsgState, mRT.stMsgTime.wDay, mRT.stMsgTime.wMonth, mRT.stMsgTime.wYear, 
  mRT.stMsgTime.wHour, mRT.stMsgTime.wMinute, mRT.stMsgTime.wSecond, mRT.stMsgTime.wMilliseconds, mRT.dwTimeDiff,
  mRT.dwCounter, mRT.dwFlags ) ;


//----------------------------BY PHS----------------------------

// AÑADIDO PARA EXTRAER TEXTO
MSRTGetMsgCSData(mRT.dwMsgNr, &sM, &pError); 
printf ("MSRTGetMsgCSData() - szText=\"%i\"\r\n", sM.dwTextID[0]);


bOK = MSRTGetMsgText(wTextBlock, sM.dwTextID[0], &scMsgText, &scError );
printf ("MSRTGetMsgText() - szText=\"%s\"\r\n", scMsgText.szText);

//ASIGNACION DE TEXTO DE ALARMA A VARIABLE PARA PASARLA y USAR EN VBS
SetTagChar("AlarmText",scMsgText.szText);	//Return-Type: BOOL 

   return( TRUE );
}

Con esto ya tenemos el texto de la alarma, cuando se dispara, guardado en “AlarmText”.

Envío de texto alarma a servidor Flask

Ahora ya con la alarma guardada, solo queda realizar la petición al servidor Flask para que la envíe por Whatsapp. Para ello lo he hecho con VBS. como se puede ver, recogeremos el texto de la variable “AlarmText” y lo enviamos al servidor IP=192.168.18.77:5000/wincc

Sub EnviarTextoAServidor()
	Dim http
	Dim url
	Dim data
	Dim mensaje
	
	Dim mensajeAlarma
	
	mensajeAlarma = HMIRuntime.Tags("AlarmText").Read(1)
	HMIRuntime.Trace mensajeAlarma
	' Definir el mensaje a enviar (puedes reemplazarlo por una variable dinámica)
	mensaje = mensajeAlarma
	
	' Crear el objeto XMLHTTP para enviar la solicitud
	Set http = CreateObject("MSXML2.XMLHTTP")
	
	' Dirección del servidor Flask
	url = "http://192.168.18.77:5000/wincc"
	
	' Abrir la conexión de tipo POST
	http.Open "POST", url, False
	
	' Configurar la cabecera para enviar datos como JSON
	http.setRequestHeader "Content-Type", "application/json"
	
	' Crear el JSON con el mensaje
	data = "{""mensaje"":""" & mensaje & """}"
	
	' Enviar los datos
	http.send data
	
	' Leer la respuesta del servidor
	Dim response
	response = http.responseText
	
	MsgBox "Respuesta del servidor: " & response
	
	' Liberar objeto
	Set http = Nothing


End Sub

Y ahora te preguntarás: ¿Y cuando se ejecuta esta función? Existen muchas formas de hacerlo, al final lo que necesitamos es que cuando la alarma se dispare, se ejecute. En mi caso y a modo de ejemplo, he asociado el texto de alarma a un Campo de texto, y cuando este varía, ejecuta el script.


Demostración

PLC: Hacemos saltar la alarma

WinCC: La alarma se dispara en SCADA y se guarda el texto de la misma en el textbox.

Servidor Flask + Navegador: recibe alarma y envía por whatsapp mediante el manejo del navegador.

MessageBox en SCADA confirmando el envío:


Conclusiones

Es posible el envío de alarmas con Whatsapp sin usar APIs y con coste mínimo. Esto puede ser de mucha utilidad para ciertos usuarios de los sistemas industriales, en los que ciertas alarmas críticas, necesitan ser monitorizadas en tiempo real.

El método usado para el envío de Whatsapp requiere de un número de teléfono para asociar la cuenta de Whatsapp que realiza los envíos, típicamente podría ser una cuenta básica de mantenimiento.

Se ha aislado completamente la funcionalidad, del SCADA. Quedando el sistema de control perfectamente aislado en la red OT sin necesidad de tener internet y exponerse.

La obtención del texto de alarma en WinCC no es trivial, y se ha proporcionado una forma de hacerlo genérica y escalable.