diff --git a/custom_components/easy_computer_manager/const.py b/custom_components/easy_computer_manager/const.py index 5390cc2..da8844c 100644 --- a/custom_components/easy_computer_manager/const.py +++ b/custom_components/easy_computer_manager/const.py @@ -9,3 +9,4 @@ SERVICE_START_COMPUTER_TO_WINDOWS = "start_computer_to_windows" SERVICE_RESTART_COMPUTER = "restart_computer" SERVICE_CHANGE_MONITORS_CONFIG = "change_monitors_config" SERVICE_STEAM_BIG_PICTURE = "steam_big_picture" +SERVICE_CHANGE_AUDIO_CONFIG = "change_audio_config" diff --git a/custom_components/easy_computer_manager/services.yaml b/custom_components/easy_computer_manager/services.yaml index d3bb87a..7dffbd9 100644 --- a/custom_components/easy_computer_manager/services.yaml +++ b/custom_components/easy_computer_manager/services.yaml @@ -96,4 +96,38 @@ steam_big_picture: - label: Stop value: stop - label: Exit and go back to the desktop Steam UI - value: exit \ No newline at end of file + value: exit +change_audio_config: + name: Change audio config + description: Change audio config (volume, mute, input, output). + target: + entity: + integration: easy_computer_manager + domain: switch + fields: + volume: + name: Volume + description: The volume to set. + example: 50 + selector: + number: + min: 0 + max: 100 + mute: + name: Mute + description: Mute the audio. + example: true + 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" + 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" + selector: + text: \ No newline at end of file diff --git a/custom_components/easy_computer_manager/switch.py b/custom_components/easy_computer_manager/switch.py index 8ded868..aa8f9fd 100644 --- a/custom_components/easy_computer_manager/switch.py +++ b/custom_components/easy_computer_manager/switch.py @@ -37,7 +37,7 @@ from paramiko.ssh_exception import AuthenticationException from . import utils from .const import SERVICE_RESTART_TO_WINDOWS_FROM_LINUX, SERVICE_PUT_COMPUTER_TO_SLEEP, \ SERVICE_START_COMPUTER_TO_WINDOWS, SERVICE_RESTART_COMPUTER, SERVICE_RESTART_TO_LINUX_FROM_WINDOWS, \ - SERVICE_CHANGE_MONITORS_CONFIG, SERVICE_STEAM_BIG_PICTURE + SERVICE_CHANGE_MONITORS_CONFIG, SERVICE_STEAM_BIG_PICTURE, SERVICE_CHANGE_AUDIO_CONFIG _LOGGER = logging.getLogger(__name__) @@ -96,19 +96,20 @@ async def async_setup_entry( platform = entity_platform.async_get_current_platform() - services = [SERVICE_RESTART_TO_WINDOWS_FROM_LINUX, - SERVICE_RESTART_TO_LINUX_FROM_WINDOWS, - SERVICE_PUT_COMPUTER_TO_SLEEP, - SERVICE_START_COMPUTER_TO_WINDOWS, - SERVICE_RESTART_COMPUTER] + basic_services = [SERVICE_RESTART_TO_WINDOWS_FROM_LINUX, + SERVICE_RESTART_TO_LINUX_FROM_WINDOWS, + SERVICE_PUT_COMPUTER_TO_SLEEP, + SERVICE_START_COMPUTER_TO_WINDOWS, + SERVICE_RESTART_COMPUTER] - for service in services: + for service in basic_services: platform.async_register_entity_service( service, {}, service, ) + # Register the service to change the monitors configuration platform.async_register_entity_service( SERVICE_CHANGE_MONITORS_CONFIG, make_entity_service_schema( @@ -116,6 +117,8 @@ async def async_setup_entry( ), SERVICE_CHANGE_MONITORS_CONFIG, ) + + # Register the service to control Steam Big Picture mode platform.async_register_entity_service( SERVICE_STEAM_BIG_PICTURE, make_entity_service_schema( @@ -124,6 +127,18 @@ async def async_setup_entry( SERVICE_STEAM_BIG_PICTURE, ) + # Register the service to change the audio configuration + platform.async_register_entity_service( + SERVICE_CHANGE_AUDIO_CONFIG, + make_entity_service_schema( + {vol.Optional("volume"): int, + vol.Optional("mute"): bool, + vol.Optional("input_device"): str, + vol.Optional("output_device"): str} + ), + SERVICE_CHANGE_AUDIO_CONFIG, + ) + class ComputerSwitch(SwitchEntity): """Representation of a computer switch.""" @@ -269,6 +284,10 @@ class ComputerSwitch(SwitchEntity): else: raise HomeAssistantError("You must specify an action.") + def change_audio_config(self, volume: int, mute: bool, input_device: str, output_device: str) -> None: + """Change the audio configuration using a YAML config file.""" + utils.change_audio_config(self._connection, volume, mute, input_device, output_device) + def update(self) -> None: """Ping the computer to see if it is online and update the state.""" ping_cmd = [ diff --git a/custom_components/easy_computer_manager/utils.py b/custom_components/easy_computer_manager/utils.py index 4278a77..3a86079 100644 --- a/custom_components/easy_computer_manager/utils.py +++ b/custom_components/easy_computer_manager/utils.py @@ -350,3 +350,91 @@ def steam_big_picture(connection: Connection, action: str): if result is None or result.return_code != 0: raise HomeAssistantError(f"Could not {action} Steam Big Picture on system running at {connection.host}.") + + +def get_audio_config(connection: Connection): + if is_unix_system(connection): + config = {'sinks': [], 'sources': []} + + def parse_device_info(lines, device_type): + devices = [] + current_device = {} + + for line in lines: + if line.startswith(f"{device_type} #"): + if current_device and "Monitor" not in current_device['description']: + devices.append(current_device) + current_device = {'id': int(re.search(r'#(\d+)', line).group(1))} + elif line.startswith(" Name:"): + current_device['name'] = line.split(":")[1].strip() + elif line.startswith(" State:"): + current_device['state'] = line.split(":")[1].strip() + elif line.startswith(" Description:"): + current_device['description'] = line.split(":")[1].strip() + + if current_device: + devices.append(current_device) + + return devices + + # Get sinks + result = connection.run("LANG=en_US.UTF-8 pactl list sinks") + if result.return_code != 0: + raise HomeAssistantError(f"Could not get audio sinks on system running at {connection.host}.") + config['sinks'] = parse_device_info(result.stdout.split('\n'), 'Sink') + + # Get sources + result = connection.run("LANG=en_US.UTF-8 pactl list sources") + if result.return_code != 0: + raise HomeAssistantError(f"Could not get audio sources on system running at {connection.host}.") + config['sources'] = parse_device_info(result.stdout.split('\n'), 'Source') + + return config + else: + raise HomeAssistantError("Not implemented yet for Windows OS.") + + +def change_audio_config(connection: Connection, volume: int, mute: bool, input_device: str = "@DEFAULT_SOURCE@", + output_device: str = "@DEFAULT_SINK@"): + """Change audio configuration on the host system.""" + + if is_unix_system(connection): + current_config = get_audio_config(connection) + executable = "pactl" + commands = [] + + def get_device_id(device_type, user_device): + for device in current_config[device_type]: + if device['description'] == user_device: + return device['name'] + return user_device + + # Set default sink if specified + if output_device and output_device != "@DEFAULT_SINK@": + output_device = get_device_id('sinks', output_device) + commands.append(f"{executable} set-default-sink {output_device}") + + # Set default source if specified + if input_device and input_device != "@DEFAULT_SOURCE@": + input_device = get_device_id('sources', input_device) + commands.append(f"{executable} set-default-source {input_device}") + + # Set sink volume if specified + if volume is not None: + commands.append(f"{executable} set-sink-volume {output_device} {volume}%") + + # 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'}") + + # Execute commands + for command in commands: + _LOGGER.debug("Running command: %s", command) + 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) + else: + raise HomeAssistantError("Not implemented yet for Windows OS.")