massive refactor and code improvements

This commit is contained in:
Mathieu Broillet 2023-12-30 20:21:37 +01:00
parent 1b4313eaa6
commit f47e581fed
Signed by: mathieu
GPG Key ID: C0E9E0E95AF03319
8 changed files with 253 additions and 272 deletions

View File

@ -7,13 +7,13 @@ from __future__ import annotations
import logging import logging
from functools import partial from functools import partial
import homeassistant.helpers.config_validation as cv
import voluptuous as vol import voluptuous as vol
import wakeonlan import wakeonlan
import homeassistant.helpers.config_validation as cv
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_BROADCAST_ADDRESS, CONF_BROADCAST_PORT, CONF_MAC from homeassistant.const import CONF_BROADCAST_ADDRESS, CONF_BROADCAST_PORT, CONF_MAC
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall
from .const import DOMAIN, SERVICE_SEND_MAGIC_PACKET, SERVICE_CHANGE_MONITORS_CONFIG from .const import DOMAIN, SERVICE_SEND_MAGIC_PACKET, SERVICE_CHANGE_MONITORS_CONFIG
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -5,10 +5,10 @@ import logging
from typing import Any from typing import Any
import voluptuous as vol import voluptuous as vol
from paramiko.ssh_exception import AuthenticationException
from homeassistant import config_entries, exceptions from homeassistant import config_entries, exceptions
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from paramiko.ssh_exception import AuthenticationException
from . import utils from . import utils
from .const import DOMAIN from .const import DOMAIN

View File

@ -1,83 +1,90 @@
send_magic_packet: send_magic_packet:
name: Send magic packet name: Send Magic Packet
description: Send a 'magic packet' to wake up a device with 'Wake-On-LAN' capabilities. description: Send a 'magic packet' to awaken a device with 'Wake-On-LAN' capabilities.
fields: fields:
mac: mac:
name: MAC address name: MAC Address
description: MAC address of the device to wake up. description: MAC address of the target device.
required: true required: true
example: "aa:bb:cc:dd:ee:ff" example: "aa:bb:cc:dd:ee:ff"
selector: selector:
text: text:
broadcast_address: broadcast_address:
name: Broadcast address name: Broadcast Address
description: Broadcast IP where to send the magic packet. description: Broadcast IP to send the magic packet.
example: 192.168.255.255 example: 192.168.255.255
selector: selector:
text: text:
broadcast_port: broadcast_port:
name: Broadcast port name: Broadcast Port
description: Port where to send the magic packet. description: Port to send the magic packet.
default: 9 default: 9
selector: selector:
number: number:
min: 1 min: 1
max: 65535 max: 65535
restart_to_windows_from_linux: restart_to_windows_from_linux:
name: 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: target:
device: device:
integration: easy_computer_manager integration: easy_computer_manager
restart_to_linux_from_windows: restart_to_linux_from_windows:
name: 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: target:
device: device:
integration: easy_computer_manager integration: easy_computer_manager
start_computer_to_windows: start_computer_to_windows:
name: 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). description: Directly start the computer into Windows (boot to Linux, set Grub reboot, then boot to Windows).
target: target:
device: device:
integration: easy_computer_manager integration: easy_computer_manager
put_computer_to_sleep: put_computer_to_sleep:
name: Put computer to sleep name: Put Computer to Sleep
description: Put the computer to sleep. description: Put the computer into sleep mode.
target: target:
device: device:
integration: easy_computer_manager integration: easy_computer_manager
restart_computer: restart_computer:
name: Restart name: Restart Computer
description: Restart the computer. description: Restart the computer.
target: target:
device: device:
integration: easy_computer_manager integration: easy_computer_manager
change_monitors_config: change_monitors_config:
name: Change monitors config name: Change Monitors Configuration
description: Change monitors config. description: Modify monitors configuration.
target: target:
entity: entity:
integration: easy_computer_manager integration: easy_computer_manager
domain: switch domain: switch
fields: fields:
monitors_config: monitors_config:
name: Monitors config name: Monitors Configuration
description: Monitors config. description: Monitors configuration details.
required: true required: true
example: | example: |
HDMI-1: HDMI-1:
enabled: true enabled: true
primary: true primary: true
position: [ 0, 0 ] position: [0, 0]
mode: 3840x2160@120.000 mode: 3840x2160@120.000
transform: normal transform: normal
scale: 2 scale: 2
selector: selector:
object: object:
steam_big_picture: steam_big_picture:
name: Start/stop Steam Big Picture name: Start/Stop Steam Big Picture
description: Start/stop Steam Big Picture. description: Initiate or terminate Steam Big Picture mode.
target: target:
entity: entity:
integration: easy_computer_manager integration: easy_computer_manager
@ -85,7 +92,7 @@ steam_big_picture:
fields: fields:
action: action:
name: 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 required: true
example: "start" example: "start"
selector: selector:
@ -95,11 +102,12 @@ steam_big_picture:
value: start value: start
- label: Stop - label: Stop
value: stop value: stop
- label: Exit and go back to the desktop Steam UI - label: Exit and return to desktop Steam UI
value: exit value: exit
change_audio_config: change_audio_config:
name: Change audio config name: Change Audio Configuration
description: Change audio config (volume, mute, input, output). description: Adjust audio settings (volume, mute, input, output).
target: target:
entity: entity:
integration: easy_computer_manager integration: easy_computer_manager
@ -107,7 +115,7 @@ change_audio_config:
fields: fields:
volume: volume:
name: Volume name: Volume
description: The volume to set. description: Set the desired volume level.
example: 50 example: 50
selector: selector:
number: number:
@ -120,14 +128,14 @@ change_audio_config:
selector: selector:
boolean: boolean:
input_device: input_device:
name: Input device name: Input Device
description: The ID/name/description of the input device. description: Specify the ID/name/description of the input device.
example: "Kraken 7.1 Chroma Stéréo analogique" example: "Kraken 7.1 Chroma Stereo Analog"
selector: selector:
text: text:
output_output: output_device:
name: Output device name: Output Device
description: The ID/name/description of the output device. description: Specify the ID/name/description of the output device.
example: "Starship/Matisse HD Audio Controller Stéréo analogique" example: "Starship/Matisse HD Audio Controller Stereo Analog"
selector: selector:
text: text:

