From f185b7e3ee66ae80016775f52b25438afba08a35 Mon Sep 17 00:00:00 2001 From: xiaochao Date: Fri, 11 Jul 2025 23:45:05 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=A3=9E=E7=89=9Bnas?= =?UTF-8?q?=E5=9C=A8=E5=85=B3=E6=9C=BA=E5=88=B0=E5=BC=80=E6=9C=BA=E6=97=B6?= =?UTF-8?q?=EF=BC=8Chome=20assistant=E5=AE=9E=E4=BD=93=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E6=97=A0=E6=B3=95=E6=9B=B4=E6=96=B0=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_components/fn_nas/__init__.py | 128 ++++++++++++++------ custom_components/fn_nas/coordinator.py | 151 ++++++++++++++++++------ 2 files changed, 208 insertions(+), 71 deletions(-) diff --git a/custom_components/fn_nas/__init__.py b/custom_components/fn_nas/__init__.py index 4eab229..c49dcf4 100644 --- a/custom_components/fn_nas/__init__.py +++ b/custom_components/fn_nas/__init__.py @@ -1,7 +1,14 @@ import logging +import asyncio +import asyncssh from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import DOMAIN, DATA_UPDATE_COORDINATOR, PLATFORMS, CONF_ENABLE_DOCKER # 导入新增常量 +from homeassistant.helpers import config_validation as cv + +from .const import ( + DOMAIN, DATA_UPDATE_COORDINATOR, PLATFORMS, CONF_ENABLE_DOCKER, + CONF_HOST, DEFAULT_PORT +) from .coordinator import FlynasCoordinator, UPSDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -9,54 +16,99 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): config = {**entry.data, **entry.options} - coordinator = FlynasCoordinator(hass, config) - await coordinator.async_config_entry_first_refresh() - _LOGGER.debug("协调器类型: %s", type(coordinator).__name__) - _LOGGER.debug("协调器是否有control_vm方法: %s", hasattr(coordinator, 'control_vm')) - _LOGGER.debug("协调器是否有vm_manager属性: %s", hasattr(coordinator, 'vm_manager')) + coordinator = FlynasCoordinator(hass, config, entry) - # 检查是否启用Docker,并初始化Docker管理器(如果有) - enable_docker = config.get(CONF_ENABLE_DOCKER, False) - if enable_docker: - # 导入Docker管理器并初始化 - from .docker_manager import DockerManager - coordinator.docker_manager = DockerManager(coordinator) - _LOGGER.debug("已启用Docker容器监控") - else: - coordinator.docker_manager = None - _LOGGER.debug("未启用Docker容器监控") + setup_task = hass.async_create_task( + async_delayed_setup(hass, entry, coordinator), + f"fn_nas_setup_{entry.entry_id}" + ) - ups_coordinator = UPSDataUpdateCoordinator(hass, config, coordinator) - await ups_coordinator.async_config_entry_first_refresh() - - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = { - DATA_UPDATE_COORDINATOR: coordinator, - "ups_coordinator": ups_coordinator, - CONF_ENABLE_DOCKER: enable_docker # 存储启用状态 - } - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - entry.async_on_unload(entry.add_update_listener(async_update_entry)) return True +async def async_delayed_setup(hass: HomeAssistant, entry: ConfigEntry, coordinator: FlynasCoordinator): + try: + # 首先进行轻量级系统状态检测 + is_online = await coordinator.ping_system() + + if not is_online: + _LOGGER.warning("系统离线,等待系统上线...") + # 等待系统上线 + while not await coordinator.ping_system(): + await asyncio.sleep(30) + + _LOGGER.info("系统已上线,继续初始化飞牛NAS集成") + + # 系统在线,继续正常初始化 + await coordinator.async_config_entry_first_refresh() + + # 检查是否启用Docker,并初始化Docker管理器(如果有) + enable_docker = coordinator.config.get(CONF_ENABLE_DOCKER, False) + if enable_docker: + # 导入Docker管理器并初始化 + from .docker_manager import DockerManager + coordinator.docker_manager = DockerManager(coordinator) + _LOGGER.debug("已启用Docker容器监控") + else: + coordinator.docker_manager = None + _LOGGER.debug("未启用Docker容器监控") + + ups_coordinator = UPSDataUpdateCoordinator(hass, coordinator.config, coordinator) + await ups_coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = { + DATA_UPDATE_COORDINATOR: coordinator, + "ups_coordinator": ups_coordinator, + CONF_ENABLE_DOCKER: enable_docker # 存储启用状态 + } + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(async_update_entry)) + + _LOGGER.info("飞牛NAS集成初始化完成") + + except Exception as e: + _LOGGER.error("飞牛NAS集成初始化失败: %s", str(e)) + # 如果初始化失败,确保清理资源 + await coordinator.async_disconnect() + if hasattr(coordinator, '_ping_task') and coordinator._ping_task: + coordinator._ping_task.cancel() + async def async_update_entry(hass: HomeAssistant, entry: ConfigEntry): - await hass.config_entries.async_reload(entry.entry_id) + """更新配置项""" + # 卸载现有集成 + await async_unload_entry(hass, entry) + # 重新加载集成 + await async_setup_entry(hass, entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + """卸载集成""" + # 获取集成数据 + domain_data = hass.data.get(DOMAIN, {}).get(entry.entry_id, {}) + unload_ok = True - if unload_ok: - domain_data = hass.data[DOMAIN][entry.entry_id] + if DATA_UPDATE_COORDINATOR in domain_data: coordinator = domain_data[DATA_UPDATE_COORDINATOR] - ups_coordinator = domain_data["ups_coordinator"] + ups_coordinator = domain_data.get("ups_coordinator") - # 关闭主协调器的SSH连接 - await coordinator.async_disconnect() - # 关闭UPS协调器 - await ups_coordinator.async_shutdown() + # 卸载平台 + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - # 从DOMAIN中移除该entry的数据 - hass.data[DOMAIN].pop(entry.entry_id) + if unload_ok: + # 关闭主协调器的SSH连接 + await coordinator.async_disconnect() + + # 关闭UPS协调器(如果存在) + if ups_coordinator: + await ups_coordinator.async_shutdown() + + # 取消监控任务(如果存在) + if hasattr(coordinator, '_ping_task') and coordinator._ping_task and not coordinator._ping_task.done(): + coordinator._ping_task.cancel() + + # 从DOMAIN中移除该entry的数据 + hass.data[DOMAIN].pop(entry.entry_id, None) + return unload_ok \ No newline at end of file diff --git a/custom_components/fn_nas/coordinator.py b/custom_components/fn_nas/coordinator.py index 7bce521..a655bce 100644 --- a/custom_components/fn_nas/coordinator.py +++ b/custom_components/fn_nas/coordinator.py @@ -1,5 +1,5 @@ import logging -import re +import asyncio import asyncssh from datetime import timedelta from homeassistant.core import HomeAssistant @@ -20,8 +20,10 @@ from .docker_manager import DockerManager _LOGGER = logging.getLogger(__name__) class FlynasCoordinator(DataUpdateCoordinator): - def __init__(self, hass: HomeAssistant, config) -> None: + def __init__(self, hass: HomeAssistant, config, config_entry) -> None: self.config = config + self.config_entry = config_entry + self.hass = hass self.host = config[CONF_HOST] self.port = config.get(CONF_PORT, DEFAULT_PORT) self.username = config[CONF_USERNAME] @@ -60,6 +62,9 @@ class FlynasCoordinator(DataUpdateCoordinator): self.disk_manager = DiskManager(self) self.system_manager = SystemManager(self) + self._system_online = False + self._ping_task = None + self._retry_interval = 30 # 系统离线时的检测间隔(秒) async def async_connect(self): if self.ssh is None or self.ssh_closed: @@ -69,7 +74,8 @@ class FlynasCoordinator(DataUpdateCoordinator): port=self.port, username=self.username, password=self.password, - known_hosts=None + known_hosts=None, + connect_timeout=5 # 缩短连接超时时间 ) if await self.is_root_user(): @@ -81,7 +87,7 @@ class FlynasCoordinator(DataUpdateCoordinator): result = await self.ssh.run( f"echo '{self.password}' | sudo -S -i", input=self.password + "\n", - timeout=10 + timeout=5 ) whoami_result = await self.ssh.run("whoami") @@ -95,7 +101,7 @@ class FlynasCoordinator(DataUpdateCoordinator): result = await self.ssh.run( f"echo '{self.root_password}' | sudo -S -i", input=self.root_password + "\n", - timeout=10 + timeout=5 ) whoami_result = await self.ssh.run("whoami") @@ -117,13 +123,13 @@ class FlynasCoordinator(DataUpdateCoordinator): except Exception as e: self.ssh = None self.ssh_closed = True - _LOGGER.error("连接失败: %s", str(e), exc_info=True) + _LOGGER.debug("连接失败: %s", str(e)) return False return True async def is_root_user(self): try: - result = await self.ssh.run("id -u", timeout=5) + result = await self.ssh.run("id -u", timeout=3) return result.stdout.strip() == "0" except Exception: return False @@ -133,9 +139,9 @@ class FlynasCoordinator(DataUpdateCoordinator): try: self.ssh.close() self.ssh_closed = True - _LOGGER.info("SSH connection closed") + _LOGGER.debug("SSH connection closed") except Exception as e: - _LOGGER.error("Error closing SSH connection: %s", str(e)) + _LOGGER.debug("Error closing SSH connection: %s", str(e)) finally: self.ssh = None @@ -150,14 +156,36 @@ class FlynasCoordinator(DataUpdateCoordinator): except (asyncssh.Error, TimeoutError): return False + async def ping_system(self) -> bool: + """轻量级系统状态检测""" + # 对于本地主机直接返回True + if self.host in ['localhost', '127.0.0.1']: + return True + + try: + # 使用异步ping检测 + proc = await asyncio.create_subprocess_exec( + 'ping', '-c', '1', '-W', '1', self.host, + stdout=asyncio.subprocess.DEVNULL, + stderr=asyncio.subprocess.DEVNULL + ) + await proc.wait() + return proc.returncode == 0 + except Exception: + return False + async def run_command(self, command: str, retries=2) -> str: + # 系统离线时直接返回空字符串,避免抛出异常 + if not self._system_online: + return "" + for attempt in range(retries): try: if not await self.is_ssh_connected(): if not await self.async_connect(): if self.data and "system" in self.data: self.data["system"]["status"] = "off" - raise UpdateFailed("SSH 连接失败") + return "" if self.use_sudo: if self.root_password or self.password: @@ -175,31 +203,26 @@ class FlynasCoordinator(DataUpdateCoordinator): except asyncssh.process.ProcessError as e: if e.exit_status in [4, 32]: return "" - _LOGGER.error("Command failed: %s (exit %d)", command, e.exit_status) + _LOGGER.debug("Command failed: %s (exit %d)", command, e.exit_status) self.ssh = None self.ssh_closed = True if attempt == retries - 1: - if self.data and "system" in self.data: - self.data["system"]["status"] = "off" - raise UpdateFailed(f"Command failed after {retries} attempts: {command}") from e + return "" except asyncssh.Error as e: - _LOGGER.error("SSH connection error: %s", str(e)) + _LOGGER.debug("SSH connection error: %s", str(e)) self.ssh = None self.ssh_closed = True if attempt == retries - 1: - if self.data and "system" in self.data: - self.data["system"]["status"] = "off" - raise UpdateFailed(f"SSH error after {retries} attempts: {str(e)}") from e + return "" except Exception as e: self.ssh = None self.ssh_closed = True - _LOGGER.error("Unexpected error: %s", str(e), exc_info=True) + _LOGGER.debug("Unexpected error: %s", str(e)) if attempt == retries - 1: - if self.data and "system" in self.data: - self.data["system"]["status"] = "off" - raise UpdateFailed(f"Unexpected error after {retries} attempts") from e + return "" + return "" async def get_network_macs(self): try: @@ -216,20 +239,73 @@ class FlynasCoordinator(DataUpdateCoordinator): return macs except Exception as e: - self.logger.error("获取MAC地址失败: %s", str(e)) + self.logger.debug("获取MAC地址失败: %s", str(e)) return {} + async def _monitor_system_status(self): + """系统离线时轮询检测状态""" + self.logger.debug("启动系统状态监控,每%d秒检测一次", self._retry_interval) + while True: + await asyncio.sleep(self._retry_interval) + + if await self.ping_system(): + self.logger.info("检测到系统已开机,触发重新加载") + # 触发集成重新加载 + self.hass.async_create_task( + self.hass.config_entries.async_reload(self.config_entry.entry_id) + ) + break + async def _async_update_data(self): _LOGGER.debug("Starting data update...") + # 先进行轻量级系统状态检测 + is_online = await self.ping_system() + self._system_online = is_online + + # 系统离线处理 + if not is_online: + _LOGGER.debug("系统离线,跳过数据更新") + self.data["system"]["status"] = "off" + + # 启动监控任务(如果尚未启动) + if not self._ping_task or self._ping_task.done(): + self._ping_task = asyncio.create_task(self._monitor_system_status()) + + # 关闭SSH连接 + await self.async_disconnect() + + return { + "disks": [], + "system": { + "uptime": "未知", + "cpu_temperature": "未知", + "motherboard_temperature": "未知", + "status": "off" + }, + "ups": {}, + "vms": [], + "docker_containers": [] + } + + # 系统在线处理 try: - if await self.is_ssh_connected(): - status = "on" - else: - if not await self.async_connect(): - status = "off" - else: - status = "on" + # 确保SSH连接 + if not await self.async_connect(): + self.data["system"]["status"] = "off" + return { + "disks": [], + "system": { + "uptime": "未知", + "cpu_temperature": "未知", + "motherboard_temperature": "未知", + "status": "off" + }, + "ups": {}, + "vms": [] + } + + status = "on" disks = await self.disk_manager.get_disks_info() system = await self.system_manager.get_system_info() @@ -257,7 +333,12 @@ class FlynasCoordinator(DataUpdateCoordinator): return data except Exception as e: - _LOGGER.error("Failed to update data: %s", str(e), exc_info=True) + _LOGGER.debug("数据更新失败: %s", str(e)) + # 检查错误类型,如果是连接问题,标记为离线 + self._system_online = False + if not self._ping_task or self._ping_task.done(): + self._ping_task = asyncio.create_task(self._monitor_system_status()) + return { "disks": [], "system": { @@ -297,10 +378,14 @@ class UPSDataUpdateCoordinator(DataUpdateCoordinator): self.ups_manager = UPSManager(main_coordinator) async def _async_update_data(self): + # 如果主协调器检测到系统离线,跳过UPS更新 + if not self.main_coordinator._system_online: + return {} + try: return await self.ups_manager.get_ups_info() except Exception as e: - _LOGGER.error("Failed to update UPS data: %s", str(e), exc_info=True) + _LOGGER.debug("UPS数据更新失败: %s", str(e)) return {} async def control_vm(self, vm_name, action): @@ -311,5 +396,5 @@ class UPSDataUpdateCoordinator(DataUpdateCoordinator): result = await self.vm_manager.control_vm(vm_name, action) return result except Exception as e: - _LOGGER.error("虚拟机控制失败: %s", str(e), exc_info=True) + _LOGGER.debug("虚拟机控制失败: %s", str(e)) return False \ No newline at end of file