Raspberry Pi собирает логи, мощный сервер анализирует, Pi получает готовые рекомендации
В этой архитектуре Raspberry Pi — это лёгкий клиент, который только собирает логи и показывает результат. А вся «тяжёлая» работа (хранение, анализ, нейросеть) вынесена на отдельный мощный сервер.
На Raspberry Pi работает Zigbee2MQTT или другой контроллер, который пишет логи. Наша задача — читать новые ошибки и отправлять их на сервер.
#!/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()
# Запуск каждую минуту
* * * * * /usr/bin/python3 /home/pi/pi_sender.py >> /home/pi/sender.log 2>&1
На сервере стоит PostgreSQL для хранения всех ошибок, Qwen 2.5 14B (или 7B) для анализа, и Flask API для общения с Raspberry Pi.
-- Создаём БД
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
);
# 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)
# 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()
# Запуск анализа каждые 15 минут
*/15 * * * * /usr/bin/python3 /opt/server_analyzer.py >> /var/log/ai_analyzer.log 2>&1
Теперь Pi просто ходит на сервер за готовым анализом и показывает его на панели. Никакой нейросети на Pi — только HTTP-запросы и HTML.
<?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' => 'Сервер недоступен']);
}
}
?>
<!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>
Только собирает логи и показывает панель.
Минимальная нагрузка
Хранит все логи в PostgreSQL, анализирует через Qwen 14B.
Вся мощь здесь
Получает готовые рекомендации по API.
Обновление каждые 30 сек
Анализирует ошибки раз в 15 минут, выдаёт severity и рекомендации.
Автоматически
Комментарии
Пока нет комментариев. Будьте первым!