@@ -414,25 +419,25 @@ export function StorageOverview() {
{disk.size && (
-
Size
+
Size
{disk.size}
)}
{disk.smart_status && disk.smart_status !== "unknown" && (
-
SMART Status
+
SMART Status
{disk.smart_status}
)}
{disk.power_on_hours !== undefined && disk.power_on_hours > 0 && (
-
Power On Time
+
Power On Time
{formatHours(disk.power_on_hours)}
)}
{disk.serial && disk.serial !== "Unknown" && (
-
Serial
+
Serial
{disk.serial}
)}
diff --git a/AppImage/scripts/flask_server.py b/AppImage/scripts/flask_server.py
index b096a38..8b5e5eb 100644
--- a/AppImage/scripts/flask_server.py
+++ b/AppImage/scripts/flask_server.py
@@ -362,6 +362,11 @@ def get_storage_info():
parts = line.split()
if len(parts) >= 3 and parts[2] == 'disk':
disk_name = parts[0]
+
+ if disk_name.startswith('zd'):
+ print(f"[v0] Skipping ZFS zvol device: {disk_name}")
+ continue
+
disk_size_bytes = int(parts[1])
disk_size_gb = disk_size_bytes / (1024**3)
disk_size_tb = disk_size_bytes / (1024**4)
@@ -516,17 +521,17 @@ def get_smart_data(disk_name):
['smartctl', '-a', '-j', f'/dev/{disk_name}'], # JSON output (preferred)
['smartctl', '-a', '-j', '-d', 'ata', f'/dev/{disk_name}'], # JSON with ATA device type
['smartctl', '-a', '-j', '-d', 'sat', f'/dev/{disk_name}'], # JSON with SAT device type
+ ['smartctl', '-a', f'/dev/{disk_name}'], # Text output (fallback)
+ ['smartctl', '-a', '-d', 'ata', f'/dev/{disk_name}'], # Text with ATA device type
+ ['smartctl', '-a', '-d', 'sat', f'/dev/{disk_name}'], # Text with SAT device type
+ ['smartctl', '-i', '-H', '-A', f'/dev/{disk_name}'], # Info + Health + Attributes
+ ['smartctl', '-i', '-H', '-A', '-d', 'ata', f'/dev/{disk_name}'], # With ATA
+ ['smartctl', '-i', '-H', '-A', '-d', 'sat', f'/dev/{disk_name}'], # With SAT
['smartctl', '-a', '-j', '-d', 'scsi', f'/dev/{disk_name}'], # JSON with SCSI device type
['smartctl', '-a', '-j', '-d', 'sat,12', f'/dev/{disk_name}'], # SAT with 12-byte commands
['smartctl', '-a', '-j', '-d', 'sat,16', f'/dev/{disk_name}'], # SAT with 16-byte commands
- ['smartctl', '-a', f'/dev/{disk_name}'], # Text output
- ['smartctl', '-a', '-d', 'ata', f'/dev/{disk_name}'], # Text with ATA device type
- ['smartctl', '-a', '-d', 'sat', f'/dev/{disk_name}'], # Text with SAT device type
['smartctl', '-a', '-d', 'sat,12', f'/dev/{disk_name}'], # Text SAT with 12-byte commands
['smartctl', '-a', '-d', 'sat,16', f'/dev/{disk_name}'], # Text SAT with 16-byte commands
- ['smartctl', '-i', '-H', f'/dev/{disk_name}'], # Basic info + health only
- ['smartctl', '-i', '-H', '-d', 'ata', f'/dev/{disk_name}'], # Basic with ATA
- ['smartctl', '-i', '-H', '-d', 'sat', f'/dev/{disk_name}'], # Basic with SAT
]
for cmd_index, cmd in enumerate(commands_to_try):
@@ -536,14 +541,18 @@ def get_smart_data(disk_name):
print(f"[v0] Command return code: {result.returncode}")
if result.stderr:
- stderr_preview = result.stderr[:300].replace('\n', ' ')
+ stderr_preview = result.stderr[:200].replace('\n', ' ')
print(f"[v0] stderr: {stderr_preview}")
- # smartctl returns: 0=OK, 2=SMART disabled, 4=threshold exceeded (still valid), 8=error log has errors
- if result.returncode in [0, 2, 4, 8] and result.stdout:
- print(f"[v0] Got output ({len(result.stdout)} bytes)")
+ # smartctl returns: 0=OK, 2=SMART disabled, 4=threshold exceeded, 8=error log has errors
+ # 64=device open failed (but sometimes still has partial data)
+ # We'll try to parse ANY output if stdout is not empty
+ has_output = result.stdout and len(result.stdout.strip()) > 50
+
+ if has_output:
+ print(f"[v0] Got output ({len(result.stdout)} bytes), attempting to parse...")
- # Try JSON parsing first
+ # Try JSON parsing first (if -j flag was used)
if '-j' in cmd:
try:
print(f"[v0] Attempting JSON parse...")
@@ -581,7 +590,6 @@ def get_smart_data(disk_name):
attr_id = attr.get('id')
raw_value = attr.get('raw', {}).get('value', 0)
- # ID mapping from Home Assistant coordinator
if attr_id == 9: # Power_On_Hours
smart_data['power_on_hours'] = raw_value
print(f"[v0] Power On Hours (ID 9): {raw_value}")
@@ -615,77 +623,97 @@ def get_smart_data(disk_name):
print(f"[v0] NVMe Power On Hours: {smart_data['power_on_hours']}")
# If we got good data, break out of the loop
- if smart_data['model'] != 'Unknown' or smart_data['serial'] != 'Unknown':
- print(f"[v0] Successfully extracted data from JSON (attempt {cmd_index + 1})")
+ if smart_data['model'] != 'Unknown' and smart_data['serial'] != 'Unknown':
+ print(f"[v0] Successfully extracted complete data from JSON (attempt {cmd_index + 1})")
break
except json.JSONDecodeError as e:
- print(f"[v0] JSON parse failed: {e}, will try next command...")
+ print(f"[v0] JSON parse failed: {e}, trying text parsing...")
- # Text parsing fallback
- if smart_data['model'] == 'Unknown' or smart_data['serial'] == 'Unknown':
- print(f"[v0] Parsing text output...")
+ if smart_data['model'] == 'Unknown' or smart_data['serial'] == 'Unknown' or smart_data['temperature'] == 0:
+ print(f"[v0] Parsing text output (model={smart_data['model']}, serial={smart_data['serial']}, temp={smart_data['temperature']})...")
output = result.stdout
# Get basic info
for line in output.split('\n'):
line = line.strip()
- if line.startswith('Device Model:') or line.startswith('Model Number:'):
+
+ # Model detection
+ if (line.startswith('Device Model:') or line.startswith('Model Number:')) and smart_data['model'] == 'Unknown':
smart_data['model'] = line.split(':', 1)[1].strip()
print(f"[v0] Found model: {smart_data['model']}")
- elif line.startswith('Serial Number:'):
- smart_data['serial'] = line.split(':', 1)[1].strip()
- print(f"[v0] Found serial: {smart_data['serial']}")
elif line.startswith('Model Family:') and smart_data['model'] == 'Unknown':
smart_data['model'] = line.split(':', 1)[1].strip()
print(f"[v0] Found model family: {smart_data['model']}")
-
- # Parse SMART status
- if 'SMART overall-health self-assessment test result: PASSED' in output:
- smart_data['smart_status'] = 'passed'
- smart_data['health'] = 'healthy'
- print(f"[v0] SMART status: PASSED")
- elif 'SMART Health Status: OK' in output: # NVMe
- smart_data['smart_status'] = 'passed'
- smart_data['health'] = 'healthy'
- print(f"[v0] NVMe Health: OK")
- elif 'SMART overall-health self-assessment test result: FAILED' in output:
- smart_data['smart_status'] = 'failed'
- smart_data['health'] = 'critical'
- print(f"[v0] SMART status: FAILED")
+
+ # Serial detection
+ elif line.startswith('Serial Number:') and smart_data['serial'] == 'Unknown':
+ smart_data['serial'] = line.split(':', 1)[1].strip()
+ print(f"[v0] Found serial: {smart_data['serial']}")
+
+ # SMART status detection
+ elif 'SMART overall-health self-assessment test result:' in line:
+ if 'PASSED' in line:
+ smart_data['smart_status'] = 'passed'
+ smart_data['health'] = 'healthy'
+ print(f"[v0] SMART status: PASSED")
+ elif 'FAILED' in line:
+ smart_data['smart_status'] = 'failed'
+ smart_data['health'] = 'critical'
+ print(f"[v0] SMART status: FAILED")
+
+ # NVMe health
+ elif 'SMART Health Status:' in line:
+ if 'OK' in line:
+ smart_data['smart_status'] = 'passed'
+ smart_data['health'] = 'healthy'
+ print(f"[v0] NVMe Health: OK")
+
+ # Temperature detection (various formats)
+ elif 'Current Temperature:' in line and smart_data['temperature'] == 0:
+ try:
+ temp_str = line.split(':')[1].strip().split()[0]
+ smart_data['temperature'] = int(temp_str)
+ print(f"[v0] Found temperature: {smart_data['temperature']}°C")
+ except (ValueError, IndexError):
+ pass
# Parse SMART attributes table
in_attributes = False
for line in output.split('\n'):
line = line.strip()
- if 'ID# ATTRIBUTE_NAME' in line:
+ if 'ID# ATTRIBUTE_NAME' in line or 'ID#' in line and 'ATTRIBUTE_NAME' in line:
in_attributes = True
print(f"[v0] Found SMART attributes table")
continue
- if in_attributes and line and not line.startswith('SMART'):
+ if in_attributes:
+ # Stop at empty line or next section
+ if not line or line.startswith('SMART') or line.startswith('==='):
+ in_attributes = False
+ continue
+
parts = line.split()
if len(parts) >= 10:
try:
attr_id = parts[0]
- raw_value = parts[9]
+ # Raw value is typically the last column
+ raw_value = parts[-1]
- # Same ID mapping as JSON parsing
+ # Parse based on attribute ID
if attr_id == '9': # Power On Hours
- # Handle different formats: "12345", "12345h", "12345 hours"
- raw_clean = raw_value.split()[0].replace('h', '')
+ raw_clean = raw_value.split()[0].replace('h', '').replace(',', '')
smart_data['power_on_hours'] = int(raw_clean)
print(f"[v0] Power On Hours: {smart_data['power_on_hours']}")
- elif attr_id == '194': # Temperature
+ elif attr_id == '194' and smart_data['temperature'] == 0: # Temperature
temp_str = raw_value.split()[0]
smart_data['temperature'] = int(temp_str)
- print(f"[v0] Temperature: {smart_data['temperature']}°C")
- elif attr_id == '190': # Airflow Temperature
- if smart_data['temperature'] == 0:
- temp_str = raw_value.split()[0]
- smart_data['temperature'] = int(temp_str)
- print(f"[v0] Airflow Temperature: {smart_data['temperature']}°C")
+ print(f"[v0] Temperature (attr 194): {smart_data['temperature']}°C")
+ elif attr_id == '190' and smart_data['temperature'] == 0: # Airflow Temperature
+ temp_str = raw_value.split()[0]
+ smart_data['temperature'] = int(temp_str)
+ print(f"[v0] Airflow Temperature (attr 190): {smart_data['temperature']}°C")
elif attr_id == '5': # Reallocated Sectors
smart_data['reallocated_sectors'] = int(raw_value)
print(f"[v0] Reallocated Sectors: {smart_data['reallocated_sectors']}")
@@ -697,26 +725,17 @@ def get_smart_data(disk_name):
print(f"[v0] CRC Errors: {smart_data['crc_errors']}")
except (ValueError, IndexError) as e:
+ print(f"[v0] Error parsing attribute line '{line}': {e}")
continue
- # Try to find temperature in other formats
- if smart_data['temperature'] == 0:
- for line in output.split('\n'):
- if 'Temperature:' in line or 'Temperature_Celsius' in line:
- try:
- temp_str = line.split(':')[1].strip().split()[0]
- smart_data['temperature'] = int(temp_str)
- print(f"[v0] Found temperature: {smart_data['temperature']}°C")
- break
- except (ValueError, IndexError):
- pass
-
- # If we got some data, break
- if smart_data['model'] != 'Unknown' or smart_data['serial'] != 'Unknown':
- print(f"[v0] Successfully extracted data from text output (attempt {cmd_index + 1})")
+ # If we got complete data, break
+ if smart_data['model'] != 'Unknown' and smart_data['serial'] != 'Unknown':
+ print(f"[v0] Successfully extracted complete data from text output (attempt {cmd_index + 1})")
break
+ elif smart_data['model'] != 'Unknown' or smart_data['serial'] != 'Unknown':
+ print(f"[v0] Extracted partial data from text output, continuing to next attempt...")
else:
- print(f"[v0] Command failed with return code {result.returncode}, trying next...")
+ print(f"[v0] No usable output (return code {result.returncode}), trying next command...")
except subprocess.TimeoutExpired:
print(f"[v0] Command timeout for attempt {cmd_index + 1}, trying next...")
@@ -726,7 +745,8 @@ def get_smart_data(disk_name):
continue
if smart_data['reallocated_sectors'] > 0 or smart_data['pending_sectors'] > 0:
- smart_data['health'] = 'warning'
+ if smart_data['health'] == 'healthy':
+ smart_data['health'] = 'warning'
print(f"[v0] Health: WARNING (reallocated/pending sectors)")
if smart_data['reallocated_sectors'] > 10 or smart_data['pending_sectors'] > 10:
smart_data['health'] = 'critical'
@@ -735,12 +755,14 @@ def get_smart_data(disk_name):
smart_data['health'] = 'critical'
print(f"[v0] Health: CRITICAL (SMART failed)")
- # Temperature-based health
+ # Temperature-based health (only if we have a valid temperature)
if smart_data['health'] == 'healthy' and smart_data['temperature'] > 0:
if smart_data['temperature'] >= 70:
smart_data['health'] = 'critical'
+ print(f"[v0] Health: CRITICAL (temperature {smart_data['temperature']}°C)")
elif smart_data['temperature'] >= 60:
smart_data['health'] = 'warning'
+ print(f"[v0] Health: WARNING (temperature {smart_data['temperature']}°C)")
except FileNotFoundError:
print(f"[v0] ERROR: smartctl not found - install smartmontools")