diff --git a/.gitignore b/.gitignore index 9332b59..2bbc24e 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,7 @@ obj/ !.vscode/extensions.json *.code-workspace -# End of https://www.toptal.com/developers/gitignore/api/vscode,dotnetcore \ No newline at end of file +# End of https://www.toptal.com/developers/gitignore/api/vscode,dotnetcore + +# ignore logs +logs/ \ No newline at end of file diff --git a/Communication/MQTT/MqttPublisher.cs b/Communication/MQTT/MqttPublisher.cs index a1eec19..76093d9 100644 --- a/Communication/MQTT/MqttPublisher.cs +++ b/Communication/MQTT/MqttPublisher.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Logging; using MQTTnet; using MQTTnet.Client; using MQTTnet.Client.Options; +using Serilog; namespace hass_workstation_service.Communication { @@ -39,8 +40,7 @@ namespace hass_workstation_service.Communication this._mqttClient = factory.CreateMqttClient(); // connect to the broker - this._mqttClient.ConnectAsync(options); - + var result = this._mqttClient.ConnectAsync(options).Result; // configure what happens on disconnect this._mqttClient.UseDisconnectedHandler(async e => { @@ -51,9 +51,9 @@ namespace hass_workstation_service.Communication { await this._mqttClient.ConnectAsync(options, CancellationToken.None); } - catch + catch (Exception ex) { - _logger.LogError("Reconnecting failed"); + _logger.LogError(ex, "Reconnecting failed"); } }); } diff --git a/Data/ConfiguredSensorsService.cs b/Data/ConfiguredSensorsService.cs index 53789c3..2e0f21c 100644 --- a/Data/ConfiguredSensorsService.cs +++ b/Data/ConfiguredSensorsService.cs @@ -2,10 +2,12 @@ using System; using System.Collections.Generic; using System.IO; using System.IO.IsolatedStorage; +using System.Reflection; using System.Text.Json; using hass_workstation_service.Communication; using hass_workstation_service.Domain.Sensors; using Microsoft.Extensions.Configuration; +using Serilog; namespace hass_workstation_service.Data { @@ -28,27 +30,55 @@ namespace hass_workstation_service.Data public async void ReadSettings() { IsolatedStorageFileStream stream = this._fileStorage.OpenFile("configured-sensors.json", FileMode.OpenOrCreate); - List sensors = await JsonSerializer.DeserializeAsync>(stream); + String filePath = stream.GetType().GetField("m_FullPath", + BindingFlags.Instance | BindingFlags.NonPublic).GetValue(stream).ToString(); + Console.WriteLine(filePath); + Log.Logger.Information($"reading configured sensors from: {filePath}"); + List sensors = new List(); + if (stream.Length > 0) + { + sensors = await JsonSerializer.DeserializeAsync>(stream); + } foreach (ConfiguredSensor configuredSensor in sensors) { AbstractSensor sensor; - #pragma warning disable IDE0066 +#pragma warning disable IDE0066 switch (configuredSensor.Type) { case "UserNotificationStateSensor": sensor = new UserNotificationStateSensor(_publisher, configuredSensor.Name, configuredSensor.Id); break; + case "DummySensor": + sensor = new DummySensor(_publisher, configuredSensor.Name, configuredSensor.Id); + break; default: throw new InvalidOperationException("unsupported sensor type in config"); } this.ConfiguredSensors.Add(sensor); } + stream.Close(); + } + + public async void WriteSettings() + { + IsolatedStorageFileStream stream = this._fileStorage.OpenFile("configured-sensors.json", FileMode.OpenOrCreate); + Log.Logger.Information($"writing configured sensors to: {stream.Name}"); + List configuredSensorsToSave = new List(); + + foreach (AbstractSensor sensor in this.ConfiguredSensors) + { + configuredSensorsToSave.Add(new ConfiguredSensor() { Id = sensor.Id, Name = sensor.Name, Type = sensor.GetType().Name }); + } + + await JsonSerializer.SerializeAsync(stream, configuredSensorsToSave); + stream.Close(); } public void AddConfiguredSensor(AbstractSensor sensor) { - + this.ConfiguredSensors.Add(sensor); + WriteSettings(); } } } \ No newline at end of file diff --git a/Domain/Sensors/DummySensor.cs b/Domain/Sensors/DummySensor.cs new file mode 100644 index 0000000..7a5974c --- /dev/null +++ b/Domain/Sensors/DummySensor.cs @@ -0,0 +1,42 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using hass_workstation_service.Communication; + +namespace hass_workstation_service.Domain.Sensors +{ + public class DummySensor : AbstractSensor + { + private readonly Random _random; + public DummySensor(MqttPublisher publisher, string name = "Dummy") + { + this.Id = Guid.NewGuid(); + this.Name = name; + this.Publisher = publisher; + this._random = new Random(); + } + + public DummySensor(MqttPublisher publisher, string name, Guid id) + { + this.Id = id; + this.Name = name; + this.Publisher = publisher; + this._random = new Random(); + } + public override AutoDiscoveryConfigModel GetAutoDiscoveryConfig() + { + return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new AutoDiscoveryConfigModel() + { + Name = this.Name, + Unique_id = this.Id.ToString(), + Device = this.Publisher.DeviceConfigModel, + State_topic = $"homeassistant/sensor/{this.Name}/state" + }); + } + + public override string GetState() + { + return _random.Next(0, 100).ToString(); + } + } +} \ No newline at end of file diff --git a/Domain/Sensors/UserNotificationStateSensor.cs b/Domain/Sensors/UserNotificationStateSensor.cs index 276945f..68600e5 100644 --- a/Domain/Sensors/UserNotificationStateSensor.cs +++ b/Domain/Sensors/UserNotificationStateSensor.cs @@ -9,7 +9,7 @@ namespace hass_workstation_service.Domain.Sensors { public UserNotificationStateSensor(MqttPublisher publisher, string name = "NotificationState") { - this.Id = new Guid(); + this.Id = Guid.NewGuid(); this.Name = name; this.Publisher = publisher; } diff --git a/Program.cs b/Program.cs index 5769512..8586ff0 100644 --- a/Program.cs +++ b/Program.cs @@ -4,28 +4,69 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using hass_workstation_service.Communication; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Json; using MQTTnet.Client.Options; using hass_workstation_service.Data; +using System.Linq; +using System.Diagnostics; +using System.Threading.Tasks; +using hass_workstation_service.ServiceHost; +using Serilog; +using Serilog.Formatting.Compact; +using System.IO.IsolatedStorage; +using System.Reflection; +using System.IO; namespace hass_workstation_service { public class Program { - public static void Main(string[] args) + public static async Task Main(string[] args) { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Console() + .WriteTo.File(new RenderedCompactJsonFormatter(), "logs/log.ndjson") + .CreateLogger(); + try { - CreateHostBuilder(args).Build().Run(); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var isService = !(Debugger.IsAttached || args.Contains("--console")); + + if (isService) + { + await CreateHostBuilder(args).RunAsServiceAsync(); + } + else + { + await CreateHostBuilder(args).RunConsoleAsync(); + } + } + else + { + // we only support MS Windows for now + throw new NotImplementedException("Your platform is not yet supported"); + } } - else + catch (Exception ex) { - // we only support MS Windows for now - throw new NotImplementedException("Your platform is not yet supported"); + Log.Fatal(ex, "Application start-up failed"); + } + finally + { + Log.CloseAndFlush(); } } - public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) + .UseSerilog() + .ConfigureAppConfiguration((hostingContext, config) => + { + config + .SetBasePath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)) + .AddJsonFile("appsettings.json"); + }) .ConfigureServices((hostContext, services) => { IConfiguration configuration = hostContext.Configuration; diff --git a/ServiceHost/ServiceBaseLifetime.cs b/ServiceHost/ServiceBaseLifetime.cs new file mode 100644 index 0000000..5957d79 --- /dev/null +++ b/ServiceHost/ServiceBaseLifetime.cs @@ -0,0 +1,64 @@ +using Microsoft.Extensions.Hosting; +using System; +using System.ServiceProcess; +using System.Threading; +using System.Threading.Tasks; + +namespace hass_workstation_service.ServiceHost +{ + + public class ServiceBaseLifetime : ServiceBase, IHostLifetime + { + private IHostApplicationLifetime ApplicationLifetime { get; } + private readonly TaskCompletionSource _delayStart = new TaskCompletionSource(); + + public ServiceBaseLifetime(IHostApplicationLifetime applicationLifetime) + { + ApplicationLifetime = applicationLifetime ?? throw new ArgumentNullException(nameof(applicationLifetime)); + } + + + public Task WaitForStartAsync(CancellationToken cancellationToken) + { + cancellationToken.Register(() => _delayStart.TrySetCanceled()); + ApplicationLifetime.ApplicationStopping.Register(Stop); + + new Thread(Run).Start(); // Otherwise this would block and prevent IHost.StartAsync from finishing. + return _delayStart.Task; + } + + private void Run() + { + try + { + Run(this); // This blocks until the service is stopped. + _delayStart.TrySetException(new InvalidOperationException("Stopped without starting")); + } + catch (Exception ex) + { + _delayStart.TrySetException(ex); + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + Stop(); + return Task.CompletedTask; + } + + // Called by base.Run when the service is ready to start. + protected override void OnStart(string[] args) + { + _delayStart.TrySetResult(null); + base.OnStart(args); + } + + // Called by base.Stop. This may be called multiple times by service Stop, ApplicationStopping, and StopAsync. + // That's OK because StopApplication uses a CancellationTokenSource and prevents any recursion. + protected override void OnStop() + { + ApplicationLifetime.StopApplication(); + base.OnStop(); + } + } +} \ No newline at end of file diff --git a/ServiceHost/ServiceBaseLifetimeHostExtensions.cs b/ServiceHost/ServiceBaseLifetimeHostExtensions.cs new file mode 100644 index 0000000..7c580aa --- /dev/null +++ b/ServiceHost/ServiceBaseLifetimeHostExtensions.cs @@ -0,0 +1,21 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace hass_workstation_service.ServiceHost +{ + + public static class ServiceBaseLifetimeHostExtensions + { + public static IHostBuilder UseServiceBaseLifetime(this IHostBuilder hostBuilder) + { + return hostBuilder.ConfigureServices((hostContext, services) => services.AddSingleton()); + } + + public static Task RunAsServiceAsync(this IHostBuilder hostBuilder, CancellationToken cancellationToken = default) + { + return hostBuilder.UseServiceBaseLifetime().Build().RunAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/Worker.cs b/Worker.cs index 5953bcd..0ff0a98 100644 --- a/Worker.cs +++ b/Worker.cs @@ -31,10 +31,16 @@ namespace hass_workstation_service { while (!_mqttPublisher.IsConnected) { - _logger.LogInformation("Connecting to MQTT broker..."); + _logger.LogInformation($"Connecting to MQTT broker..."); await Task.Delay(2000); } _logger.LogInformation("Connected. Sending auto discovery messages."); + // if there are no configured sensors we add a dummy sensor + if (_configuredSensorsService.ConfiguredSensors.Count == 0) + { + _configuredSensorsService.AddConfiguredSensor(new DummySensor(_mqttPublisher)); + _configuredSensorsService.AddConfiguredSensor(new UserNotificationStateSensor(_mqttPublisher)); + } foreach (AbstractSensor sensor in _configuredSensorsService.ConfiguredSensors) { await sensor.PublishAutoDiscoveryConfigAsync(); diff --git a/appsettings.Development.json b/appsettings.Development.json index a4fa09c..c5296f4 100644 --- a/appsettings.Development.json +++ b/appsettings.Development.json @@ -1,11 +1,4 @@ { - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, "MqttBroker": { "Host": "192.168.2.6", "Username": "tester", diff --git a/appsettings.json b/appsettings.json index 8983e0f..c5296f4 100644 --- a/appsettings.json +++ b/appsettings.json @@ -1,9 +1,7 @@ { - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } + "MqttBroker": { + "Host": "192.168.2.6", + "Username": "tester", + "Password": "tester" } } diff --git a/hass-workstation-service.csproj b/hass-workstation-service.csproj index d55a45a..d7ee1ee 100644 --- a/hass-workstation-service.csproj +++ b/hass-workstation-service.csproj @@ -9,5 +9,7 @@ + +