forked from HomeAssistant/fn_nas
增加可用内存和存储空间实体
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "fn_nas",
|
||||
"name": "飞牛NAS",
|
||||
"version": "1.3.0",
|
||||
"version": "1.3.1",
|
||||
"documentation": "https://github.com/anxms/fn_nas",
|
||||
"dependencies": [],
|
||||
"codeowners": ["@anxms"],
|
||||
|
@@ -243,6 +243,38 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
)
|
||||
)
|
||||
existing_ids.add(sensor_uid)
|
||||
|
||||
# 添加剩余内存传感器
|
||||
mem_available_uid = f"{config_entry.entry_id}_memory_available"
|
||||
if mem_available_uid not in existing_ids:
|
||||
entities.append(
|
||||
MemoryAvailableSensor(
|
||||
coordinator,
|
||||
"可用内存",
|
||||
mem_available_uid,
|
||||
"GB",
|
||||
"mdi:memory"
|
||||
)
|
||||
)
|
||||
existing_ids.add(mem_available_uid)
|
||||
|
||||
# 添加存储卷的剩余容量传感器(每个卷一个)
|
||||
system_data = coordinator.data.get("system", {})
|
||||
volumes = system_data.get("volumes", {})
|
||||
for mount_point in volumes:
|
||||
# 创建剩余容量传感器
|
||||
vol_avail_uid = f"{config_entry.entry_id}_{mount_point.replace('/', '_')}_available"
|
||||
if vol_avail_uid not in existing_ids:
|
||||
entities.append(
|
||||
VolumeAvailableSensor(
|
||||
coordinator,
|
||||
f"{mount_point} 可用空间",
|
||||
vol_avail_uid,
|
||||
"mdi:harddisk",
|
||||
mount_point
|
||||
)
|
||||
)
|
||||
existing_ids.add(vol_avail_uid)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
@@ -537,4 +569,129 @@ class DockerContainerStatusSensor(CoordinatorEntity, SensorEntity):
|
||||
"dead": "死亡"
|
||||
}
|
||||
return status_map.get(container["status"], container["status"])
|
||||
return "未知"
|
||||
return "未知"
|
||||
|
||||
class MemoryAvailableSensor(CoordinatorEntity, SensorEntity):
|
||||
"""剩余内存传感器(包含总内存和已用内存作为属性)"""
|
||||
|
||||
def __init__(self, coordinator, name, unique_id, unit, icon):
|
||||
super().__init__(coordinator)
|
||||
self._attr_name = name
|
||||
self._attr_unique_id = unique_id
|
||||
self._attr_native_unit_of_measurement = unit
|
||||
self._attr_icon = icon
|
||||
self._attr_device_info = {
|
||||
"identifiers": {(DOMAIN, DEVICE_ID_NAS)},
|
||||
"name": "飞牛NAS系统监控",
|
||||
"manufacturer": "飞牛"
|
||||
}
|
||||
self._attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
"""返回可用内存(GB)"""
|
||||
system_data = self.coordinator.data.get("system", {})
|
||||
mem_available = system_data.get("memory_available")
|
||||
|
||||
if mem_available is None or mem_available == "未知":
|
||||
return None
|
||||
|
||||
try:
|
||||
# 将字节转换为GB
|
||||
return round(float(mem_available) / (1024 ** 3), 2)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""返回总内存和已用内存(GB)以及原始字节值"""
|
||||
system_data = self.coordinator.data.get("system", {})
|
||||
mem_total = system_data.get("memory_total")
|
||||
mem_used = system_data.get("memory_used")
|
||||
mem_available = system_data.get("memory_available")
|
||||
|
||||
# 转换为GB
|
||||
try:
|
||||
mem_total_gb = round(float(mem_total) / (1024 ** 3), 2) if mem_total and mem_total != "未知" else None
|
||||
except:
|
||||
mem_total_gb = None
|
||||
|
||||
try:
|
||||
mem_used_gb = round(float(mem_used) / (1024 ** 3), 2) if mem_used and mem_used != "未知" else None
|
||||
except:
|
||||
mem_used_gb = None
|
||||
|
||||
return {
|
||||
"总内存 (GB)": mem_total_gb,
|
||||
"已用内存 (GB)": mem_used_gb
|
||||
}
|
||||
|
||||
class VolumeAvailableSensor(CoordinatorEntity, SensorEntity):
|
||||
"""存储卷剩余容量传感器(包含总容量和已用容量作为属性)"""
|
||||
|
||||
def __init__(self, coordinator, name, unique_id, icon, mount_point):
|
||||
super().__init__(coordinator)
|
||||
self._attr_name = name
|
||||
self._attr_unique_id = unique_id
|
||||
self._attr_icon = icon
|
||||
self.mount_point = mount_point
|
||||
|
||||
# 设备信息,归属到飞牛NAS系统
|
||||
self._attr_device_info = {
|
||||
"identifiers": {(DOMAIN, DEVICE_ID_NAS)},
|
||||
"name": "飞牛NAS系统监控",
|
||||
"manufacturer": "飞牛"
|
||||
}
|
||||
|
||||
self._attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
"""返回剩余容量(数值)"""
|
||||
system_data = self.coordinator.data.get("system", {})
|
||||
volumes = system_data.get("volumes", {})
|
||||
vol_info = volumes.get(self.mount_point, {})
|
||||
|
||||
avail_str = vol_info.get("available", "未知")
|
||||
if avail_str == "未知":
|
||||
return None
|
||||
|
||||
try:
|
||||
numeric_part = ''.join(filter(lambda x: x.isdigit() or x == '.', avail_str))
|
||||
return float(numeric_part)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self):
|
||||
"""动态返回单位"""
|
||||
system_data = self.coordinator.data.get("system", {})
|
||||
volumes = system_data.get("volumes", {})
|
||||
vol_info = volumes.get(self.mount_point, {})
|
||||
|
||||
avail_str = vol_info.get("available", "")
|
||||
if avail_str.endswith("T") or avail_str.endswith("Ti"):
|
||||
return "TB"
|
||||
elif avail_str.endswith("G") or avail_str.endswith("Gi"):
|
||||
return "GB"
|
||||
elif avail_str.endswith("M") or avail_str.endswith("Mi"):
|
||||
return "MB"
|
||||
else:
|
||||
return None # 未知单位
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
system_data = self.coordinator.data.get("system", {})
|
||||
volumes = system_data.get("volumes", {})
|
||||
vol_info = volumes.get(self.mount_point, {})
|
||||
|
||||
return {
|
||||
"挂载点": self.mount_point,
|
||||
"文件系统": vol_info.get("filesystem", "未知"),
|
||||
"总容量": vol_info.get("size", "未知"),
|
||||
"已用容量": vol_info.get("used", "未知"),
|
||||
"使用率": vol_info.get("use_percent", "未知")
|
||||
}
|
||||
|
||||
|
||||
return attributes
|
@@ -58,15 +58,28 @@ class SystemManager:
|
||||
if backup_cpu_temp:
|
||||
system_info["cpu_temperature"] = backup_cpu_temp
|
||||
|
||||
# 新增:获取内存信息
|
||||
mem_info = await self.get_memory_info()
|
||||
system_info.update(mem_info)
|
||||
|
||||
# 新增:获取存储卷信息
|
||||
vol_info = await self.get_vol_usage()
|
||||
system_info["volumes"] = vol_info
|
||||
|
||||
return system_info
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error("Error getting system info: %s", str(e))
|
||||
# 在异常处理中返回空数据
|
||||
return {
|
||||
"uptime_seconds": 0,
|
||||
"uptime": "未知",
|
||||
"cpu_temperature": "未知",
|
||||
"motherboard_temperature": "未知"
|
||||
"motherboard_temperature": "未知",
|
||||
"memory_total": "未知",
|
||||
"memory_used": "未知",
|
||||
"memory_available": "未知",
|
||||
"volumes": {}
|
||||
}
|
||||
|
||||
def save_sensor_data_for_debug(self, sensors_output: str):
|
||||
@@ -192,7 +205,7 @@ class SystemManager:
|
||||
if isinstance(temp_value, (int, float)):
|
||||
self.logger.debug("JSON中找到Tdie/Tctl温度: %s/%s = %.1f°C", key, subkey, temp_value)
|
||||
return f"{temp_value:.1f} °C"
|
||||
except Exception:
|
||||
except:
|
||||
pass
|
||||
except Exception as e:
|
||||
self.logger.warning("JSON解析失败: %s", str(e))
|
||||
@@ -368,11 +381,121 @@ class SystemManager:
|
||||
self.logger.warning("Using fallback motherboard temperature detection")
|
||||
return f"{avg_temp:.1f} °C"
|
||||
|
||||
# self.logger.warning("No motherboard temperature found in sensors output")
|
||||
return "未知"
|
||||
|
||||
|
||||
|
||||
async def get_memory_info(self) -> dict:
|
||||
"""获取内存使用信息"""
|
||||
try:
|
||||
# 使用 free 命令获取内存信息(-b 选项以字节为单位)
|
||||
mem_output = await self.coordinator.run_command("free -b")
|
||||
if not mem_output:
|
||||
return {}
|
||||
|
||||
# 解析输出
|
||||
lines = mem_output.splitlines()
|
||||
if len(lines) < 2:
|
||||
return {}
|
||||
|
||||
# 第二行是内存信息(Mem行)
|
||||
mem_line = lines[1].split()
|
||||
if len(mem_line) < 7:
|
||||
return {}
|
||||
|
||||
return {
|
||||
"memory_total": int(mem_line[1]),
|
||||
"memory_used": int(mem_line[2]),
|
||||
"memory_available": int(mem_line[6])
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error("获取内存信息失败: %s", str(e))
|
||||
return {}
|
||||
|
||||
async def get_vol_usage(self) -> dict:
|
||||
"""获取 /vol* 开头的存储卷使用信息"""
|
||||
try:
|
||||
# 优先使用字节单位
|
||||
df_output = await self.coordinator.run_command("df -B 1 /vol* 2>/dev/null")
|
||||
if df_output:
|
||||
return self.parse_df_bytes(df_output)
|
||||
|
||||
df_output = await self.coordinator.run_command("df -h /vol*")
|
||||
if df_output:
|
||||
return self.parse_df_human_readable(df_output)
|
||||
|
||||
return {}
|
||||
except Exception as e:
|
||||
self.logger.error("获取存储卷信息失败: %s", str(e))
|
||||
return {}
|
||||
|
||||
def parse_df_bytes(self, df_output: str) -> dict:
|
||||
volumes = {}
|
||||
for line in df_output.splitlines()[1:]:
|
||||
parts = line.split()
|
||||
if len(parts) < 6:
|
||||
continue
|
||||
|
||||
mount_point = parts[-1]
|
||||
# 只处理 /vol 开头的挂载点
|
||||
if not mount_point.startswith("/vol"):
|
||||
continue
|
||||
|
||||
try:
|
||||
size_bytes = int(parts[1])
|
||||
used_bytes = int(parts[2])
|
||||
avail_bytes = int(parts[3])
|
||||
use_percent = parts[4]
|
||||
|
||||
def bytes_to_human(b):
|
||||
for unit in ['', 'K', 'M', 'G', 'T']:
|
||||
if abs(b) < 1024.0:
|
||||
return f"{b:.1f}{unit}"
|
||||
b /= 1024.0
|
||||
return f"{b:.1f}P"
|
||||
|
||||
volumes[mount_point] = {
|
||||
"filesystem": parts[0],
|
||||
"size": bytes_to_human(size_bytes),
|
||||
"used": bytes_to_human(used_bytes),
|
||||
"available": bytes_to_human(avail_bytes),
|
||||
"use_percent": use_percent
|
||||
}
|
||||
except (ValueError, IndexError) as e:
|
||||
self.logger.debug("解析存储卷行失败: %s - %s", line, str(e))
|
||||
continue
|
||||
|
||||
return volumes
|
||||
|
||||
def parse_df_human_readable(self, df_output: str) -> dict:
|
||||
volumes = {}
|
||||
for line in df_output.splitlines()[1:]:
|
||||
parts = line.split()
|
||||
if len(parts) < 6:
|
||||
continue
|
||||
|
||||
mount_point = parts[-1]
|
||||
if not mount_point.startswith("/vol"):
|
||||
continue
|
||||
|
||||
try:
|
||||
size = parts[1]
|
||||
used = parts[2]
|
||||
avail = parts[3]
|
||||
use_percent = parts[4]
|
||||
|
||||
volumes[mount_point] = {
|
||||
"filesystem": parts[0],
|
||||
"size": size,
|
||||
"used": used,
|
||||
"available": avail,
|
||||
"use_percent": use_percent
|
||||
}
|
||||
except (ValueError, IndexError) as e:
|
||||
self.logger.debug("解析存储卷行失败: %s - %s", line, str(e))
|
||||
continue
|
||||
|
||||
return volumes
|
||||
|
||||
async def reboot_system(self):
|
||||
"""重启系统"""
|
||||
self.logger.info("Initiating system reboot...")
|
||||
@@ -380,7 +503,6 @@ class SystemManager:
|
||||
await self.coordinator.run_command("sudo reboot")
|
||||
self.logger.info("Reboot command sent")
|
||||
|
||||
# 更新系统状态为重启中
|
||||
if "system" in self.coordinator.data:
|
||||
self.coordinator.data["system"]["status"] = "rebooting"
|
||||
self.coordinator.async_update_listeners()
|
||||
@@ -395,7 +517,6 @@ class SystemManager:
|
||||
await self.coordinator.run_command("sudo shutdown -h now")
|
||||
self.logger.info("Shutdown command sent")
|
||||
|
||||
# 立即更新系统状态为关闭
|
||||
if "system" in self.coordinator.data:
|
||||
self.coordinator.data["system"]["status"] = "off"
|
||||
self.coordinator.async_update_listeners()
|
||||
|
Reference in New Issue
Block a user