diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..073e637
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,4 @@
+{
+ "todo-tree.tree.showBadges": true,
+ "todo-tree.tree.showCountsInTree": true
+}
\ No newline at end of file
diff --git a/Communication/MQTT/AutoDiscoveryConfigModel.cs b/Communication/MQTT/AutoDiscoveryConfigModel.cs
new file mode 100644
index 0000000..1a610f0
--- /dev/null
+++ b/Communication/MQTT/AutoDiscoveryConfigModel.cs
@@ -0,0 +1,130 @@
+using System;
+using System.Collections.Generic;
+
+namespace hass_desktop_service.Communication
+{
+ public class AutoDiscoveryConfigModel
+ {
+ ///
+ /// (Optional) The MQTT topic subscribed to receive availability (online/offline) updates.
+ ///
+ ///
+ public string Availability_topic { get; set; }
+ ///
+ /// (Optional) Information about the device this sensor is a part of to tie it into the device registry. Only works through MQTT discovery and when unique_id is set.
+ ///
+ ///
+ public DeviceConfigModel Device { get; set; }
+ ///
+ /// (Optional) The type/class of the sensor to set the icon in the frontend. See https://www.home-assistant.io/integrations/sensor/#device-class for options.
+ ///
+ ///
+ public string Device_class { get; set; }
+ ///
+ /// (Optional) Defines the number of seconds after the sensor’s state expires, if it’s not updated. After expiry, the sensor’s state becomes unavailable. Defaults to 0 in hass.
+ ///
+ ///
+ public int? Expire_after { get; set; }
+ ///
+ /// Sends update events even if the value hasn’t changed. Useful if you want to have meaningful value graphs in history.
+ ///
+ ///
+ public bool? Force_update { get; set; }
+
+ ///
+ /// (Optional) The icon for the sensor.
+ ///
+ ///
+ public string Icon { get; set; }
+ ///
+ /// (Optional) Defines a template to extract the JSON dictionary from messages received on the json_attributes_topic.
+ ///
+ ///
+ public string Json_attributes_template { get; set; }
+ ///
+ /// (Optional) The MQTT topic subscribed to receive a JSON dictionary payload and then set as sensor attributes. Implies force_update of the current sensor state when a message is received on this topic.
+ ///
+ ///
+ public string Json_attributes_topic { get; set; }
+ ///
+ /// (Optional) The name of the MQTT sensor. Defaults to MQTT Sensor in hass.
+ ///
+ ///
+ public string Name { get; set; }
+ ///
+ /// (Optional) The payload that represents the available state.
+ ///
+ ///
+ public string Payload_available { get; set; }
+ ///
+ /// (Optional) The payload that represents the unavailable state.
+ ///
+ ///
+ public string Payload_not_available { get; set; }
+ ///
+ /// (Optional) The maximum QoS level of the state topic.
+ ///
+ ///
+ public int? Qos { get; set; }
+
+ ///
+ /// The MQTT topic subscribed to receive sensor values.
+ ///
+ ///
+ public string State_topic { get; set; }
+ ///
+ /// (Optional) An ID that uniquely identifies this sensor. If two sensors have the same unique ID, Home Assistant will raise an exception.
+ ///
+ ///
+ public string Unique_id { get; set; }
+ ///
+ /// (Optional) Defines the units of measurement of the sensor, if any.
+ ///
+ ///
+ public string Unit_of_measurement { get; set; }
+ ///
+ /// (Optional) Defines a template to extract the value.
+ ///
+ ///
+ public string Value_template { get; set; }
+ }
+
+ public class DeviceConfigModel
+ {
+ ///
+ /// (Optional) A list of connections of the device to the outside world as a list of tuples [connection_type, connection_identifier]. For example the MAC address of a network interface: "connections": [["mac", "02:5b:26:a8:dc:12"]].
+ ///
+ ///
+ public ICollection> Connections { get; set; }
+ ///
+ /// (Optional) An Id to identify the device. For example a serial number.
+ ///
+ ///
+ public string Identifiers { get; set; }
+ ///
+ /// (Optional) The manufacturer of the device.
+ ///
+ ///
+ public string Manufacturer { get; set; }
+ ///
+ /// (Optional) The model of the device.
+ ///
+ ///
+ public string Model { get; set; }
+ ///
+ /// (Optional) The name of the device.
+ ///
+ ///
+ public string Name { get; set; }
+ ///
+ /// (Optional) The firmware version of the device.
+ ///
+ ///
+ public string Sw_version { get; set; }
+ ///
+ /// (Optional) Identifier of a device that routes messages between this device and Home Assistant. Examples of such devices are hubs, or parent devices of a sub-device. This is used to show device topology in Home Assistant.
+ ///
+ ///
+ public string Via_device { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Communication/MQTT/MqttPublisher.cs b/Communication/MQTT/MqttPublisher.cs
new file mode 100644
index 0000000..4c890ea
--- /dev/null
+++ b/Communication/MQTT/MqttPublisher.cs
@@ -0,0 +1,93 @@
+using System;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using hass_desktop_service.Communication.Util;
+using Microsoft.Extensions.Logging;
+using MQTTnet;
+using MQTTnet.Client;
+using MQTTnet.Client.Options;
+
+namespace hass_desktop_service.Communication
+{
+
+ public class MqttPublisher
+ {
+ private readonly IMqttClient _mqttClient;
+ private readonly ILogger _logger;
+ public DateTime LastConfigAnnounce { get; private set; }
+ public DeviceConfigModel DeviceConfigModel { get; private set; }
+ public bool IsConnected
+ {
+ get
+ {
+ return this._mqttClient.IsConnected;
+ }
+ }
+
+ public MqttPublisher(
+ ILogger logger,
+ IMqttClientOptions options,
+ DeviceConfigModel deviceConfigModel)
+ {
+
+ this._logger = logger;
+ this.DeviceConfigModel = deviceConfigModel;
+
+
+ var factory = new MqttFactory();
+ this._mqttClient = factory.CreateMqttClient();
+
+ // connect to the broker
+ this._mqttClient.ConnectAsync(options);
+
+ // configure what happens on disconnect
+ this._mqttClient.UseDisconnectedHandler(async e =>
+ {
+ _logger.LogWarning("Disconnected from server");
+ await Task.Delay(TimeSpan.FromSeconds(5));
+
+ try
+ {
+ await this._mqttClient.ConnectAsync(options, CancellationToken.None);
+ }
+ catch
+ {
+ _logger.LogError("Reconnecting failed");
+ }
+ });
+ }
+
+ public async Task Publish(MqttApplicationMessage message)
+ {
+ if (this._mqttClient.IsConnected)
+ {
+ await this._mqttClient.PublishAsync(message);
+ }
+ else
+ {
+ this._logger.LogInformation($"message dropped because mqtt not connected: {message}");
+ }
+ }
+
+ public async Task PublishAutoDiscoveryConfig(AutoDiscoveryConfigModel config, bool clearPreviousConfig = false)
+ {
+ if (this._mqttClient.IsConnected)
+ {
+ var options = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = new CamelCaseJsonNamingpolicy(),
+ IgnoreNullValues = true,
+ PropertyNameCaseInsensitive = true
+ };
+ var message = new MqttApplicationMessageBuilder()
+ .WithTopic($"homeassistant/sensor/{config.Device.Identifiers}/config")
+ .WithPayload(clearPreviousConfig ? "" : JsonSerializer.Serialize(config, options))
+ .WithRetainFlag()
+ .Build();
+ await this.Publish(message);
+ LastConfigAnnounce = DateTime.UtcNow;
+ }
+ }
+ }
+}
diff --git a/Communication/Util/CamelCaseJsonNamingpolicy.cs b/Communication/Util/CamelCaseJsonNamingpolicy.cs
new file mode 100644
index 0000000..a4c02c2
--- /dev/null
+++ b/Communication/Util/CamelCaseJsonNamingpolicy.cs
@@ -0,0 +1,9 @@
+using System.Text.Json;
+
+namespace hass_desktop_service.Communication.Util
+{
+ public class CamelCaseJsonNamingpolicy : JsonNamingPolicy
+ {
+ public override string ConvertName(string name) => name.ToLowerInvariant();
+ }
+}
\ No newline at end of file
diff --git a/Data/ConfiguredSensor.cs b/Data/ConfiguredSensor.cs
new file mode 100644
index 0000000..d720ec8
--- /dev/null
+++ b/Data/ConfiguredSensor.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace hass_desktop_service.Data
+{
+ public class ConfiguredSensor
+ {
+ public string Type { get; set; }
+ public Guid Id { get; set; }
+ public string Name { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Data/ConfiguredSensorsService.cs b/Data/ConfiguredSensorsService.cs
new file mode 100644
index 0000000..210abaa
--- /dev/null
+++ b/Data/ConfiguredSensorsService.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.IsolatedStorage;
+using System.Text.Json;
+using hass_desktop_service.Communication;
+using hass_desktop_service.Domain.Sensors;
+using Microsoft.Extensions.Configuration;
+
+namespace hass_desktop_service.Data
+{
+ public class ConfiguredSensorsService
+ {
+ public ICollection ConfiguredSensors { get; private set; }
+ public IConfiguration Configuration { get; private set; }
+ private readonly MqttPublisher _publisher;
+ private readonly IsolatedStorageFile _fileStorage;
+
+ public ConfiguredSensorsService(MqttPublisher publisher)
+ {
+ this._fileStorage = IsolatedStorageFile.GetUserStoreForApplication();
+
+ ConfiguredSensors = new List();
+ _publisher = publisher;
+ ReadSettings();
+ }
+
+ public async void ReadSettings()
+ {
+ IsolatedStorageFileStream stream = this._fileStorage.OpenFile("configured-sensors.json", FileMode.OpenOrCreate);
+ List sensors = await JsonSerializer.DeserializeAsync>(stream);
+
+ foreach (ConfiguredSensor configuredSensor in sensors)
+ {
+ AbstractSensor sensor;
+ #pragma warning disable IDE0066
+ switch (configuredSensor.Type)
+ {
+ case "UserNotificationStateSensor":
+ sensor = new UserNotificationStateSensor(_publisher, configuredSensor.Name, configuredSensor.Id);
+ break;
+ default:
+ throw new InvalidOperationException("unsupported sensor type in config");
+ }
+ this.ConfiguredSensors.Add(sensor);
+ }
+ }
+
+ public void AddConfiguredSensor(AbstractSensor sensor)
+ {
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/Domain/Device.cs b/Domain/Device.cs
new file mode 100644
index 0000000..993d92c
--- /dev/null
+++ b/Domain/Device.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using hass_desktop_service.Domain.Sensors;
+
+namespace hass_desktop_service.Domain
+{
+ public abstract class Device
+ {
+ public Guid Id { get; private set; }
+ public string Name { get; private set; }
+ public string Manufacturer { get; private set; }
+ public string Model { get; private set; }
+ public string Version { get; private set; }
+
+ public ICollection Sensors { get; set; }
+
+ }
+}
\ No newline at end of file
diff --git a/Domain/Sensors/AbstractSensor.cs b/Domain/Sensors/AbstractSensor.cs
new file mode 100644
index 0000000..5da0ae7
--- /dev/null
+++ b/Domain/Sensors/AbstractSensor.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Threading.Tasks;
+using hass_desktop_service.Communication;
+using MQTTnet;
+
+namespace hass_desktop_service.Domain.Sensors
+{
+ public abstract class AbstractSensor
+ {
+ public Guid Id { get; protected set; }
+ public string Name { get; protected set; }
+ public MqttPublisher Publisher { get; protected set; }
+ protected AutoDiscoveryConfigModel _autoDiscoveryConfigModel;
+ protected AutoDiscoveryConfigModel SetAutoDiscoveryConfigModel(AutoDiscoveryConfigModel config)
+ {
+ this._autoDiscoveryConfigModel = config;
+ return config;
+ }
+
+ public abstract AutoDiscoveryConfigModel GetAutoDiscoveryConfig();
+ public abstract string GetState();
+
+ public async Task PublishStateAsync()
+ {
+ var message = new MqttApplicationMessageBuilder()
+ .WithTopic(this.GetAutoDiscoveryConfig().State_topic)
+ .WithPayload(this.GetState())
+ .WithExactlyOnceQoS()
+ .WithRetainFlag()
+ .Build();
+ await Publisher.Publish(message);
+ }
+ public async Task PublishAutoDiscoveryConfigAsync()
+ {
+ await this.Publisher.PublishAutoDiscoveryConfig(this.GetAutoDiscoveryConfig());
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/StateDetectors/FullscreenDetector/UserNotificationStateDetector.cs b/Domain/Sensors/UserNotificationStateSensor.cs
similarity index 56%
rename from StateDetectors/FullscreenDetector/UserNotificationStateDetector.cs
rename to Domain/Sensors/UserNotificationStateSensor.cs
index 91a9f0b..96188c1 100644
--- a/StateDetectors/FullscreenDetector/UserNotificationStateDetector.cs
+++ b/Domain/Sensors/UserNotificationStateSensor.cs
@@ -1,10 +1,54 @@
+using System;
using System.Runtime.InteropServices;
-using PInvoke;
-using static PInvoke.Shell32;
+using System.Threading.Tasks;
+using hass_desktop_service.Communication;
-namespace hass_desktop_service.StateDetectors.Windows.Fullscreen
+namespace hass_desktop_service.Domain.Sensors
{
-public enum UserNotificationState
+ public class UserNotificationStateSensor : AbstractSensor
+ {
+ public UserNotificationStateSensor(MqttPublisher publisher, string name = "NotificationState")
+ {
+ this.Id = new Guid();
+ this.Name = name;
+ this.Publisher = publisher;
+ }
+
+ public UserNotificationStateSensor(MqttPublisher publisher, string name, Guid id)
+ {
+ this.Id = id;
+ this.Name = name;
+ this.Publisher = publisher;
+ }
+ 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",
+ Icon = "mdi:laptop",
+ });
+ }
+
+ public override string GetState()
+ {
+ return GetStateEnum().ToString();
+ }
+
+ [DllImport("shell32.dll")]
+ static extern int SHQueryUserNotificationState(out UserNotificationState state);
+
+ public UserNotificationState GetStateEnum()
+ {
+ SHQueryUserNotificationState(out UserNotificationState state);
+
+ return state;
+ }
+ }
+
+ public enum UserNotificationState
{
///
/// A screen saver is displayed, the machine is locked,
@@ -43,17 +87,4 @@ public enum UserNotificationState
///
QuietTime = 6
};
-
- public class UserNotificationStateDetector
- {
- [DllImport("shell32.dll")]
- static extern int SHQueryUserNotificationState(out UserNotificationState state);
-
- public UserNotificationState GetState(){
- UserNotificationState state;
- SHQueryUserNotificationState(out state);
-
- return state;
- }
- }
}
\ No newline at end of file
diff --git a/Program.cs b/Program.cs
index b10e81a..cf8973a 100644
--- a/Program.cs
+++ b/Program.cs
@@ -1,11 +1,11 @@
using System;
-using System.Collections.Generic;
-using System.Linq;
using System.Runtime.InteropServices;
-using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
-using hass_desktop_service.StateDetectors.Windows.Fullscreen;
+using hass_desktop_service.Communication;
+using Microsoft.Extensions.Configuration;
+using MQTTnet.Client.Options;
+using hass_desktop_service.Data;
namespace hass_desktop_service
{
@@ -13,7 +13,6 @@ namespace hass_desktop_service
{
public static void Main(string[] args)
{
-
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
CreateHostBuilder(args).Build().Run();
@@ -29,7 +28,25 @@ namespace hass_desktop_service
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
- services.AddSingleton();
+ IConfiguration configuration = hostContext.Configuration;
+ IConfigurationSection mqttSection = configuration.GetSection("MqttBroker");
+ var mqttClientOptions = new MqttClientOptionsBuilder()
+ .WithTcpServer(mqttSection.GetValue("Host"))
+ // .WithTls()
+ .WithCredentials(mqttSection.GetValue("Username"), mqttSection.GetValue("Password"))
+ .Build();
+ var deviceConfig = new DeviceConfigModel
+ {
+ Name = "hass-workstation-service3",
+ //TODO: make this more dynamic
+ Identifiers = "hass-workstation-service_unique4",
+ Sw_version = "0.0.4"
+ };
+ services.AddSingleton(configuration);
+ services.AddSingleton(deviceConfig);
+ services.AddSingleton(mqttClientOptions);
+ services.AddSingleton();
+ services.AddSingleton();
services.AddHostedService();
});
}
diff --git a/Worker.cs b/Worker.cs
index 5bc61fb..b2ade0d 100644
--- a/Worker.cs
+++ b/Worker.cs
@@ -3,30 +3,55 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using hass_desktop_service.StateDetectors.Windows.Fullscreen;
+using hass_desktop_service.Communication;
+using hass_desktop_service.Data;
+using hass_desktop_service.Domain.Sensors;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
+using MQTTnet.Client;
namespace hass_desktop_service
{
public class Worker : BackgroundService
{
private readonly ILogger _logger;
- private readonly UserNotificationStateDetector _userNotificationStateDetector;
+ private readonly ConfiguredSensorsService _configuredSensorsService;
+ private readonly MqttPublisher _mqttPublisher;
- public Worker(ILogger logger, UserNotificationStateDetector userNotificationStateDetector)
+ public Worker(ILogger logger,
+ ConfiguredSensorsService configuredSensorsService,
+ MqttPublisher mqttPublisher)
{
_logger = logger;
- this._userNotificationStateDetector = userNotificationStateDetector;
+ this._configuredSensorsService = configuredSensorsService;
+ this._mqttPublisher = mqttPublisher;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
+ while (!_mqttPublisher.IsConnected)
+ {
+ _logger.LogInformation("Connecting to MQTT broker...");
+ await Task.Delay(2000);
+ }
+ _logger.LogInformation("Connected. Sending auto discovery messages.");
+ foreach (AbstractSensor sensor in _configuredSensorsService.ConfiguredSensors)
+ {
+ await sensor.PublishAutoDiscoveryConfigAsync();
+ }
while (!stoppingToken.IsCancellationRequested)
{
- _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
- _logger.LogInformation($"Notificationstate: {this._userNotificationStateDetector.GetState()}");
+ _logger.LogDebug("Worker running at: {time}", DateTimeOffset.Now);
+ foreach (AbstractSensor sensor in _configuredSensorsService.ConfiguredSensors)
+ {
+ await sensor.PublishStateAsync();
+ }
+ // announce autodiscovery every 30 seconds
+ if (_mqttPublisher.LastConfigAnnounce < DateTime.UtcNow.AddSeconds(-30))
+ {
+ // TODO: make every sensor publish its auto discovery config
+ }
await Task.Delay(1000, stoppingToken);
}
}
diff --git a/appsettings.Development.json b/appsettings.Development.json
index 8983e0f..ed75265 100644
--- a/appsettings.Development.json
+++ b/appsettings.Development.json
@@ -5,5 +5,15 @@
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
- }
+ },
+ "MqttBroker": {
+ "Host": "192.168.2.6",
+ "Username": "tester",
+ "Password": "tester"
+ },
+ "ConfiguredSensors": [
+ {"Type": "UserNotificationStateSensor", "Ïd": "17fec74e-5d82-4334-8d40-45cfa8449228", "Name": "Sensor1"},
+ {"Type": "UserNotificationStateSensor", "Ïd": "27fec74e-5d82-4334-8d40-45cfa8449228", "Name": "Sensor2"},
+ {"Type": "UserNotificationStateSensor", "Ïd": "37fec74e-5d82-4334-8d40-45cfa8449228", "Name": "Sensor3"}
+ ]
}
diff --git a/hass-desktop-service.csproj b/hass-desktop-service.csproj
index f5b201e..73a8085 100644
--- a/hass-desktop-service.csproj
+++ b/hass-desktop-service.csproj
@@ -8,5 +8,6 @@
+