mirror of
https://github.com/anxms/fn_nas.git
synced 2025-10-15 09:38:26 +00:00
新增docker容器控制功能
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from .const import DOMAIN, DATA_UPDATE_COORDINATOR, PLATFORMS
|
||||
from .const import DOMAIN, DATA_UPDATE_COORDINATOR, PLATFORMS, CONF_ENABLE_DOCKER # 导入新增常量
|
||||
from .coordinator import FlynasCoordinator, UPSDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -16,13 +16,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
_LOGGER.debug("协调器是否有control_vm方法: %s", hasattr(coordinator, 'control_vm'))
|
||||
_LOGGER.debug("协调器是否有vm_manager属性: %s", hasattr(coordinator, 'vm_manager'))
|
||||
|
||||
# 检查是否启用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容器监控")
|
||||
|
||||
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
|
||||
"ups_coordinator": ups_coordinator,
|
||||
CONF_ENABLE_DOCKER: enable_docker # 存储启用状态
|
||||
}
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
@@ -2,13 +2,16 @@ import logging
|
||||
from homeassistant.components.button import ButtonEntity
|
||||
from homeassistant.helpers.entity import EntityCategory
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from .const import DOMAIN, DATA_UPDATE_COORDINATOR, DEVICE_ID_NAS
|
||||
from .const import (
|
||||
DOMAIN, DATA_UPDATE_COORDINATOR, DEVICE_ID_NAS, CONF_ENABLE_DOCKER
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
domain_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = domain_data[DATA_UPDATE_COORDINATOR]
|
||||
enable_docker = domain_data.get(CONF_ENABLE_DOCKER, False)
|
||||
|
||||
entities = []
|
||||
|
||||
@@ -23,7 +26,21 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
coordinator,
|
||||
vm["name"],
|
||||
vm.get("title", vm["name"]),
|
||||
config_entry.entry_id # 传递entry_id用于生成唯一ID
|
||||
config_entry.entry_id
|
||||
)
|
||||
)
|
||||
|
||||
# 3. 添加Docker容器重启按钮(如果启用了Docker功能)
|
||||
if enable_docker and "docker_containers" in coordinator.data:
|
||||
for container in coordinator.data["docker_containers"]:
|
||||
# 使用容器名称生成安全ID(替换特殊字符)
|
||||
safe_name = container["name"].replace(" ", "_").replace("/", "_").replace(".", "_")
|
||||
entities.append(
|
||||
DockerContainerRestartButton(
|
||||
coordinator,
|
||||
container["name"],
|
||||
safe_name,
|
||||
config_entry.entry_id
|
||||
)
|
||||
)
|
||||
|
||||
@@ -33,7 +50,7 @@ class RebootButton(CoordinatorEntity, ButtonEntity):
|
||||
def __init__(self, coordinator, entry_id):
|
||||
super().__init__(coordinator)
|
||||
self._attr_name = "重启"
|
||||
self._attr_unique_id = f"{entry_id}_flynas_reboot" # 使用entry_id确保唯一性
|
||||
self._attr_unique_id = f"{entry_id}_flynas_reboot"
|
||||
self._attr_entity_category = EntityCategory.CONFIG
|
||||
self._attr_device_info = {
|
||||
"identifiers": {(DOMAIN, DEVICE_ID_NAS)},
|
||||
@@ -58,7 +75,7 @@ class VMRebootButton(CoordinatorEntity, ButtonEntity):
|
||||
self.vm_name = vm_name
|
||||
self.vm_title = vm_title
|
||||
self._attr_name = f"{vm_title} 重启"
|
||||
self._attr_unique_id = f"{entry_id}_flynas_vm_{vm_name}_reboot" # 使用entry_id确保唯一性
|
||||
self._attr_unique_id = f"{entry_id}_flynas_vm_{vm_name}_reboot"
|
||||
self._attr_device_info = {
|
||||
"identifiers": {(DOMAIN, f"vm_{vm_name}")},
|
||||
"name": vm_title,
|
||||
@@ -86,3 +103,63 @@ class VMRebootButton(CoordinatorEntity, ButtonEntity):
|
||||
self.coordinator.async_add_listener(self.async_write_ha_state)
|
||||
except Exception as e:
|
||||
_LOGGER.error("重启虚拟机时出错: %s", str(e), exc_info=True)
|
||||
|
||||
class DockerContainerRestartButton(CoordinatorEntity, ButtonEntity):
|
||||
def __init__(self, coordinator, container_name, safe_name, entry_id):
|
||||
super().__init__(coordinator)
|
||||
self.container_name = container_name
|
||||
self.safe_name = safe_name
|
||||
self._attr_name = f"{container_name} 重启"
|
||||
self._attr_unique_id = f"{entry_id}_docker_{safe_name}_restart"
|
||||
self._attr_device_info = {
|
||||
"identifiers": {(DOMAIN, f"docker_{safe_name}")},
|
||||
"name": container_name,
|
||||
"via_device": (DOMAIN, DEVICE_ID_NAS)
|
||||
}
|
||||
self._attr_icon = "mdi:docker"
|
||||
|
||||
async def async_press(self):
|
||||
"""重启Docker容器"""
|
||||
# 检查是否启用了Docker功能
|
||||
if not hasattr(self.coordinator, 'docker_manager') or self.coordinator.docker_manager is None:
|
||||
_LOGGER.error("Docker管理功能未启用,无法重启容器 %s", self.container_name)
|
||||
return
|
||||
|
||||
try:
|
||||
# 更新状态为"重启中"
|
||||
for container in self.coordinator.data.get("docker_containers", []):
|
||||
if container["name"] == self.container_name:
|
||||
container["status"] = "restarting"
|
||||
self.async_write_ha_state()
|
||||
|
||||
# 执行重启命令
|
||||
success = await self.coordinator.docker_manager.control_container(self.container_name, "restart")
|
||||
|
||||
if success:
|
||||
_LOGGER.info("Docker容器 %s 重启命令已发送", self.container_name)
|
||||
|
||||
# 强制刷新状态(因为容器重启可能需要时间)
|
||||
self.coordinator.async_request_refresh()
|
||||
else:
|
||||
_LOGGER.error("Docker容器 %s 重启失败", self.container_name)
|
||||
# 恢复原始状态
|
||||
for container in self.coordinator.data.get("docker_containers", []):
|
||||
if container["name"] == self.container_name:
|
||||
container["status"] = "running" # 假设重启失败后状态不变
|
||||
self.async_write_ha_state()
|
||||
|
||||
except Exception as e:
|
||||
_LOGGER.error("重启Docker容器 %s 时出错: %s", self.container_name, str(e), exc_info=True)
|
||||
# 恢复原始状态
|
||||
for container in self.coordinator.data.get("docker_containers", []):
|
||||
if container["name"] == self.container_name:
|
||||
container["status"] = "running"
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
return {
|
||||
"容器名称": self.container_name,
|
||||
"操作类型": "重启容器",
|
||||
"提示": "重启操作可能需要一些时间完成"
|
||||
}
|
@@ -17,7 +17,8 @@ from .const import (
|
||||
CONF_FAN_CONFIG_PATH,
|
||||
CONF_UPS_SCAN_INTERVAL,
|
||||
DEFAULT_UPS_SCAN_INTERVAL,
|
||||
CONF_ROOT_PASSWORD
|
||||
CONF_ROOT_PASSWORD,
|
||||
CONF_ENABLE_DOCKER
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -68,7 +69,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
vol.Optional(
|
||||
CONF_SCAN_INTERVAL,
|
||||
default=DEFAULT_SCAN_INTERVAL
|
||||
): int
|
||||
): int,
|
||||
# 添加启用Docker的选项
|
||||
vol.Optional(CONF_ENABLE_DOCKER, default=False): bool
|
||||
})
|
||||
|
||||
return self.async_show_form(
|
||||
@@ -96,7 +99,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if user_input is not None:
|
||||
selected_mac = user_input.get(CONF_MAC)
|
||||
if selected_mac:
|
||||
# 将CONF_ENABLE_DOCKER从ssh_config复制到最终配置
|
||||
enable_docker = self.ssh_config.get(CONF_ENABLE_DOCKER, False)
|
||||
self.ssh_config[CONF_MAC] = selected_mac
|
||||
# 确保将CONF_ENABLE_DOCKER也存入配置项
|
||||
self.ssh_config[CONF_ENABLE_DOCKER] = enable_docker
|
||||
return self.async_create_entry(
|
||||
title=self.ssh_config[CONF_HOST],
|
||||
data=self.ssh_config
|
||||
@@ -208,7 +215,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
vol.Optional(
|
||||
CONF_UPS_SCAN_INTERVAL,
|
||||
default=data.get(CONF_UPS_SCAN_INTERVAL, DEFAULT_UPS_SCAN_INTERVAL)
|
||||
): int
|
||||
): int,
|
||||
# 在选项流程中也添加启用Docker的选项
|
||||
vol.Optional(
|
||||
CONF_ENABLE_DOCKER,
|
||||
default=data.get(CONF_ENABLE_DOCKER, False)
|
||||
): bool
|
||||
})
|
||||
|
||||
return self.async_show_form(
|
||||
|
@@ -18,6 +18,8 @@ CONF_FAN_CONFIG_PATH = "fan_config_path"
|
||||
CONF_IGNORE_DISKS = "ignore_disks"
|
||||
CONF_MAC = "mac"
|
||||
CONF_UPS_SCAN_INTERVAL = "ups_scan_interval"
|
||||
CONF_ENABLE_DOCKER = "enable_docker"
|
||||
DOCKER_CONTAINERS = "docker_containers"
|
||||
|
||||
DEFAULT_PORT = 22
|
||||
DEFAULT_SCAN_INTERVAL = 60
|
||||
|
@@ -9,12 +9,13 @@ from .const import (
|
||||
DOMAIN, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_IGNORE_DISKS, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
|
||||
DEFAULT_PORT, CONF_MAC, CONF_UPS_SCAN_INTERVAL, DEFAULT_UPS_SCAN_INTERVAL,
|
||||
CONF_ROOT_PASSWORD
|
||||
CONF_ROOT_PASSWORD, CONF_ENABLE_DOCKER
|
||||
)
|
||||
from .disk_manager import DiskManager
|
||||
from .system_manager import SystemManager
|
||||
from .ups_manager import UPSManager
|
||||
from .vm_manager import VMManager
|
||||
from .docker_manager import DockerManager
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -27,6 +28,8 @@ class FlynasCoordinator(DataUpdateCoordinator):
|
||||
self.password = config[CONF_PASSWORD]
|
||||
self.root_password = config.get(CONF_ROOT_PASSWORD)
|
||||
self.mac = config.get(CONF_MAC, "")
|
||||
self.enable_docker = config.get(CONF_ENABLE_DOCKER, False)
|
||||
self.docker_manager = DockerManager(self) if self.enable_docker else None
|
||||
self.ssh = None
|
||||
self.ssh_closed = True
|
||||
self.ups_manager = UPSManager(self)
|
||||
@@ -236,6 +239,10 @@ class FlynasCoordinator(DataUpdateCoordinator):
|
||||
for vm in vms:
|
||||
vm["title"] = await self.vm_manager.get_vm_title(vm["name"])
|
||||
|
||||
docker_containers = []
|
||||
if self.enable_docker:
|
||||
docker_containers = await self.docker_manager.get_containers()
|
||||
|
||||
data = {
|
||||
"disks": disks,
|
||||
"system": {
|
||||
@@ -243,7 +250,8 @@ class FlynasCoordinator(DataUpdateCoordinator):
|
||||
"status": status
|
||||
},
|
||||
"ups": ups_info,
|
||||
"vms": vms
|
||||
"vms": vms,
|
||||
"docker_containers": docker_containers
|
||||
}
|
||||
|
||||
return data
|
||||
|
55
custom_components/fn_nas/docker_manager.py
Normal file
55
custom_components/fn_nas/docker_manager.py
Normal file
@@ -0,0 +1,55 @@
|
||||
import logging
|
||||
import json
|
||||
from typing import List, Dict
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
class DockerManager:
|
||||
def __init__(self, coordinator):
|
||||
self.coordinator = coordinator
|
||||
self.logger = _LOGGER.getChild("docker_manager")
|
||||
self.logger.setLevel(logging.DEBUG)
|
||||
|
||||
async def get_containers(self) -> List[Dict[str, str]]:
|
||||
"""获取Docker容器列表及其状态"""
|
||||
try:
|
||||
# 使用docker命令获取容器列表,格式为JSON
|
||||
output = await self.coordinator.run_command("docker ps -a --format '{{json .}}'")
|
||||
self.logger.debug("Docker容器原始输出: %s", output)
|
||||
containers = []
|
||||
|
||||
# 每行一个容器的JSON
|
||||
for line in output.splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
try:
|
||||
# 解析JSON
|
||||
container_info = json.loads(line)
|
||||
# 提取所需字段
|
||||
container = {
|
||||
"id": container_info.get("ID", ""),
|
||||
"name": container_info.get("Names", ""),
|
||||
"status": container_info.get("State", "").lower(),
|
||||
"image": container_info.get("Image", ""),
|
||||
}
|
||||
containers.append(container)
|
||||
except json.JSONDecodeError:
|
||||
self.logger.warning("解析Docker容器信息失败: %s", line)
|
||||
return containers
|
||||
except Exception as e:
|
||||
self.logger.error("获取Docker容器列表失败: %s", str(e), exc_info=True)
|
||||
return []
|
||||
|
||||
async def control_container(self, container_name, action):
|
||||
"""控制容器操作"""
|
||||
valid_actions = ["start", "stop", "restart"]
|
||||
if action not in valid_actions:
|
||||
raise ValueError(f"无效操作: {action}")
|
||||
|
||||
command = f"docker {action} {container_name}"
|
||||
try:
|
||||
await self.coordinator.run_command(command)
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error("执行Docker容器操作失败: %s", str(e), exc_info=True)
|
||||
return False
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "fn_nas",
|
||||
"name": "飞牛NAS",
|
||||
"version": "1.2.4",
|
||||
"version": "1.3.0",
|
||||
"documentation": "https://github.com/anxms/fn_nas",
|
||||
"dependencies": [],
|
||||
"codeowners": ["@anxms"],
|
||||
|
@@ -229,6 +229,21 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
)
|
||||
existing_ids.add(ups_status_uid)
|
||||
|
||||
if coordinator.data.get("docker_containers") and coordinator.enable_docker:
|
||||
for container in coordinator.data["docker_containers"]:
|
||||
safe_name = container["name"].replace(" ", "_").replace("/", "_")
|
||||
sensor_uid = f"{config_entry.entry_id}_docker_{safe_name}_status"
|
||||
if sensor_uid not in existing_ids:
|
||||
entities.append(
|
||||
DockerContainerStatusSensor(
|
||||
coordinator,
|
||||
container["name"],
|
||||
safe_name,
|
||||
config_entry.entry_id
|
||||
)
|
||||
)
|
||||
existing_ids.add(sensor_uid)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@@ -495,3 +510,31 @@ class VMStatusSensor(CoordinatorEntity, SensorEntity):
|
||||
elif vm["state"] == "rebooting":
|
||||
return "mdi:server-security"
|
||||
return "mdi:server"
|
||||
|
||||
# 添加DockerContainerStatusSensor类
|
||||
class DockerContainerStatusSensor(CoordinatorEntity, SensorEntity):
|
||||
def __init__(self, coordinator, container_name, safe_name, entry_id):
|
||||
super().__init__(coordinator)
|
||||
self.container_name = container_name
|
||||
self._attr_name = f"{container_name} 状态"
|
||||
self._attr_unique_id = f"{entry_id}_docker_{safe_name}_status"
|
||||
self._attr_device_info = {
|
||||
"identifiers": {(DOMAIN, f"docker_{safe_name}")},
|
||||
"name": container_name,
|
||||
"via_device": (DOMAIN, DEVICE_ID_NAS)
|
||||
}
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
for container in self.coordinator.data.get("docker_containers", []):
|
||||
if container["name"] == self.container_name:
|
||||
# 状态映射为中文
|
||||
status_map = {
|
||||
"running": "运行中",
|
||||
"exited": "已停止",
|
||||
"paused": "已暂停",
|
||||
"restarting": "重启中",
|
||||
"dead": "死亡"
|
||||
}
|
||||
return status_map.get(container["status"], container["status"])
|
||||
return "未知"
|
@@ -24,6 +24,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
)
|
||||
)
|
||||
|
||||
if coordinator.data.get("docker_containers") and coordinator.enable_docker:
|
||||
for container in coordinator.data["docker_containers"]:
|
||||
# 使用容器名称作为唯一ID的一部分
|
||||
safe_name = container["name"].replace(" ", "_").replace("/", "_")
|
||||
entities.append(
|
||||
DockerContainerSwitch(
|
||||
coordinator,
|
||||
container["name"],
|
||||
safe_name
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
class PowerSwitch(CoordinatorEntity, SwitchEntity):
|
||||
@@ -147,3 +159,42 @@ class VMSwitch(CoordinatorEntity, SwitchEntity):
|
||||
"原始状态": vm["state"]
|
||||
}
|
||||
return {}
|
||||
|
||||
# 添加DockerContainerSwitch类
|
||||
class DockerContainerSwitch(CoordinatorEntity, SwitchEntity):
|
||||
def __init__(self, coordinator, container_name, safe_name):
|
||||
super().__init__(coordinator)
|
||||
self.container_name = container_name
|
||||
self._attr_name = f"{container_name} 容器"
|
||||
self._attr_unique_id = f"docker_{safe_name}_switch"
|
||||
self._attr_device_info = {
|
||||
"identifiers": {(DOMAIN, f"docker_{safe_name}")},
|
||||
"name": container_name,
|
||||
"via_device": (DOMAIN, DEVICE_ID_NAS)
|
||||
}
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
for container in self.coordinator.data.get("docker_containers", []):
|
||||
if container["name"] == self.container_name:
|
||||
return container["status"] == "running"
|
||||
return False
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
if self.coordinator.docker_manager:
|
||||
success = await self.coordinator.docker_manager.control_container(self.container_name, "start")
|
||||
if success:
|
||||
# 更新状态
|
||||
for container in self.coordinator.data.get("docker_containers", []):
|
||||
if container["name"] == self.container_name:
|
||||
container["status"] = "running"
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_off(self, **kwargs):
|
||||
if self.coordinator.docker_manager:
|
||||
success = await self.coordinator.docker_manager.control_container(self.container_name, "stop")
|
||||
if success:
|
||||
for container in self.coordinator.data.get("docker_containers", []):
|
||||
if container["name"] == self.container_name:
|
||||
container["status"] = "exited" # Docker停止后状态为exited
|
||||
self.async_write_ha_state()
|
@@ -9,7 +9,8 @@
|
||||
"port": "端口",
|
||||
"username": "用户名",
|
||||
"password": "密码",
|
||||
"scan_interval": "数据更新间隔(秒)"
|
||||
"scan_interval": "数据更新间隔(秒)",
|
||||
"enable_docker": "启用docker控制"
|
||||
}
|
||||
},
|
||||
"select_mac": {
|
||||
|
Reference in New Issue
Block a user