convert all to async (somehow it works ?? :)), update reqs and manifest
This commit is contained in:
parent
0f72986d2a
commit
7e6736c8b3
@ -1,10 +1,8 @@
|
|||||||
import subprocess as sp
|
import asyncio
|
||||||
|
|
||||||
import fabric2
|
|
||||||
import wakeonlan
|
|
||||||
from fabric2 import Connection
|
|
||||||
|
|
||||||
|
import asyncssh
|
||||||
from custom_components.easy_computer_manager import const, _LOGGER
|
from custom_components.easy_computer_manager import const, _LOGGER
|
||||||
|
from wakeonlan import send_magic_packet
|
||||||
|
|
||||||
|
|
||||||
class OSType:
|
class OSType:
|
||||||
@ -31,53 +29,54 @@ class Computer:
|
|||||||
self._audio_config = None
|
self._audio_config = None
|
||||||
self._bluetooth_devices = None
|
self._bluetooth_devices = None
|
||||||
|
|
||||||
self._connection = None
|
asyncio.create_task(self.update())
|
||||||
|
|
||||||
self.setup()
|
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
|
||||||
|
)
|
||||||
|
return client
|
||||||
|
except (OSError, asyncssh.Error) as exc:
|
||||||
|
_LOGGER.error(f"SSH connection failed: {exc}")
|
||||||
|
return None
|
||||||
|
|
||||||
def _open_ssh_connection(self) -> Connection:
|
async def _close_ssh_connection(self, client: asyncssh.SSHClientConnection) -> None:
|
||||||
"""Open an SSH connection."""
|
|
||||||
conf = fabric2.Config()
|
|
||||||
conf.run.hide = True
|
|
||||||
conf.run.warn = True
|
|
||||||
conf.warn = True
|
|
||||||
conf.sudo.password = self._password
|
|
||||||
conf.password = self._password
|
|
||||||
|
|
||||||
client = Connection(
|
|
||||||
host=self.host, user=self._username, port=self._port, connect_timeout=3,
|
|
||||||
connect_kwargs={"password": self._password},
|
|
||||||
config=conf
|
|
||||||
)
|
|
||||||
|
|
||||||
self._connection = client
|
|
||||||
return client
|
|
||||||
|
|
||||||
def _close_ssh_connection(self, client: Connection) -> None:
|
|
||||||
"""Close the SSH connection."""
|
"""Close the SSH connection."""
|
||||||
if client.is_connected:
|
client.close()
|
||||||
client.close()
|
|
||||||
|
|
||||||
def setup(self):
|
async def update(self) -> None:
|
||||||
"""Setup method that opens an SSH connection and keeps it open for subsequent setup commands."""
|
"""Setup method that opens an SSH connection and runs setup commands asynchronously."""
|
||||||
client = self._open_ssh_connection()
|
client = await self._open_ssh_connection()
|
||||||
|
if not client:
|
||||||
|
return
|
||||||
|
|
||||||
# TODO: run commands here
|
self._operating_system = OSType.LINUX if (await self.run_manually("uname")).get(
|
||||||
self._operating_system = OSType.LINUX # if self.run_manually("uname").return_code == 0 else OSType.WINDOWS # TODO: improve this
|
"return_code") == 0 else OSType.WINDOWS # TODO: improve this
|
||||||
self._operating_system_version = self.run_action("operating_system_version").output
|
self._operating_system_version = (await self.run_action("operating_system_version")).get("output")
|
||||||
self._windows_entry_grub = self.run_action("get_windows_entry_grub").output
|
self._windows_entry_grub = (await self.run_action("get_windows_entry_grub")).get("output")
|
||||||
self._monitors_config = {}
|
self._monitors_config = {}
|
||||||
self._audio_config = {'speakers': None, 'microphones': None}
|
self._audio_config = {'speakers': None, 'microphones': None}
|
||||||
self._bluetooth_devices = {}
|
self._bluetooth_devices = {}
|
||||||
|
|
||||||
self._close_ssh_connection(client)
|
await self._close_ssh_connection(client)
|
||||||
|
|
||||||
# Getters
|
# Getters
|
||||||
def is_on(self, timeout: int = 1) -> bool:
|
async def is_on(self, timeout: int = 1) -> bool:
|
||||||
"""Check if the computer is on (ping)."""
|
"""Check if the computer is on (ping)."""
|
||||||
ping_cmd = ["ping", "-c", "1", "-W", str(timeout), str(self.host)]
|
ping_cmd = ["ping", "-c", "1", "-W", str(timeout), str(self.host)]
|
||||||
status = sp.call(ping_cmd, stdout=sp.DEVNULL, stderr=sp.DEVNULL)
|
proc = await asyncio.create_subprocess_exec(
|
||||||
return status == 0
|
*ping_cmd,
|
||||||
|
stdout=asyncio.subprocess.DEVNULL,
|
||||||
|
stderr=asyncio.subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
await proc.communicate()
|
||||||
|
return proc.returncode == 0
|
||||||
|
|
||||||
def get_operating_system(self) -> str:
|
def get_operating_system(self) -> str:
|
||||||
"""Get the operating system of the computer."""
|
"""Get the operating system of the computer."""
|
||||||
@ -97,11 +96,11 @@ class Computer:
|
|||||||
|
|
||||||
def get_speakers(self) -> str:
|
def get_speakers(self) -> str:
|
||||||
"""Get the audio configuration of the computer."""
|
"""Get the audio configuration of the computer."""
|
||||||
return self._audio_config.speakers
|
return self._audio_config.get("speakers")
|
||||||
|
|
||||||
def get_microphones(self) -> str:
|
def get_microphones(self) -> str:
|
||||||
"""Get the audio configuration of the computer."""
|
"""Get the audio configuration of the computer."""
|
||||||
return self._audio_config.microphones
|
return self._audio_config.get("microphones")
|
||||||
|
|
||||||
def get_bluetooth_devices(self, as_str: bool = False) -> str:
|
def get_bluetooth_devices(self, as_str: bool = False) -> str:
|
||||||
"""Get the Bluetooth devices of the computer."""
|
"""Get the Bluetooth devices of the computer."""
|
||||||
@ -112,88 +111,90 @@ class Computer:
|
|||||||
return self._dualboot
|
return self._dualboot
|
||||||
|
|
||||||
# Actions
|
# Actions
|
||||||
def start(self) -> None:
|
async def start(self) -> None:
|
||||||
"""Start the computer."""
|
"""Start the computer."""
|
||||||
wakeonlan.send_magic_packet(self.mac)
|
send_magic_packet(self.mac)
|
||||||
|
|
||||||
def shutdown(self) -> None:
|
async def shutdown(self) -> None:
|
||||||
"""Shutdown the computer."""
|
"""Shutdown the computer."""
|
||||||
self.run_action("shutdown")
|
await self.run_action("shutdown")
|
||||||
|
|
||||||
def restart(self, from_os: OSType = None, to_os: OSType = None) -> None:
|
async def restart(self, from_os: OSType = None, to_os: OSType = None) -> None:
|
||||||
"""Restart the computer."""
|
"""Restart the computer."""
|
||||||
self.run_action("restart")
|
await self.run_action("restart")
|
||||||
|
|
||||||
def put_to_sleep(self) -> None:
|
async def put_to_sleep(self) -> None:
|
||||||
"""Put the computer to sleep."""
|
"""Put the computer to sleep."""
|
||||||
self.run_action("sleep")
|
await self.run_action("sleep")
|
||||||
|
|
||||||
def change_monitors_config(self, monitors_config: dict) -> None:
|
async def change_monitors_config(self, monitors_config: dict) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def change_audio_config(self, volume: int | None = None, mute: bool | None = None, input_device: str | None = None,
|
async def change_audio_config(self, volume: int | None = None, mute: bool | None = None,
|
||||||
output_device: str | None = None) -> None:
|
input_device: str | None = None,
|
||||||
|
output_device: str | None = None) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def install_nircmd(self) -> None:
|
async def install_nircmd(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def start_steam_big_picture(self) -> None:
|
async def start_steam_big_picture(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def stop_steam_big_picture(self) -> None:
|
async def stop_steam_big_picture(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def exit_steam_big_picture(self) -> None:
|
async def exit_steam_big_picture(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def run_action(self, action: str, params=None) -> dict:
|
async def run_action(self, action: str, params=None) -> dict:
|
||||||
"""Run a command via SSH. Opens a new connection for each command."""
|
"""Run a command via SSH. Opens a new connection for each command."""
|
||||||
if params is None:
|
if params is None:
|
||||||
params = {}
|
params = {}
|
||||||
|
|
||||||
if action not in const.COMMANDS:
|
if action in const.COMMANDS:
|
||||||
_LOGGER.error(f"Invalid action: {action}")
|
command_template = const.COMMANDS[action]
|
||||||
return {}
|
|
||||||
|
|
||||||
command_template = const.COMMANDS[action]
|
# 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")
|
||||||
|
|
||||||
# Check if the command has the required parameters
|
# Check if the command is available for the operating system
|
||||||
if "params" in command_template:
|
if self._operating_system in command_template:
|
||||||
if sorted(command_template["params"]) != sorted(params.keys()):
|
match self._operating_system:
|
||||||
raise ValueError("Invalid parameters")
|
case OSType.WINDOWS:
|
||||||
|
commands = command_template[OSType.WINDOWS]
|
||||||
|
case OSType.LINUX:
|
||||||
|
commands = command_template[OSType.LINUX]
|
||||||
|
case _:
|
||||||
|
raise ValueError("Invalid operating system")
|
||||||
|
|
||||||
# Check if the command is available for the operating system
|
for command in commands:
|
||||||
match self._operating_system:
|
# Replace the parameters in the command
|
||||||
case OSType.WINDOWS:
|
for param, value in params.items():
|
||||||
commands = command_template[OSType.WINDOWS]
|
command = command.replace(f"%{param}%", value)
|
||||||
case OSType.LINUX:
|
|
||||||
commands = command_template[OSType.LINUX]
|
|
||||||
case _:
|
|
||||||
raise ValueError("Invalid operating system")
|
|
||||||
|
|
||||||
for command in commands:
|
result = await self.run_manually(command)
|
||||||
# Replace the parameters in the command
|
|
||||||
for param, value in params.items():
|
|
||||||
command = command.replace(f"%{param}%", value)
|
|
||||||
|
|
||||||
result = self.run_manually(command)
|
if result['return_code'] == 0:
|
||||||
|
_LOGGER.debug(f"Command successful: {command}")
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
_LOGGER.debug(f"Command failed: {command}")
|
||||||
|
|
||||||
if result['return_code'] == 0:
|
return {"output": "", "error": "", "return_code": 1}
|
||||||
_LOGGER.debug(f"Command successful: {command}")
|
|
||||||
return result
|
|
||||||
else:
|
|
||||||
_LOGGER.debug(f"Command failed: {command}")
|
|
||||||
|
|
||||||
return {}
|
async def run_manually(self, command: str) -> dict:
|
||||||
|
|
||||||
def run_manually(self, command: str) -> dict:
|
|
||||||
"""Run a command manually (not from predefined commands)."""
|
"""Run a command manually (not from predefined commands)."""
|
||||||
|
|
||||||
# Open SSH connection, execute command, and close connection
|
# Open SSH connection, execute command, and close connection
|
||||||
client = self._open_ssh_connection()
|
client = await self._open_ssh_connection()
|
||||||
result = client.run(command)
|
if not client:
|
||||||
self._close_ssh_connection(client)
|
return {"output": "", "error": "SSH connection failed", "return_code": 1}
|
||||||
|
|
||||||
|
result = await client.run(command)
|
||||||
|
await self._close_ssh_connection(client)
|
||||||
|
|
||||||
return {"output": result.stdout, "error": result.stderr,
|
return {"output": result.stdout, "error": result.stderr,
|
||||||
"return_code": result.return_code}
|
"return_code": result.exit_status}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from custom_components.easy_computer_manager.computer import Computer
|
from custom_components.easy_computer_manager.computer import Computer
|
||||||
|
|
||||||
|
|
||||||
def get_debug_info(computer: Computer):
|
async def get_debug_info(computer: Computer):
|
||||||
"""Return debug information about the host system."""
|
"""Return debug information about the host system."""
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
@ -15,7 +15,7 @@ def get_debug_info(computer: Computer):
|
|||||||
'username': computer._username,
|
'username': computer._username,
|
||||||
'port': computer._port,
|
'port': computer._port,
|
||||||
'dualboot': computer._dualboot,
|
'dualboot': computer._dualboot,
|
||||||
'is_on': computer.is_on()
|
'is_on': await computer.is_on()
|
||||||
},
|
},
|
||||||
'grub':{
|
'grub':{
|
||||||
'windows_entry': computer.get_windows_entry_grub()
|
'windows_entry': computer.get_windows_entry_grub()
|
||||||
|
@ -2,8 +2,7 @@
|
|||||||
"domain": "easy_computer_manager",
|
"domain": "easy_computer_manager",
|
||||||
"name": "Easy Computer Manager",
|
"name": "Easy Computer Manager",
|
||||||
"codeowners": [
|
"codeowners": [
|
||||||
"@M4TH1EU",
|
"@M4TH1EU"
|
||||||
"@ntilley905"
|
|
||||||
],
|
],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
@ -11,10 +10,8 @@
|
|||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"issue_tracker": "https://github.com/M4TH1EU/HA-EasyComputerManager/issues",
|
"issue_tracker": "https://github.com/M4TH1EU/HA-EasyComputerManager/issues",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"construct==2.10.70",
|
|
||||||
"wakeonlan==3.1.0",
|
"wakeonlan==3.1.0",
|
||||||
"fabric2==3.2.2",
|
"asyncssh==2.16.0"
|
||||||
"paramiko==3.4.0"
|
|
||||||
],
|
],
|
||||||
"version": "1.3.0"
|
"version": "2.0.0"
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,6 @@ from homeassistant.const import (
|
|||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
CONF_USERNAME, )
|
CONF_USERNAME, )
|
||||||
from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse
|
from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
|
||||||
from homeassistant.helpers import (
|
from homeassistant.helpers import (
|
||||||
device_registry as dr,
|
device_registry as dr,
|
||||||
entity_platform,
|
entity_platform,
|
||||||
@ -26,7 +25,7 @@ from homeassistant.helpers.config_validation import make_entity_service_schema
|
|||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import utils, computer_utils
|
from . import computer_utils
|
||||||
from .computer import Computer, OSType
|
from .computer import Computer, OSType
|
||||||
from .const import SERVICE_RESTART_TO_WINDOWS_FROM_LINUX, SERVICE_PUT_COMPUTER_TO_SLEEP, \
|
from .const import SERVICE_RESTART_TO_WINDOWS_FROM_LINUX, SERVICE_PUT_COMPUTER_TO_SLEEP, \
|
||||||
SERVICE_START_COMPUTER_TO_WINDOWS, SERVICE_RESTART_COMPUTER, SERVICE_RESTART_TO_LINUX_FROM_WINDOWS, \
|
SERVICE_START_COMPUTER_TO_WINDOWS, SERVICE_RESTART_COMPUTER, SERVICE_RESTART_TO_LINUX_FROM_WINDOWS, \
|
||||||
@ -150,74 +149,72 @@ class ComputerSwitch(SwitchEntity):
|
|||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
def turn_on(self, **kwargs: Any) -> None:
|
async def turn_on(self, **kwargs: Any) -> None:
|
||||||
self.computer.start()
|
await self.computer.start()
|
||||||
|
|
||||||
if self._attr_assumed_state:
|
if self._attr_assumed_state:
|
||||||
self._state = True
|
self._state = True
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
def turn_off(self, **kwargs: Any) -> None:
|
async def turn_off(self, **kwargs: Any) -> None:
|
||||||
self.computer.shutdown()
|
await self.computer.shutdown()
|
||||||
|
|
||||||
if self._attr_assumed_state:
|
if self._attr_assumed_state:
|
||||||
self._state = False
|
self._state = False
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
# Services
|
# Services
|
||||||
def restart_to_windows_from_linux(self) -> None:
|
async def restart_to_windows_from_linux(self) -> None:
|
||||||
"""Restart the computer to Windows from a running Linux by setting grub-reboot and restarting."""
|
"""Restart the computer to Windows from a running Linux by setting grub-reboot and restarting."""
|
||||||
self.computer.restart(OSType.LINUX, OSType.WINDOWS)
|
await self.computer.restart(OSType.LINUX, OSType.WINDOWS)
|
||||||
|
|
||||||
def restart_to_linux_from_windows(self) -> None:
|
async def restart_to_linux_from_windows(self) -> None:
|
||||||
"""Restart the computer to Linux from a running Windows by setting grub-reboot and restarting."""
|
"""Restart the computer to Linux from a running Windows by setting grub-reboot and restarting."""
|
||||||
self.computer.restart()
|
await self.computer.restart()
|
||||||
|
|
||||||
def put_computer_to_sleep(self) -> None:
|
async def put_computer_to_sleep(self) -> None:
|
||||||
self.computer.setup()
|
await self.computer.put_to_sleep()
|
||||||
|
|
||||||
def start_computer_to_windows(self) -> None:
|
async def start_computer_to_windows(self) -> None:
|
||||||
async def wait_task():
|
async def wait_task():
|
||||||
while not self.is_on:
|
while not self.is_on:
|
||||||
pass
|
await asyncio.sleep(3)
|
||||||
# await asyncio.sleep(3)
|
|
||||||
|
|
||||||
self.computer.restart(OSType.LINUX, OSType.WINDOWS)
|
await self.computer.restart(OSType.LINUX, OSType.WINDOWS)
|
||||||
|
|
||||||
"""Start the computer to Linux, wait for it to boot, and then set grub-reboot and restart."""
|
"""Start the computer to Linux, wait for it to boot, and then set grub-reboot and restart."""
|
||||||
self.computer.start()
|
await self.computer.start()
|
||||||
self._hass.loop.create_task(wait_task())
|
self._hass.loop.create_task(wait_task())
|
||||||
|
|
||||||
def restart_computer(self) -> None:
|
async def restart_computer(self) -> None:
|
||||||
self.computer.restart()
|
await self.computer.restart()
|
||||||
|
|
||||||
def change_monitors_config(self, monitors_config: dict | None = None) -> None:
|
async def change_monitors_config(self, monitors_config: dict | None = None) -> None:
|
||||||
"""Change the monitors configuration using a YAML config file."""
|
"""Change the monitors configuration using a YAML config file."""
|
||||||
self.computer.change_monitors_config(monitors_config)
|
await self.computer.change_monitors_config(monitors_config)
|
||||||
|
|
||||||
def steam_big_picture(self, action: str) -> None:
|
async def steam_big_picture(self, action: str) -> None:
|
||||||
match action:
|
match action:
|
||||||
case "start":
|
case "start":
|
||||||
self.computer.start_steam_big_picture()
|
await self.computer.start_steam_big_picture()
|
||||||
case "stop":
|
case "stop":
|
||||||
self.computer.stop_steam_big_picture()
|
await self.computer.stop_steam_big_picture()
|
||||||
case "exit":
|
case "exit":
|
||||||
self.computer.exit_steam_big_picture()
|
await self.computer.exit_steam_big_picture()
|
||||||
|
|
||||||
|
async def change_audio_config(self, volume: int | None = None, mute: bool | None = None,
|
||||||
def change_audio_config(self, volume: int | None = None, mute: bool | None = None, input_device: str | None = None,
|
input_device: str | None = None,
|
||||||
output_device: str | None = None) -> None:
|
output_device: str | None = None) -> None:
|
||||||
"""Change the audio configuration using a YAML config file."""
|
"""Change the audio configuration using a YAML config file."""
|
||||||
self.computer.change_audio_config(volume, mute, input_device, output_device)
|
await self.computer.change_audio_config(volume, mute, input_device, output_device)
|
||||||
|
|
||||||
def debug_info(self) -> ServiceResponse:
|
async def debug_info(self) -> ServiceResponse:
|
||||||
"""Prints debug info."""
|
"""Prints debug info."""
|
||||||
return computer_utils.get_debug_info(self.computer)
|
return await computer_utils.get_debug_info(self.computer)
|
||||||
|
|
||||||
|
async def async_update(self) -> None:
|
||||||
def update(self) -> None:
|
|
||||||
"""Ping the computer to see if it is online and update the state."""
|
"""Ping the computer to see if it is online and update the state."""
|
||||||
self._state = self.computer.is_on()
|
self._state = await self.computer.is_on()
|
||||||
|
|
||||||
# Update the state attributes and the connection only if the computer is on
|
# Update the state attributes and the connection only if the computer is on
|
||||||
if self._state:
|
if self._state:
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
fabric2~=3.2.2
|
|
||||||
paramiko~=3.4.0
|
|
||||||
voluptuous~=0.14.2
|
|
||||||
wakeonlan~=3.1.0
|
wakeonlan~=3.1.0
|
||||||
homeassistant~=2024.3.1
|
homeassistant~=2024.3.1
|
||||||
|
asyncssh~=2.16.0
|
Loading…
Reference in New Issue
Block a user