增加可用内存和存储空间实体

This commit is contained in:
xiaochao
2025-07-01 14:18:22 +08:00
parent 671f5a48f8
commit e3bb42e3de
3 changed files with 287 additions and 9 deletions

View File

@@ -1,7 +1,7 @@
{ {
"domain": "fn_nas", "domain": "fn_nas",
"name": "飞牛NAS", "name": "飞牛NAS",
"version": "1.3.0", "version": "1.3.1",
"documentation": "https://github.com/anxms/fn_nas", "documentation": "https://github.com/anxms/fn_nas",
"dependencies": [], "dependencies": [],
"codeowners": ["@anxms"], "codeowners": ["@anxms"],

View File

@@ -243,6 +243,38 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
) )
) )
existing_ids.add(sensor_uid) 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) async_add_entities(entities)
@@ -537,4 +569,129 @@ class DockerContainerStatusSensor(CoordinatorEntity, SensorEntity):
"dead": "死亡" "dead": "死亡"
} }
return status_map.get(container["status"], container["status"]) 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

View File

@@ -58,15 +58,28 @@ class SystemManager:
if backup_cpu_temp: if backup_cpu_temp:
system_info["cpu_temperature"] = 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 return system_info
except Exception as e: except Exception as e:
self.logger.error("Error getting system info: %s", str(e)) self.logger.error("Error getting system info: %s", str(e))
# 在异常处理中返回空数据
return { return {
"uptime_seconds": 0, "uptime_seconds": 0,
"uptime": "未知", "uptime": "未知",
"cpu_temperature": "未知", "cpu_temperature": "未知",
"motherboard_temperature": "未知" "motherboard_temperature": "未知",
"memory_total": "未知",
"memory_used": "未知",
"memory_available": "未知",
"volumes": {}
} }
def save_sensor_data_for_debug(self, sensors_output: str): def save_sensor_data_for_debug(self, sensors_output: str):
@@ -192,7 +205,7 @@ class SystemManager:
if isinstance(temp_value, (int, float)): if isinstance(temp_value, (int, float)):
self.logger.debug("JSON中找到Tdie/Tctl温度: %s/%s = %.1f°C", key, subkey, temp_value) self.logger.debug("JSON中找到Tdie/Tctl温度: %s/%s = %.1f°C", key, subkey, temp_value)
return f"{temp_value:.1f} °C" return f"{temp_value:.1f} °C"
except Exception: except:
pass pass
except Exception as e: except Exception as e:
self.logger.warning("JSON解析失败: %s", str(e)) self.logger.warning("JSON解析失败: %s", str(e))
@@ -368,11 +381,121 @@ class SystemManager:
self.logger.warning("Using fallback motherboard temperature detection") self.logger.warning("Using fallback motherboard temperature detection")
return f"{avg_temp:.1f} °C" return f"{avg_temp:.1f} °C"
# self.logger.warning("No motherboard temperature found in sensors output")
return "未知" 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): async def reboot_system(self):
"""重启系统""" """重启系统"""
self.logger.info("Initiating system reboot...") self.logger.info("Initiating system reboot...")
@@ -380,7 +503,6 @@ class SystemManager:
await self.coordinator.run_command("sudo reboot") await self.coordinator.run_command("sudo reboot")
self.logger.info("Reboot command sent") self.logger.info("Reboot command sent")
# 更新系统状态为重启中
if "system" in self.coordinator.data: if "system" in self.coordinator.data:
self.coordinator.data["system"]["status"] = "rebooting" self.coordinator.data["system"]["status"] = "rebooting"
self.coordinator.async_update_listeners() self.coordinator.async_update_listeners()
@@ -395,7 +517,6 @@ class SystemManager:
await self.coordinator.run_command("sudo shutdown -h now") await self.coordinator.run_command("sudo shutdown -h now")
self.logger.info("Shutdown command sent") self.logger.info("Shutdown command sent")
# 立即更新系统状态为关闭
if "system" in self.coordinator.data: if "system" in self.coordinator.data:
self.coordinator.data["system"]["status"] = "off" self.coordinator.data["system"]["status"] = "off"
self.coordinator.async_update_listeners() self.coordinator.async_update_listeners()