feat: S.M.A.R.T. disk health monitoring with CLI and GUI

- Add core module with SMART data parsing and health calculation
- Add CLI with Rich-based terminal UI and health bar visualization
- Add GUI with PyQt6 tabs for summary and detailed views
- Support multiple health indicators (ID 231, 169, 233) for different SSD manufacturers
- Add bilingual support (Russian/English) with auto-detection
- Add GitHub Actions workflow for building binaries on Linux, Windows, macOS
- Calculate health based on reallocated sectors, pending sectors, SSD life, and more

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
kilyabin
2026-03-15 00:15:21 +04:00
commit 19b79a4e13
12 changed files with 2333 additions and 0 deletions

244
smart-info-2.py Normal file
View File

@@ -0,0 +1,244 @@
#!/usr/bin/env python3
"""
Мониторинг здоровья дисков с S.M.A.R.T (аналог CrystalDiskInfo)
Требует: sudo smartctl (smartmontools)
"""
import subprocess
import sys
from datetime import datetime
def check_smartctl():
"""Проверка наличия smartctl"""
try:
subprocess.run(['which', 'smartctl'], capture_output=True, check=True)
except subprocess.CalledProcessError:
print("❌ smartmontools не установлен!")
print("Установите: sudo pacman -S smartmontools")
sys.exit(1)
def get_disks():
"""Получить список дисков"""
try:
result = subprocess.run(['lsblk', '-d', '-n', '-o', 'NAME'],
capture_output=True, text=True, check=True)
return [f"/dev/{disk}" for disk in result.stdout.strip().split('\n') if disk]
except:
return []
def get_disk_info(disk):
"""Получить информацию о диске"""
try:
model = subprocess.run(['lsblk', '-d', '-n', '-o', 'MODEL', disk],
capture_output=True, text=True).stdout.strip() or "Unknown"
size = subprocess.run(['lsblk', '-d', '-n', '-o', 'SIZE', disk],
capture_output=True, text=True).stdout.strip() or "Unknown"
return model, size
except:
return "Unknown", "Unknown"
def parse_smart_data(disk):
"""Парсить S.M.A.R.T данные"""
try:
result = subprocess.run(['sudo', 'smartctl', '-a', disk],
capture_output=True, text=True)
output = result.stdout
data = {
'status': 'GOOD' if 'PASSED' in output else ('BAD' if 'FAILED' in output else 'UNKNOWN'),
'temp': 'N/A',
'power_hours': 'N/A',
'power_cycles': 'N/A',
'reallocated': 0,
'pending': 0,
'uncorrectable': 0,
'attrs': {}
}
for line in output.split('\n'):
parts = line.split()
if len(parts) < 10:
continue
if '194' in parts[0] or 'Temperature_Celsius' in line:
try:
data['temp'] = f"{parts[9]}°C"
except:
pass
if '9' in parts[0] or 'Power_On_Hours' in line:
try:
hours = int(parts[9])
data['power_hours'] = f"{hours}h ({hours//24}d)"
except:
pass
if '12' in parts[0] or 'Power_Cycle_Count' in line:
try:
data['power_cycles'] = parts[9]
except:
pass
if '5' in parts[0] or 'Reallocated_Sector_Ct' in line:
try:
data['reallocated'] = int(parts[9])
except:
pass
if '197' in parts[0] or 'Current_Pending_Sect' in line:
try:
data['pending'] = int(parts[9])
except:
pass
if '198' in parts[0] or 'Offline_Uncorrectable' in line:
try:
data['uncorrectable'] = int(parts[9])
except:
pass
if parts and parts[0].isdigit() and len(parts) >= 10:
try:
attr_id = parts[0]
attr_name = parts[1] if len(parts) > 1 else 'Unknown'
data['attrs'][attr_id] = {
'name': attr_name,
'value': parts[3],
'worst': parts[4],
'threshold': parts[5],
'raw': parts[9]
}
except:
pass
return data
except:
return None
def calculate_health(smart_data):
"""Правильный расчёт здоровья диска"""
if not smart_data:
return 50, []
if smart_data['status'] == 'BAD':
return 5, ["🔴 S.M.A.R.T статус: BAD"]
health = 100
warnings = []
reallocated = smart_data['reallocated']
pending = smart_data['pending']
uncorrectable = smart_data['uncorrectable']
# === ПЕРЕНАЗНАЧЕННЫЕ СЕКТОРА ===
if reallocated > 0:
if reallocated > 500:
penalty = min(80, reallocated * 0.5)
health -= penalty
warnings.append(f"🔴 КРИТИЧНО: {reallocated} переназначенных секторов! Диск может отказать!")
elif reallocated > 100:
penalty = min(70, reallocated * 0.3)
health -= penalty
warnings.append(f"🟠 ВНИМАНИЕ: {reallocated} переназначенных секторов. Начните резервное копирование!")
elif reallocated > 10:
penalty = reallocated * 0.2
health -= penalty
warnings.append(f"🟡 ВНИМАНИЕ: {reallocated} переназначенных секторов")
else:
health -= reallocated * 0.1
# === ОЖИДАЮЩИЕ СЕКТОРА ===
if pending > 0:
health -= min(70, pending * 2)
warnings.append(f"🔴 КРИТИЧНО: {pending} ожидающих секторов!")
# === НЕИСПРАВИМЫЕ ОШИБКИ ===
if uncorrectable > 0:
health -= min(80, uncorrectable * 5)
warnings.append(f"🔴 КРИТИЧНО: {uncorrectable} неисправимых ошибок!")
return max(5, int(health)), warnings
def get_color(health):
"""Цвет в зависимости от процента"""
if health >= 85:
return '\033[92m' # Зеленый
elif health >= 60:
return '\033[93m' # Желтый
elif health >= 30:
return '\033[91m' # Красный
else:
return '\033[41m' # Красный фон
def get_icon(health):
"""Иконка здоровья"""
if health >= 85:
return ""
elif health >= 60:
return ""
elif health >= 30:
return ""
else:
return "💥"
def print_disk(disk, model, size, health, warnings, smart_data):
"""Вывести информацию о диске"""
color = get_color(health)
icon = get_icon(health)
reset = '\033[0m'
print(f"\n{'='*75}")
print(f"Диск: {disk:15} Модель: {model:25} Размер: {size}")
bar_len = 40
filled = int(bar_len * health / 100)
bar = '' * filled + '' * (bar_len - filled)
print(f"Здоровье: {color}{icon} {bar} {health}%{reset}")
if warnings:
print("\n⚠️ ПРЕДУПРЕЖДЕНИЯ:")
for warning in warnings:
print(f" {warning}")
if not smart_data:
print("S.M.A.R.T: Не поддерживается")
return
print(f"\nСтатус: {smart_data['status']:8} | Температура: {smart_data['temp']:8} | "
f"Часов: {smart_data['power_hours']:15} | Циклов: {smart_data['power_cycles']}")
print(f"\nКритические атрибуты:")
print(f" • Переназначенные сектора: {smart_data['reallocated']}")
print(f" • Ожидающие сектора: {smart_data['pending']}")
print(f" • Неисправимые ошибки: {smart_data['uncorrectable']}")
def main():
"""Главная функция"""
check_smartctl()
print("\n" + "="*75)
print(f" МОНИТОРИНГ ЗДОРОВЬЯ ДИСКОВ (S.M.A.R.T)")
print(f" {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}")
print("="*75)
disks = get_disks()
if not disks:
print("❌ Диски не найдены")
return
for disk in disks:
model, size = get_disk_info(disk)
smart_data = parse_smart_data(disk)
health, warnings = calculate_health(smart_data)
print_disk(disk, model, size, health, warnings, smart_data)
print("\n" + "="*75 + "\n")
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n\nПрограмма прервана")
except Exception as e:
print(f"❌ Ошибка: {e}")