massive refactor and code improvements
This commit is contained in:
parent
1b4313eaa6
commit
f47e581fed
@ -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__)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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:
|
@ -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):
|
||||||
|
self.renew_ssh_connection()
|
||||||
|
|
||||||
|
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),
|
||||||
|
"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)
|
_LOGGER.info("Renewing SSH connection to %s using username %s", self._host, self._username)
|
||||||
|
|
||||||
if self._connection is not None:
|
if self._connection is not None:
|
||||||
self._connection.close()
|
self._connection.close()
|
||||||
|
|
||||||
self._connection = utils.create_ssh_connection(self._host, self._username, self._password)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
self._connection = utils.create_ssh_connection(self._host, self._username, self._password)
|
||||||
self._connection.open()
|
self._connection.open()
|
||||||
except AuthenticationException as error:
|
except AuthenticationException as error:
|
||||||
_LOGGER.error("Could not authenticate to %s using username %s : %s", self._host, self._username,
|
_LOGGER.error("Could not authenticate to %s using username %s: %s", self._host, self._username, error)
|
||||||
error)
|
|
||||||
self._state = False
|
self._state = False
|
||||||
return
|
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
_LOGGER.error("Could not connect to %s using username %s : %s", self._host, self._username, 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
|
# Check if the error is due to timeout
|
||||||
if "timed out" in str(error):
|
if "timed out" in str(error):
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Computer at %s does not respond to the SSH request. Possibles causes : might be offline, "
|
"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.",
|
"the firewall is blocking the SSH port, or the SSH server is offline and/or misconfigured.",
|
||||||
self._host)
|
self._host
|
||||||
|
)
|
||||||
|
|
||||||
self._state = False
|
self._state = False
|
||||||
return
|
|
||||||
|
|
||||||
self._attr_extra_state_attributes = {
|
|
||||||
"operating_system": utils.get_operating_system(self._connection),
|
|
||||||
"operating_system_version": utils.get_operating_system_version(
|
|
||||||
self._connection
|
|
||||||
),
|
|
||||||
"mac_address": self._mac_address,
|
|
||||||
"ip_address": self._host,
|
|
||||||
}
|
|
||||||
|
@ -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:
|
|
||||||
# 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()
|
connection.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
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,24 +98,17 @@ 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:
|
|
||||||
# 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.")
|
raise HomeAssistantError(f"Cannot restart system running at {connection.host}, all methods failed.")
|
||||||
|
|
||||||
|
|
||||||
@ -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:
|
|
||||||
_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
|
|
||||||
if result.stdout.strip() != "":
|
|
||||||
return result.stdout.strip()
|
return result.stdout.strip()
|
||||||
else:
|
|
||||||
_LOGGER.error("Could not find Windows entry on computer with address %s.")
|
_LOGGER.error("Could not find Windows entry in GRUB for system running at %s.", connection.host)
|
||||||
return None
|
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:
|
else:
|
||||||
raise HomeAssistantError(f"Could not find Windows entry in grub for system running at {connection.host}.")
|
raise HomeAssistantError(f"Could not find Windows entry in grub for system running at {connection.host}.")
|
||||||
else:
|
|
||||||
raise HomeAssistantError(f"System running at {connection.host} is not a Linux system.")
|
|
||||||
|
|
||||||
|
|
||||||
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):
|
|
||||||
result = connection.run("export WAYLAND_DISPLAY=wayland-0; export DISPLAY=:0; steam -shutdown &")
|
|
||||||
else:
|
|
||||||
# TODO: check for different Steam install paths
|
# TODO: check for different Steam install paths
|
||||||
result = connection.run("C:\\Program Files (x86)\\Steam\\steam.exe -shutdown")
|
},
|
||||||
case "exit":
|
"exit": {
|
||||||
if is_unix_system(connection):
|
"unix": None, # TODO: find a way to exit Steam Big Picture
|
||||||
# TODO: find a way to exit Steam Big Picture
|
"windows": "nircmd win close title \"Steam Big Picture Mode\""
|
||||||
pass
|
|
||||||
else:
|
|
||||||
# TODO: need to test (thx @MasterHidra https://www.reddit.com/r/Steam/comments/5c9l20/comment/k5fmb3k)
|
# 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 _:
|
}
|
||||||
|
|
||||||
|
command = steam_commands.get(action)
|
||||||
|
|
||||||
|
if command is None:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
f"Invalid action {action} for Steam Big Picture on system running at {connection.host}.")
|
f"Invalid action {action} for Steam Big Picture on system running at {connection.host}.")
|
||||||
|
|
||||||
if result is None or result.return_code != 0:
|
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.")
|
||||||
|
Loading…
Reference in New Issue
Block a user