You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
hass-workstation-service/hass-workstation-service/Data/ConfigurationService.cs

669 lines
30 KiB

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json;
using System.Threading.Tasks;
using hass_workstation_service.Communication;
using hass_workstation_service.Communication.InterProcesCommunication.Models;
using hass_workstation_service.Communication.NamedPipe;
using hass_workstation_service.Domain.Commands;
using hass_workstation_service.Domain.Sensors;
using Microsoft.Extensions.Configuration;
using Microsoft.Win32;
using MQTTnet;
using MQTTnet.Client;
using MQTTnet.Client.Options;
using MQTTnet.Extensions.ManagedClient;
using Serilog;
namespace hass_workstation_service.Data
{
public class ConfigurationService : IConfigurationService
{
public ICollection<AbstractSensor> ConfiguredSensors { get; private set; }
public ICollection<AbstractCommand> ConfiguredCommands { get; private set; }
public GeneralSettings GeneralSettings { get; private set; }
public Action<IManagedMqttClientOptions> MqqtConfigChangedHandler { get; set; }
public Action<string> NamePrefixChangedHandler { get; set; }
private readonly DeviceConfigModel _deviceConfigModel;
private bool BrokerSettingsFileLocked { get; set; }
private bool SensorsSettingsFileLocked { get; set; }
private bool CommandSettingsFileLocked { get; set; }
private bool GeneralSettingsFileLocked { get; set; }
private bool _sensorsLoading { get; set; }
private readonly string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Hass Workstation Service");
private const string MQTT_SETTINGS_FILENAME = "mqttbroker.json";
private const string SENSORS_SETTINGS_FILENAME = "configured-sensors.json";
private const string COMMANDS_SETTINGS_FILENAME = "configured-commands.json";
private const string GENERAL_SETTINGS_FILENAME = "general-settings.json";
public ConfigurationService(DeviceConfigModel deviceConfigModel)
{
this._deviceConfigModel = deviceConfigModel;
if (!File.Exists(Path.Combine(path, MQTT_SETTINGS_FILENAME)))
{
File.Create(Path.Combine(path, MQTT_SETTINGS_FILENAME)).Close();
}
if (!File.Exists(Path.Combine(path, SENSORS_SETTINGS_FILENAME)))
{
File.Create(Path.Combine(path, SENSORS_SETTINGS_FILENAME)).Close();
}
if (!File.Exists(Path.Combine(path, COMMANDS_SETTINGS_FILENAME)))
{
File.Create(Path.Combine(path, COMMANDS_SETTINGS_FILENAME)).Close();
}
if (!File.Exists(Path.Combine(path, GENERAL_SETTINGS_FILENAME)))
{
File.Create(Path.Combine(path, GENERAL_SETTINGS_FILENAME)).Close();
}
ConfiguredSensors = new List<AbstractSensor>();
ConfiguredCommands = new List<AbstractCommand>();
this.ReadGeneralSettings();
}
public async void ReadSensorSettings(MqttPublisher publisher)
{
this._sensorsLoading = true;
while (this.SensorsSettingsFileLocked)
{
await Task.Delay(500);
}
this.SensorsSettingsFileLocked = true;
List<ConfiguredSensor> sensors = new List<ConfiguredSensor>();
using (var stream = new FileStream(Path.Combine(path, SENSORS_SETTINGS_FILENAME), FileMode.Open))
{
Log.Logger.Information($"reading configured sensors from: {stream.Name}");
if (stream.Length > 0)
{
sensors = await JsonSerializer.DeserializeAsync<List<ConfiguredSensor>>(stream);
}
stream.Close();
this.SensorsSettingsFileLocked = false;
}
foreach (ConfiguredSensor configuredSensor in sensors)
{
AbstractSensor sensor = null;
switch (configuredSensor.Type)
{
case "UserNotificationStateSensor":
sensor = new UserNotificationStateSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id);
break;
case "DummySensor":
sensor = new DummySensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id);
break;
case "CurrentClockSpeedSensor":
sensor = new CurrentClockSpeedSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id);
break;
case "CPULoadSensor":
sensor = new CPULoadSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id);
break;
case "MemoryUsageSensor":
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
sensor = new MemoryUsageSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id);
}
break;
case "ActiveWindowSensor":
sensor = new ActiveWindowSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id);
break;
case "NamedWindowSensor":
sensor = new NamedWindowSensor(publisher, configuredSensor.WindowName, configuredSensor.Name, configuredSensor.UpdateInterval, configuredSensor.Id);
break;
case "LastActiveSensor":
sensor = new LastActiveSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id);
break;
case "LastBootSensor":
sensor = new LastBootSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id);
break;
case "WebcamActiveSensor":
sensor = new WebcamActiveSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id);
break;
case "WebcamProcessSensor":
sensor = new WebcamProcessSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id);
break;
case "MicrophoneActiveSensor":
sensor = new MicrophoneActiveSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id);
break;
case "MicrophoneProcessSensor":
sensor = new MicrophoneProcessSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id);
break;
case "SessionStateSensor":
sensor = new SessionStateSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id);
break;
case "CurrentVolumeSensor":
sensor = new CurrentVolumeSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id);
break;
case "MasterVolumeSensor":
sensor = new MasterVolumeSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id);
break;
case "GpuTemperatureSensor":
sensor = new GpuTemperatureSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id);
break;
case "GpuLoadSensor":
sensor = new GpuLoadSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id);
break;
// keep this one last!
case "WMIQuerySensor":
sensor = new WMIQuerySensor(publisher, configuredSensor.Query, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id, configuredSensor.Scope);
break;
default:
Log.Logger.Error("unsupported sensor type in config");
break;
}
if (sensor != null)
{
this.ConfiguredSensors.Add(sensor);
}
this._sensorsLoading = false;
}
}
public async void ReadCommandSettings(MqttPublisher publisher)
{
while (this.CommandSettingsFileLocked)
{
await Task.Delay(500);
}
this.CommandSettingsFileLocked = true;
List<ConfiguredCommand> commands = new List<ConfiguredCommand>();
using (var stream = new FileStream(Path.Combine(path, COMMANDS_SETTINGS_FILENAME), FileMode.Open))
{
Log.Logger.Information($"reading configured commands from: {stream.Name}");
if (stream.Length > 0)
{
commands = await JsonSerializer.DeserializeAsync<List<ConfiguredCommand>>(stream);
}
stream.Close();
this.CommandSettingsFileLocked = false;
}
foreach (ConfiguredCommand configuredCommand in commands)
{
AbstractCommand command = null;
switch (configuredCommand.Type)
{
case "ShutdownCommand":
command = new ShutdownCommand(publisher, configuredCommand.Name, configuredCommand.Id);
break;
case "RestartCommand":
command = new RestartCommand(publisher, configuredCommand.Name, configuredCommand.Id);
break;
case "HibernateCommand":
command = new HibernateCommand(publisher, configuredCommand.Name, configuredCommand.Id);
break;
case "LogOffCommand":
command = new LogOffCommand(publisher, configuredCommand.Name, configuredCommand.Id);
break;
case "CustomCommand":
command = new CustomCommand(publisher, configuredCommand.Command, configuredCommand.Name, configuredCommand.Id);
break;
case "PlayPauseCommand":
command = new PlayPauseCommand(publisher, configuredCommand.Name, configuredCommand.Id);
break;
case "NextCommand":
command = new NextCommand(publisher, configuredCommand.Name, configuredCommand.Id);
break;
case "PreviousCommand":
command = new PreviousCommand(publisher, configuredCommand.Name, configuredCommand.Id);
break;
case "VolumeUpCommand":
command = new VolumeUpCommand(publisher, configuredCommand.Name, configuredCommand.Id);
break;
case "VolumeDownCommand":
command = new VolumeDownCommand(publisher, configuredCommand.Name, configuredCommand.Id);
break;
case "MuteCommand":
command = new MuteCommand(publisher, configuredCommand.Name, configuredCommand.Id);
break;
case "KeyCommand":
command = new KeyCommand(publisher, configuredCommand.KeyCode, configuredCommand.Name, configuredCommand.Id);
break;
default:
Log.Logger.Error("unsupported command type in config");
break;
}
if (command != null)
{
this.ConfiguredCommands.Add(command);
}
}
}
public async Task<GeneralSettings> ReadGeneralSettings()
{
while (this.GeneralSettingsFileLocked)
{
await Task.Delay(500);
}
this.GeneralSettingsFileLocked = true;
GeneralSettings settings = new GeneralSettings();
using (var stream = new FileStream(Path.Combine(path, GENERAL_SETTINGS_FILENAME), FileMode.Open))
{
Log.Logger.Information($"reading general settings from: {stream.Name}");
if (stream.Length > 0)
{
settings = await JsonSerializer.DeserializeAsync<GeneralSettings>(stream);
}
stream.Close();
this.GeneralSettings = settings;
this.GeneralSettingsFileLocked = false;
return settings;
}
}
/// <summary>
/// Writes provided settings to the config file and reconfigures all sensors and commands if the nameprefix changed
/// </summary>
/// <param name="settings"></param>
public async void WriteGeneralSettingsAsync(GeneralSettings settings)
{
while (this.GeneralSettingsFileLocked)
{
await Task.Delay(500);
}
this.GeneralSettingsFileLocked = true;
using (FileStream stream = new FileStream(Path.Combine(path, GENERAL_SETTINGS_FILENAME), FileMode.Open))
{
stream.SetLength(0);
Log.Logger.Information($"writing general settings to: {stream.Name}");
await JsonSerializer.SerializeAsync(stream, settings);
stream.Close();
}
this.GeneralSettingsFileLocked = false;
// if the nameprefix changed, we need to update all sensors and commands to reflect the new name
if (settings.NamePrefix != this.GeneralSettings.NamePrefix)
{
// notify the mqtt publisher of the new prefix
this.NamePrefixChangedHandler.Invoke(settings.NamePrefix);
foreach (AbstractSensor sensor in this.ConfiguredSensors)
{
await sensor.UnPublishAutoDiscoveryConfigAsync();
sensor.PublishAutoDiscoveryConfigAsync();
}
foreach (AbstractCommand command in this.ConfiguredCommands)
{
await command.UnPublishAutoDiscoveryConfigAsync();
command.PublishAutoDiscoveryConfigAsync();
}
}
}
public async Task<IManagedMqttClientOptions> GetMqttClientOptionsAsync()
{
ConfiguredMqttBroker configuredBroker = await ReadMqttSettingsAsync();
if (configuredBroker != null && configuredBroker.Host != null)
{
var mqttClientOptionsBuilder = new MqttClientOptionsBuilder()
.WithTcpServer(configuredBroker.Host, configuredBroker.Port)
.WithCredentials(configuredBroker.Username, configuredBroker.Password.ToString())
.WithKeepAlivePeriod(TimeSpan.FromSeconds(30));
/* Start LWT */
var lwtMessage = new MqttApplicationMessageBuilder()
.WithTopic($"homeassistant/sensor/{_deviceConfigModel.Name}/availability")
.WithPayload("offline");
if (configuredBroker.RetainLWT) {
lwtMessage.WithRetainFlag();
}
mqttClientOptionsBuilder.WithWillMessage(lwtMessage.Build());
/* End LWT */
/* Start TLS/Certificate configuration */
var tlsParameters = new MqttClientOptionsBuilderTlsParameters()
{
UseTls = configuredBroker.UseTLS,
AllowUntrustedCertificates = true,
SslProtocol = configuredBroker.UseTLS ? System.Security.Authentication.SslProtocols.Tls12 : System.Security.Authentication.SslProtocols.None
};
var certs = new List<X509Certificate>();
if (!string.IsNullOrEmpty(configuredBroker.RootCAPath)) {
certs.Add(new X509Certificate2(configuredBroker.RootCAPath));
}
if (!string.IsNullOrEmpty(configuredBroker.ClientCertPath))
{
certs.Add(new X509Certificate2(configuredBroker.ClientCertPath));
}
if (certs.Count > 0) {
// IF certs are configured, let's add them here
tlsParameters.Certificates = certs;
}
mqttClientOptionsBuilder.WithTls(tlsParameters);
/* End TLS/Certificate Configuration */
var mqttClientOptions = mqttClientOptionsBuilder.Build();
return new ManagedMqttClientOptionsBuilder().WithClientOptions(mqttClientOptions).Build();
}
else
{
Program.StartUI();
return null;
}
}
/// <summary>
/// Gets the saved broker settings from configfile. Null if not found.
/// </summary>
/// <returns></returns>
public async Task<ConfiguredMqttBroker> ReadMqttSettingsAsync()
{
while (this.BrokerSettingsFileLocked)
{
await Task.Delay(500);
}
this.BrokerSettingsFileLocked = true;
ConfiguredMqttBroker configuredBroker = null;
using (FileStream stream = new FileStream(Path.Combine(path, MQTT_SETTINGS_FILENAME), FileMode.Open))
{
Log.Logger.Information($"reading configured mqttbroker from: {stream.Name}");
if (stream.Length > 0)
{
configuredBroker = await JsonSerializer.DeserializeAsync<ConfiguredMqttBroker>(stream);
}
stream.Close();
}
this.BrokerSettingsFileLocked = false;
return configuredBroker;
}
public async void WriteSensorSettingsAsync()
{
while (this.SensorsSettingsFileLocked)
{
await Task.Delay(500);
}
this.SensorsSettingsFileLocked = true;
List<ConfiguredSensor> configuredSensorsToSave = new List<ConfiguredSensor>();
using (FileStream stream = new FileStream(Path.Combine(path, SENSORS_SETTINGS_FILENAME), FileMode.Open))
{
stream.SetLength(0);
Log.Logger.Information($"writing configured sensors to: {stream.Name}");
foreach (AbstractSensor sensor in this.ConfiguredSensors)
{
if (sensor is WMIQuerySensor wmiSensor)
{
#pragma warning disable CA1416 // Validate platform compatibility. We ignore it here because this would never happen. A cleaner solution may be implemented later.
configuredSensorsToSave.Add(new ConfiguredSensor() { Id = wmiSensor.Id, Name = wmiSensor.Name, Type = wmiSensor.GetType().Name, UpdateInterval = wmiSensor.UpdateInterval, Query = wmiSensor.Query, Scope = wmiSensor.Scope });
#pragma warning restore CA1416 // Validate platform compatibility
}
else if (sensor is NamedWindowSensor namedWindowSensor)
{
configuredSensorsToSave.Add(new ConfiguredSensor() { Id = namedWindowSensor.Id, Name = namedWindowSensor.Name, Type = namedWindowSensor.GetType().Name, UpdateInterval = namedWindowSensor.UpdateInterval, WindowName = namedWindowSensor.WindowName });
}
else
{
configuredSensorsToSave.Add(new ConfiguredSensor() { Id = sensor.Id, Name = sensor.Name, Type = sensor.GetType().Name, UpdateInterval = sensor.UpdateInterval });
}
}
await JsonSerializer.SerializeAsync(stream, configuredSensorsToSave);
stream.Close();
}
this.SensorsSettingsFileLocked = false;
}
public async void WriteCommandSettingsAsync()
{
while (this.CommandSettingsFileLocked)
{
await Task.Delay(500);
}
this.CommandSettingsFileLocked = true;
List<ConfiguredCommand> configuredCommandsToSave = new List<ConfiguredCommand>();
using (FileStream stream = new FileStream(Path.Combine(path, COMMANDS_SETTINGS_FILENAME), FileMode.Open))
{
stream.SetLength(0);
Log.Logger.Information($"writing configured commands to: {stream.Name}");
foreach (AbstractCommand command in this.ConfiguredCommands)
{
if (command is CustomCommand customCommand)
{
configuredCommandsToSave.Add(new ConfiguredCommand() { Id = customCommand.Id, Name = customCommand.Name, Type = customCommand.GetType().Name, Command = customCommand.Command });
}
if (command is KeyCommand customKeyCommand)
{
configuredCommandsToSave.Add(new ConfiguredCommand() { Id = customKeyCommand.Id, Name = customKeyCommand.Name, Type = customKeyCommand.GetType().Name, KeyCode = customKeyCommand.KeyCode });
}
}
await JsonSerializer.SerializeAsync(stream, configuredCommandsToSave);
stream.Close();
}
this.CommandSettingsFileLocked = false;
}
public void AddConfiguredSensor(AbstractSensor sensor)
{
AddSensor(sensor);
WriteSensorSettingsAsync();
}
public void AddConfiguredCommand(AbstractCommand command)
{
AddCommand(command);
WriteCommandSettingsAsync();
}
public void AddConfiguredSensors(List<AbstractSensor> sensors)
{
sensors.ForEach(sensor => AddSensor(sensor));
WriteSensorSettingsAsync();
}
public void AddConfiguredCommands(List<AbstractCommand> commands)
{
commands.ForEach(command => AddCommand(command));
WriteCommandSettingsAsync();
}
public async void DeleteConfiguredSensor(Guid id)
{
await DeleteSensor(id);
WriteSensorSettingsAsync();
}
public async void DeleteConfiguredCommand(Guid id)
{
await DeleteCommand(id);
WriteCommandSettingsAsync();
}
/// <summary>
///
/// </summary>
/// <param name="id">The Id of the sensor to replace</param>
/// <param name="sensor">The new sensor</param>
public async void UpdateConfiguredSensor(Guid id, AbstractSensor sensor)
{
await DeleteSensor(id);
await Task.Delay(500);
AddSensor(sensor);
WriteSensorSettingsAsync();
}
public async void UpdateConfiguredCommand(Guid id, AbstractCommand command)
{
await DeleteCommand(id);
await Task.Delay(500);
AddCommand(command);
WriteCommandSettingsAsync();
}
private void AddSensor(AbstractSensor sensor)
{
ConfiguredSensors.Add(sensor);
sensor.PublishAutoDiscoveryConfigAsync();
}
private void AddCommand(AbstractCommand command)
{
ConfiguredCommands.Add(command);
command.PublishAutoDiscoveryConfigAsync();
}
private async Task DeleteSensor(Guid id)
{
var sensorToRemove = ConfiguredSensors.FirstOrDefault(s => s.Id == id);
if (sensorToRemove == null)
{
Log.Logger.Warning($"sensor with id {id} not found");
return;
}
await sensorToRemove.UnPublishAutoDiscoveryConfigAsync();
ConfiguredSensors.Remove(sensorToRemove);
}
private async Task DeleteCommand(Guid id)
{
var commandToRemove = ConfiguredCommands.FirstOrDefault(c => c.Id == id);
if (commandToRemove == null)
{
Log.Logger.Warning($"command with id {id} not found");
return;
}
await commandToRemove.UnPublishAutoDiscoveryConfigAsync();
ConfiguredCommands.Remove(commandToRemove);
}
/// <summary>
/// Writes provided settings to the config file and invokes a reconfigure to the current mqqtClient
/// </summary>
/// <param name="settings"></param>
public async void WriteMqttBrokerSettingsAsync(MqttSettings settings)
{
while (this.BrokerSettingsFileLocked)
{
await Task.Delay(500);
}
this.BrokerSettingsFileLocked = true;
using (FileStream stream = new FileStream(Path.Combine(path, MQTT_SETTINGS_FILENAME), FileMode.Open))
{
stream.SetLength(0);
Log.Logger.Information($"writing configured mqttbroker to: {stream.Name}");
ConfiguredMqttBroker configuredBroker = new ConfiguredMqttBroker()
{
Host = settings.Host,
Username = settings.Username,
Password = settings.Password ?? "",
Port = settings.Port ?? 1883,
UseTLS = settings.UseTLS,
RetainLWT = settings.RetainLWT,
RootCAPath = settings.RootCAPath,
ClientCertPath = settings.ClientCertPath
};
await JsonSerializer.SerializeAsync(stream, configuredBroker);
stream.Close();
}
this.BrokerSettingsFileLocked = false;
this.MqqtConfigChangedHandler.Invoke(await this.GetMqttClientOptionsAsync());
}
public async Task<MqttSettings> GetMqttBrokerSettings()
{
ConfiguredMqttBroker broker = await ReadMqttSettingsAsync();
return new MqttSettings
{
Host = broker?.Host,
Username = broker?.Username,
Password = broker?.Password,
Port = broker?.Port,
UseTLS = broker?.UseTLS ?? false,
RetainLWT = broker?.RetainLWT ?? true,
RootCAPath = broker?.RootCAPath,
ClientCertPath = broker?.RootCAPath
};
}
/// <summary>
/// Enable or disable autostarting the background service. It does this by adding the application shortcut (appref-ms) to the registry run key for the current user
/// </summary>
/// <param name="enable"></param>
[SupportedOSPlatform("windows")]
public void EnableAutoStart(bool enable)
{
if (enable)
{
Log.Logger.Information("configuring autostart");
// The path to the key where Windows looks for startup applications
RegistryKey rkApp = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Run", true);
Log.Information("currentDir: " + Environment.CurrentDirectory);
Log.Information("appData: " + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData));
string startPath;
// if the app is installed in appdata, we can assume it was installed using the installer
if (Environment.CurrentDirectory.Contains(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)))
{
// so we set the autostart Path to launch shortcut
startPath = Environment.GetFolderPath(Environment.SpecialFolder.Programs) + @"\Sleevezipper\Hass Workstation Service.appref-ms";
}
else
{
// if it isn't in appdata, it's probably running as standalone and we set the startpath to the path of the executable
startPath = Environment.CurrentDirectory + @"\hass-workstation-service.exe";
}
rkApp.SetValue("hass-workstation-service", startPath);
rkApp.Close();
}
else
{
Log.Logger.Information("removing autostart");
RegistryKey rkApp = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Run", true);
rkApp.DeleteValue("hass-workstation-service");
rkApp.Close();
}
}
[SupportedOSPlatform("windows")]
public bool IsAutoStartEnabled()
{
RegistryKey rkApp = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Run", true);
return rkApp.GetValue("hass-workstation-service") != null;
}
public async Task<ICollection<AbstractSensor>> GetSensorsAfterLoadingAsync()
{
while (this._sensorsLoading)
{
await Task.Delay(500);
}
return this.ConfiguredSensors;
}
}
}