2024-08-26 14:29:09 +02:00
|
|
|
import asyncio
|
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 14:44:20 +02:00
|
|
|
from custom_components.easy_computer_manager import const, _LOGGER
|
|
|
|
|
2024-08-25 23:20:39 +02:00
|
|
|
|
|
|
|
class OSType:
|
2024-08-26 13:56:29 +02:00
|
|
|
WINDOWS = "windows"
|
|
|
|
LINUX = "linux"
|
|
|
|
MACOS = "macos"
|
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:
|
|
|
|
"""Init computer."""
|
|
|
|
self.host = host
|
|
|
|
self.mac = mac
|
|
|
|
self._username = username
|
|
|
|
self._password = password
|
|
|
|
self._port = port
|
|
|
|
self._dualboot = dualboot
|
|
|
|
|
|
|
|
self._operating_system = None
|
|
|
|
self._operating_system_version = None
|
|
|
|
self._windows_entry_grub = None
|
|
|
|
self._monitors_config = None
|
|
|
|
self._audio_config = None
|
|
|
|
self._bluetooth_devices = None
|
|
|
|
|
2024-08-26 14:44:20 +02:00
|
|
|
self._connection: SSHClientConnection | None = None
|
|
|
|
|
2024-08-26 14:29:09 +02:00
|
|
|
asyncio.create_task(self.update())
|
|
|
|
|
|
|
|
async def _open_ssh_connection(self) -> asyncssh.SSHClientConnection:
|
|
|
|
"""Open an asynchronous SSH connection."""
|
|
|
|
try:
|
|
|
|
client = await asyncssh.connect(
|
|
|
|
self.host,
|
|
|
|
username=self._username,
|
|
|
|
password=self._password,
|
|
|
|
port=self._port,
|
|
|
|
known_hosts=None
|
|
|
|
)
|
2024-08-26 17:43:15 +02:00
|
|
|
# TODO: implement key auth client_keys=['my_ssh_key'] (mixed field detect if key or pass)
|
|
|
|
asyncssh.set_log_level("ERROR")
|
2024-08-26 14:29:09 +02:00
|
|
|
return client
|
|
|
|
except (OSError, asyncssh.Error) as exc:
|
|
|
|
_LOGGER.error(f"SSH connection failed: {exc}")
|
|
|
|
return None
|
|
|
|
|
2024-08-26 17:43:15 +02:00
|
|
|
async def _close_ssh_connection(self) -> None:
|
2024-08-25 23:20:39 +02:00
|
|
|
"""Close the SSH connection."""
|
2024-08-26 17:43:15 +02:00
|
|
|
if self._connection is not None:
|
|
|
|
self._connection.close()
|
|
|
|
await self._connection.wait_closed()
|
2024-08-26 14:29:09 +02:00
|
|
|
|
|
|
|
async def update(self) -> None:
|
|
|
|
"""Setup method that opens an SSH connection and runs setup commands asynchronously."""
|
2024-08-26 14:44:20 +02:00
|
|
|
|
|
|
|
# Open SSH connection or renew it if it is closed
|
|
|
|
if self._connection is None or self._connection.is_closed:
|
2024-08-26 17:43:15 +02:00
|
|
|
await self._close_ssh_connection()
|
2024-08-26 14:44:20 +02:00
|
|
|
self._connection = await self._open_ssh_connection()
|
2024-08-26 14:29:09 +02:00
|
|
|
|
|
|
|
self._operating_system = OSType.LINUX if (await self.run_manually("uname")).get(
|
|
|
|
"return_code") == 0 else OSType.WINDOWS # TODO: improve this
|
|
|
|
self._operating_system_version = (await self.run_action("operating_system_version")).get("output")
|
|
|
|
self._windows_entry_grub = (await self.run_action("get_windows_entry_grub")).get("output")
|
2024-08-26 17:43:15 +02:00
|
|
|
self._monitors_config = (await self.run_action("get_monitors_config")).get("output")
|
2024-08-25 23:20:39 +02:00
|
|
|
self._audio_config = {'speakers': None, 'microphones': None}
|
|
|
|
self._bluetooth_devices = {}
|
|
|
|
|
|
|
|
# Getters
|
2024-08-26 14:29:09 +02:00
|
|
|
async def is_on(self, timeout: int = 1) -> bool:
|
2024-08-25 23:20:39 +02:00
|
|
|
"""Check if the computer is on (ping)."""
|
|
|
|
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
|
|
|
|
|
|
|
def get_operating_system(self) -> str:
|
|
|
|
"""Get the operating system of the computer."""
|
|
|
|
return self._operating_system
|
|
|
|
|
|
|
|
def get_operating_system_version(self) -> str:
|
|
|
|
"""Get the operating system version of the computer."""
|
|
|
|
return self._operating_system_version
|
|
|
|
|
|
|
|
def get_windows_entry_grub(self) -> str:
|
|
|
|
"""Get the Windows entry in the GRUB configuration file."""
|
|
|
|
return self._windows_entry_grub
|
|
|
|
|
|
|
|
def get_monitors_config(self) -> str:
|
|
|
|
"""Get the monitors configuration of the computer."""
|
|
|
|
return self._monitors_config
|
|
|
|
|
|
|
|
def get_speakers(self) -> str:
|
|
|
|
"""Get the audio configuration of the computer."""
|
2024-08-26 14:29:09 +02:00
|
|
|
return self._audio_config.get("speakers")
|
2024-08-25 23:20:39 +02:00
|
|
|
|
|
|
|
def get_microphones(self) -> str:
|
|
|
|
"""Get the audio configuration of the computer."""
|
2024-08-26 14:29:09 +02:00
|
|
|
return self._audio_config.get("microphones")
|
2024-08-25 23:20:39 +02:00
|
|
|
|
|
|
|
def get_bluetooth_devices(self, as_str: bool = False) -> str:
|
|
|
|
"""Get the Bluetooth devices of the computer."""
|
|
|
|
return self._bluetooth_devices
|
|
|
|
|
|
|
|
def is_dualboot(self) -> bool:
|
|
|
|
"""Check if the computer is dualboot."""
|
|
|
|
return self._dualboot
|
|
|
|
|
|
|
|
# Actions
|
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 14:29:09 +02:00
|
|
|
async def change_monitors_config(self, monitors_config: dict) -> None:
|
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-25 23:20:39 +02:00
|
|
|
pass
|
|
|
|
|
2024-08-26 14:29:09 +02:00
|
|
|
async def install_nircmd(self) -> None:
|
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-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-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-25 23:20:39 +02:00
|
|
|
pass
|
|
|
|
|
2024-08-26 14:29:09 +02:00
|
|
|
async def run_action(self, action: str, params=None) -> dict:
|
2024-08-25 23:20:39 +02:00
|
|
|
"""Run a command via SSH. Opens a new connection for each command."""
|
|
|
|
if params is None:
|
|
|
|
params = {}
|
|
|
|
|
2024-08-26 17:43:15 +02:00
|
|
|
if action in const.ACTIONS:
|
|
|
|
command_template = const.ACTIONS[action]
|
2024-08-25 23:20:39 +02:00
|
|
|
|
2024-08-26 14:29:09 +02:00
|
|
|
# Check if the command has the required parameters
|
|
|
|
if "params" in command_template:
|
|
|
|
if sorted(command_template["params"]) != sorted(params.keys()):
|
|
|
|
raise ValueError("Invalid parameters")
|
2024-08-25 23:20:39 +02:00
|
|
|
|
2024-08-26 14:29:09 +02:00
|
|
|
# Check if the command is available for the operating system
|
|
|
|
if self._operating_system in command_template:
|
|
|
|
match self._operating_system:
|
|
|
|
case OSType.WINDOWS:
|
|
|
|
commands = command_template[OSType.WINDOWS]
|
|
|
|
case OSType.LINUX:
|
|
|
|
commands = command_template[OSType.LINUX]
|
|
|
|
case _:
|
|
|
|
raise ValueError("Invalid operating system")
|
2024-08-25 23:20:39 +02:00
|
|
|
|
2024-08-26 14:29:09 +02:00
|
|
|
for command in commands:
|
|
|
|
# Replace the parameters in the command
|
|
|
|
for param, value in params.items():
|
|
|
|
command = command.replace(f"%{param}%", value)
|
2024-08-25 23:20:39 +02:00
|
|
|
|
2024-08-26 14:29:09 +02:00
|
|
|
result = await self.run_manually(command)
|
2024-08-26 13:56:29 +02:00
|
|
|
|
2024-08-26 14:29:09 +02:00
|
|
|
if result['return_code'] == 0:
|
|
|
|
_LOGGER.debug(f"Command successful: {command}")
|
|
|
|
return result
|
|
|
|
else:
|
|
|
|
_LOGGER.debug(f"Command failed: {command}")
|
2024-08-25 23:20:39 +02:00
|
|
|
|
2024-08-26 14:29:09 +02:00
|
|
|
return {"output": "", "error": "", "return_code": 1}
|
2024-08-25 23:20:39 +02:00
|
|
|
|
2024-08-26 14:29:09 +02:00
|
|
|
async def run_manually(self, command: str) -> dict:
|
2024-08-25 23:20:39 +02:00
|
|
|
"""Run a command manually (not from predefined commands)."""
|
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 13:56:29 +02:00
|
|
|
return {"output": result.stdout, "error": result.stderr,
|
2024-08-26 14:29:09 +02:00
|
|
|
"return_code": result.exit_status}
|