2024-08-26 14:29:09 +02:00
|
|
|
import asyncio
|
2024-08-26 19:16:44 +02:00
|
|
|
from typing import Optional, Dict, Any
|
2024-08-25 23:20:39 +02:00
|
|
|
|
2024-08-26 14:29:09 +02:00
|
|
|
import asyncssh
|
2024-08-26 14:44:20 +02:00
|
|
|
from asyncssh import SSHClientConnection
|
2024-08-26 14:29:09 +02:00
|
|
|
from wakeonlan import send_magic_packet
|
2024-08-25 23:20:39 +02:00
|
|
|
|
2024-08-26 19:16:44 +02:00
|
|
|
from custom_components.easy_computer_manager import const, LOGGER
|
2024-08-26 20:39:22 +02:00
|
|
|
from custom_components.easy_computer_manager.computer.common import OSType, CommandOutput
|
|
|
|
from custom_components.easy_computer_manager.computer.utils import parse_gnome_monitors_output, \
|
|
|
|
parse_pactl_output, parse_bluetoothctl
|
2024-08-25 23:20:39 +02:00
|
|
|
|
|
|
|
|
|
|
|
class Computer:
|
|
|
|
def __init__(self, host: str, mac: str, username: str, password: str, port: int = 22,
|
|
|
|
dualboot: bool = False) -> None:
|
2024-08-26 19:16:44 +02:00
|
|
|
"""Initialize the Computer object."""
|
2024-08-25 23:20:39 +02:00
|
|
|
self.host = host
|
|
|
|
self.mac = mac
|
2024-08-26 20:39:22 +02:00
|
|
|
self.username = username
|
2024-08-25 23:20:39 +02:00
|
|
|
self._password = password
|
2024-08-26 20:39:22 +02:00
|
|
|
self.port = port
|
|
|
|
self.dualboot = dualboot
|
2024-08-25 23:20:39 +02:00
|
|
|
|
2024-08-26 20:39:22 +02:00
|
|
|
self.operating_system: Optional[OSType] = None
|
|
|
|
self.operating_system_version: Optional[str] = None
|
|
|
|
self.desktop_environment: Optional[str] = None
|
|
|
|
self.windows_entry_grub: Optional[str] = None
|
|
|
|
self.monitors_config: Optional[Dict[str, Any]] = None
|
|
|
|
self.audio_config: Dict[str, Optional[Dict]] = {}
|
|
|
|
self.bluetooth_devices: Dict[str, Any] = {}
|
2024-08-25 23:20:39 +02:00
|
|
|
|
2024-08-26 19:16:44 +02:00
|
|
|
self._connection: Optional[SSHClientConnection] = None
|
2024-08-26 14:44:20 +02:00
|
|
|
|
2024-08-26 14:29:09 +02:00
|
|
|
asyncio.create_task(self.update())
|
|
|
|
|
2024-08-26 19:16:44 +02:00
|
|
|
async def _connect(self) -> None:
|
2024-08-26 14:29:09 +02:00
|
|
|
"""Open an asynchronous SSH connection."""
|
|
|
|
try:
|
|
|
|
client = await asyncssh.connect(
|
|
|
|
self.host,
|
2024-08-26 20:39:22 +02:00
|
|
|
username=self.username,
|
2024-08-26 14:29:09 +02:00
|
|
|
password=self._password,
|
2024-08-26 20:39:22 +02:00
|
|
|
port=self.port,
|
2024-08-26 14:29:09 +02:00
|
|
|
known_hosts=None
|
|
|
|
)
|
2024-08-26 17:43:15 +02:00
|
|
|
asyncssh.set_log_level("ERROR")
|
2024-08-26 19:16:44 +02:00
|
|
|
self._connection = client
|
2024-08-26 14:29:09 +02:00
|
|
|
except (OSError, asyncssh.Error) as exc:
|
2024-08-26 19:16:44 +02:00
|
|
|
raise ValueError(f"Failed to connect to {self.host}: {exc}")
|
2024-08-26 14:29:09 +02:00
|
|
|
|
2024-08-26 19:16:44 +02:00
|
|
|
async def _renew_connection(self) -> None:
|
|
|
|
"""Renew the SSH connection if it is closed."""
|
|
|
|
if self._connection is None or self._connection.is_closed:
|
|
|
|
self._connection = await self._connect()
|
2024-08-26 14:29:09 +02:00
|
|
|
|
|
|
|
async def update(self) -> None:
|
2024-08-26 19:16:44 +02:00
|
|
|
"""Update computer details."""
|
|
|
|
|
|
|
|
await self._renew_connection()
|
|
|
|
|
|
|
|
async def update_operating_system():
|
2024-08-26 20:39:22 +02:00
|
|
|
self.operating_system = await self._detect_operating_system()
|
2024-08-26 19:16:44 +02:00
|
|
|
|
|
|
|
async def update_operating_system_version():
|
2024-08-26 20:39:22 +02:00
|
|
|
self.operating_system_version = (await self.run_action("operating_system_version")).output
|
|
|
|
|
|
|
|
async def update_desktop_environment():
|
|
|
|
self.desktop_environment = (await self.run_action("desktop_environment")).output
|
2024-08-26 19:16:44 +02:00
|
|
|
|
|
|
|
async def update_windows_entry_grub():
|
2024-08-26 20:39:22 +02:00
|
|
|
self.windows_entry_grub = (await self.run_action("get_windows_entry_grub")).output
|
2024-08-26 19:16:44 +02:00
|
|
|
|
|
|
|
async def update_monitors_config():
|
2024-08-26 20:39:22 +02:00
|
|
|
monitors_config = (await self.run_action("get_monitors_config")).output
|
|
|
|
if self.operating_system == OSType.LINUX:
|
2024-08-26 19:16:44 +02:00
|
|
|
# TODO: add compatibility for KDE/others
|
2024-08-26 20:39:22 +02:00
|
|
|
self.monitors_config = parse_gnome_monitors_output(monitors_config)
|
|
|
|
elif self.operating_system == OSType.WINDOWS:
|
2024-08-26 19:16:44 +02:00
|
|
|
# TODO: implement for Windows
|
|
|
|
pass
|
|
|
|
|
|
|
|
async def update_audio_config():
|
2024-08-26 20:39:22 +02:00
|
|
|
speakers_config = (await self.run_action("get_speakers")).output
|
|
|
|
microphones_config = (await self.run_action("get_microphones")).output
|
2024-08-26 19:16:44 +02:00
|
|
|
|
2024-08-26 20:39:22 +02:00
|
|
|
if self.operating_system == OSType.LINUX:
|
|
|
|
self.audio_config = parse_pactl_output(speakers_config, microphones_config)
|
|
|
|
elif self.operating_system == OSType.WINDOWS:
|
2024-08-26 19:16:44 +02:00
|
|
|
# TODO: implement for Windows
|
|
|
|
pass
|
|
|
|
|
|
|
|
async def update_bluetooth_devices():
|
|
|
|
bluetooth_config = await self.run_action("get_bluetooth_devices")
|
2024-08-26 20:39:22 +02:00
|
|
|
if self.operating_system == OSType.LINUX:
|
|
|
|
self.bluetooth_devices = parse_bluetoothctl(bluetooth_config)
|
|
|
|
elif self.operating_system == OSType.WINDOWS:
|
2024-08-26 19:16:44 +02:00
|
|
|
# TODO: implement for Windows
|
|
|
|
pass
|
|
|
|
|
|
|
|
await update_operating_system()
|
|
|
|
await update_operating_system_version()
|
2024-08-26 20:39:22 +02:00
|
|
|
await update_desktop_environment()
|
2024-08-26 19:16:44 +02:00
|
|
|
await update_windows_entry_grub()
|
|
|
|
await update_monitors_config()
|
|
|
|
await update_audio_config()
|
|
|
|
await update_bluetooth_devices()
|
|
|
|
|
|
|
|
async def _detect_operating_system(self) -> OSType:
|
|
|
|
"""Detect the operating system of the computer."""
|
|
|
|
uname_result = await self.run_manually("uname")
|
2024-08-26 20:39:22 +02:00
|
|
|
return OSType.LINUX if uname_result.successful() else OSType.WINDOWS
|
2024-08-26 14:44:20 +02:00
|
|
|
|
2024-08-26 14:29:09 +02:00
|
|
|
async def is_on(self, timeout: int = 1) -> bool:
|
2024-08-26 19:16:44 +02:00
|
|
|
"""Check if the computer is on by pinging it."""
|
2024-08-25 23:20:39 +02:00
|
|
|
ping_cmd = ["ping", "-c", "1", "-W", str(timeout), str(self.host)]
|
2024-08-26 14:29:09 +02:00
|
|
|
proc = await asyncio.create_subprocess_exec(
|
|
|
|
*ping_cmd,
|
|
|
|
stdout=asyncio.subprocess.DEVNULL,
|
|
|
|
stderr=asyncio.subprocess.DEVNULL,
|
|
|
|
)
|
|
|
|
await proc.communicate()
|
|
|
|
return proc.returncode == 0
|
2024-08-25 23:20:39 +02:00
|
|
|
|
2024-08-26 14:29:09 +02:00
|
|
|
async def start(self) -> None:
|
2024-08-25 23:20:39 +02:00
|
|
|
"""Start the computer."""
|
2024-08-26 14:29:09 +02:00
|
|
|
send_magic_packet(self.mac)
|
2024-08-25 23:20:39 +02:00
|
|
|
|
2024-08-26 14:29:09 +02:00
|
|
|
async def shutdown(self) -> None:
|
2024-08-25 23:20:39 +02:00
|
|
|
"""Shutdown the computer."""
|
2024-08-26 14:29:09 +02:00
|
|
|
await self.run_action("shutdown")
|
2024-08-25 23:20:39 +02:00
|
|
|
|
2024-08-26 14:29:09 +02:00
|
|
|
async def restart(self, from_os: OSType = None, to_os: OSType = None) -> None:
|
2024-08-25 23:20:39 +02:00
|
|
|
"""Restart the computer."""
|
2024-08-26 14:29:09 +02:00
|
|
|
await self.run_action("restart")
|
2024-08-25 23:20:39 +02:00
|
|
|
|
2024-08-26 14:29:09 +02:00
|
|
|
async def put_to_sleep(self) -> None:
|
2024-08-25 23:20:39 +02:00
|
|
|
"""Put the computer to sleep."""
|
2024-08-26 14:29:09 +02:00
|
|
|
await self.run_action("sleep")
|
2024-08-25 23:20:39 +02:00
|
|
|
|
2024-08-26 19:16:44 +02:00
|
|
|
async def change_monitors_config(self, monitors_config: Dict[str, Any]) -> None:
|
|
|
|
"""Change the monitors configuration."""
|
|
|
|
# Implementation needed
|
2024-08-25 23:20:39 +02:00
|
|
|
pass
|
|
|
|
|
2024-08-26 14:29:09 +02:00
|
|
|
async def change_audio_config(self, volume: int | None = None, mute: bool | None = None,
|
|
|
|
input_device: str | None = None,
|
|
|
|
output_device: str | None = None) -> None:
|
2024-08-26 19:16:44 +02:00
|
|
|
"""Change the audio configuration."""
|
|
|
|
# Implementation needed
|
2024-08-25 23:20:39 +02:00
|
|
|
pass
|
|
|
|
|
2024-08-26 14:29:09 +02:00
|
|
|
async def install_nircmd(self) -> None:
|
2024-08-26 19:16:44 +02:00
|
|
|
"""Install NirCmd tool (Windows specific)."""
|
|
|
|
# Implementation needed
|
2024-08-25 23:20:39 +02:00
|
|
|
pass
|
|
|
|
|
2024-08-26 14:29:09 +02:00
|
|
|
async def start_steam_big_picture(self) -> None:
|
2024-08-26 19:16:44 +02:00
|
|
|
"""Start Steam Big Picture mode."""
|
|
|
|
# Implementation needed
|
2024-08-25 23:20:39 +02:00
|
|
|
pass
|
|
|
|
|
2024-08-26 14:29:09 +02:00
|
|
|
async def stop_steam_big_picture(self) -> None:
|
2024-08-26 19:16:44 +02:00
|
|
|
"""Stop Steam Big Picture mode."""
|
|
|
|
# Implementation needed
|
2024-08-25 23:20:39 +02:00
|
|
|
pass
|
|
|
|
|
2024-08-26 14:29:09 +02:00
|
|
|
async def exit_steam_big_picture(self) -> None:
|
2024-08-26 19:16:44 +02:00
|
|
|
"""Exit Steam Big Picture mode."""
|
|
|
|
# Implementation needed
|
2024-08-25 23:20:39 +02:00
|
|
|
pass
|
|
|
|
|
2024-08-26 20:39:22 +02:00
|
|
|
async def run_action(self, action: str, params: Dict[str, Any] = None, exit: bool = None) -> CommandOutput:
|
2024-08-26 19:16:44 +02:00
|
|
|
"""Run a predefined command via SSH."""
|
2024-08-25 23:20:39 +02:00
|
|
|
if params is None:
|
|
|
|
params = {}
|
|
|
|
|
2024-08-26 19:16:44 +02:00
|
|
|
if action not in const.ACTIONS:
|
2024-08-26 20:39:22 +02:00
|
|
|
return CommandOutput("", 1, "", "Action not found")
|
2024-08-26 19:16:44 +02:00
|
|
|
|
|
|
|
command_template = const.ACTIONS[action]
|
|
|
|
|
|
|
|
if "params" in command_template and sorted(command_template["params"]) != sorted(params.keys()):
|
|
|
|
raise ValueError(f"Invalid parameters for action: {action}")
|
|
|
|
|
|
|
|
if "exit" in command_template and exit is None:
|
|
|
|
exit = command_template["exit"]
|
2024-08-25 23:20:39 +02:00
|
|
|
|
2024-08-26 20:39:22 +02:00
|
|
|
commands = command_template.get(self.operating_system.lower())
|
2024-08-26 19:16:44 +02:00
|
|
|
if not commands:
|
2024-08-26 20:39:22 +02:00
|
|
|
raise ValueError(f"Action not supported for OS: {self.operating_system}")
|
2024-08-25 23:20:39 +02:00
|
|
|
|
2024-08-26 20:39:22 +02:00
|
|
|
command_result = None
|
2024-08-26 19:16:44 +02:00
|
|
|
for command in commands:
|
|
|
|
for param, value in params.items():
|
|
|
|
command = command.replace(f"%{param}%", value)
|
2024-08-25 23:20:39 +02:00
|
|
|
|
2024-08-26 20:39:22 +02:00
|
|
|
command_result = await self.run_manually(command)
|
|
|
|
if command_result.successful():
|
2024-08-26 19:16:44 +02:00
|
|
|
LOGGER.debug(f"Command successful: {command}")
|
2024-08-26 20:39:22 +02:00
|
|
|
return command_result
|
2024-08-25 23:20:39 +02:00
|
|
|
|
2024-08-26 19:16:44 +02:00
|
|
|
LOGGER.debug(f"Command failed: {command}")
|
2024-08-26 13:56:29 +02:00
|
|
|
|
2024-08-26 19:16:44 +02:00
|
|
|
if exit:
|
|
|
|
raise ValueError(f"Failed to run action: {action}")
|
2024-08-25 23:20:39 +02:00
|
|
|
|
2024-08-26 20:39:22 +02:00
|
|
|
return command_result
|
2024-08-25 23:20:39 +02:00
|
|
|
|
2024-08-26 20:39:22 +02:00
|
|
|
async def run_manually(self, command: str) -> CommandOutput:
|
2024-08-26 19:16:44 +02:00
|
|
|
"""Run a custom command manually via SSH."""
|
|
|
|
if not self._connection:
|
|
|
|
await self._connect()
|
2024-08-26 13:56:29 +02:00
|
|
|
|
2024-08-26 14:44:20 +02:00
|
|
|
result = await self._connection.run(command)
|
2024-08-25 23:20:39 +02:00
|
|
|
|
2024-08-26 20:39:22 +02:00
|
|
|
return CommandOutput(command, result.exit_status, result.stdout, result.stderr)
|