Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using HASS.Agent.Satellite.Service.Commands;
using HASS.Agent.Satellite.Service.Sensors;
using HASS.Agent.Shared.Managers.Audio;
using HASS.Agent.Shared.Managers.VoiceMeeterAudio;
using Serilog;

namespace HASS.Agent.Satellite.Service.Functions
Expand Down Expand Up @@ -63,6 +64,9 @@ internal static async Task ShutdownAsync(TimeSpan waitBeforeClosing)
//stop audio manager
AudioManager.Shutdown();

//stop voicemeeter audio manager
VoiceMeeterAudioManager.Shutdown();

// stop mqtt
await Variables.MqttManager.AnnounceAvailabilityAsync(true);
Variables.MqttManager.Disconnect();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ await Task.Run(delegate
case CommandType.SetVolumeCommand:
abstractCommand = new SetVolumeCommand(command.EntityName, command.Name, command.Command, command.EntityType, command.Id.ToString());
break;
case CommandType.VoicemeeterCommand:
abstractCommand = new VoicemeeterCommand(command.Command, command.EntityName, command.Name, command.EntityType, command.Id.ToString());
break;
default:
Log.Error("[SETTINGS_COMMANDS] [{name}] Unknown configured command type: {type}", command.EntityName, command.Type.ToString());
break;
Expand Down Expand Up @@ -237,6 +240,20 @@ await Task.Run(delegate
EntityType = multipleKeysCommand.EntityType,
};
}

case VoicemeeterCommand voicemeeterCommand:
{
_ = Enum.TryParse<CommandType>(voicemeeterCommand.GetType().Name, out var type);
return new ConfiguredCommand()
{
Id = Guid.Parse(voicemeeterCommand.Id),
EntityName = voicemeeterCommand.EntityName,
Name = voicemeeterCommand.Name,
Type = type,
EntityType = voicemeeterCommand.EntityType,
Command = voicemeeterCommand.Command
};
}
}

return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ await Task.Run(delegate
case SensorType.WebcamProcessSensor:
abstractSensor = new WebcamProcessSensor(sensor.UpdateInterval, sensor.EntityName, sensor.Name, sensor.Id.ToString(), sensor.AdvancedSettings);
break;
case SensorType.VoicemeeterSensor:
abstractSensor = new VoicemeeterSensor(sensor.Query, sensor.ApplyRounding, sensor.Round, sensor.UpdateInterval, sensor.EntityName, sensor.Name, sensor.Id.ToString(), sensor.AdvancedSettings);
break;
default:
Log.Error("[SETTINGS_SENSORS] [{name}] Unknown configured single-value sensor type: {type}", sensor.EntityName, sensor.Type.ToString());
break;
Expand Down Expand Up @@ -363,6 +366,24 @@ internal static ConfiguredSensor ConvertAbstractSingleValueToConfigured(Abstract
};
}

case VoicemeeterSensor voicemeeterSensor:
{
_ = Enum.TryParse<SensorType>(voicemeeterSensor.GetType().Name, out var type);
return new ConfiguredSensor
{
Id = Guid.Parse(voicemeeterSensor.Id),
EntityName = voicemeeterSensor.EntityName,
Name = voicemeeterSensor.Name,
Type = type,
UpdateInterval = voicemeeterSensor.UpdateIntervalSeconds,
IgnoreAvailability = voicemeeterSensor.IgnoreAvailability,
Query = voicemeeterSensor.Command,
ApplyRounding = voicemeeterSensor.ApplyRounding,
Round = voicemeeterSensor.Round,
AdvancedSettings = voicemeeterSensor.AdvancedSettings
};
}

default:
{
_ = Enum.TryParse<SensorType>(sensor.GetType().Name, out var type);
Expand Down
6 changes: 5 additions & 1 deletion src/HASS.Agent/HASS.Agent.Shared/Enums/CommandType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ public enum CommandType

[LocalizedDescription("CommandType_WinformsSleepCommand", typeof(Languages))]
[EnumMember(Value = "WinformsSleepCommand")]
WinformsSleepCommand
WinformsSleepCommand,

[LocalizedDescription("CommandType_VoicemeeterCommand", typeof(Languages))]
[EnumMember(Value = "VoicemeeterCommand")]
VoicemeeterCommand
}
}
6 changes: 5 additions & 1 deletion src/HASS.Agent/HASS.Agent.Shared/Enums/SensorType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ public enum SensorType

