diff --git a/custom_components/fn_nas/manifest.json b/custom_components/fn_nas/manifest.json index 6aa0db7..b92cee9 100644 --- a/custom_components/fn_nas/manifest.json +++ b/custom_components/fn_nas/manifest.json @@ -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"], diff --git a/custom_components/fn_nas/sensor.py b/custom_components/fn_nas/sensor.py index 8c7b88f..9cfe745 100644 --- a/custom_components/fn_nas/sensor.py +++ b/custom_components/fn_nas/sensor.py @@ -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 "未知" \ No newline at end of file + 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 \ No newline at end of file diff --git a/custom_components/fn_nas/system_manager.py b/custom_components/fn_nas/system_manager.py index efb1362..213ba64 100644 --- a/custom_components/fn_nas/system_manager.py +++ b/custom_components/fn_nas/system_manager.py @@ -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()