Skip to content

Feature: application volume command, audio output command & audio sensor overhaul #19

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
fb1dc7b
AudioSessionInfo now includes information about session state / first…
amadeo-alex Jul 25, 2023
5005849
AudioSessionInfo now also contains information about the playback device
amadeo-alex Jul 26, 2023
6b9728f
poc
amadeo-alex Jul 26, 2023
57b4b6f
completed base functionality, mvp string translations
amadeo-alex Jul 26, 2023
a4567d3
added precheck so audiodg doesn't show up in list of audio sessions
amadeo-alex Jul 26, 2023
51d4d09
cleanup
amadeo-alex Jul 26, 2023
7acc555
added json check; setappvolume command now supports only agent
amadeo-alex Aug 2, 2023
4c92fb2
removed setappvolume command switch case
amadeo-alex Aug 2, 2023
b52c031
untested poc of SetAudioOutputCommand
amadeo-alex Aug 2, 2023
8d2d427
simplified audio output activation
amadeo-alex Aug 2, 2023
3305b02
added using statements
amadeo-alex Aug 2, 2023
c8c08aa
added audio output devices sensor
amadeo-alex Aug 2, 2023
e82e883
cleanup
amadeo-alex Aug 2, 2023
0f9c81a
fixed device being disposed during first cycle of session foreach
amadeo-alex Aug 2, 2023
3db6695
fixed input devices sensor id
amadeo-alex Aug 2, 2023
8401884
removed device name from the sensor name to adhere to HA 2023.8 MQTT …
amadeo-alex Aug 2, 2023
cbde431
cleaned up "GetAudioDevices" functions
amadeo-alex Aug 2, 2023
0d06aa5
removed debug print
amadeo-alex Aug 2, 2023
3691690
removed debug print lines
amadeo-alex Aug 2, 2023
5edbab2
removed redundant class
amadeo-alex Aug 4, 2023
26040a5
simplified using statement
amadeo-alex Aug 4, 2023
71abc9b
added precheck so audiodg doesn't show up in list of audio sessions v2
amadeo-alex Aug 4, 2023
8c11d98
added missing string translations
amadeo-alex Sep 4, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/HASS.Agent.Staging/HASS.Agent.Shared/Enums/CommandType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ public enum CommandType
[EnumMember(Value = "SetVolumeCommand")]
SetVolumeCommand,

[LocalizedDescription("CommandType_SetApplicationVolumeCommand", typeof(Languages))]
[EnumMember(Value = "SetApplicationVolumeCommand")]
SetApplicationVolumeCommand,

[LocalizedDescription("CommandType_ShutdownCommand", typeof(Languages))]
[EnumMember(Value = "ShutdownCommand")]
ShutdownCommand,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using CoreAudio;
using HASS.Agent.Shared.Enums;
using Newtonsoft.Json;
using Serilog;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;

