From f47e581fede2d8c9a9dc477565512c972711efbe Mon Sep 17 00:00:00 2001 From: Mathieu Broillet Date: Sat, 30 Dec 2023 20:21:37 +0100 Subject: [PATCH] massive refactor and code improvements --- .../easy_computer_manager/__init__.py | 4 +- .../easy_computer_manager/config_flow.py | 4 +- .../easy_computer_manager/services.yaml | 78 ++--- .../easy_computer_manager/switch.py | 73 +++-- .../translations/en.json | 42 +-- .../translations/fr.json | 42 +-- .../easy_computer_manager/utils.py | 278 ++++++++---------- hacs.json | 4 +- 8 files changed, 253 insertions(+), 272 deletions(-) diff --git a/custom_components/easy_computer_manager/__init__.py b/custom_components/easy_computer_manager/__init__.py index c18d8bf..2d2bc26 100644 --- a/custom_components/easy_computer_manager/__init__.py +++ b/custom_components/easy_computer_manager/__init__.py @@ -7,13 +7,13 @@ from __future__ import annotations import logging from functools import partial +import homeassistant.helpers.config_validation as cv import voluptuous as vol import wakeonlan - -import homeassistant.helpers.config_validation as cv from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_BROADCAST_ADDRESS, CONF_BROADCAST_PORT, CONF_MAC from homeassistant.core import HomeAssistant, ServiceCall + from .const import DOMAIN, SERVICE_SEND_MAGIC_PACKET, SERVICE_CHANGE_MONITORS_CONFIG _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/easy_computer_manager/config_flow.py b/custom_components/easy_computer_manager/config_flow.py index 36d540d..6a02776 100644 --- a/custom_components/easy_computer_manager/config_flow.py +++ b/custom_components/easy_computer_manager/config_flow.py @@ -5,10 +5,10 @@ import logging from typing import Any import voluptuous as vol -from paramiko.ssh_exception import AuthenticationException - from homeassistant import config_entries, exceptions from homeassistant.core import HomeAssistant +from paramiko.ssh_exception import AuthenticationException + from . import utils from .const import DOMAIN diff --git a/custom_components/easy_computer_manager/services.yaml b/custom_components/easy_computer_manager/services.yaml index 7dffbd9..a64e07d 100644 --- a/custom_components/easy_computer_manager/services.yaml +++ b/custom_components/easy_computer_manager/services.yaml @@ -1,83 +1,90 @@ send_magic_packet: - name: Send magic packet - description: Send a 'magic packet' to wake up a device with 'Wake-On-LAN' capabilities. + name: Send Magic Packet + description: Send a 'magic packet' to awaken a device with 'Wake-On-LAN' capabilities. fields: mac: - name: MAC address - description: MAC address of the device to wake up. + name: MAC Address + description: MAC address of the target device. required: true example: "aa:bb:cc:dd:ee:ff" selector: text: broadcast_address: - name: Broadcast address - description: Broadcast IP where to send the magic packet. + name: Broadcast Address + description: Broadcast IP to send the magic packet. example: 192.168.255.255 selector: text: broadcast_port: - name: Broadcast port - description: Port where to send the magic packet. + name: Broadcast Port + description: Port to send the magic packet. default: 9 selector: number: min: 1 max: 65535 + restart_to_windows_from_linux: name: Restart to Windows from Linux - description: Restart the computer to Windows when running Linux using Grub. + description: Restart the computer to Windows while running Linux using Grub. target: device: integration: easy_computer_manager + restart_to_linux_from_windows: name: Restart to Linux from Windows - description: Restart the computer to Linux when running Windows. + description: Restart the computer to Linux while running Windows. target: device: integration: easy_computer_manager + start_computer_to_windows: - name: Start computer to Windows - description: Start the computer directly Windows (boots to Linux, set grub reboot, then boots to Windows). + name: Start Computer to Windows + description: Directly start the computer into Windows (boot to Linux, set Grub reboot, then boot to Windows). target: device: integration: easy_computer_manager + put_computer_to_sleep: - name: Put computer to sleep - description: Put the computer to sleep. + name: Put Computer to Sleep + description: Put the computer into sleep mode. target: device: integration: easy_computer_manager + restart_computer: - name: Restart + name: Restart Computer description: Restart the computer. target: device: integration: easy_computer_manager + change_monitors_config: - name: Change monitors config - description: Change monitors config. + name: Change Monitors Configuration + description: Modify monitors configuration. target: entity: integration: easy_computer_manager domain: switch fields: monitors_config: - name: Monitors config - description: Monitors config. + name: Monitors Configuration + description: Monitors configuration details. required: true example: | HDMI-1: enabled: true primary: true - position: [ 0, 0 ] + position: [0, 0] mode: 3840x2160@120.000 transform: normal scale: 2 selector: object: + steam_big_picture: - name: Start/stop Steam Big Picture - description: Start/stop Steam Big Picture. + name: Start/Stop Steam Big Picture + description: Initiate or terminate Steam Big Picture mode. target: entity: integration: easy_computer_manager @@ -85,7 +92,7 @@ steam_big_picture: fields: action: name: Action - description: Choose whether to start/stop Steam Big Picture or go back to the desktop Steam UI. + description: Choose whether to start, stop, or return to the desktop Steam UI. required: true example: "start" selector: @@ -95,11 +102,12 @@ steam_big_picture: value: start - label: Stop value: stop - - label: Exit and go back to the desktop Steam UI + - label: Exit and return to desktop Steam UI value: exit + change_audio_config: - name: Change audio config - description: Change audio config (volume, mute, input, output). + name: Change Audio Configuration + description: Adjust audio settings (volume, mute, input, output). target: entity: integration: easy_computer_manager @@ -107,7 +115,7 @@ change_audio_config: fields: volume: name: Volume - description: The volume to set. + description: Set the desired volume level. example: 50 selector: number: @@ -120,14 +128,14 @@ change_audio_config: selector: boolean: input_device: - name: Input device - description: The ID/name/description of the input device. - example: "Kraken 7.1 Chroma Stéréo analogique" + name: Input Device + description: Specify the ID/name/description of the input device. + example: "Kraken 7.1 Chroma Stereo Analog" selector: text: - output_output: - name: Output device - description: The ID/name/description of the output device. - example: "Starship/Matisse HD Audio Controller Stéréo analogique" + output_device: + name: Output Device + description: Specify the ID/name/description of the output device. + example: "Starship/Matisse HD Audio Controller Stereo Analog" selector: - text: \ No newline at end of file + text: diff --git a/custom_components/easy_computer_manager/switch.py b/custom_components/easy_computer_manager/switch.py index 515f321..5764b4f 100644 --- a/custom_components/easy_computer_manager/switch.py +++ b/custom_components/easy_computer_manager/switch.py @@ -1,4 +1,4 @@ -# Some code is from the official wake_on_lan integration +# Some snippets of code are from the official wake_on_lan integration (inspiration for this custom component) from __future__ import annotations @@ -274,7 +274,7 @@ class ComputerSwitch(SwitchEntity): if monitors_config is not None and len(monitors_config) > 0: utils.change_monitors_config(self._connection, monitors_config) else: - raise HomeAssistantError("The monitors config is empty.") + raise HomeAssistantError("The 'monitors_config' parameter must be a non-empty dictionary.") def steam_big_picture(self, action: str) -> None: """Controls Steam Big Picture mode.""" @@ -282,7 +282,7 @@ class ComputerSwitch(SwitchEntity): if action is not None: utils.steam_big_picture(self._connection, action) else: - raise HomeAssistantError("You must specify an action.") + raise HomeAssistantError("The 'action' parameter must be specified.") def change_audio_config(self, volume: int | None = None, mute: bool | None = None, input_device: str | None = None, output_device: str | None = None) -> None: @@ -291,52 +291,47 @@ class ComputerSwitch(SwitchEntity): def update(self) -> None: """Ping the computer to see if it is online and update the state.""" - ping_cmd = [ - "ping", - "-c", - "1", - "-W", - str(DEFAULT_PING_TIMEOUT), - str(self._host), - ] + ping_cmd = ["ping", "-c", "1", "-W", str(DEFAULT_PING_TIMEOUT), str(self._host)] status = sp.call(ping_cmd, stdout=sp.DEVNULL, stderr=sp.DEVNULL) self._state = not bool(status) # Update the state attributes and the connection only if the computer is on if self._state: - if not utils.test_connection(self._connection): - _LOGGER.info("Renewing SSH connection to %s using username %s", self._host, self._username) + if self._connection is None or not utils.test_connection(self._connection): + self.renew_ssh_connection() - if self._connection is not None: - self._connection.close() - - self._connection = utils.create_ssh_connection(self._host, self._username, self._password) - - try: - self._connection.open() - except AuthenticationException as error: - _LOGGER.error("Could not authenticate to %s using username %s : %s", self._host, self._username, - error) - self._state = False - return - except Exception as error: - _LOGGER.error("Could not connect to %s using username %s : %s", self._host, self._username, error) - - # Check if the error is due to timeout - if "timed out" in str(error): - _LOGGER.warning( - "Computer at %s does not respond to the SSH request. Possibles causes : might be offline, " - "the firewall is blocking the SSH port or the SSH server is offline and/or misconfigured.", - self._host) - - self._state = False + if not self._state: return self._attr_extra_state_attributes = { "operating_system": utils.get_operating_system(self._connection), - "operating_system_version": utils.get_operating_system_version( - self._connection - ), + "operating_system_version": utils.get_operating_system_version(self._connection), "mac_address": self._mac_address, "ip_address": self._host, } + + def renew_ssh_connection(self) -> None: + """Renew the SSH connection.""" + _LOGGER.info("Renewing SSH connection to %s using username %s", self._host, self._username) + + if self._connection is not None: + self._connection.close() + + try: + self._connection = utils.create_ssh_connection(self._host, self._username, self._password) + self._connection.open() + except AuthenticationException as error: + _LOGGER.error("Could not authenticate to %s using username %s: %s", self._host, self._username, error) + self._state = False + except Exception as error: + _LOGGER.error("Could not connect to %s using username %s: %s", self._host, self._username, error) + + # Check if the error is due to timeout + if "timed out" in str(error): + _LOGGER.warning( + "Computer at %s does not respond to the SSH request. Possible causes: might be offline, " + "the firewall is blocking the SSH port, or the SSH server is offline and/or misconfigured.", + self._host + ) + + self._state = False diff --git a/custom_components/easy_computer_manager/translations/en.json b/custom_components/easy_computer_manager/translations/en.json index 7bd761f..0bdc0d4 100644 --- a/custom_components/easy_computer_manager/translations/en.json +++ b/custom_components/easy_computer_manager/translations/en.json @@ -1,25 +1,25 @@ { - "config": { - "abort": { - "already_configured": "Device is already configured" - }, - "error": { - "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" - }, - "step": { - "user": { - "data": { - "host": "Host", - "username": "Username", - "password": "Password", - "dualboot": "Is this a Linux/Windows dualboot computer?", - "port": "Port", - "name": "Name", - "mac": "MAC Address" - } - } + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host", + "username": "Username", + "password": "Password", + "dualboot": "Is this a Linux/Windows dualboot computer?", + "port": "Port", + "name": "Name", + "mac": "MAC Address" } + } } + } } \ No newline at end of file diff --git a/custom_components/easy_computer_manager/translations/fr.json b/custom_components/easy_computer_manager/translations/fr.json index 0969cfb..79a29f7 100644 --- a/custom_components/easy_computer_manager/translations/fr.json +++ b/custom_components/easy_computer_manager/translations/fr.json @@ -1,25 +1,25 @@ { - "config": { - "abort": { - "already_configured": "L'appareil est déjà configuré." - }, - "error": { - "cannot_connect": "Impossible de se connecter à l'appareil.", - "invalid_auth": "Identifiant ou mot de passe invalide.", - "unknown": "Erreur inconnue." - }, - "step": { - "user": { - "data": { - "host": "Adresse IP", - "username": "Nom d'utilisateur", - "password": "Mot de passe", - "dualboot": "Est-ce que cet ordinateur est un dualboot Linux/Windows?", - "port": "Port", - "name": "Nom de l'appareil", - "mac": "Adresse MAC" - } - } + "config": { + "abort": { + "already_configured": "L'appareil est déjà configuré." + }, + "error": { + "cannot_connect": "Impossible de se connecter à l'appareil.", + "invalid_auth": "Identifiant ou mot de passe invalide.", + "unknown": "Erreur inconnue." + }, + "step": { + "user": { + "data": { + "host": "Adresse IP", + "username": "Nom d'utilisateur", + "password": "Mot de passe", + "dualboot": "Est-ce que cet ordinateur est un dualboot Linux/Windows?", + "port": "Port", + "name": "Nom de l'appareil", + "mac": "Adresse MAC" } + } } + } } \ No newline at end of file diff --git a/custom_components/easy_computer_manager/utils.py b/custom_components/easy_computer_manager/utils.py index 9871e36..de54c6e 100644 --- a/custom_components/easy_computer_manager/utils.py +++ b/custom_components/easy_computer_manager/utils.py @@ -53,15 +53,12 @@ def get_operating_system_version(connection: Connection, is_unix=None): result = connection.run( "awk -F'=' '/^NAME=|^VERSION=/{gsub(/\"/, \"\", $2); printf $2\" \"}\' /etc/os-release && echo").stdout if result == "": - result = connection.run( - "lsb_release -a | awk '/Description/ {print $2, $3, $4}'" - ).stdout + result = connection.run("lsb_release -a | awk '/Description/ {print $2, $3, $4}'").stdout return result else: return connection.run( - 'for /f "tokens=1 delims=|" %i in (\'wmic os get Name ^| findstr /B /C:"Microsoft"\') do @echo %i' - ).stdout + 'for /f "tokens=1 delims=|" %i in (\'wmic os get Name ^| findstr /B /C:"Microsoft"\') do @echo %i').stdout def get_operating_system(connection: Connection): @@ -80,29 +77,19 @@ def shutdown_system(connection: Connection, is_unix=None): if is_unix is None: is_unix = is_unix_system(connection) - if is_unix: - # First method using shutdown command - result = connection.run("sudo shutdown -h now") - if result.return_code != 0: - # Try a second method using init command - result = connection.run("sudo init 0") - if result.return_code != 0: - # Try a third method using systemctl command - result = connection.run("sudo systemctl poweroff") - if result.return_code != 0: - raise HomeAssistantError( - f"Cannot shutdown system running at {connection.host}, all methods failed.") + shutdown_commands = { + "unix": ["sudo shutdown -h now", "sudo init 0", "sudo systemctl poweroff"], + "windows": ["shutdown /s /t 0", "wmic os where Primary=TRUE call Shutdown"] + } - else: - # First method using shutdown command - result = connection.run("shutdown /s /t 0") - if result.return_code != 0: - # Try a second method using init command - result = connection.run("wmic os where Primary=TRUE call Shutdown") - if result.return_code != 0: - raise HomeAssistantError(f"Cannot shutdown system running at {connection.host}, all methods failed.") + for command in shutdown_commands["unix" if is_unix else "windows"]: + result = connection.run(command) + if result.return_code == 0: + _LOGGER.debug("System shutting down on %s.", connection.host) + connection.close() + return - connection.close() + raise HomeAssistantError(f"Cannot shutdown system running at {connection.host}, all methods failed.") def restart_system(connection: Connection, is_unix=None): @@ -111,25 +98,18 @@ def restart_system(connection: Connection, is_unix=None): if is_unix is None: is_unix = is_unix_system(connection) - if is_unix: - # First method using shutdown command - result = connection.run("sudo shutdown -r now") - if result.return_code != 0: - # Try a second method using init command - result = connection.run("sudo init 6") - if result.return_code != 0: - # Try a third method using systemctl command - result = connection.run("sudo systemctl reboot") - if result.return_code != 0: - raise HomeAssistantError(f"Cannot restart system running at {connection.host}, all methods failed.") - else: - # First method using shutdown command - result = connection.run("shutdown /r /t 0") - if result.return_code != 0: - # Try a second method using wmic command - result = connection.run("wmic os where Primary=TRUE call Reboot") - if result.return_code != 0: - raise HomeAssistantError(f"Cannot restart system running at {connection.host}, all methods failed.") + restart_commands = { + "unix": ["sudo shutdown -r now", "sudo init 6", "sudo systemctl reboot"], + "windows": ["shutdown /r /t 0", "wmic os where Primary=TRUE call Reboot"] + } + + for command in restart_commands["unix" if is_unix else "windows"]: + result = connection.run(command) + if result.return_code == 0: + _LOGGER.debug("System restarting on %s.", connection.host) + return + + raise HomeAssistantError(f"Cannot restart system running at {connection.host}, all methods failed.") def sleep_system(connection: Connection, is_unix=None): @@ -138,24 +118,18 @@ def sleep_system(connection: Connection, is_unix=None): if is_unix is None: is_unix = is_unix_system(connection) - if is_unix: - # First method using systemctl command - result = connection.run("sudo systemctl suspend") - if result.return_code != 0: - # Try a second method using pm-suspend command - result = connection.run("sudo pm-suspend") - if result.return_code != 0: - raise HomeAssistantError( - f"Cannot put system running at {connection.host} to sleep, all methods failed.") - else: - # First method using shutdown command - result = connection.run("shutdown /h /t 0") - if result.return_code != 0: - # Try a second method using rundll32 command - result = connection.run("rundll32.exe powrprof.dll,SetSuspendState Sleep") - if result.return_code != 0: - raise HomeAssistantError( - f"Cannot put system running at {connection.host} to sleep, all methods failed.") + sleep_commands = { + "unix": ["sudo systemctl suspend", "sudo pm-suspend"], + "windows": ["shutdown /h /t 0", "rundll32.exe powrprof.dll,SetSuspendState Sleep"] + } + + for command in sleep_commands["unix" if is_unix else "windows"]: + result = connection.run(command) + if result.return_code == 0: + _LOGGER.debug("System sleeping on %s.", connection.host) + return + + raise HomeAssistantError(f"Cannot put system running at {connection.host} to sleep, all methods failed.") def get_windows_entry_in_grub(connection: Connection): @@ -163,81 +137,70 @@ def get_windows_entry_in_grub(connection: Connection): Grabs the Windows entry name in GRUB. Used later with grub-reboot to specify which entry to boot. """ - result = connection.run("sudo awk -F \"'\" '/windows/ {print $2}' /boot/grub/grub.cfg") + commands = [ + "sudo awk -F \"'\" '/windows/ {print $2}' /boot/grub/grub.cfg", + "sudo awk -F \"'\" '/windows/ {print $2}' /boot/grub2/grub.cfg" + ] - if result.return_code == 0: - _LOGGER.debug("Found Windows entry in grub : " + result.stdout.strip()) - else: - result = connection.run("sudo awk -F \"'\" '/windows/ {print $2}' /boot/grub2/grub.cfg") - if result.return_code == 0: - _LOGGER.debug("Successfully found Windows Grub entry (%s) for system running at %s.", result.stdout.strip(), - connection.host) - else: - _LOGGER.error("Could not find Windows entry on computer with address %s.") - return None + for command in commands: + result = connection.run(command) + if result.return_code == 0 and result.stdout.strip(): + _LOGGER.debug("Found Windows entry in GRUB: " + result.stdout.strip()) + return result.stdout.strip() - # Check if the entry is valid - if result.stdout.strip() != "": - return result.stdout.strip() - else: - _LOGGER.error("Could not find Windows entry on computer with address %s.") - return None + _LOGGER.error("Could not find Windows entry in GRUB for system running at %s.", connection.host) + return None def restart_to_windows_from_linux(connection: Connection): """Restart a running Linux system to Windows.""" - if is_unix_system(connection): - windows_entry = get_windows_entry_in_grub(connection) - if windows_entry is not None: - # First method using grub-reboot command - result = connection.run(f"sudo grub-reboot \"{windows_entry}\"") - if result.return_code != 0: - # Try a second method using grub2-reboot command - result = connection.run(f"sudo grub2-reboot \"{windows_entry}\"") + if not is_unix_system(connection): + raise HomeAssistantError(f"System running at {connection.host} is not a Linux system.") + + windows_entry = get_windows_entry_in_grub(connection) + + if windows_entry is not None: + reboot_commands = ["sudo grub-reboot", "sudo grub2-reboot"] + + for reboot_command in reboot_commands: + result = connection.run(f"{reboot_command} \"{windows_entry}\"") - # Restart system if successful grub(2)-reboot command if result.return_code == 0: _LOGGER.debug("Rebooting to Windows") restart_system(connection) - else: - raise HomeAssistantError( - f"Could not restart system running on {connection.host} to Windows from Linux, all methods failed.") - else: - raise HomeAssistantError(f"Could not find Windows entry in grub for system running at {connection.host}.") + return + + raise HomeAssistantError(f"Failed to restart system running on {connection.host} to Windows from Linux.") else: - raise HomeAssistantError(f"System running at {connection.host} is not a Linux system.") + raise HomeAssistantError(f"Could not find Windows entry in grub for system running at {connection.host}.") def change_monitors_config(connection: Connection, monitors_config: dict): - """From a YAML config, changes the monitors configuration on the host, only works on Linux and Gnome (for now).""" - # TODO: Add support for Windows + """Change monitors configuration on the host (Linux + Gnome, and partial Windows support).""" if is_unix_system(connection): command_parts = ["gnome-monitor-config", "set"] for monitor, settings in monitors_config.items(): if settings.get('enabled', False): - if 'primary' in settings and settings['primary']: - command_parts.append(f'-LpM {monitor}') - else: - command_parts.append(f'-LM {monitor}') + command_parts.extend(['-LpM' if settings.get('primary', False) else '-LM', monitor]) if 'position' in settings: - command_parts.append(f'-x {settings["position"][0]} -y {settings["position"][1]}') + command_parts.extend(['-x', str(settings["position"][0]), '-y', str(settings["position"][1])]) if 'mode' in settings: - command_parts.append(f'-m {settings["mode"]}') + command_parts.extend(['-m', settings["mode"]]) if 'scale' in settings: - command_parts.append(f'-s {settings["scale"]}') + command_parts.extend(['-s', str(settings["scale"])]) if 'transform' in settings: - command_parts.append(f'-t {settings["transform"]}') + command_parts.extend(['-t', settings["transform"]]) command = ' '.join(command_parts) - _LOGGER.debug("Running command: %s", command) + result = connection.run(command) if result.return_code == 0: @@ -245,44 +208,53 @@ def change_monitors_config(connection: Connection, monitors_config: dict): else: raise HomeAssistantError("Could not change monitors config on system running on %s, check logs with debug", connection.host) + else: raise HomeAssistantError("Not implemented yet for Windows OS.") - # Use NIRCMD to change monitors config on Windows - # setdisplay {monitor:index/name} [width] [height] [color bits] {refresh rate} {-updatereg} {-allusers} - # TODO: Work in progress - + # TODO: Implement Windows support using NIRCMD command_parts = ["nircmd.exe", "setdisplay"] + # setdisplay {monitor:index/name} [width] [height] [color bits] {refresh rate} {-updatereg} {-allusers} for monitor, settings in monitors_config.items(): if settings.get('enabled', False): - if 'primary' in settings and settings['primary']: - command_parts.append(f'{monitor} -primary') - else: - command_parts.append(f'{monitor} -secondary') + command_parts.extend( + [f'{monitor} -primary' if settings.get('primary', False) else f'{monitor} -secondary']) if 'resolution' in settings: - command_parts.append(f'{settings["resolution"][0]} {settings["resolution"][1]}') + command_parts.extend([str(settings["resolution"][0]), str(settings["resolution"][1])]) if 'refresh_rate' in settings: - command_parts.append(f'-hz {settings["refresh_rate"]}') + command_parts.extend(['-hz', str(settings["refresh_rate"])]) if 'color_bits' in settings: - command_parts.append(f'-bits {settings["color_bits"]}') + command_parts.extend(['-bits', str(settings["color_bits"])]) + + command = ' '.join(command_parts) + _LOGGER.debug("Running command: %s", command) + + result = connection.run(command) + + if result.return_code == 0: + _LOGGER.info("Successfully changed monitors config on system running on %s.", connection.host) + else: + raise HomeAssistantError("Could not change monitors config on system running on %s, check logs with debug", + connection.host) def silent_install_nircmd(connection: Connection): """Silently install NIRCMD on a Windows system.""" if not is_unix_system(connection): - download_url = "http://www.nirsoft.net/utils/nircmd.zip" + download_url = "https://www.nirsoft.net/utils/nircmd.zip" + install_path = f"C:\\Users\\{connection.user}\\AppData\\Local\\EasyComputerManager" - # Download NIRCMD and save it in C:\Users\{username}\AppData\Local\EasyComputerManager\nircmd.zip and unzip it - commands = [ - f"powershell -Command \"Invoke-WebRequest -Uri {download_url} -OutFile ( New-Item -Path \"C:\\Users\\{connection.user}\\AppData\\Local\\EasyComputerManager\\nircmd.zip\" -Force ) -UseBasicParsing\"", - f"powershell -Command \"Expand-Archive C:\\Users\\{connection.user}\\AppData\\Local\\EasyComputerManager\\nircmd.zip -DestinationPath C:\\Users\\{connection.user}\\AppData\\Local\\EasyComputerManager\\", - f"powershell -Command \"Remove-Item C:\\Users\\{connection.user}\\AppData\\Local\\EasyComputerManager\\nircmd.zip\"" - ] + # Download and unzip NIRCMD + download_command = f"powershell -Command \"Invoke-WebRequest -Uri {download_url} -OutFile {install_path}\\nircmd.zip -UseBasicParsing\"" + unzip_command = f"powershell -Command \"Expand-Archive {install_path}\\nircmd.zip -DestinationPath {install_path}\"" + remove_zip_command = f"powershell -Command \"Remove-Item {install_path}\\nircmd.zip\"" + + commands = [download_command, unzip_command, remove_zip_command] for command in commands: result = connection.run(command) @@ -324,31 +296,35 @@ def steam_big_picture(connection: Connection, action: str): _LOGGER.debug(f"Running Steam Big Picture action {action} on system running at {connection.host}.") - result = None - match action: - case "start": - if is_unix_system(connection): - result = connection.run("export WAYLAND_DISPLAY=wayland-0; export DISPLAY=:0; steam -bigpicture &") - else: - result = connection.run("start steam://open/bigpicture") - case "stop": - if is_unix_system(connection): - result = connection.run("export WAYLAND_DISPLAY=wayland-0; export DISPLAY=:0; steam -shutdown &") - else: - # TODO: check for different Steam install paths - result = connection.run("C:\\Program Files (x86)\\Steam\\steam.exe -shutdown") - case "exit": - if is_unix_system(connection): - # TODO: find a way to exit Steam Big Picture - pass - else: - # TODO: need to test (thx @MasterHidra https://www.reddit.com/r/Steam/comments/5c9l20/comment/k5fmb3k) - result = connection.run("nircmd win close title \"Steam Big Picture Mode\"") - case _: - raise HomeAssistantError( - f"Invalid action {action} for Steam Big Picture on system running at {connection.host}.") + steam_commands = { + "start": { + "unix": "export WAYLAND_DISPLAY=wayland-0; export DISPLAY=:0; steam -bigpicture &", + "windows": "start steam://open/bigpicture" + }, + "stop": { + "unix": "export WAYLAND_DISPLAY=wayland-0; export DISPLAY=:0; steam -shutdown &", + "windows": "C:\\Program Files (x86)\\Steam\\steam.exe -shutdown" + # TODO: check for different Steam install paths + }, + "exit": { + "unix": None, # TODO: find a way to exit Steam Big Picture + "windows": "nircmd win close title \"Steam Big Picture Mode\"" + # TODO: need to test (thx @MasterHidra https://www.reddit.com/r/Steam/comments/5c9l20/comment/k5fmb3k) + } + } - if result is None or result.return_code != 0: + command = steam_commands.get(action) + + if command is None: + raise HomeAssistantError( + f"Invalid action {action} for Steam Big Picture on system running at {connection.host}.") + + if is_unix_system(connection): + result = connection.run(command.get("unix")) + else: + result = connection.run(command.get("windows")) + + if result.return_code != 0: raise HomeAssistantError(f"Could not {action} Steam Big Picture on system running at {connection.host}.") @@ -432,7 +408,7 @@ def change_audio_config(connection: Connection, volume: int, mute: bool, input_d # Set sink and source mute status if specified if mute is not None: commands.append(f"{executable} set-sink-mute {output_device} {'yes' if mute else 'no'}") - commands.append(f"{executable} set-source-mute {output_device} {'yes' if mute else 'no'}") + commands.append(f"{executable} set-source-mute {input_device} {'yes' if mute else 'no'}") # Execute commands for command in commands: @@ -440,7 +416,7 @@ def change_audio_config(connection: Connection, volume: int, mute: bool, input_d result = connection.run(command) if result.return_code != 0: - raise HomeAssistantError("Could not change audio config on system running on %s, check logs with debug", - connection.host) + raise HomeAssistantError( + f"Could not change audio config on system running on {connection.host}, check logs with debug") else: raise HomeAssistantError("Not implemented yet for Windows OS.") diff --git a/hacs.json b/hacs.json index 0d36d5e..19fa876 100644 --- a/hacs.json +++ b/hacs.json @@ -1,5 +1,7 @@ { "name": "Easy Computer Manager", - "country": ["CH"], + "country": [ + "CH" + ], "render_readme": true } \ No newline at end of file