View File

@ -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 from __future__ import annotations
@ -274,7 +274,7 @@ class ComputerSwitch(SwitchEntity):
if monitors_config is not None and len(monitors_config) > 0: if monitors_config is not None and len(monitors_config) > 0:
utils.change_monitors_config(self._connection, monitors_config) utils.change_monitors_config(self._connection, monitors_config)
else: 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: def steam_big_picture(self, action: str) -> None:
"""Controls Steam Big Picture mode.""" """Controls Steam Big Picture mode."""
@ -282,7 +282,7 @@ class ComputerSwitch(SwitchEntity):
if action is not None: if action is not None:
utils.steam_big_picture(self._connection, action) utils.steam_big_picture(self._connection, action)
else: 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, def change_audio_config(self, volume: int | None = None, mute: bool | None = None, input_device: str | None = None,
output_device: str | None = None) -> None: output_device: str | None = None) -> None:
@ -291,52 +291,47 @@ class ComputerSwitch(SwitchEntity):
def 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."""
ping_cmd = [ ping_cmd = ["ping", "-c", "1", "-W", str(DEFAULT_PING_TIMEOUT), str(self._host)]
"ping",
"-c",
"1",
"-W",
str(DEFAULT_PING_TIMEOUT),
str(self._host),
]
status = sp.call(ping_cmd, stdout=sp.DEVNULL, stderr=sp.DEVNULL) status = sp.call(ping_cmd, stdout=sp.DEVNULL, stderr=sp.DEVNULL)
self._state = not bool(status) self._state = not bool(status)
# 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:
if not utils.test_connection(self._connection): if self._connection is None or not utils.test_connection(self._connection):
_LOGGER.info("Renewing SSH connection to %s using username %s", self._host, self._username) self.renew_ssh_connection()
if self._connection is not None: if not self._state:
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
return return
self._attr_extra_state_attributes = { self._attr_extra_state_attributes = {
"operating_system": utils.get_operating_system(self._connection), "operating_system": utils.get_operating_system(self._connection),
"operating_system_version": utils.get_operating_system_version( "operating_system_version": utils.get_operating_system_version(self._connection),
self._connection
),
"mac_address": self._mac_address, "mac_address": self._mac_address,
"ip_address": self._host, "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

View File

@ -1,25 +1,25 @@
{ {
"config": { "config": {
"abort": { "abort": {
"already_configured": "Device is already configured" "already_configured": "Device is already configured"
}, },
"error": { "error": {
"cannot_connect": "Failed to connect", "cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication", "invalid_auth": "Invalid authentication",
"unknown": "Unexpected error" "unknown": "Unexpected error"
}, },
"step": { "step": {
"user": { "user": {
"data": { "data": {
"host": "Host", "host": "Host",
"username": "Username", "username": "Username",
"password": "Password", "password": "Password",
"dualboot": "Is this a Linux/Windows dualboot computer?", "dualboot": "Is this a Linux/Windows dualboot computer?",
"port": "Port", "port": "Port",
"name": "Name", "name": "Name",
"mac": "MAC Address" "mac": "MAC Address"
}
}
} }
}
} }
}
} }

View File

@ -1,25 +1,25 @@
{ {
"config": { "config": {
"abort": { "abort": {
"already_configured": "L'appareil est déjà configuré." "already_configured": "L'appareil est déjà configuré."
}, },
"error": { "error": {
"cannot_connect": "Impossible de se connecter à l'appareil.", "cannot_connect": "Impossible de se connecter à l'appareil.",
"invalid_auth": "Identifiant ou mot de passe invalide.", "invalid_auth": "Identifiant ou mot de passe invalide.",
"unknown": "Erreur inconnue." "unknown": "Erreur inconnue."
}, },
"step": { "step": {
"user": { "user": {
"data": { "data": {
"host": "Adresse IP", "host": "Adresse IP",
"username": "Nom d'utilisateur", "username": "Nom d'utilisateur",
"password": "Mot de passe", "password": "Mot de passe",
"dualboot": "Est-ce que cet ordinateur est un dualboot Linux/Windows?", "dualboot": "Est-ce que cet ordinateur est un dualboot Linux/Windows?",
"port": "Port", "port": "Port",
"name": "Nom de l'appareil", "name": "Nom de l'appareil",
"mac": "Adresse MAC" "mac": "Adresse MAC"
}
}
} }
}
} }
}
} }

View File

@ -53,15 +53,12 @@ def get_operating_system_version(connection: Connection, is_unix=None):
result = connection.run( result = connection.run(
"awk -F'=' '/^NAME=|^VERSION=/{gsub(/\"/, \"\", $2); printf $2\" \"}\' /etc/os-release && echo").stdout "awk -F'=' '/^NAME=|^VERSION=/{gsub(/\"/, \"\", $2); printf $2\" \"}\' /etc/os-release && echo").stdout
if result == "": if result == "":
result = connection.run( result = connection.run("lsb_release -a | awk '/Description/ {print $2, $3, $4}'").stdout
"lsb_release -a | awk '/Description/ {print $2, $3, $4}'"
).stdout
return result return result
else: else:
return connection.run( return connection.run(
'for /f "tokens=1 delims=|" %i in (\'wmic os get Name ^| findstr /B /C:"Microsoft"\') do @echo %i' 'for /f "tokens=1 delims=|" %i in (\'wmic os get Name ^| findstr /B /C:"Microsoft"\') do @echo %i').stdout
).stdout
def get_operating_system(connection: Connection): def get_operating_system(connection: Connection):
@ -80,29 +77,19 @@ def shutdown_system(connection: Connection, is_unix=None):
if is_unix is None: if is_unix is None:
is_unix = is_unix_system(connection) is_unix = is_unix_system(connection)
if is_unix: shutdown_commands = {
# First method using shutdown command "unix": ["sudo shutdown -h now", "sudo init 0", "sudo systemctl poweroff"],
result = connection.run("sudo shutdown -h now") "windows": ["shutdown /s /t 0", "wmic os where Primary=TRUE call Shutdown"]
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.")
else: for command in shutdown_commands["unix" if is_unix else "windows"]:
# First method using shutdown command result = connection.run(command)
result = connection.run("shutdown /s /t 0") if result.return_code == 0:
if result.return_code != 0: _LOGGER.debug("System shutting down on %s.", connection.host)
# Try a second method using init command connection.close()
result = connection.run("wmic os where Primary=TRUE call Shutdown") return
if result.return_code != 0:
raise HomeAssistantError(f"Cannot shutdown system running at {connection.host}, all methods failed.")
connection.close() raise HomeAssistantError(f"Cannot shutdown system running at {connection.host}, all methods failed.")
def restart_system(connection: Connection, is_unix=None): 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: if is_unix is None:
is_unix = is_unix_system(connection) is_unix = is_unix_system(connection)
if is_unix: restart_commands = {
# First method using shutdown command "unix": ["sudo shutdown -r now", "sudo init 6", "sudo systemctl reboot"],
result = connection.run("sudo shutdown -r now") "windows": ["shutdown /r /t 0", "wmic os where Primary=TRUE call Reboot"]
if result.return_code != 0: }
# Try a second method using init command
result = connection.run("sudo init 6") for command in restart_commands["unix" if is_unix else "windows"]:
if result.return_code != 0: result = connection.run(command)
# Try a third method using systemctl command if result.return_code == 0:
result = connection.run("sudo systemctl reboot") _LOGGER.debug("System restarting on %s.", connection.host)
if result.return_code != 0: return
raise HomeAssistantError(f"Cannot restart system running at {connection.host}, all methods failed.")
else: raise HomeAssistantError(f"Cannot restart system running at {connection.host}, all methods failed.")
# 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.")
def sleep_system(connection: Connection, is_unix=None): 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: if is_unix is None:
is_unix = is_unix_system(connection) is_unix = is_unix_system(connection)
if is_unix: sleep_commands = {
# First method using systemctl command "unix": ["sudo systemctl suspend", "sudo pm-suspend"],
result = connection.run("sudo systemctl suspend") "windows": ["shutdown /h /t 0", "rundll32.exe powrprof.dll,SetSuspendState Sleep"]
if result.return_code != 0: }
# Try a second method using pm-suspend command
result = connection.run("sudo pm-suspend") for command in sleep_commands["unix" if is_unix else "windows"]:
if result.return_code != 0: result = connection.run(command)
raise HomeAssistantError( if result.return_code == 0:
f"Cannot put system running at {connection.host} to sleep, all methods failed.") _LOGGER.debug("System sleeping on %s.", connection.host)
else: return
# First method using shutdown command
result = connection.run("shutdown /h /t 0") raise HomeAssistantError(f"Cannot put system running at {connection.host} to sleep, all methods failed.")
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.")
def get_windows_entry_in_grub(connection: Connection): 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. Grabs the Windows entry name in GRUB.
Used later with grub-reboot to specify which entry to boot. 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: for command in commands:
_LOGGER.debug("Found Windows entry in grub : " + result.stdout.strip()) result = connection.run(command)
else: if result.return_code == 0 and result.stdout.strip():
result = connection.run("sudo awk -F \"'\" '/windows/ {print $2}' /boot/grub2/grub.cfg") _LOGGER.debug("Found Windows entry in GRUB: " + result.stdout.strip())
if result.return_code == 0: return result.stdout.strip()
_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
# Check if the entry is valid _LOGGER.error("Could not find Windows entry in GRUB for system running at %s.", connection.host)
if result.stdout.strip() != "": return None
return result.stdout.strip()
else:
_LOGGER.error("Could not find Windows entry on computer with address %s.")
return None
def restart_to_windows_from_linux(connection: Connection): def restart_to_windows_from_linux(connection: Connection):
"""Restart a running Linux system to Windows.""" """Restart a running Linux system to Windows."""
if is_unix_system(connection): if not is_unix_system(connection):
windows_entry = get_windows_entry_in_grub(connection) raise HomeAssistantError(f"System running at {connection.host} is not a Linux system.")
if windows_entry is not None:
# First method using grub-reboot command windows_entry = get_windows_entry_in_grub(connection)
result = connection.run(f"sudo grub-reboot \"{windows_entry}\"")
if result.return_code != 0: if windows_entry is not None:
# Try a second method using grub2-reboot command reboot_commands = ["sudo grub-reboot", "sudo grub2-reboot"]
result = connection.run(f"sudo grub2-reboot \"{windows_entry}\"")
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: if result.return_code == 0:
_LOGGER.debug("Rebooting to Windows") _LOGGER.debug("Rebooting to Windows")
restart_system(connection) restart_system(connection)
else: return
raise HomeAssistantError(
f"Could not restart system running on {connection.host} to Windows from Linux, all methods failed.") raise HomeAssistantError(f"Failed to restart system running on {connection.host} to Windows from Linux.")
else:
raise HomeAssistantError(f"Could not find Windows entry in grub for system running at {connection.host}.")
else: 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): 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).""" """Change monitors configuration on the host (Linux + Gnome, and partial Windows support)."""
# TODO: Add support for Windows
if is_unix_system(connection): if is_unix_system(connection):
command_parts = ["gnome-monitor-config", "set"] command_parts = ["gnome-monitor-config", "set"]
for monitor, settings in monitors_config.items(): for monitor, settings in monitors_config.items():
if settings.get('enabled', False): if settings.get('enabled', False):
if 'primary' in settings and settings['primary']: command_parts.extend(['-LpM' if settings.get('primary', False) else '-LM', monitor])
command_parts.append(f'-LpM {monitor}')
else:
command_parts.append(f'-LM {monitor}')
if 'position' in settings: 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: if 'mode' in settings:
command_parts.append(f'-m {settings["mode"]}') command_parts.extend(['-m', settings["mode"]])
if 'scale' in settings: if 'scale' in settings:
command_parts.append(f'-s {settings["scale"]}') command_parts.extend(['-s', str(settings["scale"])])
if 'transform' in settings: if 'transform' in settings:
command_parts.append(f'-t {settings["transform"]}') command_parts.extend(['-t', settings["transform"]])
command = ' '.join(command_parts) command = ' '.join(command_parts)
_LOGGER.debug("Running command: %s", command) _LOGGER.debug("Running command: %s", command)
result = connection.run(command) result = connection.run(command)
if result.return_code == 0: if result.return_code == 0:
@ -245,44 +208,53 @@ def change_monitors_config(connection: Connection, monitors_config: dict):
else: else:
raise HomeAssistantError("Could not change monitors config on system running on %s, check logs with debug", raise HomeAssistantError("Could not change monitors config on system running on %s, check logs with debug",
connection.host) connection.host)
else: else:
raise HomeAssistantError("Not implemented yet for Windows OS.") raise HomeAssistantError("Not implemented yet for Windows OS.")
# Use NIRCMD to change monitors config on Windows # TODO: Implement Windows support using NIRCMD
# setdisplay {monitor:index/name} [width] [height] [color bits] {refresh rate} {-updatereg} {-allusers}
# TODO: Work in progress
command_parts = ["nircmd.exe", "setdisplay"] command_parts = ["nircmd.exe", "setdisplay"]
# setdisplay {monitor:index/name} [width] [height] [color bits] {refresh rate} {-updatereg} {-allusers}
for monitor, settings in monitors_config.items(): for monitor, settings in monitors_config.items():
if settings.get('enabled', False): if settings.get('enabled', False):
if 'primary' in settings and settings['primary']: command_parts.extend(
command_parts.append(f'{monitor} -primary') [f'{monitor} -primary' if settings.get('primary', False) else f'{monitor} -secondary'])
else:
command_parts.append(f'{monitor} -secondary')
if 'resolution' in settings: 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: 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: 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): def silent_install_nircmd(connection: Connection):
"""Silently install NIRCMD on a Windows system.""" """Silently install NIRCMD on a Windows system."""
if not is_unix_system(connection): 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 # Download and unzip NIRCMD
commands = [ download_command = f"powershell -Command \"Invoke-WebRequest -Uri {download_url} -OutFile {install_path}\\nircmd.zip -UseBasicParsing\""
f"powershell -Command \"Invoke-WebRequest -Uri {download_url} -OutFile ( New-Item -Path \"C:\\Users\\{connection.user}\\AppData\\Local\\EasyComputerManager\\nircmd.zip\" -Force ) -UseBasicParsing\"", unzip_command = f"powershell -Command \"Expand-Archive {install_path}\\nircmd.zip -DestinationPath {install_path}\""
f"powershell -Command \"Expand-Archive C:\\Users\\{connection.user}\\AppData\\Local\\EasyComputerManager\\nircmd.zip -DestinationPath C:\\Users\\{connection.user}\\AppData\\Local\\EasyComputerManager\\", remove_zip_command = f"powershell -Command \"Remove-Item {install_path}\\nircmd.zip\""
f"powershell -Command \"Remove-Item C:\\Users\\{connection.user}\\AppData\\Local\\EasyComputerManager\\nircmd.zip\""
] commands = [download_command, unzip_command, remove_zip_command]
for command in commands: for command in commands:
result = connection.run(command) 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}.") _LOGGER.debug(f"Running Steam Big Picture action {action} on system running at {connection.host}.")
result = None steam_commands = {
match action: "start": {
case "start": "unix": "export WAYLAND_DISPLAY=wayland-0; export DISPLAY=:0; steam -bigpicture &",
if is_unix_system(connection): "windows": "start steam://open/bigpicture"
result = connection.run("export WAYLAND_DISPLAY=wayland-0; export DISPLAY=:0; steam -bigpicture &") },
else: "stop": {
result = connection.run("start steam://open/bigpicture") "unix": "export WAYLAND_DISPLAY=wayland-0; export DISPLAY=:0; steam -shutdown &",
case "stop": "windows": "C:\\Program Files (x86)\\Steam\\steam.exe -shutdown"
if is_unix_system(connection): # TODO: check for different Steam install paths
result = connection.run("export WAYLAND_DISPLAY=wayland-0; export DISPLAY=:0; steam -shutdown &") },
else: "exit": {
# TODO: check for different Steam install paths "unix": None, # TODO: find a way to exit Steam Big Picture
result = connection.run("C:\\Program Files (x86)\\Steam\\steam.exe -shutdown") "windows": "nircmd win close title \"Steam Big Picture Mode\""
case "exit": # TODO: need to test (thx @MasterHidra https://www.reddit.com/r/Steam/comments/5c9l20/comment/k5fmb3k)
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}.")
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}.") 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 # Set sink and source mute status if specified
if mute is not None: if mute is not None:
commands.append(f"{executable} set-sink-mute {output_device} {'yes' if mute else 'no'}") 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 # Execute commands
for command in 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) result = connection.run(command)
if result.return_code != 0: if result.return_code != 0:
raise HomeAssistantError("Could not change audio config on system running on %s, check logs with debug", raise HomeAssistantError(
connection.host) f"Could not change audio config on system running on {connection.host}, check logs with debug")
else: else:
raise HomeAssistantError("Not implemented yet for Windows OS.") raise HomeAssistantError("Not implemented yet for Windows OS.")

View File

@ -1,5 +1,7 @@
{ {
"name": "Easy Computer Manager", "name": "Easy Computer Manager",
"country": ["CH"], "country": [
"CH"
],
"render_readme": true "render_readme": true
} }