esp/tools/rule_evaluator.php

393 lines
12 KiB
PHP

<?php
/**
* Evaluador de Reglas en Tiempo Real
*
* Este script escucha eventos SSE de cambios en pines y evalúa las reglas
* de automatización configuradas para cada dispositivo.
*
* Ejecución: php rule_evaluator.php
*
* Mantener corriendo en background:
* - Linux: nohup php rule_evaluator.php > /dev/null 2>&1 &
* - Windows: start /B php rule_evaluator.php
*/
// Configuración
set_time_limit(0);
ini_set('display_errors', 1);
error_reporting(E_ALL);
// Cargar dependencias
require_once __DIR__ . '/../vendor/autoload.php';
// Cargar variables de entorno
if (class_exists('Dotenv\\Dotenv')) {
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/..');
$dotenv->safeLoad();
}
use PhpMqtt\Client\MqttClient;
use PhpMqtt\Client\ConnectionSettings;
// Archivos de datos
$reglas_file = __DIR__ . '/../data/reglas.json';
$puertos_file = __DIR__ . '/../data/puertos.json';
$log_file = __DIR__ . '/../data/rule_executions.log';
// Estado actual de todos los pines (chipid => [pin => valor])
$pinStates = [];
// Última ejecución de cada regla (para evitar spam)
$lastExecution = [];
// Cliente MQTT
$mqtt = null;
/**
* Cargar reglas desde archivo
*/
function loadRules() {
global $reglas_file;
if (!file_exists($reglas_file)) return [];
$content = @file_get_contents($reglas_file);
if ($content === false) return [];
$data = json_decode($content, true);
return is_array($data) ? $data : [];
}
/**
* Cargar puertos desde archivo
*/
function loadPorts() {
global $puertos_file;
if (!file_exists($puertos_file)) return [];
$content = @file_get_contents($puertos_file);
if ($content === false) return [];
$data = json_decode($content, true);
return is_array($data) ? $data : [];
}
/**
* Logging de ejecuciones
*/
function logExecution($chipid, $ruleId, $ruleName, $message) {
global $log_file;
$timestamp = date('Y-m-d H:i:s');
$line = "[$timestamp] [$chipid] [$ruleId] $ruleName: $message\n";
@file_put_contents($log_file, $line, FILE_APPEND);
echo $line; // También a stdout
}
/**
* Evaluar condiciones de una regla
*/
function evaluateConditions($conditions, $chipid) {
global $pinStates;
if (empty($conditions)) return true; // Sin condiciones = siempre true
foreach ($conditions as $cond) {
$pin = $cond['pin'] ?? '';
$operator = $cond['operator'] ?? '==';
$expectedValue = strtoupper($cond['value'] ?? 'ON');
$currentValue = strtoupper($pinStates[$chipid][$pin] ?? 'OFF');
$result = false;
if ($operator === '==') {
$result = ($currentValue === $expectedValue);
} elseif ($operator === '!=') {
$result = ($currentValue !== $expectedValue);
}
// Si alguna condición falla, retornar false (lógica AND)
if (!$result) {
return false;
}
}
return true; // Todas las condiciones pasaron
}
/**
* Ejecutar acciones de una regla
*/
function executeActions($actions, $chipid) {
global $mqtt;
foreach ($actions as $action) {
$type = $action['type'] ?? 'set';
if ($type === 'set') {
$pin = $action['pin'] ?? '';
$value = strtoupper($action['value'] ?? 'ON');
if (empty($pin)) continue;
// Publicar comando MQTT
$topic = "dispositivo/$chipid/comando";
$payload = json_encode(['alias' => $pin, 'valor' => $value]);
try {
if ($mqtt && $mqtt->isConnected()) {
$mqtt->publish($topic, $payload, 0, true);
logExecution($chipid, '', '', "Acción ejecutada: $pin$value");
}
} catch (Exception $e) {
logExecution($chipid, '', '', "Error al publicar: " . $e->getMessage());
}
} elseif ($type === 'delay') {
$seconds = intval($action['seconds'] ?? 1);
logExecution($chipid, '', '', "Esperando $seconds segundos...");
sleep($seconds);
}
}
}
/**
* Evaluar reglas de un dispositivo
*/
function evaluateRules($chipid, $changedPin = null) {
global $lastExecution;
$allRules = loadRules();
$deviceRules = $allRules[$chipid] ?? [];
if (empty($deviceRules)) return;
foreach ($deviceRules as $rule) {
// Solo evaluar reglas activas
if (!($rule['enabled'] ?? true)) continue;
$ruleId = $rule['id'] ?? '';
$ruleName = $rule['name'] ?? 'Sin nombre';
$trigger = $rule['trigger'] ?? [];
$conditions = $rule['conditions'] ?? [];
$actions = $rule['actions'] ?? [];
// Verificar si el trigger coincide
$triggerType = $trigger['type'] ?? 'input_change';
if ($triggerType === 'input_change') {
$triggerPin = $trigger['pin'] ?? '';
$triggerValue = strtoupper($trigger['value'] ?? 'ON');
// Solo evaluar si el pin que cambió es el trigger
if ($changedPin !== $triggerPin) continue;
// Verificar valor del trigger
global $pinStates;
$currentValue = strtoupper($pinStates[$chipid][$triggerPin] ?? 'OFF');
if ($currentValue !== $triggerValue) continue;
// Evitar ejecutar la misma regla múltiples veces en poco tiempo
$lastKey = "$chipid:$ruleId";
$now = time();
if (isset($lastExecution[$lastKey]) && ($now - $lastExecution[$lastKey]) < 2) {
continue; // Cooldown de 2 segundos
}
// Evaluar condiciones adicionales
if (!evaluateConditions($conditions, $chipid)) {
logExecution($chipid, $ruleId, $ruleName, "Condiciones no cumplidas");
continue;
}
// Todas las condiciones se cumplen, ejecutar acciones
logExecution($chipid, $ruleId, $ruleName, "✅ Regla activada");
executeActions($actions, $chipid);
$lastExecution[$lastKey] = $now;
}
// TODO: Implementar triggers de tipo 'timer' y 'manual'
}
}
/**
* Conectar a MQTT
*/
function connectMQTT() {
$host = getenv('MQTT_HOST') ?: 'mqtt.penki.com.ar';
$port = intval(getenv('MQTT_PORT') ?: 8883);
$username = getenv('MQTT_USERNAME') ?: 'penki';
$password = getenv('MQTT_PASSWORD') ?: 'Penki615';
$useTLS = filter_var(getenv('MQTT_TLS') ?: 'true', FILTER_VALIDATE_BOOLEAN);
$clientId = 'rule_evaluator_' . uniqid();
try {
$connectionSettings = (new ConnectionSettings())
->setUsername($username)
->setPassword($password)
->setUseTls($useTLS)
->setTlsSelfSignedAllowed(true)
->setKeepAliveInterval(60)
->setConnectTimeout(10);
$mqtt = new MqttClient($host, $port, $clientId);
$mqtt->connect($connectionSettings, true);
echo "✅ Conectado a MQTT broker: $host:$port\n";
return $mqtt;
} catch (Exception $e) {
echo "❌ Error conectando a MQTT: " . $e->getMessage() . "\n";
return null;
}
}
/**
* Escuchar SSE y evaluar reglas
*/
function listenSSE() {
global $pinStates, $mqtt;
// Usar localhost si está en el mismo servidor, o la URL completa
$sseUrl = getenv('SSE_URL') ?: 'http://localhost/api/index.php?r=sse_states';
// Usar cURL para mantener conexión SSE abierta
$ch = curl_init($sseUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 0);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
// Buffer para acumular datos
$buffer = '';
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($curl, $data) use (&$buffer) {
global $pinStates, $mqtt;
$buffer .= $data;
$lines = explode("\n", $buffer);
// Procesar líneas completas
for ($i = 0; $i < count($lines) - 1; $i++) {
$line = trim($lines[$i]);
// Parsear eventos SSE
if (strpos($line, 'event:') === 0) {
$eventType = trim(substr($line, 6));
} elseif (strpos($line, 'data:') === 0) {
$jsonData = trim(substr($line, 5));
try {
$data = json_decode($jsonData, true);
if (isset($data['chipid']) && isset($data['alias']) && isset($data['valor'])) {
$chipid = $data['chipid'];
$pin = $data['alias'];
$value = strtoupper($data['valor']);
// Actualizar estado
if (!isset($pinStates[$chipid])) {
$pinStates[$chipid] = [];
}
$oldValue = $pinStates[$chipid][$pin] ?? null;
$pinStates[$chipid][$pin] = $value;
// Solo evaluar si el valor cambió
if ($oldValue !== $value) {
echo "[" . date('H:i:s') . "] 📍 $chipid/$pin: $oldValue$value\n";
// Evaluar reglas para este dispositivo
evaluateRules($chipid, $pin);
}
}
} catch (Exception $e) {
echo "Error parseando SSE: " . $e->getMessage() . "\n";
}
}
}
// Mantener última línea incompleta en buffer
$buffer = $lines[count($lines) - 1];
return strlen($data);
});
echo "🎧 Escuchando eventos SSE desde: $sseUrl\n";
echo "⚙️ Evaluador de reglas iniciado...\n\n";
curl_exec($ch);
if (curl_errno($ch)) {
echo "❌ Error en SSE: " . curl_error($ch) . "\n";
}
curl_close($ch);
}
/**
* Main loop
*/
function main() {
global $mqtt;
echo "🚀 Iniciando Evaluador de Reglas ESP Admin\n";
echo "==========================================\n\n";
// Conectar a MQTT
$mqtt = connectMQTT();
if (!$mqtt) {
echo "❌ No se pudo conectar a MQTT. Abortando.\n";
exit(1);
}
// Cargar reglas iniciales
$allRules = loadRules();
$totalRules = 0;
foreach ($allRules as $chipid => $rules) {
$totalRules += count($rules);
}
echo "📋 Reglas cargadas: $totalRules reglas en " . count($allRules) . " dispositivos\n\n";
// Iniciar escucha de SSE
while (true) {
try {
listenSSE();
} catch (Exception $e) {
echo "❌ Error en loop principal: " . $e->getMessage() . "\n";
echo "⏳ Reintentando en 5 segundos...\n\n";
sleep(5);
// Reconectar MQTT si es necesario
if (!$mqtt || !$mqtt->isConnected()) {
$mqtt = connectMQTT();
}
}
}
}
// Manejo de señales para shutdown graceful
if (function_exists('pcntl_signal')) {
pcntl_signal(SIGTERM, function() {
global $mqtt;
echo "\n🛑 Señal de terminación recibida. Cerrando...\n";
if ($mqtt && $mqtt->isConnected()) {
$mqtt->disconnect();
}
exit(0);
});
pcntl_signal(SIGINT, function() {
global $mqtt;
echo "\n🛑 Interrupción recibida (Ctrl+C). Cerrando...\n";
if ($mqtt && $mqtt->isConnected()) {
$mqtt->disconnect();
}
exit(0);
});
}
// Ejecutar
main();