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 @@ +