↩️ Назад

Категории

От счётчика ошибок к умному анализу с Qwen 2.5 + PHP + HTML

16.07.2026 | из категории: Нейросети

Интеграция AI-агента для аналитики в панель умного дома

Raspberry Pi собирает логи, мощный сервер анализирует, Pi получает готовые рекомендации

В этой архитектуре Raspberry Pi — это лёгкий клиент, который только собирает логи и показывает результат. А вся «тяжёлая» работа (хранение, анализ, нейросеть) вынесена на отдельный мощный сервер.

🧱 Архитектура: Pi → Сервер → Pi

🍓 RASPBERRY PI (умный дом)
↓ собирает логи Zigbee ↓
📄 /var/log/zigbee.log
⬇️ HTTP POST /api/logs
─────── СЕТЬ ───────
⬇️
🖥️ МОЩНЫЙ СЕРВЕР (AI-ядро)
↓ принимает логи ↓
🗄️ PostgreSQL (все ошибки + индексы)
↓ раз в 15 минут ↓
🧠 Qwen 2.5 14B (llama.cpp + CUDA)
↓ анализ → JSON с рекомендациями ↓
📊 сохраняет результат в БД
⬇️ HTTP GET /api/recommendations
─────── СЕТЬ ───────
⬇️
🍓 RASPBERRY PI получает готовые рекомендации
📊 показывает на HTML-панели
🎯 Ключевая идея: Raspberry Pi не напрягается — он только отправляет данные и показывает результат. Вся аналитика на сервере с мощной видеокартой.

1. Raspberry Pi — сбор и отправка логов

На Raspberry Pi работает Zigbee2MQTT или другой контроллер, который пишет логи. Наша задача — читать новые ошибки и отправлять их на сервер.

1.1 Скрипт-отправщик на Python (Raspberry Pi)

#!/usr/bin/env python3
# pi_sender.py — читает логи с Pi и отправляет на сервер

import re
import requests
import time
from datetime import datetime

# Конфиг
SERVER_URL = "http://192.168.1.100:5000/api/logs"  # IP мощного сервера
LOG_FILE = "/var/log/zigbee.log"
LAST_POSITION_FILE = "/tmp/zigbee_last_pos.txt"

def get_last_position():
    try:
        with open(LAST_POSITION_FILE, 'r') as f:
            return int(f.read().strip())
    except:
        return 0

def save_last_position(pos):
    with open(LAST_POSITION_FILE, 'w') as f:
        f.write(str(pos))

def parse_line(line):
    """Парсит строку лога, вытаскивает ошибки"""
    # [2026-07-17 12:34:56] error: device offline
    match = re.search(r'\[(.*?)\] (\w+): (.*)', line)
    if match:
        level = match.group(2).lower()
        if level in ('error', 'warn', 'critical', 'fatal'):
            return {
                'timestamp': match.group(1),
                'level': level.upper(),
                'message': match.group(3),
                'device': 'unknown'
            }
    return None

def send_to_server(errors):
    """Отправляет ошибки на сервер"""
    if not errors:
        return

    payload = {
        'source': 'raspberry_pi',
        'errors': errors
    }

    try:
        response = requests.post(
            SERVER_URL,
            json=payload,
            timeout=10
        )
        if response.status_code == 200:
            print(f"✅ Отправлено {len(errors)} ошибок")
        else:
            print(f"❌ Ошибка отправки: {response.status_code}")
    except Exception as e:
        print(f"❌ Ошибка соединения: {e}")

def tail_log():
    """Читает новые строки из лога"""
    last_pos = get_last_position()

    with open(LOG_FILE, 'r') as f:
        f.seek(last_pos)
        errors = []

        while True:
            line = f.readline()
            if not line:
                break

            parsed = parse_line(line.strip())
            if parsed:
                errors.append(parsed)

            # Отправляем пачками по 10 ошибок
            if len(errors) >= 10:
                send_to_server(errors)
                errors = []

        # Отправляем остатки
        if errors:
            send_to_server(errors)

        # Сохраняем позицию
        save_last_position(f.tell())

