新增docker容器控制功能

This commit is contained in:
xiaochao
2025-06-30 16:54:37 +08:00
parent 8211d194cf
commit d9b02d93d2
10 changed files with 279 additions and 18 deletions

View File

@@ -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)

View File

@@ -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,
@@ -85,4 +102,64 @@ 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)
_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,
"操作类型": "重启容器",
"提示": "重启操作可能需要一些时间完成"
}

View File

@@ -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(

View File

@@ -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

View File

@@ -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)
@@ -235,6 +238,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,
@@ -243,7 +250,8 @@ class FlynasCoordinator(DataUpdateCoordinator):
"status": status
},
"ups": ups_info,
"vms": vms
"vms": vms,
"docker_containers": docker_containers
}
return data

View 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

View File

@@ -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"],

View File

@@ -228,7 +228,22 @@ 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)
@@ -494,4 +509,32 @@ class VMStatusSensor(CoordinatorEntity, SensorEntity):
return "mdi:server-off"
elif vm["state"] == "rebooting":
return "mdi:server-security"
return "mdi:server"
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 "未知"

View File

@@ -23,7 +23,19 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
vm.get("title", vm["name"])
)
)
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):
@@ -146,4 +158,43 @@ class VMSwitch(CoordinatorEntity, SwitchEntity):
"虚拟机ID": vm["id"],
"原始状态": vm["state"]
}
return {}
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()

View File

@@ -9,7 +9,8 @@
"port": "端口",
"username": "用户名",
"password": "密码",
"scan_interval": "数据更新间隔(秒)"
"scan_interval": "数据更新间隔(秒)",
"enable_docker": "启用docker控制"
}
},
"select_mac": {