add logging and running as a service

pull/9/head
sleevezipper 4 years ago
parent a5820a5911
commit 2590bd50b0

5
.gitignore vendored

@ -19,4 +19,7 @@ obj/
!.vscode/extensions.json !.vscode/extensions.json
*.code-workspace *.code-workspace
# End of https://www.toptal.com/developers/gitignore/api/vscode,dotnetcore # End of https://www.toptal.com/developers/gitignore/api/vscode,dotnetcore
# ignore logs
logs/

@ -7,6 +7,7 @@ using Microsoft.Extensions.Logging;
using MQTTnet; using MQTTnet;
using MQTTnet.Client; using MQTTnet.Client;
using MQTTnet.Client.Options; using MQTTnet.Client.Options;
using Serilog;
namespace hass_workstation_service.Communication namespace hass_workstation_service.Communication
{ {
@ -39,8 +40,7 @@ namespace hass_workstation_service.Communication
this._mqttClient = factory.CreateMqttClient(); this._mqttClient = factory.CreateMqttClient();
// connect to the broker // connect to the broker
this._mqttClient.ConnectAsync(options); var result = this._mqttClient.ConnectAsync(options).Result;
// configure what happens on disconnect // configure what happens on disconnect
this._mqttClient.UseDisconnectedHandler(async e => this._mqttClient.UseDisconnectedHandler(async e =>
{ {
@ -51,9 +51,9 @@ namespace hass_workstation_service.Communication
{ {
await this._mqttClient.ConnectAsync(options, CancellationToken.None); await this._mqttClient.ConnectAsync(options, CancellationToken.None);
} }
catch catch (Exception ex)
{ {
_logger.LogError("Reconnecting failed"); _logger.LogError(ex, "Reconnecting failed");
} }
}); });
} }

@ -2,10 +2,12 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.IO.IsolatedStorage; using System.IO.IsolatedStorage;
using System.Reflection;
using System.Text.Json; using System.Text.Json;
using hass_workstation_service.Communication; using hass_workstation_service.Communication;
using hass_workstation_service.Domain.Sensors; using hass_workstation_service.Domain.Sensors;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Serilog;
namespace hass_workstation_service.Data namespace hass_workstation_service.Data
{ {
@ -28,27 +30,55 @@ namespace hass_workstation_service.Data
public async void ReadSettings() public async void ReadSettings()
{ {
IsolatedStorageFileStream stream = this._fileStorage.OpenFile("configured-sensors.json", FileMode.OpenOrCreate); IsolatedStorageFileStream stream = this._fileStorage.OpenFile("configured-sensors.json", FileMode.OpenOrCreate);
List<ConfiguredSensor> sensors = await JsonSerializer.DeserializeAsync<List<ConfiguredSensor>>(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<ConfiguredSensor> sensors = new List<ConfiguredSensor>();
if (stream.Length > 0)
{
sensors = await JsonSerializer.DeserializeAsync<List<ConfiguredSensor>>(stream);
}
foreach (ConfiguredSensor configuredSensor in sensors) foreach (ConfiguredSensor configuredSensor in sensors)
{ {
AbstractSensor sensor; AbstractSensor sensor;
#pragma warning disable IDE0066 #pragma warning disable IDE0066
switch (configuredSensor.Type) switch (configuredSensor.Type)
{ {
case "UserNotificationStateSensor": case "UserNotificationStateSensor":
sensor = new UserNotificationStateSensor(_publisher, configuredSensor.Name, configuredSensor.Id); sensor = new UserNotificationStateSensor(_publisher, configuredSensor.Name, configuredSensor.Id);
break; break;
case "DummySensor":
sensor = new DummySensor(_publisher, configuredSensor.Name, configuredSensor.Id);
break;
default: default:
throw new InvalidOperationException("unsupported sensor type in config"); throw new InvalidOperationException("unsupported sensor type in config");
} }
this.ConfiguredSensors.Add(sensor); 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<ConfiguredSensor> configuredSensorsToSave = new List<ConfiguredSensor>();
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) public void AddConfiguredSensor(AbstractSensor sensor)
{ {
this.ConfiguredSensors.Add(sensor);
WriteSettings();
} }
} }
} }

@ -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();
}
}
}

@ -9,7 +9,7 @@ namespace hass_workstation_service.Domain.Sensors
{ {
public UserNotificationStateSensor(MqttPublisher publisher, string name = "NotificationState") public UserNotificationStateSensor(MqttPublisher publisher, string name = "NotificationState")
{ {
this.Id = new Guid(); this.Id = Guid.NewGuid();
this.Name = name; this.Name = name;
this.Publisher = publisher; this.Publisher = publisher;
} }

@ -4,28 +4,69 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using hass_workstation_service.Communication; using hass_workstation_service.Communication;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Json;
using MQTTnet.Client.Options; using MQTTnet.Client.Options;
using hass_workstation_service.Data; 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 namespace hass_workstation_service
{ {
public class Program 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 Log.Fatal(ex, "Application start-up failed");
throw new NotImplementedException("Your platform is not yet supported"); }
finally
{
Log.CloseAndFlush();
} }
} }
public static IHostBuilder CreateHostBuilder(string[] args) => public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args) Host.CreateDefaultBuilder(args)
.UseSerilog()
.ConfigureAppConfiguration((hostingContext, config) =>
{
config
.SetBasePath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location))
.AddJsonFile("appsettings.json");
})
.ConfigureServices((hostContext, services) => .ConfigureServices((hostContext, services) =>
{ {
IConfiguration configuration = hostContext.Configuration; IConfiguration configuration = hostContext.Configuration;

@ -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<object> _delayStart = new TaskCompletionSource<object>();
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();
}
}
}

@ -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<IHostLifetime, ServiceBaseLifetime>());
}
public static Task RunAsServiceAsync(this IHostBuilder hostBuilder, CancellationToken cancellationToken = default)
{
return hostBuilder.UseServiceBaseLifetime().Build().RunAsync(cancellationToken);
}
}
}

@ -31,10 +31,16 @@ namespace hass_workstation_service
{ {
while (!_mqttPublisher.IsConnected) while (!_mqttPublisher.IsConnected)
{ {
_logger.LogInformation("Connecting to MQTT broker..."); _logger.LogInformation($"Connecting to MQTT broker...");
await Task.Delay(2000); await Task.Delay(2000);
} }
_logger.LogInformation("Connected. Sending auto discovery messages."); _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) foreach (AbstractSensor sensor in _configuredSensorsService.ConfiguredSensors)
{ {
await sensor.PublishAutoDiscoveryConfigAsync(); await sensor.PublishAutoDiscoveryConfigAsync();

@ -1,11 +1,4 @@
{ {
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"MqttBroker": { "MqttBroker": {
"Host": "192.168.2.6", "Host": "192.168.2.6",
"Username": "tester", "Username": "tester",

@ -1,9 +1,7 @@
{ {
"Logging": { "MqttBroker": {
"LogLevel": { "Host": "192.168.2.6",
"Default": "Information", "Username": "tester",
"Microsoft": "Warning", "Password": "tester"
"Microsoft.Hosting.Lifetime": "Information"
}
} }
} }

@ -9,5 +9,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="3.1.10" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="3.1.10" />
<PackageReference Include="MQTTnet" Version="3.0.13" /> <PackageReference Include="MQTTnet" Version="3.0.13" />
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="5.0.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

Loading…
Cancel
Save