if __name__ == "__main__":
    # Запускаем раз в минуту через cron или в цикле
    tail_log()
    

1.2 Настройка cron на Pi

# Запуск каждую минуту
* * * * * /usr/bin/python3 /home/pi/pi_sender.py >> /home/pi/sender.log 2>&1
    

2. Мощный сервер — приём, хранение, анализ

На сервере стоит PostgreSQL для хранения всех ошибок, Qwen 2.5 14B (или 7B) для анализа, и Flask API для общения с Raspberry Pi.

2.1 База данных (PostgreSQL)

-- Создаём БД
CREATE DATABASE smart_home;

-- Таблица ошибок
CREATE TABLE errors (
    id BIGSERIAL PRIMARY KEY,
    source VARCHAR(50) NOT NULL,      -- 'raspberry_pi'
    device VARCHAR(100),
    level VARCHAR(20) NOT NULL,
    message TEXT NOT NULL,
    context JSONB,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    processed BOOLEAN DEFAULT FALSE
);

-- Индексы для скорости
CREATE INDEX idx_errors_level ON errors(level);
CREATE INDEX idx_errors_created ON errors(created_at DESC);
CREATE INDEX idx_errors_processed ON errors(processed);
CREATE INDEX idx_errors_source ON errors(source);

-- Таблица результатов анализа
CREATE TABLE analyses (
    id BIGSERIAL PRIMARY KEY,
    analyzed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    errors_count INTEGER,
    severity VARCHAR(20),
    summary TEXT,
    recommendations JSONB,
    full_response TEXT,
    executed BOOLEAN DEFAULT FALSE
);
    

2.2 Flask API — приём логов

# server_api.py — принимает логи от Raspberry Pi

from flask import Flask, request, jsonify
import psycopg2
import json
from datetime import datetime

app = Flask(__name__)

# Подключение к PostgreSQL
conn = psycopg2.connect(
    host='localhost',
    database='smart_home',
    user='postgres',
    password='secure_pass'
)

@app.route('/api/logs', methods=['POST'])
def receive_logs():
    """Принимает логи от Raspberry Pi"""
    data = request.json
    source = data.get('source', 'raspberry_pi')
    errors = data.get('errors', [])

    if not errors:
        return jsonify({'status': 'ok', 'received': 0}), 200

    cursor = conn.cursor()
    saved = 0

    for error in errors:
        cursor.execute("""
            INSERT INTO errors (source, device, level, message, context, created_at)
            VALUES (%s, %s, %s, %s, %s, %s)
        """, (
            source,
            error.get('device', 'unknown'),
            error.get('level', 'ERROR'),
            error.get('message'),
            json.dumps(error.get('context', {})),
            datetime.now()
        ))
        saved += 1

    conn.commit()
    cursor.close()

    print(f"📥 Получено {saved} ошибок от {source}")
    return jsonify({'status': 'ok', 'received': saved}), 200

@app.route('/api/recommendations', methods=['GET'])
def get_recommendations():
    """Отдаёт последний анализ для Raspberry Pi"""
    cursor = conn.cursor()
    cursor.execute("""
        SELECT severity, summary, recommendations, analyzed_at
        FROM analyses
        ORDER BY analyzed_at DESC
        LIMIT 1
    """)

    row = cursor.fetchone()
    cursor.close()

    if not row:
        return jsonify({'status': 'no_data'}), 200

    return jsonify({
        'severity': row[0],
        'summary': row[1],
        'recommendations': row[2],
        'analyzed_at': row[3].isoformat()
    })

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

2.3 AI-аналитик — запускается по cron

# server_analyzer.py — анализирует новые ошибки через нейросеть

import psycopg2
import json
import requests
from datetime import datetime

# Подключение к БД
conn = psycopg2.connect(
    host='localhost',
    database='smart_home',
    user='postgres',
    password='secure_pass'
)

# Локальная нейросеть (llama.cpp)
LLM_URL = "http://localhost:8080/v1/chat/completions"
MODEL = "qwen2.5-14b-instruct-q4_k_m"