namespace HASS.Agent.Shared.HomeAssistant.Commands.InternalCommands
{
public class SetApplicationVolumeCommand : InternalCommand
{
private const string DefaultName = "setappvolume";
private static readonly Dictionary<int, string> ApplicationNames = new Dictionary<int, string>();

public SetApplicationVolumeCommand(string name = DefaultName, string friendlyName = DefaultName, string commandConfig = "", CommandEntityType entityType = CommandEntityType.Button, string id = default) : base(name ?? DefaultName, friendlyName ?? null, commandConfig, entityType, id)
{
State = "OFF";
}

public override void TurnOn()
{
if (string.IsNullOrWhiteSpace(CommandConfig))
{
Log.Error("[SETAPPVOLUME] Error, command config is null/empty/blank");

return;
}


TurnOnWithAction(CommandConfig);
}

private MMDevice GetAudioDeviceOrDefault(string playbackDeviceName)
{
var devices = Variables.AudioDeviceEnumerator.EnumerateAudioEndPoints(DataFlow.eRender, DeviceState.Active);
var playbackDevice = devices.Where(d => d.DeviceFriendlyName == playbackDeviceName).FirstOrDefault();

return playbackDevice ?? Variables.AudioDeviceEnumerator.GetDefaultAudioEndpoint(DataFlow.eRender, Role.Multimedia);
}

private string GetSessionDisplayName(AudioSessionControl2 session)
{
var procId = (int)session.ProcessID;

if (procId <= 0)
return session.DisplayName;

if (ApplicationNames.ContainsKey(procId))
return ApplicationNames[procId];

using var p = Process.GetProcessById(procId);
ApplicationNames.Add(procId, p.ProcessName);

return p.ProcessName;
}

public override void TurnOnWithAction(string action)
{
State = "ON";

try
{
var actionData = JsonConvert.DeserializeObject<ApplicationVolumeAction>(action);

if (string.IsNullOrWhiteSpace(actionData.ApplicationName))
{
Log.Error("[SETAPPVOLUME] Error, this command can be run only with action");

return;
}

using var audioDevice = GetAudioDeviceOrDefault(actionData.PlaybackDevice);
using var session = audioDevice.AudioSessionManager2?.Sessions?.Where(s =>
s != null &&
actionData.ApplicationName == GetSessionDisplayName(s)
).FirstOrDefault();

if (session == null)
{
Log.Error("[SETAPPVOLUME] Error, no session of application {app} can be found", actionData.ApplicationName);

return;
}

session.SimpleAudioVolume.Mute = actionData.Mute;
if (actionData.Volume == -1)
{
Log.Debug("[SETAPPVOLUME] No volume value provided, only mute has been set for {app}", actionData.ApplicationName);

return;
}

var volume = Math.Clamp(actionData.Volume, 0, 100) / 100.0f;
session.SimpleAudioVolume.MasterVolume = volume;
}
catch (Exception ex)
{
Log.Error("[SETAPPVOLUME] Error while processing action '{action}': {err}", action, ex.Message);
}
finally
{
State = "OFF";
}
}

private class ApplicationVolumeAction
{
public int Volume { get; set; } = -1;
public bool Mute { get; set; } = false;
public string ApplicationName { get; set; } = string.Empty;
public string PlaybackDevice { get; set; } = string.Empty;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using CoreAudio;
using HASS.Agent.Shared.Enums;
using Newtonsoft.Json;
using Serilog;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;

namespace HASS.Agent.Shared.HomeAssistant.Commands.InternalCommands
{
public class SetAudioOutputCommand : InternalCommand
{
private const string DefaultName = "setaudiooutput";

private string OutputDevice { get => CommandConfig; }

public SetAudioOutputCommand(string name = DefaultName, string friendlyName = DefaultName, string audioDevice = "", CommandEntityType entityType = CommandEntityType.Button, string id = default) : base(name ?? DefaultName, friendlyName ?? null, audioDevice, entityType, id)
{
State = "OFF";
}

public override void TurnOn()
{
if (string.IsNullOrWhiteSpace(OutputDevice))
{
Log.Error("[SETAUDIOOUT] Error, output device name cannot be null/blank");

return;
}

TurnOnWithAction(OutputDevice);
}

private MMDevice GetAudioDeviceOrDefault(string playbackDeviceName)
{
var devices = Variables.AudioDeviceEnumerator.EnumerateAudioEndPoints(DataFlow.eRender, DeviceState.Active);
var playbackDevice = devices.Where(d => d.DeviceFriendlyName == playbackDeviceName).FirstOrDefault();

return playbackDevice ?? Variables.AudioDeviceEnumerator.GetDefaultAudioEndpoint(DataFlow.eRender, Role.Multimedia);
}

public override void TurnOnWithAction(string action)
{
State = "ON";

try
{
var outputDevice = GetAudioDeviceOrDefault(action);
if (outputDevice == Variables.AudioDeviceEnumerator.GetDefaultAudioEndpoint(DataFlow.eRender, Role.Multimedia))
return;

outputDevice.Selected = true;
}
catch (Exception ex)
{
Log.Error("[SETAUDIOOUT] Error while processing action '{action}': {err}", action, ex.Message);
}
finally
{
State = "OFF";
}
}
}
}
Loading