[LocalizedDescription("SensorType_ScreenshotSensor", typeof(Languages))]
[EnumMember(Value = "ScreenshotSensor")]
ScreenshotSensor
ScreenshotSensor,

[LocalizedDescription("SensorType_VoicemeeterSensor", typeof(Languages))]
[EnumMember(Value = "VoicemeeterSensor")]
VoicemeeterSensor,
}
}
1 change: 1 addition & 0 deletions src/HASS.Agent/HASS.Agent.Shared/HASS.Agent.Shared.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<DebugType>full</DebugType>
<SupportedOSPlatformVersion>10.0.17763.0</SupportedOSPlatformVersion>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using HASS.Agent.Shared.Enums;
using HASS.Agent.Shared.Managers.VoiceMeeterAudio;
using HASS.Agent.Shared.Models.HomeAssistant;
using Serilog;
using System.Diagnostics;

namespace HASS.Agent.Shared.HomeAssistant.Commands
{
/// <summary>
/// Command to perform an action or script through Voicemeeter
/// </summary>
public class VoicemeeterCommand : AbstractCommand
{
private const string DefaultName = "Voicemeeter";

public string Command { get; protected set; }
public string State { get; protected set; }
public Process Process { get; set; }

public VoicemeeterCommand(string command, string entityName = DefaultName, string name = DefaultName, CommandEntityType entityType = CommandEntityType.Switch, string id = default) : base(entityName ?? DefaultName, name ?? null, entityType, id)
{
Command = command;
State = "OFF";
}

public override void TurnOn()
{
State = "ON";

if (string.IsNullOrWhiteSpace(Command))
{
Log.Warning("[Voicemeeter] [{name}] Unable to execute, it's configured as action-only", EntityName);

State = "OFF";
return;
}

VoiceMeeterAudioManager.SetParameters(Command);

State = "OFF";
}

public override void TurnOff()
{
State = "OFF";
}

public override void TurnOnWithAction(string action)
{
State = "ON";
VoiceMeeterAudioManager.SetParameters(action);
State = "OFF";
}

public override DiscoveryConfigModel GetAutoDiscoveryConfig()
{
if (Variables.MqttManager == null) return null;

var deviceConfig = Variables.MqttManager.GetDeviceConfigModel();
if (deviceConfig == null) return null;

return new CommandDiscoveryConfigModel(Domain)
{
EntityName = EntityName,
Name = Name,
Unique_id = Id,
Availability_topic = $"{Variables.MqttManager.MqttDiscoveryPrefix()}/sensor/{deviceConfig.Name}/availability",
Command_topic = $"{Variables.MqttManager.MqttDiscoveryPrefix()}/{Domain}/{deviceConfig.Name}/{ObjectId}/set",
Action_topic = $"{Variables.MqttManager.MqttDiscoveryPrefix()}/{Domain}/{deviceConfig.Name}/{ObjectId}/action",
State_topic = $"{Variables.MqttManager.MqttDiscoveryPrefix()}/{Domain}/{deviceConfig.Name}/{ObjectId}/state",
Device = deviceConfig,
};
}

public override string GetState() => State;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using HASS.Agent.Shared.Managers.VoiceMeeterAudio;
using HASS.Agent.Shared.Models.HomeAssistant;
using System;

namespace HASS.Agent.Shared.HomeAssistant.Sensors;

/// <summary>
/// Sensor containing the result of the provided Powershell command or script
/// </summary>
public class VoicemeeterSensor : AbstractSingleValueSensor
{
private const string DefaultName = "voicemeetersensor";

public string Command { get; private set; }
public bool ApplyRounding { get; private set; }
public int? Round { get; private set; }

public VoicemeeterSensor(string command, bool applyRounding = false, int? round = null, int? updateInterval = null, string entityName = DefaultName, string name = DefaultName, string id = default, string advancedSettings = default) : base(entityName ?? DefaultName, name ?? null, updateInterval ?? 10, id, advancedSettings: advancedSettings)
{
Command = command;
ApplyRounding = applyRounding;
Round = round;
}

public override DiscoveryConfigModel GetAutoDiscoveryConfig()
{
if (Variables.MqttManager == null)
return null;

var deviceConfig = Variables.MqttManager.GetDeviceConfigModel();
if (deviceConfig == null)
return null;

return AutoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel(Domain)
{
EntityName = EntityName,
Name = Name,
Unique_id = Id,
Device = deviceConfig,
State_topic = $"{Variables.MqttManager.MqttDiscoveryPrefix()}/{Domain}/{deviceConfig.Name}/{EntityName}/state",
Availability_topic = $"{Variables.MqttManager.MqttDiscoveryPrefix()}/{Domain}/{deviceConfig.Name}/availability"
});
}

public override string GetState()
{
var result = VoiceMeeterAudioManager.GetParameter(Command);
if (result == float.NaN)
{
return null;
}
// optionally apply rounding
if (ApplyRounding && Round != null)
{
result = (float)Math.Round(result, (int)Round);
}
return result.ToString();
}

public override string GetAttributes() => string.Empty;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;

namespace HASS.Agent.Shared.Managers.VoiceMeeterAudio.Native;

internal interface IVoicemeeterPInvoke : IDisposable
{
bool IsLoggedIn { get; }
void Login();
bool SetParameters(string paramScript);
int IsParametersDirtyRaw();
bool IsParametersDirty();
float GetParameter(string param);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace HASS.Agent.Shared.Managers.VoiceMeeterAudio.Native;
public enum LoginResponse
{
AlreadyLoggedIn = -2,
NoClient = -1,
Ok = 0,
VoiceMeeterNotRunning = 1,
LoggedOff
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
using Serilog;
using System;
using System.Runtime.InteropServices;

namespace HASS.Agent.Shared.Managers.VoiceMeeterAudio.Native;
internal partial class Voicemeeter64PInvoke : IVoicemeeterPInvoke
{
private const string RemoteLibraryPath = @"C:\Program Files (x86)\VB\Voicemeeter\VoicemeeterRemote64.dll";
private bool disposedValue;
private LoginResponse currentLoginStatus = LoginResponse.LoggedOff;

public bool IsLoggedIn => currentLoginStatus >= 0;

public Voicemeeter64PInvoke()
{
currentLoginStatus = VBVMR_Login();
if (!IsLoggedIn)
{
Log.Error($"[VMAUDIOMGR] Voicemeeter did not login correctly, statuscode: {currentLoginStatus}");
}
}

public void Login()
{
currentLoginStatus = VBVMR_Login();
}

public bool SetParameters(string paramScript)
{
if (IsLoggedIn)
{
var result = VBVMR_SetParameters(paramScript);
if (result == 0)
{
return true;
}
if (result < 0)
{
Log.Error("[VMAUDIOMGR] Unexpected error running parameter script");
}
if (result > 0)
{
Log.Error($"[VMAUDIOMGR] Error in script detected at line nr: {result}");
}
}
return false;
}

public int IsParametersDirtyRaw()
{
return VBVMR_IsParametersDirty();
}

public bool IsParametersDirty()
{
return IsLoggedIn && VBVMR_IsParametersDirty() != 0;
}

public float GetParameter(string param)
{
if (IsLoggedIn)
{
var result = VBVMR_GetParameter(param, out float val);
if (result == 0)
{
return val;
}
if (result < 0)
{
/* -1: error
-2: no server.
-3: unknown parameter
-5: structure mismatch */
Log.Error("[VMAUDIOMGR] Unexpected error getting parameter {param}, code: {result}", param, result);
}
}
return float.NaN;
}

protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
VBVMR_Logout();
disposedValue = true;
}
}

~Voicemeeter64PInvoke()
{
Dispose(disposing: false);
}

public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}

[LibraryImport(RemoteLibraryPath, EntryPoint = "VBVMR_Login")]
private static partial LoginResponse VBVMR_Login();

[LibraryImport(RemoteLibraryPath, EntryPoint = "VBVMR_Logout")]
private static partial int VBVMR_Logout();

[LibraryImport(RemoteLibraryPath, EntryPoint = "VBVMR_SetParametersW", StringMarshalling = StringMarshalling.Utf16)]
private static partial int VBVMR_SetParameters(string paramScript);

[LibraryImport(RemoteLibraryPath, EntryPoint = "VBVMR_IsParametersDirty")]
private static partial int VBVMR_IsParametersDirty();

[LibraryImport(RemoteLibraryPath, EntryPoint = "VBVMR_GetParameterFloat", StringMarshalling = StringMarshalling.Utf16)]
private static partial int VBVMR_GetParameter([MarshalAs(UnmanagedType.LPStr)] string paramName, out float value);
}
Loading