def get_new_errors(limit=100):
    """Забирает новые ошибки из БД"""
    cursor = conn.cursor()
    cursor.execute("""
        SELECT id, device, level, message, created_at
        FROM errors
        WHERE processed = FALSE
        ORDER BY created_at DESC
        LIMIT %s
    """, (limit,))

    errors = []
    for row in cursor.fetchall():
        errors.append({
            'id': row[0],
            'device': row[1],
            'level': row[2],
            'message': row[3],
            'time': row[4].isoformat()
        })

    cursor.close()
    return errors

def mark_as_processed(error_ids):
    """Отмечает ошибки как обработанные"""
    if not error_ids:
        return

    cursor = conn.cursor()
    cursor.execute(
        "UPDATE errors SET processed = TRUE WHERE id = ANY(%s)",
        (error_ids,)
    )
    conn.commit()
    cursor.close()

def analyze_with_llm(errors):
    """Отправляет ошибки в нейросеть"""
    if not errors:
        return None

    # Формируем текст
    log_text = "\n".join([
        f"[{e['time']}] {e['level']}: {e['message']} (device: {e['device']})"
        for e in errors[:50]
    ])

    prompt = f"""Ты — эксперт по умному дому. Проанализируй ошибки Zigbee-устройств.
Верни JSON с полями:
- severity: critical | high | medium | low
- summary: краткий анализ (2-3 предложения)
- recommendations: массив рекомендаций (строки)

Ошибки:
{log_text}
"""

    payload = {
        "model": MODEL,
        "messages": [
            {"role": "system", "content": "Ты — аналитик умного дома. Отвечай только в формате JSON."},
            {"role": "user", "content": prompt}
        ],
        "temperature": 0.2,
        "max_tokens": 800
    }

    try:
        response = requests.post(LLM_URL, json=payload, timeout=60)
        data = response.json()
        content = data['choices'][0]['message']['content']

        # Парсим JSON
        start = content.find('{')
        end = content.rfind('}') + 1
        return json.loads(content[start:end])

    except Exception as e:
        print(f"❌ Ошибка LLM: {e}")
        return None

def save_analysis(analysis, errors):
    """Сохраняет результат анализа в БД"""
    cursor = conn.cursor()
    cursor.execute("""
        INSERT INTO analyses (errors_count, severity, summary, recommendations, full_response)
        VALUES (%s, %s, %s, %s, %s)
    """, (
        len(errors),
        analysis.get('severity', 'low'),
        analysis.get('summary', ''),
        json.dumps(analysis.get('recommendations', [])),
        json.dumps(analysis)
    ))
    conn.commit()
    cursor.close()

def main():
    print("🧠 Запуск анализа...")

    # 1. Забираем новые ошибки
    errors = get_new_errors()
    if not errors:
        print("ℹ️ Новых ошибок нет")
        return

    print(f"📥 Найдено {len(errors)} новых ошибок")

    # 2. Анализируем через нейросеть
    analysis = analyze_with_llm(errors)
    if not analysis:
        print("❌ Ошибка анализа")
        return

    print(f"📊 Severity: {analysis.get('severity')}")
    print(f"📝 {analysis.get('summary')}")

    # 3. Сохраняем результат
    save_analysis(analysis, errors)

    # 4. Отмечаем ошибки как обработанные
    error_ids = [e['id'] for e in errors]
    mark_as_processed(error_ids)

    print(f"✅ Анализ сохранён, {len(error_ids)} ошибок обработано")

if __name__ == "__main__":
    main()
    

2.4 Cron на сервере

# Запуск анализа каждые 15 минут
*/15 * * * * /usr/bin/python3 /opt/server_analyzer.py >> /var/log/ai_analyzer.log 2>&1
    

3. Raspberry Pi — получает и показывает рекомендации

Теперь Pi просто ходит на сервер за готовым анализом и показывает его на панели. Никакой нейросети на Pi — только HTTP-запросы и HTML.

3.1 PHP-скрипт на Pi — получает рекомендации

<?php
// get_recommendations.php — забирает анализ с сервера

$serverUrl = 'http://192.168.1.100:5000/api/recommendations';
$cacheFile = '/tmp/recommendations_cache.json';

// 1. Пробуем взять из кеша (чтобы не дёргать сервер каждую секунду)
if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < 60) {
    $data = json_decode(file_get_contents($cacheFile), true);
    if ($data) {
        header('Content-Type: application/json');
        echo json_encode($data);
        exit;
    }
}

// 2. Запрашиваем с сервера
$ch = curl_init($serverUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($httpCode === 200) {
    // Сохраняем в кеш
    file_put_contents($cacheFile, $response);
    header('Content-Type: application/json');
    echo $response;
} else {
    // Если сервер недоступен — отдаём последний кеш
    if (file_exists($cacheFile)) {
        header('Content-Type: application/json');
        echo file_get_contents($cacheFile);
    } else {
        echo json_encode(['status' => 'error', 'message' => 'Сервер недоступен']);
    }
}
?>
    

3.2 HTML-панель на Raspberry Pi

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>🏠 Умный дом — рекомендации AI</title>
    <style>
        * { margin:0; padding:0; box-sizing:border-box; }
        body {
            background: #0a0e14;
            font-family: system-ui, sans-serif;
            color: #d4d4d4;
            padding: 1rem;
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .container {
            max-width: 700px;
            width: 100%;
            background: #141a22;
            border-radius: 24px;
            padding: 2rem;
            border: 1px solid #2a3340;
        }
        h1 {
            font-size: 1.8rem;
            background: linear-gradient(135deg, #60a5fa, #a78bfa);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            background-clip: text;
            margin-bottom: 0.5rem;
        }
        .sub {
            color: #8896a8;
            font-size: 0.9rem;
            margin-bottom: 1.5rem;
        }
        .badge {
            display: inline-block;
            padding: 0.3rem 1.2rem;
            border-radius: 30px;
            font-weight: 600;
            font-size: 0.85rem;
        }
        .badge-critical { background: #4a1a2a; color: #ff8a9a; }
        .badge-high { background: #4a2a1a; color: #ffb07a; }
        .badge-medium { background: #3a3a1a; color: #ffd97a; }
        .badge-low { background: #1a3a2a; color: #7ddfb0; }
        .badge-none { background: #1f2a36; color: #8896a8; }

        .analysis-box {
            background: #0d121c;
            padding: 1.5rem;
            border-radius: 16px;
            margin: 1.5rem 0;
            border-left: 4px solid #3b82f6;
        }
        .analysis-box .label {
            color: #8896a8;
            font-size: 0.75rem;
            text-transform: uppercase;
            letter-spacing: 0.04em;
        }
        .analysis-box .text {
            margin-top: 0.3rem;
            color: #e8edf5;
            font-size: 1.05rem;
        }
        .rec-item {
            background: #1a2a3d;
            padding: 0.7rem 1rem;
            border-radius: 10px;
            margin: 0.5rem 0;
            display: flex;
            align-items: center;
            gap: 0.7rem;
        }
        .rec-item .icon { font-size: 1.2rem; }
        .rec-item .text { color: #c5cdd8; font-size: 0.95rem; }

        .status {
            display: flex;
            justify-content: space-between;
            align-items: center;
            color: #5a6e85;
            font-size: 0.8rem;
            margin-top: 1.5rem;
            border-top: 1px solid #1f2a36;
            padding-top: 1rem;
        }
        .status .dot {
            display: inline-block;
            width: 8px;
            height: 8px;
            border-radius: 50%;
            margin-right: 0.5rem;
        }
        .dot-green { background: #7ddfb0; }
        .dot-red { background: #ff8a9a; }

        .error-msg {
            color: #ff8a9a;
            text-align: center;
            padding: 2rem 0;
        }
        .loader {
            text-align: center;
            padding: 2rem 0;
            color: #8896a8;
        }
        @media (max-width: 500px) {
            .container { padding: 1.2rem; }
            h1 { font-size: 1.4rem; }
        }
    </style>
</head>
<body>
<div class="container">
    <h1>🏠 Умный дом</h1>
    <div class="sub">AI-рекомендации по состоянию Zigbee-устройств</div>

    <div id="content">
        <div class="loader">⏳ Загрузка рекомендаций...</div>
    </div>

    <div class="status">
        <span><span class="dot dot-green" id="statusDot"></span> <span id="statusText">Подключено</span></span>
        <span id="updateTime">--:--:--</span>
    </div>
</div>

<script>
    function fetchRecommendations() {
        fetch('get_recommendations.php?t=' + Date.now())
            .then(res => res.json())
            .then(data => {
                const content = document.getElementById('content');

                if (data.status === 'error' || data.status === 'no_data') {
                    content.innerHTML = `
                        <div class="error-msg">😴 Нет данных. Ожидайте первый анализ.</div>
                    `;
                    return;
                }

                const severity = data.severity || 'none';
                const severityLabels = {
                    critical: '🚨 КРИТИЧЕСКИЙ',
                    high: '⚠️ ВЫСОКИЙ',
                    medium: '📊 СРЕДНИЙ',
                    low: '✅ НИЗКИЙ',
                    none: 'ℹ️ Нет данных'
                };

                let recHtml = '';
                if (data.recommendations && data.recommendations.length > 0) {
                    data.recommendations.forEach(rec => {
                        recHtml += `
                            <div class="rec-item">
                                <span class="icon">💡</span>
                                <span class="text">${rec}</span>
                            </div>
                        `;
                    });
                } else {
                    recHtml = '<div style="color:#8896a8; padding:0.5rem 0;">Нет рекомендаций</div>';
                }

                content.innerHTML = `
                    <div style="display:flex; align-items:center; gap:1rem; margin-bottom:1rem;">
                        <span class="badge badge-${severity}">${severityLabels[severity] || severity}</span>
                        <span style="color:#8896a8; font-size:0.8rem;">${data.analyzed_at || ''}</span>
                    </div>

                    <div class="analysis-box">
                        <div class="label">📌 Сводка</div>
                        <div class="text">${data.summary || 'Анализ не выполнен'}</div>
                    </div>

                    <div style="margin-top: 1.5rem;">
                        <div style="color:#8896a8; font-size:0.8rem; text-transform:uppercase; letter-spacing:0.04em; margin-bottom:0.5rem;">💡 Рекомендации</div>
                        ${recHtml}
                    </div>
                `;

                document.getElementById('updateTime').textContent = new Date().toLocaleTimeString();
            })
            .catch(() => {
                document.getElementById('content').innerHTML = `
                    <div class="error-msg">❌ Ошибка соединения с сервером</div>
                `;
                document.getElementById('statusDot').className = 'dot dot-red';
                document.getElementById('statusText').textContent = 'Сервер недоступен';
            });
    }

    fetchRecommendations();
    setInterval(fetchRecommendations, 30000); // каждые 30 секунд
</script>
</body>
</html>
    

🎯 Итог: что получилось

🍓 Raspberry Pi

Только собирает логи и показывает панель.
Минимальная нагрузка

🖥️ Мощный сервер

Хранит все логи в PostgreSQL, анализирует через Qwen 14B.
Вся мощь здесь

📊 Панель на Pi

Получает готовые рекомендации по API.
Обновление каждые 30 сек

🧠 Нейросеть

Анализирует ошибки раз в 15 минут, выдаёт severity и рекомендации.
Автоматически

🚀 Преимущества:
• Raspberry Pi не тормозит — вся аналитика на сервере
• Все логи хранятся в PostgreSQL с индексами — быстрый доступ
• Нейросеть запускается только когда есть новые ошибки
• Панель на Pi показывает готовый анализ — никакой задержки
• Можно подключить несколько Pi к одному серверу
Теги: #AI #linux #Qwen #анализ #аналитика #ии_агент #логи #нейросеть #умный_дома
👍 Оцените статью
Всего голосов: 0



Категории:

Категории

Комментарии

Пока нет комментариев. Будьте первым!

Оставить комментарий

← Назад к списку

Посетителей сегодня: 0
о блоге | карта блога | 📡 Подписаться на RSS

© Digital Specialist | Не являемся сотрудниками Google, Яндекса и NASA