From 7dfe106d6aafe5dfbd09dc244e7c29176a231c51 Mon Sep 17 00:00:00 2001 From: XtremeOwnage <5262735+XtremeOwnageDotCom@users.noreply.github.com> Date: Sat, 31 Aug 2024 22:03:25 -0500 Subject: [PATCH] Round 2 (#24) * Remove DeviceID from Config * Removed device ID from appsettings. * YAML.NET added in preperation for #23 * Created YAML representation of configuration.... with optimizations. * All Configuration models updated to be compatible with YAML. * Refactored application startup, and configuration discovery. * Renamed helper class, to be more descriptive of its name. * Removed Configuration from appsettings.json * Added custom configuration to override list, for now. This- branch is not yet completed. But, will implement both #17, and #23 when done. [skip ci] * Created config object for connection. Credentials for PDU moved to PDU, from Actions. * Flattened configuration for actions. * Added better error handling, and messages for configuration-related issues. * Use "config.yaml", instead of "configuration.yaml" * Set default values. * Tweaked yaml deserializing settings. * Added ability to set scheme via configuration. * Cleaned up logic for setting names, overrides, etc. * Created a helper, which contains default naming methods for entities. * Updated helpers to compensate for changes. Removed a few unused extesions. Added a few extension. * Re-enable discovery message publish- was disabled for testing. * #16 - Specify automatic reconnect. * #16 - Log an error if the broker is disconnected when publishing a message. * Set URL on RootData, and pass to discovery service. * Verified that Measurement / Outlet naming is working as expected post-changes. * Moved logic, to cleanup top-level. * Added default "deviceclass" when we are unable to determine the correct Device Class. * Use int-based key for outlets. * Since, entity has both a label, and name, inherit EntityWithNameAndLabel * Refactored a bit- still lots of cleanup to do here. Key notes- entities will inherit "Device" name, and not the name of the entity. * This logic was moved to base discovery. * Renamed Sensor, to Sensor Discovery, to be better named. * Set key, to be parsed as int, make it much easier to find the root entity. * Discoveries are now handled recursively. The logic flows a bit better this way. Also- each discover publishes, instead of batching messages. --- .gitignore | 1 + rPDU2MQTT/Classes/Config.cs | 27 --- ...ndancies.cs => MQTTServiceDependancies.cs} | 7 +- rPDU2MQTT/Classes/PDU.cs | 102 ++++++----- .../Extensions/EntityWithName_Overrides.cs | 166 +++++++++--------- .../Extensions/IEntityWithState_Extensions.cs | 29 --- rPDU2MQTT/Extensions/RootData_Extensiosn.cs | 4 +- rPDU2MQTT/Helpers/DefaultNames.cs | 14 ++ rPDU2MQTT/Helpers/MeasurementHelper.cs | 6 +- rPDU2MQTT/Helpers/ThrowError.cs | 30 ++++ rPDU2MQTT/Models/Config/ActionsConfig.cs | 26 --- rPDU2MQTT/Models/Config/Config.cs | 22 +++ .../Models/Config/HomeAssistantConfig.cs | 5 +- rPDU2MQTT/Models/Config/MQTTConfig.cs | 34 ++-- rPDU2MQTT/Models/Config/Overrides.cs | 30 ++-- rPDU2MQTT/Models/Config/PduConfig.cs | 35 ++-- rPDU2MQTT/Models/Config/Schemas/Connection.cs | 33 ++++ .../Models/Config/Schemas/Credentials.cs | 15 ++ .../Models/Config/Schemas/EntityOverride.cs | 37 ++++ rPDU2MQTT/Models/Config/TypeOverride.cs | 56 ------ .../{Sensor.cs => SensorDiscovery.cs} | 2 +- .../Models/HomeAssistant/Enums/DeviceClass.cs | 2 + .../HomeAssistant/baseClasses/baseEntity.cs | 2 +- rPDU2MQTT/Models/PDU/Device.cs | 19 +- rPDU2MQTT/Models/PDU/RootData.cs | 6 + rPDU2MQTT/Program.cs | 85 +-------- .../Services/HomeAssistantDiscoveryService.cs | 114 +++++++----- rPDU2MQTT/Services/MQTTPublishingService.cs | 2 +- .../baseTypes/baseDiscoveryService.cs | 81 ++++++--- .../Services/baseTypes/baseMQTTTService.cs | 7 +- .../baseTypes/basePublishingService.cs | 4 +- rPDU2MQTT/Startup/ConfigLoader.cs | 28 +++ rPDU2MQTT/Startup/FindYamlConfig.cs | 78 ++++++++ rPDU2MQTT/Startup/ServiceConfiguration.cs | 86 +++++++++ rPDU2MQTT/appsettings.json | 129 -------------- rPDU2MQTT/config.defaults.yaml | 145 +++++++++++++++ rPDU2MQTT/rPDU2MQTT.csproj | 5 +- 37 files changed, 841 insertions(+), 633 deletions(-) delete mode 100644 rPDU2MQTT/Classes/Config.cs rename rPDU2MQTT/Classes/{ServiceDependancies.cs => MQTTServiceDependancies.cs} (50%) create mode 100644 rPDU2MQTT/Helpers/DefaultNames.cs create mode 100644 rPDU2MQTT/Helpers/ThrowError.cs delete mode 100644 rPDU2MQTT/Models/Config/ActionsConfig.cs create mode 100644 rPDU2MQTT/Models/Config/Config.cs create mode 100644 rPDU2MQTT/Models/Config/Schemas/Connection.cs create mode 100644 rPDU2MQTT/Models/Config/Schemas/Credentials.cs create mode 100644 rPDU2MQTT/Models/Config/Schemas/EntityOverride.cs delete mode 100644 rPDU2MQTT/Models/Config/TypeOverride.cs rename rPDU2MQTT/Models/HomeAssistant/DiscoveryTypes/{Sensor.cs => SensorDiscovery.cs} (93%) create mode 100644 rPDU2MQTT/Startup/ConfigLoader.cs create mode 100644 rPDU2MQTT/Startup/FindYamlConfig.cs create mode 100644 rPDU2MQTT/Startup/ServiceConfiguration.cs create mode 100644 rPDU2MQTT/config.defaults.yaml diff --git a/.gitignore b/.gitignore index 8208bc3..a52350b 100644 --- a/.gitignore +++ b/.gitignore @@ -363,3 +363,4 @@ MigrationBackup/ FodyWeavers.xsd /rPDU2MQTT/appsettings.Development.json +/rPDU2MQTT/config.yaml diff --git a/rPDU2MQTT/Classes/Config.cs b/rPDU2MQTT/Classes/Config.cs deleted file mode 100644 index f9e2d7f..0000000 --- a/rPDU2MQTT/Classes/Config.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Microsoft.Extensions.Options; -using rPDU2MQTT.Models.Config; - -namespace rPDU2MQTT.Classes; - -public class Config -{ - public Config(IOptionsSnapshot MQTT, IOptionsSnapshot PDU, IOptionsSnapshot HASS, IOptionsSnapshot Overrides, IOptionsSnapshot outlets, IOptionsSnapshot measurements) - { - this.MQTT = MQTT.Value; - this.PDU = PDU.Value; - this.HASS = HASS.Value; - this.Overrides = Overrides.Value; - this.Outlets = outlets.Value; - this.Measurements = measurements.Value; - } - - public MQTTConfig MQTT { get; } - public PduConfig PDU { get; } - public HomeAssistantConfig HASS { get; } - - public Overrides Overrides { get; } - - public OutletOverrides Outlets { get; } - - public MeasurementOverrides Measurements { get; } -} diff --git a/rPDU2MQTT/Classes/ServiceDependancies.cs b/rPDU2MQTT/Classes/MQTTServiceDependancies.cs similarity index 50% rename from rPDU2MQTT/Classes/ServiceDependancies.cs rename to rPDU2MQTT/Classes/MQTTServiceDependancies.cs index 2eebfb0..889e6f3 100644 --- a/rPDU2MQTT/Classes/ServiceDependancies.cs +++ b/rPDU2MQTT/Classes/MQTTServiceDependancies.cs @@ -2,9 +2,12 @@ namespace rPDU2MQTT.Classes; -public class ServiceDependancies +/// +/// Just a helper class, to reduce the constructor size when passing these dependanceies. +/// +public class MQTTServiceDependancies { - public ServiceDependancies(IHiveMQClient mqtt, Config cfg, PDU pdu) + public MQTTServiceDependancies(IHiveMQClient mqtt, Config cfg, PDU pdu) { Mqtt = mqtt; Cfg = cfg; diff --git a/rPDU2MQTT/Classes/PDU.cs b/rPDU2MQTT/Classes/PDU.cs index 03bbaf4..361d032 100644 --- a/rPDU2MQTT/Classes/PDU.cs +++ b/rPDU2MQTT/Classes/PDU.cs @@ -4,6 +4,7 @@ using rPDU2MQTT.Models.PDU; using rPDU2MQTT.Models.PDU.DummyDevices; using rPDU2MQTT.Models.PDUResponse; +using System.Diagnostics.CodeAnalysis; using System.Net.Http.Json; namespace rPDU2MQTT.Classes; @@ -33,79 +34,88 @@ public async Task GetRootData_Public(CancellationToken cancellationTok log.LogDebug($"Query response {model.RetCode}"); //Process device data. - processData(model.Data); + processData(model.Data, cancellationToken); return model.Data; } - public void processData(RootData data) + public async void processData(RootData data, CancellationToken cancellationToken) { //Set basic details. data.Record_Parent = null; data.Record_Key = config.MQTT.ParentTopic; + data.URL = http.BaseAddress.ToString(); - data.Entity_Identifier = Coalesce(config.Overrides.PduID, "rPDU2MQTT")!; - data.Entity_Name = data.Entity_DisplayName = Coalesce(config.Overrides.PduName, data.Sys.Label, data.Sys.Name, "rPDU2MQTT")!; + data.Entity_Identifier = Coalesce(config.Overrides?.PDU?.ID, "rPDU2MQTT")!; + data.Entity_Name = data.Entity_DisplayName = Coalesce(config.Overrides?.PDU?.Name, data.Sys.Label, data.Sys.Name, "rPDU2MQTT")!; // Propagate down the parent, and identifier. - data.Devices.SetParentAndIdentifier(data); + data.Devices.SetParentAndIdentifier(data, (k, v) => k); + + // Populate Name, DisplayName, and Enabled for devices. + data.Devices.SetEntityNameAndEnabled(config.Overrides!, (k, d) => d.Name, (k, d) => d.Label); //Process devices - processDevices(data.Devices); + await processRecursive(data.Devices, data, cancellationToken); } - private void processDevices(Dictionary devices) + private async Task processRecursive([AllowNull] TEntity entity, TParent? parent, CancellationToken cancellationToken) + where TEntity : BaseEntity + where TParent : NamedEntity { - foreach (var (key, device) in devices) + if (entity is null) + return; + else if (entity is Device device) { - // Remove any disabled outlets. - config.Outlets.RemoveDisabledRecords(device.Outlets); - // Propagate down the parent, and identifier. - device.Entity.SetParentAndIdentifier(BaseEntity.FromDevice(device, MqttPath.Entity)); - device.Outlets.SetParentAndIdentifier(BaseEntity.FromDevice(device, MqttPath.Outlets)); + device.Entity.SetParentAndIdentifier(BaseEntity.FromDevice(device, MqttPath.Entity), (k, v) => k); + device.Outlets.SetParentAndIdentifier(BaseEntity.FromDevice(device, MqttPath.Outlets), (k, v) => k.ToString()); + + // Set Overrides + device.Outlets.SetEntityNameAndEnabled(config.Overrides, DefaultNames.UseEntityName, DefaultNames.UseEntityLabel); + device.Entity.SetEntityNameAndEnabled(config.Overrides, DefaultNames.UseEntityName, DefaultNames.UseEntityLabel); + + // Prune disabled items. + device.Outlets.PruneDisabled(); + device.Entity.PruneDisabled(); - // Update properties for children. - processChildDevice(device.Entity); - processChildDevice(device.Outlets); + // Recurse to the next tier. + await processRecursive(device.Outlets, device, cancellationToken); + await processRecursive(device.Entity, device, cancellationToken); } - } - private void processChildDevice(Dictionary entities) where T : NamedEntityWithMeasurements - { - foreach (var (key, entity) in entities) + else if (entity is NamedEntityWithMeasurements nem) { - if (entity is Outlet o) - { - int k = int.TryParse(key, out int s) ? s + 1 : 0; //Note- the plus one, is so the number aligns with what is seen on the GUI. - entity.ApplyOverrides(k.ToString(), config.Outlets); - } - else - { - entity.Entity_Name = (entity.Label ?? entity.Name).FormatName(); - entity.Entity_DisplayName = (entity.Label ?? entity.Name); - } + // All measurements will be stored into a sub-key. + nem.Measurements.SetParentAndIdentifier(BaseEntity.FromDevice(entity, MqttPath.Measurements), IdentifierFunc: (k, v) => v.Type); - // Remove any disabled measurements. - config.Measurements.RemoveDisabledRecords(entity.Measurements, o => o.Type); + // Set Overrides + Func MeasurementNamingFunc = (k, m) => m.Type; - // All measurements will be stored into a sub-key. - entity.Measurements.SetParentAndIdentifier(BaseEntity.FromDevice(entity, MqttPath.Measurements)); + nem.Measurements.SetEntityNameAndEnabled(config.Overrides, MeasurementNamingFunc, DefaultNames.UseMeasurementType); + nem.Measurements.PruneDisabled(); - // Update properties for measurements. - processMeasurements(entity.Measurements); + if (entity is Entity) + // For entities- these belong directly to a "Device" + // We want to set the prefix to the parent device's name. + nem.Measurements.SetEntityNamePrefix(parent!.Entity_Name); + else + // We want to prefix the measurements, with the outlet name. + // ie, mydevice_power + nem.Measurements.SetEntityNamePrefix(nem.Entity_Name); + } + else + { + if (System.Diagnostics.Debugger.IsAttached) + System.Diagnostics.Debugger.Break(); } } - private void processMeasurements(Dictionary measurements) where T : Measurement + private async Task processRecursive(Dictionary entities, TParent parent, CancellationToken cancellationToken) + where TKey : notnull + where TEntity : notnull, BaseEntity + where TParent : NamedEntity { - foreach (var (key, entity) in measurements) - { - // We want to override the default key here- to give a nice, readable key. - entity.Record_Key = entity.Type; - entity.ApplyOverrides(entity.Type, config.Measurements, DefaultName: entity.Type, DefaultDisplayName: entity.Type); - - //SInce- ApplyNameOverridesReturnIsValid already set the EntityName, we are just going to append a suffix to it. - entity.Entity_Name = entity.GetEntityName(entity.Entity_Name); - } + foreach (var (_, entity) in entities) + await processRecursive(entity, parent, cancellationToken); } } diff --git a/rPDU2MQTT/Extensions/EntityWithName_Overrides.cs b/rPDU2MQTT/Extensions/EntityWithName_Overrides.cs index 61e9ad5..b937b1f 100644 --- a/rPDU2MQTT/Extensions/EntityWithName_Overrides.cs +++ b/rPDU2MQTT/Extensions/EntityWithName_Overrides.cs @@ -1,129 +1,121 @@ using rPDU2MQTT.Helpers; using rPDU2MQTT.Interfaces; using rPDU2MQTT.Models.Config; -using rPDU2MQTT.Models.PDU.basePDU; +using rPDU2MQTT.Models.Config.Schemas; +using rPDU2MQTT.Models.PDU; using rPDU2MQTT.Models.PDU.DummyDevices; +using System.Diagnostics.CodeAnalysis; namespace rPDU2MQTT.Extensions; public static class EntityWithName_Overrides { - /// - /// This method will both apply any overides specified for entity_id, and name. And, it will return if this entity is enabled or not. - /// - /// - /// - /// - /// - /// - /// - public static void ApplyOverrides(this TEntity entity, string key, TypeOverride Overrides, string? DefaultName = null, string? DefaultDisplayName = null) - where TEntity : NamedEntity - { - entity.Entity_Name = entity.GetOverrideOrDefault(key, Overrides.ID, FormatAsName: true, DefaultValue: DefaultName); - entity.Entity_DisplayName = entity.GetOverrideOrDefault(key, Overrides.Name, FormatAsName: false, DefaultValue: DefaultDisplayName); - entity.Entity_Enabled = tryGetValue(Overrides.Enabled, key, out bool enabled, DefaultValue: true) ? enabled : true; - } /// - /// This calculates the name of an entity, based on a collection of overrides. + /// Sets the , , and properties + /// based on the provided or default functions. /// - /// - /// Suppose you could always do String1 ?? String2 ?? String3 ?? "Default. - /// But- this was funner. also- this checks for empty/whitespace strings. - /// - /// - /// - /// - public static string GetOverrideOrDefault(this T entity, string? key, Dictionary Overrides, string? DefaultValue = null, bool FormatAsName = false) - where T : NamedEntity + /// The type of the key used in the dictionary of entities. + /// The type of the entity in the dictionary, which must be a . + /// The dictionary of entities to update. + /// The overrides object that may contain specific override values for the entities. + /// A function that provides the default name for an entity based on the key and the entity itself. + /// A function that provides the default display name for an entity based on the key and the entity itself. If null, it defaults to . + /// Thrown when is null. + /// Thrown when unable to determine the entity ID or name, or if any other unexpected error occurs during processing. + public static void SetEntityNameAndEnabled([DisallowNull] this Dictionary entities, [DisallowNull] Overrides overrides, [DisallowNull] Func DefaultNameFunc, [DisallowNull] Func DefaultDisplayNameFunc) + where TKey : notnull + where TEntity : notnull, NamedEntity { - string formatIfNeeded(string input) => FormatAsName switch - { - true => input.FormatName(), - false => input - }; - - if (string.IsNullOrEmpty(key)) - throw new NullReferenceException("Key is null"); - - if (Overrides.TryGetValue(key, out string overrideValue)) - return formatIfNeeded(overrideValue); - - if (!string.IsNullOrEmpty(DefaultValue)) - return formatIfNeeded(DefaultValue); + if (DefaultNameFunc is null) + throw new ArgumentNullException(nameof(DefaultNameFunc)); - if (entity is EntityWithNameAndLabel entityWithNameAndLabel) - return formatIfNeeded(entityWithNameAndLabel.Label ?? entityWithNameAndLabel.Name); + DefaultDisplayNameFunc ??= DefaultNameFunc; - else - throw new Exception("Unable to determine suitable name."); + foreach (var (key, entity) in entities) + { + EntityOverride? entityOverride = (key, entity) switch + { + // We are adding + 1 to the outlet's key- because the PDU gives data 0-based. However, when the entities are viewed through + // its UI, they are 1-based. This corrects that. + (int k, Outlet o) => overrides.Outlets!.TryGetValue(k + 1, out var outletOverride) ? outletOverride : null, + (string k, Device o) => overrides.Devices!.TryGetValue(k, out var outletOverride) ? outletOverride : null, + (string k, Measurement o) => overrides.Measurements!.TryGetValue(o.Type, out var outletOverride) ? outletOverride : null, + _ => null + }; + + // If overrides are defined, use those. + if (entityOverride is not null && entity is Measurement m) + { + // Measurements inherits part of the parents name. aka, "outlet_1_power" + entity.Entity_Name = Coalesce(entityOverride.ID, DefaultNameFunc?.Invoke(key, entity), entity.Entity_Name)?.FormatName() ?? throw new Exception("Unable to determine entity ID."); + entity.Entity_DisplayName = Coalesce(entityOverride.Name, DefaultDisplayNameFunc?.Invoke(key, entity), entity.Entity_DisplayName) ?? throw new Exception("Unable to determine entity name."); + entity.Entity_Enabled = entityOverride.Enabled; //Always default to enabled. + } + else if (entityOverride is not null) + { + entity.Entity_Name = Coalesce(entityOverride.ID, DefaultNameFunc?.Invoke(key, entity), entity.Entity_Name)?.FormatName() ?? throw new Exception("Unable to determine entity ID."); + entity.Entity_DisplayName = Coalesce(entityOverride.Name, DefaultDisplayNameFunc?.Invoke(key, entity), entity.Entity_DisplayName) ?? throw new Exception("Unable to determine entity name."); + entity.Entity_Enabled = entityOverride.Enabled; //Always default to enabled. + } + else // No overrides defined. Set defaults. + { + entity.Entity_Name = DefaultNameFunc!.Invoke(key, entity).FormatName(); + entity.Entity_DisplayName = DefaultDisplayNameFunc!.Invoke(key, entity); + entity.Entity_Enabled = true; + } + } } /// - /// Multi-type lookup. Does case-insensitive compare for strings. + /// Prune disabled items from dictionary. /// /// - /// - /// - /// - /// - private static bool tryGetValue(this Dictionary dictionary, TKey? key, out TValue Result, TValue DefaultValue) - where TKey : notnull + /// + /// + public static void PruneDisabled([DisallowNull] this Dictionary entities) + where TKey : notnull + where TEntity : notnull, NamedEntity { - if (key is null) - { - Result = DefaultValue; - return false; - } - if (key is string sKey && dictionary is Dictionary stringDictionary) - return caseInsensitiveLookup(stringDictionary, sKey, out Result, DefaultValue); - - if (dictionary.ContainsKey(key)) - { - Result = dictionary[key]; - return true; - } + var disabled = entities.Where(o => o.Value.Entity_Enabled == false).ToArray(); - Result = DefaultValue; - return false; + foreach (var entity in disabled) + entities.Remove(entity.Key); } - - private static bool caseInsensitiveLookup(this Dictionary Dictionary, string Key, out TValue Result, TValue DefaultValue) + /// + /// Set a prefix for all contained entities. (This- is used to prefix measurements, with the parent's ID.) + /// + /// + /// + /// + /// + public static void SetEntityNamePrefix([DisallowNull] this Dictionary entities, string Prefix) + where TKey : notnull + where TEntity : notnull, NamedEntity { - var match = Dictionary.Keys.FirstOrDefault(o => string.Equals(o, Key, StringComparison.OrdinalIgnoreCase)); - if (match is null) - { - Result = DefaultValue; - return false; - } - - Result = Dictionary[match]; - - if (Result is null) - return false; - if (Result is string s) - return !string.IsNullOrWhiteSpace(s); - return Result is not null; + foreach (var (_, entity) in entities) + entity.Entity_Name = $"{Prefix}_{entity.Entity_Name}"; } + /// /// Sets all properties of . /// /// /// is set to key from device. /// - /// + /// /// /// - public static void SetParentAndIdentifier(this Dictionary Items, IMQTTKey Parent) where T : BaseEntity + public static void SetParentAndIdentifier(this Dictionary Items, IMQTTKey Parent, [DisallowNull] Func? IdentifierFunc) where TEntity : BaseEntity { foreach (var (key, item) in Items) { + string childKey = IdentifierFunc.Invoke(key, item); item.Record_Parent = Parent; - item.Record_Key = key; - item.Entity_Identifier = Parent.CreateChildIdentifier(key); + item.Record_Key = childKey; + item.Entity_Identifier = Parent.CreateChildIdentifier(childKey); } } diff --git a/rPDU2MQTT/Extensions/IEntityWithState_Extensions.cs b/rPDU2MQTT/Extensions/IEntityWithState_Extensions.cs index 94d2610..1034c5c 100644 --- a/rPDU2MQTT/Extensions/IEntityWithState_Extensions.cs +++ b/rPDU2MQTT/Extensions/IEntityWithState_Extensions.cs @@ -19,33 +19,4 @@ public static string GetStateTopic(this T Entity) { return MQTTHelper.JoinPaths(Entity.GetTopicPath(), Entity.State_Topic); } - - public static BinarySensorDiscovery CreateStateDiscovery(this T item, DiscoveryDevice Device) where T : NamedEntity, IEntityWithState - { - var sensor = new BinarySensorDiscovery - { - //Identifying Details - ID = item.Entity_Identifier + "_state", - Name = item.Entity_Name + "_state", - DisplayName = $"State", - - //Device Details - Device = Device, - - //Sensor Specific Details - EntityType = Models.HomeAssistant.Enums.EntityType.BinarySensor, - EntityCategory = null, - - //State - Pulled from IEntityWithState - StateTopic = item.GetStateTopic(), - ValueTemplate = item.State_ValueTemplate, - PayloadOn = item.State_On, - PayloadOff = item.State_Off, - - //Availbility - //Availability = outlet.GetAvailability() - }; - - return sensor; - } } diff --git a/rPDU2MQTT/Extensions/RootData_Extensiosn.cs b/rPDU2MQTT/Extensions/RootData_Extensiosn.cs index 8d087e8..56e1284 100644 --- a/rPDU2MQTT/Extensions/RootData_Extensiosn.cs +++ b/rPDU2MQTT/Extensions/RootData_Extensiosn.cs @@ -10,11 +10,11 @@ public static class RootData_Extensiosn /// /// /// - public static DiscoveryDevice GetDiscoveryDevice(this RootData data, string DeviceURL) + public static DiscoveryDevice GetDiscoveryDevice(this RootData data) { return new DiscoveryDevice { - ConfigurationUrl = DeviceURL, + ConfigurationUrl = data.URL, HardwareVersion = data.Sys.Version, Manufacturer = data.Sys.Oem, UniqueIdentifier = data.Entity_Identifier, diff --git a/rPDU2MQTT/Helpers/DefaultNames.cs b/rPDU2MQTT/Helpers/DefaultNames.cs new file mode 100644 index 0000000..99e79c0 --- /dev/null +++ b/rPDU2MQTT/Helpers/DefaultNames.cs @@ -0,0 +1,14 @@ +using rPDU2MQTT.Models.PDU; +using rPDU2MQTT.Models.PDU.basePDU; + +namespace rPDU2MQTT.Helpers; + +/// +/// Defines default names for various entity types. +/// +public static class DefaultNames +{ + public static string UseEntityName(TKey key, TEntity entity) where TEntity : EntityWithNameAndLabel => entity.Name; + public static string UseEntityLabel(TKey key, TEntity entity) where TEntity : EntityWithNameAndLabel => entity.Label ?? entity.Name; + public static string UseMeasurementType(TKey key, TEntity entity) where TEntity : Measurement => entity.Type; +} diff --git a/rPDU2MQTT/Helpers/MeasurementHelper.cs b/rPDU2MQTT/Helpers/MeasurementHelper.cs index a351257..bc30cd4 100644 --- a/rPDU2MQTT/Helpers/MeasurementHelper.cs +++ b/rPDU2MQTT/Helpers/MeasurementHelper.cs @@ -15,15 +15,15 @@ public static class MeasurementHelper { return measurement.Type.ToLower() switch { - "currentcrestfactor" => null, - "balance" => null, + //"currentcrestfactor" => new SensorDTO(StateClass.Measurement, DeviceClass.Unknown), + //"balance" => new SensorDTO(StateClass.Measurement, DeviceClass.Unknown), "apparentpower" => new SensorDTO(StateClass.Measurement, DeviceClass.ApparentPower), "realpower" => new SensorDTO(StateClass.Measurement, DeviceClass.Power), "energy" => new SensorDTO(StateClass.TotalIncreasing, DeviceClass.Energy), "powerfactor" => new SensorDTO(StateClass.Measurement, DeviceClass.PowerFactor), "current" => new SensorDTO(StateClass.Measurement, DeviceClass.Current), "voltage" => new SensorDTO(StateClass.Measurement, DeviceClass.Voltage), - _ => null + _ => new SensorDTO(StateClass.Measurement, DeviceClass.Unknown) }; } } diff --git a/rPDU2MQTT/Helpers/ThrowError.cs b/rPDU2MQTT/Helpers/ThrowError.cs new file mode 100644 index 0000000..be0a7d5 --- /dev/null +++ b/rPDU2MQTT/Helpers/ThrowError.cs @@ -0,0 +1,30 @@ +using System.Diagnostics.CodeAnalysis; + +namespace rPDU2MQTT.Helpers; + +public static class ThrowError +{ + [DoesNotReturn] + public static T ConfigurationMissing(string ConfigurationPath) + { + Console.WriteLine("Please validate configuration.yaml"); + string msg = $"Missing required configuration of type {typeof(T).Name}. Path: " + ConfigurationPath; + Console.WriteLine(msg); + + throw new Exception(msg); + } + + public static void TestRequiredConfigurationSection([AllowNull, NotNull] object section, string ConfigurationPath) + { + if (section is null) + { + Console.WriteLine("Please validate configuration.yaml"); + string msg = $"Missing required configuration. Path: " + ConfigurationPath; + Console.WriteLine(msg); + + throw new Exception(msg); + } + } + + +} diff --git a/rPDU2MQTT/Models/Config/ActionsConfig.cs b/rPDU2MQTT/Models/Config/ActionsConfig.cs deleted file mode 100644 index c91b46d..0000000 --- a/rPDU2MQTT/Models/Config/ActionsConfig.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace rPDU2MQTT.Models.Config; -#nullable disable - -/// -/// Configuration settings for actions. -/// -public class ActionsConfig -{ - /// - /// Gets or sets a value indicating whether actions are enabled. - /// - [Display(Description = "Indicates whether actions are enabled.")] - public bool Enabled { get; set; } = false; - - /// - /// Gets or sets the username for performing actions. - /// - [Display(Description = "The username for performing actions.")] - public string Username { get; set; } - - /// - /// Gets or sets the password for performing actions. - /// - [Display(Description = "The password for performing actions.")] - public string Password { get; set; } -} \ No newline at end of file diff --git a/rPDU2MQTT/Models/Config/Config.cs b/rPDU2MQTT/Models/Config/Config.cs new file mode 100644 index 0000000..c3852a4 --- /dev/null +++ b/rPDU2MQTT/Models/Config/Config.cs @@ -0,0 +1,22 @@ +using rPDU2MQTT.Models.Config; +using YamlDotNet.Serialization; + +namespace rPDU2MQTT.Classes; + +/// +/// This represents the entire configuration used for this project. +/// +public class Config +{ + [YamlMember(Alias = "MQTT", DefaultValuesHandling = DefaultValuesHandling.OmitDefaults, Description = "MQTT Configuration")] + public MQTTConfig MQTT { get; set; } = new MQTTConfig(); + + [YamlMember(Alias = "PDU", DefaultValuesHandling = DefaultValuesHandling.OmitDefaults, Description = "PDU Configuration")] + public PduConfig PDU { get; set; } = new PduConfig(); + + [YamlMember(Alias = "HomeAssistant", DefaultValuesHandling = DefaultValuesHandling.OmitDefaults, Description = "Home Assistant Configuration")] + public HomeAssistantConfig HASS { get; set; } = new HomeAssistantConfig(); + + [YamlMember(Alias = "Overrides", DefaultValuesHandling = DefaultValuesHandling.OmitDefaults, Description = "Overrides")] + public Overrides Overrides { get; set; } = new Overrides(); +} diff --git a/rPDU2MQTT/Models/Config/HomeAssistantConfig.cs b/rPDU2MQTT/Models/Config/HomeAssistantConfig.cs index b41983f..639b475 100644 --- a/rPDU2MQTT/Models/Config/HomeAssistantConfig.cs +++ b/rPDU2MQTT/Models/Config/HomeAssistantConfig.cs @@ -1,5 +1,4 @@ namespace rPDU2MQTT.Models.Config; -#nullable disable /// /// Configuration settings for Home Assistant integration. @@ -16,12 +15,12 @@ public class HomeAssistantConfig /// Gets or sets the discovery topic for Home Assistant. /// [Display(Description = "The discovery topic for Home Assistant.")] - public string DiscoveryTopic { get; set; } + public string? DiscoveryTopic { get; set; } /// /// How often should discovery data be published? /// - public int DiscoveryInterval { get; set; } + public int DiscoveryInterval { get; set; } = 300; /// /// Default expireAfter interval applied to all sensors. After this time- the sensor will be marked as unavailable. diff --git a/rPDU2MQTT/Models/Config/MQTTConfig.cs b/rPDU2MQTT/Models/Config/MQTTConfig.cs index d26cbca..56fa26f 100644 --- a/rPDU2MQTT/Models/Config/MQTTConfig.cs +++ b/rPDU2MQTT/Models/Config/MQTTConfig.cs @@ -1,21 +1,16 @@ -namespace rPDU2MQTT.Models.Config; +using rPDU2MQTT.Models.Config.Schemas; +using YamlDotNet.Serialization; + +namespace rPDU2MQTT.Models.Config; /// /// Configuration settings for the MQTT broker. /// +[YamlSerializable] public class MQTTConfig { - /// - /// Gets or sets the username for connecting to the MQTT broker. - /// - [Display(Description = "The username for connecting to the MQTT broker.")] - public string? Username { get; set; } = null; - - /// - /// Gets or sets the password for connecting to the MQTT broker. - /// - [Display(Description = "The password for connecting to the MQTT broker.")] - public string? Password { get; set; } = null; + [YamlMember(Alias = "Credentials", DefaultValuesHandling = DefaultValuesHandling.OmitNull, Description = "Optional credentials to connect to MQTT")] + public Schemas.Credentials? Credentials { get; set; } = null; /// /// Gets or sets the client-id for connecting to the MQTT broker. @@ -31,18 +26,11 @@ public class MQTTConfig public string ParentTopic { get; set; } = "Rack_PDU"; /// - /// Gets or sets the host of the MQTT broker. - /// - [Required(ErrorMessage = "Host is required.")] - [Display(Description = "The host of the MQTT broker.")] - public string Host { get; set; } = "localhost"; - - /// - /// Gets or sets the port of the MQTT broker. + /// Gets or sets the connection details for MQTT Broker. /// - [Range(0, 65535, ErrorMessage = "Port must be between 0 and 65535.")] - [Display(Description = "The port of the MQTT broker.")] - public int Port { get; set; } = 1883; + [Required(ErrorMessage = "Connection is required")] + [Display(Description = "Connection details for MQTT Broker")] + public Connection Connection { get; set; } = new Connection(); /// /// Gets or sets the keepalive interval for the MQTT connection in seconds. diff --git a/rPDU2MQTT/Models/Config/Overrides.cs b/rPDU2MQTT/Models/Config/Overrides.cs index be07319..33aff47 100644 --- a/rPDU2MQTT/Models/Config/Overrides.cs +++ b/rPDU2MQTT/Models/Config/Overrides.cs @@ -1,26 +1,20 @@ -using System.Text.Json.Serialization; +using rPDU2MQTT.Models.Config.Schemas; +using YamlDotNet.Serialization; namespace rPDU2MQTT.Models.Config; +[YamlSerializable] public class Overrides { - /// - /// Allows overriding the generated entity ID for the PDU. - /// - public string? PduID { get; set; } = null; + [YamlMember(Alias = "PDU", DefaultValuesHandling = DefaultValuesHandling.OmitNull, Description = "Allows overriding values for the PDU itself.")] + public EntityOverride? PDU { get; set; } - /// - /// Allows overriding the generated entity name for the PDU. - /// - public string? PduName { get; set; } = null; -} + [YamlMember(Alias = "Devices", DefaultValuesHandling = DefaultValuesHandling.OmitNull, Description = "Allows overriding configuration for individual devices.")] + public Dictionary? Devices { get; set; } -/// -/// Defines overrides for measurements. -/// -public class MeasurementOverrides : TypeOverride { } + [YamlMember(Alias = "Outlets", DefaultValuesHandling = DefaultValuesHandling.OmitNull, Description = "Allows overriding values for individual outlets.")] + public Dictionary? Outlets { get; set; } -/// -/// Defines overrides for outlets. -/// -public class OutletOverrides : TypeOverride { } + [YamlMember(Alias = "Measurements", DefaultValuesHandling = DefaultValuesHandling.OmitNull, Description = "Allows overriding individual measurements")] + public Dictionary? Measurements { get; set; } +} diff --git a/rPDU2MQTT/Models/Config/PduConfig.cs b/rPDU2MQTT/Models/Config/PduConfig.cs index cdc8586..af871bc 100644 --- a/rPDU2MQTT/Models/Config/PduConfig.cs +++ b/rPDU2MQTT/Models/Config/PduConfig.cs @@ -1,5 +1,8 @@ -namespace rPDU2MQTT.Models.Config; -#nullable disable +using rPDU2MQTT.Models.Config.Schemas; +using System.ComponentModel; +using YamlDotNet.Serialization; + +namespace rPDU2MQTT.Models.Config; /// /// Configuration settings for the PDU. @@ -7,28 +10,32 @@ public class PduConfig { /// - /// Gets or sets the device ID of the PDU. + /// Gets or sets the connection details for MQTT Broker. /// - [Required(ErrorMessage = "DeviceId is required.")] - [Display(Description = "The device ID of the PDU.")] - public string DeviceId { get; set; } + [Required(ErrorMessage = "Connection is required")] + [Display(Description = "Connection details for PDU")] + public Connection Connection { get; set; } = new Connection(); /// - /// Gets or sets the URL of the PDU API. + /// Credentials used when connection to PDU. /// - [Required(ErrorMessage = "Url is required.")] - [Url(ErrorMessage = "Url must be a valid URL.")] - [Display(Description = "The URL of the PDU API.")] - public string Url { get; set; } + [YamlMember(Alias = "Credentials", DefaultValuesHandling = DefaultValuesHandling.OmitNull)] + [DefaultValue(null)] + public Schemas.Credentials? Credentials { get; set; } = null; /// /// Gets or sets the polling interval for the PDU in seconds. /// [Range(1, int.MaxValue, ErrorMessage = "PollInterval must be greater than 0.")] [Display(Description = "The polling interval for the PDU in seconds.")] + [DefaultValue(5)] public int PollInterval { get; set; } = 5; - [Range(1, 60 * 10, ErrorMessage = "Expected timeout between 1 second, and 10 minutes.")] - [Display(Description = "Http timeout for requests to PDU")] - public int Timeout { get; set; } = 5; + [DefaultValue(false)] + [YamlMember(Alias = "ActionsEnabled", DefaultValuesHandling = DefaultValuesHandling.OmitNull, Description = "Configuration to enable write actions via PDU")] + /// + /// Gets or sets a value indicating whether actions are enabled. + /// + [Display(Description = "Indicates whether actions are enabled.")] + public bool ActionsEnabled { get; set; } } \ No newline at end of file diff --git a/rPDU2MQTT/Models/Config/Schemas/Connection.cs b/rPDU2MQTT/Models/Config/Schemas/Connection.cs new file mode 100644 index 0000000..28b0ecc --- /dev/null +++ b/rPDU2MQTT/Models/Config/Schemas/Connection.cs @@ -0,0 +1,33 @@ +using System.ComponentModel; +using YamlDotNet.Serialization; + +namespace rPDU2MQTT.Models.Config.Schemas; + +/// +/// This defines the schema used for connecting to another service. +/// +public class Connection +{ + [YamlMember(Alias = "Host", DefaultValuesHandling = DefaultValuesHandling.OmitNull)] + [Required(ErrorMessage = "Host is required.")] + [Display(Description = "Hostname or IP to connect to.")] + [Description("IP, or DNS Name")] + public string? Host { get; set; } + + [YamlMember(Alias = "Port", DefaultValuesHandling = DefaultValuesHandling.OmitNull)] + [Range(0, 65535, ErrorMessage = "Port must be between 0 and 65535.")] + [Display(Description = "The port to connect to.")] + [Description("Default Port")] + public int? Port { get; set; } + + [YamlMember(Alias = "Timeout", DefaultValuesHandling = DefaultValuesHandling.OmitNull)] + [Range(1, 3600, ErrorMessage = "Timeout must be between 0 and 3600.")] + [Display(Name = "Connection Timeout", Description = "Default connection timeout.")] + [Description("Default connection timeout.")] + public int? TimeoutSecs { get; set; } = 15; + + [YamlMember(Alias = "Scheme", DefaultValuesHandling = DefaultValuesHandling.OmitNull)] + [Display(Name = "Connection Scheme", Description = "Connection scheme used")] + [Description("Default connection scheme.")] + public string? Scheme { get; set; } +} diff --git a/rPDU2MQTT/Models/Config/Schemas/Credentials.cs b/rPDU2MQTT/Models/Config/Schemas/Credentials.cs new file mode 100644 index 0000000..a0d0d32 --- /dev/null +++ b/rPDU2MQTT/Models/Config/Schemas/Credentials.cs @@ -0,0 +1,15 @@ +using System.ComponentModel; + +namespace rPDU2MQTT.Models.Config.Schemas; + +/// +/// This defines the schema used for credentials. +/// +public class Credentials +{ + [Description("Username to log in as")] + public string? Username { get; set; } + + [Description("Password to login with")] + public string? Password { get; set; } +} diff --git a/rPDU2MQTT/Models/Config/Schemas/EntityOverride.cs b/rPDU2MQTT/Models/Config/Schemas/EntityOverride.cs new file mode 100644 index 0000000..32d4e7a --- /dev/null +++ b/rPDU2MQTT/Models/Config/Schemas/EntityOverride.cs @@ -0,0 +1,37 @@ +using YamlDotNet.Serialization; + +namespace rPDU2MQTT.Models.Config.Schemas; + +/// +/// This defines the schema for overrides for a specific type of entity. +/// +/// This is the type of key used. +public class EntityOverride +{ + /// + /// Allows overriding the generated EntityName. + /// + /// + /// This maps to , ie, "object_id" + /// + [YamlMember(Alias = "ID", DefaultValuesHandling = DefaultValuesHandling.OmitNull, Description = "Overridden ID")] + public string? ID { get; set; } + + /// + /// Allows overriding the Name / Display Name. + /// + /// + /// This maps to , ie, "name" + /// + [YamlMember(Alias = "Name", DefaultValuesHandling = DefaultValuesHandling.OmitNull, Description = "Overridden Name")] + public string? Name { get; set; } + + /// + /// Should this entity be enabled, or disabled? + /// + /// + /// Disabled entities will not be published. + /// + [YamlMember(Alias = "Enabled", DefaultValuesHandling = DefaultValuesHandling.OmitNull, Description = "Is this entity enabled?")] + public bool Enabled { get; set; } = true; +} diff --git a/rPDU2MQTT/Models/Config/TypeOverride.cs b/rPDU2MQTT/Models/Config/TypeOverride.cs deleted file mode 100644 index 47773bc..0000000 --- a/rPDU2MQTT/Models/Config/TypeOverride.cs +++ /dev/null @@ -1,56 +0,0 @@ -using rPDU2MQTT.Models.Converters; -using System.Text.Json.Serialization; - -namespace rPDU2MQTT.Models.Config; - -/// -/// This defines the schema for overrides for a specific section. -/// -/// This is the type of key used. -public class TypeOverride -{ - [JsonConverter(typeof(CaseInsensitiveDictionaryConverter))] - [JsonPropertyName("ID")] - /// - /// Allows overriding the generated EntityName. - /// - /// - /// This maps to , ie, "object_id" - /// - public Dictionary ID { get; set; } = new(); - - [JsonConverter(typeof(CaseInsensitiveDictionaryConverter))] - [JsonPropertyName("Name")] - /// - /// Allows overriding the Name / Display Name. - /// - /// - /// This maps to , ie, "name" - /// - public Dictionary Name { get; set; } = new(); - - [JsonConverter(typeof(CaseInsensitiveDictionaryConverter))] - [JsonPropertyName("Enabled")] - /// - /// Allows enabling, or disabling specific entities. - /// - public Dictionary Enabled { get; set; } = new(); - - /// - /// Remove any entites which are marked as disabled. - /// - /// - /// - public void RemoveDisabledRecords(Dictionary Entities, Func? KeyFunc = null) - { - //Remove any disabled entities. - foreach (var item in Entities.ToList()) - { - string Key = KeyFunc is null ? item.Key : KeyFunc.Invoke(item.Value); - if (Enabled.TryGetValue(Key, out bool IsEnabled) && IsEnabled == false) - { - Entities.Remove(item.Key); - } - } - } -} diff --git a/rPDU2MQTT/Models/HomeAssistant/DiscoveryTypes/Sensor.cs b/rPDU2MQTT/Models/HomeAssistant/DiscoveryTypes/SensorDiscovery.cs similarity index 93% rename from rPDU2MQTT/Models/HomeAssistant/DiscoveryTypes/Sensor.cs rename to rPDU2MQTT/Models/HomeAssistant/DiscoveryTypes/SensorDiscovery.cs index 3365ab7..ee2920c 100644 --- a/rPDU2MQTT/Models/HomeAssistant/DiscoveryTypes/Sensor.cs +++ b/rPDU2MQTT/Models/HomeAssistant/DiscoveryTypes/SensorDiscovery.cs @@ -4,7 +4,7 @@ namespace rPDU2MQTT.Models.HomeAssistant.DiscoveryTypes; -public class Sensor : baseSensorEntity +public class SensorDiscovery : baseSensorEntity { [JsonPropertyName("device_class")] public DeviceClass? SensorClass { get; set; } diff --git a/rPDU2MQTT/Models/HomeAssistant/Enums/DeviceClass.cs b/rPDU2MQTT/Models/HomeAssistant/Enums/DeviceClass.cs index 5b2ff34..39c3df8 100644 --- a/rPDU2MQTT/Models/HomeAssistant/Enums/DeviceClass.cs +++ b/rPDU2MQTT/Models/HomeAssistant/Enums/DeviceClass.cs @@ -7,6 +7,8 @@ /// public enum DeviceClass { + [JsonPropertyName("")] + Unknown = 0, /// /// Date. /// Unit of measurement: None diff --git a/rPDU2MQTT/Models/HomeAssistant/baseClasses/baseEntity.cs b/rPDU2MQTT/Models/HomeAssistant/baseClasses/baseEntity.cs index b868c05..f71de28 100644 --- a/rPDU2MQTT/Models/HomeAssistant/baseClasses/baseEntity.cs +++ b/rPDU2MQTT/Models/HomeAssistant/baseClasses/baseEntity.cs @@ -17,7 +17,7 @@ namespace rPDU2MQTT.Models.HomeAssistant.baseClasses; // Why didn't I just use Newtonsoft. [JsonDerivedType(typeof(baseSensorEntity))] [JsonDerivedType(typeof(BinarySensorDiscovery))] -[JsonDerivedType(typeof(Sensor))] +[JsonDerivedType(typeof(SensorDiscovery))] [JsonPolymorphic(IgnoreUnrecognizedTypeDiscriminators = false, TypeDiscriminatorPropertyName = nameof(JsonPolyMorphicTypeName), UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor)] public abstract class baseEntity : IBaseDiscovery { diff --git a/rPDU2MQTT/Models/PDU/Device.cs b/rPDU2MQTT/Models/PDU/Device.cs index 811c55a..34c86a7 100644 --- a/rPDU2MQTT/Models/PDU/Device.cs +++ b/rPDU2MQTT/Models/PDU/Device.cs @@ -1,28 +1,19 @@ -using rPDU2MQTT.Helpers; -using rPDU2MQTT.Interfaces; -using rPDU2MQTT.Models.PDU.DummyDevices; +using rPDU2MQTT.Interfaces; +using rPDU2MQTT.Models.PDU.basePDU; using System.Text.Json.Serialization; namespace rPDU2MQTT.Models.PDU; -public partial class Device : BaseEntity, IEntityWithState +public partial class Device : EntityWithNameAndLabel, IEntityWithState { #region State string IEntityWithState.State_On => "normal"; string IEntityWithState.State_Off => ""; #endregion - [JsonPropertyName("label")] - public string Label { get; set; } - - [JsonPropertyName("name")] - public string Name { get; set; } - [JsonPropertyName("state")] public string State { get; set; } - - [JsonPropertyName("order")] public long Order { get; set; } @@ -43,13 +34,13 @@ public partial class Device : BaseEntity, IEntityWithState public long LifetimeEnergy { get; set; } [JsonPropertyName("outlet")] - public Dictionary Outlets { get; set; } + public Dictionary Outlets { get; set; } [JsonPropertyName("alarm")] public A0Ae260C851900C3Alarm Alarm { get; set; } [JsonPropertyName("layout")] - public Dictionary Layout { get; set; } + public Dictionary Layout { get; set; } [JsonPropertyName("entity")] public Dictionary Entity { get; set; } diff --git a/rPDU2MQTT/Models/PDU/RootData.cs b/rPDU2MQTT/Models/PDU/RootData.cs index aa8094d..a732538 100644 --- a/rPDU2MQTT/Models/PDU/RootData.cs +++ b/rPDU2MQTT/Models/PDU/RootData.cs @@ -6,6 +6,12 @@ namespace rPDU2MQTT.Models.PDU; public partial class RootData : NamedEntity { + /// + /// This is the URL used. + /// + [JsonIgnore] + public string URL { get; set; } = string.Empty; + [JsonPropertyName("sys")] public Sys Sys { get; set; } diff --git a/rPDU2MQTT/Program.cs b/rPDU2MQTT/Program.cs index 32ea38b..bfd0fba 100644 --- a/rPDU2MQTT/Program.cs +++ b/rPDU2MQTT/Program.cs @@ -1,92 +1,17 @@ using HiveMQtt.Client; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using rPDU2MQTT.Classes; -using rPDU2MQTT.Models.Config; -using rPDU2MQTT.Services; +using rPDU2MQTT.Startup; +using System.Runtime.InteropServices; var host = Host.CreateDefaultBuilder(args) - .ConfigureAppConfiguration((context, config) => - { - if (Directory.Exists("/config")) - { - Console.WriteLine($"Found /config directory"); - Console.WriteLine($"/config.appsettings.json exists: {File.Exists("/config/appsettings.json")}"); - - // Check /Config directory. - config - .SetBasePath("/config") - .AddJsonFile("appsettings.Development.json", optional: true, reloadOnChange: true); - } - else - { - Console.WriteLine($"Did not find /config directory. Using {Directory.GetCurrentDirectory()}"); - Console.WriteLine($"appsettings.json exists: {File.Exists("appsettings.json")}"); - //Check for configuration files in the current directory. - config.SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("appsettings.Development.json", optional: true, reloadOnChange: true); - } - - - config.AddEnvironmentVariables(); - - }) - .ConfigureServices((context, services) => - { - //Bind Configuration - services.Configure(context.Configuration.GetSection("Mqtt")); - services.Configure(context.Configuration.GetSection("Pdu")); - services.Configure(context.Configuration.GetSection("Actions")); - services.Configure(context.Configuration.GetSection("HomeAssistant")); - services.Configure(context.Configuration.GetSection("Overrides")); - services.Configure(context.Configuration.GetSection("Outlets")); - services.Configure(context.Configuration.GetSection("Measurements")); - - //Bind MQTT - var mqttConfig = context.Configuration.GetSection("Mqtt").Get() ?? throw new NullReferenceException("Unable to load MQTT configuration."); - var mqttOptions = new HiveMQClientOptionsBuilder() - .WithBroker(mqttConfig.Host) - .WithPort(mqttConfig.Port) - .WithUserName(mqttConfig.Username) - .WithPassword(mqttConfig.Password) - .WithClientId("rpdu2mqtt") - .Build(); - - var client = new HiveMQClient(mqttOptions); - services.AddSingleton(client); - services.AddSingleton(); - services.AddSingleton(); - - // Create HttpClient for rPDU - var pduConfiguration = context.Configuration.GetSection("Pdu").Get() ?? throw new NullReferenceException(); - services.AddHttpClient("pdu", client => - { - client.BaseAddress = new Uri(pduConfiguration.Url); - client.Timeout = TimeSpan.FromSeconds(pduConfiguration.Timeout); - }); - - //Configure Services - services.AddSingleton(sp => - { - var cfg = sp.GetRequiredService(); - var fac = sp.GetService(); - var logFac = sp.GetService(); - var hc = fac.CreateClient("pdu"); - var log = logFac.CreateLogger(); - return new PDU(cfg, hc, log); - }); - - - services.AddHostedService(); - services.AddHostedService(); - - }) + .ConfigureAppConfiguration(ConfigLoader.Configure) + .ConfigureServices(ServiceConfiguration.Configure) .ConfigureLogging(logging => { logging.ClearProviders(); - logging.AddConsole(); +logging.AddConsole(); }) .Build(); diff --git a/rPDU2MQTT/Services/HomeAssistantDiscoveryService.cs b/rPDU2MQTT/Services/HomeAssistantDiscoveryService.cs index ec32cee..7ae3417 100644 --- a/rPDU2MQTT/Services/HomeAssistantDiscoveryService.cs +++ b/rPDU2MQTT/Services/HomeAssistantDiscoveryService.cs @@ -1,9 +1,11 @@ using Microsoft.Extensions.Logging; using rPDU2MQTT.Classes; using rPDU2MQTT.Extensions; -using rPDU2MQTT.Helpers; -using rPDU2MQTT.Models.HomeAssistant.baseClasses; +using rPDU2MQTT.Models.HomeAssistant; +using rPDU2MQTT.Models.PDU; +using rPDU2MQTT.Models.PDU.DummyDevices; using rPDU2MQTT.Services.baseTypes; +using System.Diagnostics.CodeAnalysis; namespace rPDU2MQTT.Services; @@ -12,61 +14,83 @@ namespace rPDU2MQTT.Services; /// public class HomeAssistantDiscoveryService : baseDiscoveryService { - public HomeAssistantDiscoveryService(ILogger log, ServiceDependancies deps) : base(deps, log) { } + public HomeAssistantDiscoveryService(ILogger log, MQTTServiceDependancies deps) : base(deps, log) { } protected override async Task Execute(CancellationToken cancellationToken) { var data = await pdu.GetRootData_Public(cancellationToken); - var ParentDevice = data.GetDiscoveryDevice(this.cfg.PDU.Url); + var pduDevice = data.GetDiscoveryDevice(); log.LogDebug("Starting discovery job."); - List Sensors = new(); + // Recursively discover everything. + await recursiveDiscovery(data.Devices, pduDevice, cancellationToken); - foreach (var device in data.Devices.Values) + log.LogInformation("Discovery information published."); + } + + + protected async Task recursiveDiscovery([AllowNull] TEntity entity, DiscoveryDevice parent, CancellationToken cancellationToken) where TEntity : BaseEntity + { + if (entity is null) + return; + else if (entity is Device device) { - foreach (var entity in device.Entity.Values) - { - //So... I only have a single-phase PDU. - // Duct-tape to just attach phase-A data to the primary device. - // If- this integration ever gets used for a dual-phase PDU, this will need to be updated. - if (entity.Label != "Input") - continue; - - foreach (var measurement in entity.Measurements.Values) - { - //If we are unable to parse this measurement as valid, skip to the next. - var dto = measurement.TryParseValue(); - if (dto is null) - continue; - - Sensors.Add(CreateSensorDiscovery(measurement, ParentDevice, dto)); - } - } - - //if(false) - foreach (var outlet in device.Outlets.Values) - { - var childDevice = ParentDevice.CreateChild(outlet); - - Sensors.Add(outlet.CreateStateDiscovery(childDevice)); - - foreach (var measurement in outlet.Measurements.Values) - { - //If we are unable to parse this measurement as valid, skip to the next. - var dto = measurement.TryParseValue(); - if (dto is null) - continue; - - Sensors.Add(CreateSensorDiscovery(measurement, childDevice, dto)); - } - } + // Create a device, to represent this device. + var newParent = parent.CreateChild(device); + + // Discover outlets. + await recursiveDiscovery(device.Outlets, newParent, cancellationToken); + + #region Hack - Discover Entities + // Discover Entity + // Hack- but, for my testing data, my PDU exposes four seperate entities. + // (outlets) -> (breaker0, breaker1) -> (phase0) + // And, total0 + // I don't really see any value in getting the data for the breakers, as it doesn't really correspond to any useful data. + // As such- the phase0, and total0 entities are the only interesting ones... + // And, as it turns out- they both expose the same data, but, phase0 exposes more sensors (such as voltage, and other data that does not total very well) + // So- Only want to discover the ROOT value. + // To do this- we are just going to find the entity, at the top of the layout. + + // In my testing, the root entity, contains a single name, "entity/phase0". So- this next line, extracts, "phase0" + var rootEntityName = device.Layout[0].First().Split("/", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Last(); + + var rootEntity = device.Entity + .Where(o => o.Key == rootEntityName) + .Select(o => o.Value) + .FirstOrDefault(); + + await recursiveDiscovery(rootEntity!, newParent, cancellationToken); + #endregion + } + else if (entity is Outlet outlet) + { + // Create a device to represent the outlet. + var newParent = parent.CreateChild(outlet); - log.LogInformation("Publishing discovery messages"); + // Discover outlet's state. + await DiscoverStateAsync(outlet, newParent, cancellationToken); - await this.PublishDeviceSensors(Sensors, cancellationToken); + // Discover measurements + await recursiveDiscovery(outlet.Measurements, newParent, cancellationToken); + } + else if (entity is Entity pduEntity) + { + // Discover measurements + await recursiveDiscovery(pduEntity.Measurements, parent, cancellationToken); + } + else if(entity is Measurement measurement) + { + await DiscoverMeasurementAsync(measurement, parent, cancellationToken); + } + } - log.LogInformation("Discovery information published."); + protected async Task recursiveDiscovery(Dictionary entities, DiscoveryDevice parent, CancellationToken cancellationToken) where TEntity : BaseEntity + { + foreach (var (_, entity) in entities) + await recursiveDiscovery(entity, parent, cancellationToken); } + } \ No newline at end of file diff --git a/rPDU2MQTT/Services/MQTTPublishingService.cs b/rPDU2MQTT/Services/MQTTPublishingService.cs index 3a9e27d..3aaa200 100644 --- a/rPDU2MQTT/Services/MQTTPublishingService.cs +++ b/rPDU2MQTT/Services/MQTTPublishingService.cs @@ -9,7 +9,7 @@ namespace rPDU2MQTT.Services; /// public class MQTTPublishingService : basePublishingService { - public MQTTPublishingService(ILogger log, ServiceDependancies deps) : base(log, deps) { } + public MQTTPublishingService(ILogger log, MQTTServiceDependancies deps) : base(log, deps) { } protected override async Task Execute(CancellationToken cancellationToken) { diff --git a/rPDU2MQTT/Services/baseTypes/baseDiscoveryService.cs b/rPDU2MQTT/Services/baseTypes/baseDiscoveryService.cs index daf3d5e..d388a71 100644 --- a/rPDU2MQTT/Services/baseTypes/baseDiscoveryService.cs +++ b/rPDU2MQTT/Services/baseTypes/baseDiscoveryService.cs @@ -2,11 +2,11 @@ using Microsoft.Extensions.Logging; using rPDU2MQTT.Classes; using rPDU2MQTT.Extensions; +using rPDU2MQTT.Helpers; using rPDU2MQTT.Interfaces; using rPDU2MQTT.Models.HomeAssistant; using rPDU2MQTT.Models.HomeAssistant.baseClasses; using rPDU2MQTT.Models.HomeAssistant.DiscoveryTypes; -using rPDU2MQTT.Models.HomeAssistant.ObjectDTOs; using rPDU2MQTT.Models.PDU; using rPDU2MQTT.Models.PDU.DummyDevices; @@ -14,11 +14,20 @@ namespace rPDU2MQTT.Services.baseTypes; public abstract class baseDiscoveryService : baseMQTTTService { - public baseDiscoveryService(ServiceDependancies deps, ILogger log) : base(deps, log, deps.Cfg.HASS.DiscoveryInterval) { } + public baseDiscoveryService(MQTTServiceDependancies deps, ILogger log) : base(deps, log, deps.Cfg.HASS.DiscoveryInterval) { } - public Sensor CreateSensorDiscovery(Measurement measurement, DiscoveryDevice Device, SensorDTO dto) + /// + /// Publish a discovery message for the specified , for device + /// + /// + /// + /// + public Task DiscoverMeasurementAsync(Measurement measurement, DiscoveryDevice Parent, CancellationToken cancellationToken) { - var sensor = new Sensor + //If we are unable to parse this measurement as valid, skip to the next. + var dto = measurement.TryParseValue(); + + var discovery = new SensorDiscovery { //Identifying Details ID = measurement.Entity_Identifier, @@ -26,7 +35,7 @@ public Sensor CreateSensorDiscovery(Measurement measurement, DiscoveryDevice Dev DisplayName = measurement.Entity_DisplayName, //Device Details - Device = Device, + Device = Parent, //Sensor Specific Details EntityType = Models.HomeAssistant.Enums.EntityType.Sensor, @@ -44,7 +53,36 @@ public Sensor CreateSensorDiscovery(Measurement measurement, DiscoveryDevice Dev //Availability = new Models.HomeAssistant.baseClasses.EntityAvailability }; - return sensor; + return PushDiscoveryMessage(discovery, cancellationToken); + } + + public Task DiscoverStateAsync(T item, DiscoveryDevice Parent, CancellationToken cancellationToken) where T : NamedEntity, IEntityWithState + { + var discovery = new BinarySensorDiscovery + { + //Identifying Details + ID = item.Entity_Identifier + "_state", + Name = item.Entity_Name + "_state", + DisplayName = $"State", + + //Device Details + Device = Parent, + + //Sensor Specific Details + EntityType = Models.HomeAssistant.Enums.EntityType.BinarySensor, + EntityCategory = null, + + //State - Pulled from IEntityWithState + StateTopic = item.GetStateTopic(), + ValueTemplate = item.State_ValueTemplate, + PayloadOn = item.State_On, + PayloadOff = item.State_Off, + + //Availbility + //Availability = outlet.GetAvailability() + }; + + return PushDiscoveryMessage(discovery, cancellationToken); } /// @@ -54,27 +92,30 @@ public Sensor CreateSensorDiscovery(Measurement measurement, DiscoveryDevice Dev /// This will automatically split results by Device, and EntityType. /// /// - /// + /// /// /// - protected async Task PublishDeviceSensors(List Sensors, CancellationToken cancellationToken) where T : baseEntity + protected async Task PushDiscoveryMessages(List Discoveries, CancellationToken cancellationToken) where T : baseEntity { - foreach (T sensor in Sensors) - { - var topic = $"{cfg.HASS.DiscoveryTopic}/{sensor.EntityType.ToJsonString()}/{sensor.ID}/config"; + foreach (T sensor in Discoveries) + await PushDiscoveryMessage(sensor, cancellationToken); + } + + protected Task PushDiscoveryMessage(T sensor, CancellationToken cancellationToken) where T : baseEntity + { + var topic = $"{cfg.HASS.DiscoveryTopic}/{sensor.EntityType.ToJsonString()}/{sensor.ID}/config"; - log.LogDebug($"Publishing Discovery of type {sensor.EntityType.ToJsonString()} for {sensor.ID} to {topic}"); + log.LogDebug($"Publishing Discovery of type {sensor.EntityType.ToJsonString()} for {sensor.ID} to {topic}"); - var msg = new MQTT5PublishMessage(topic, QualityOfService.AtLeastOnceDelivery) - { - ContentType = "json", - PayloadAsString = System.Text.Json.JsonSerializer.Serialize(sensor, this.jsonOptions) - }; + var msg = new MQTT5PublishMessage(topic, QualityOfService.AtLeastOnceDelivery) + { + ContentType = "json", + PayloadAsString = System.Text.Json.JsonSerializer.Serialize(sensor, this.jsonOptions) + }; - Console.WriteLine(msg.PayloadAsString); + Console.WriteLine(msg.PayloadAsString); - await this.Publish(msg, cancellationToken); - } + return this.Publish(msg, cancellationToken); } } diff --git a/rPDU2MQTT/Services/baseTypes/baseMQTTTService.cs b/rPDU2MQTT/Services/baseTypes/baseMQTTTService.cs index 78dfc5d..54f766c 100644 --- a/rPDU2MQTT/Services/baseTypes/baseMQTTTService.cs +++ b/rPDU2MQTT/Services/baseTypes/baseMQTTTService.cs @@ -28,8 +28,8 @@ public abstract class baseMQTTTService : IHostedService, IDisposable protected System.Text.Json.JsonSerializerOptions jsonOptions { get; init; } - protected baseMQTTTService(ServiceDependancies dependancies, ILogger log) : this(dependancies, log, dependancies.Cfg.PDU.PollInterval) { } - protected baseMQTTTService(ServiceDependancies dependancies, ILogger log, int Interval) + protected baseMQTTTService(MQTTServiceDependancies dependancies, ILogger log) : this(dependancies, log, dependancies.Cfg.PDU.PollInterval) { } + protected baseMQTTTService(MQTTServiceDependancies dependancies, ILogger log, int Interval) { interval = Interval; this.log = log; @@ -110,6 +110,9 @@ protected Task PublishString(string Topic, string Message, CancellationToken can /// protected Task Publish(MQTT5PublishMessage msg, CancellationToken cancellationToken) { + if (!mqtt.IsConnected()) + log.LogError("MQTT Broker is not connected!!!!!"); + return mqtt.PublishAsync(msg, cancellationToken); } diff --git a/rPDU2MQTT/Services/baseTypes/basePublishingService.cs b/rPDU2MQTT/Services/baseTypes/basePublishingService.cs index 5194304..cea857e 100644 --- a/rPDU2MQTT/Services/baseTypes/basePublishingService.cs +++ b/rPDU2MQTT/Services/baseTypes/basePublishingService.cs @@ -9,8 +9,8 @@ namespace rPDU2MQTT.Services.baseTypes; public abstract class basePublishingService : baseMQTTTService { - protected basePublishingService(ILogger log, ServiceDependancies dependancies) : base(dependancies, log, dependancies.Cfg.PDU.PollInterval) { } - protected basePublishingService(ServiceDependancies dependancies, ILogger log, int Interval) : base(dependancies, log, Interval) { } + protected basePublishingService(ILogger log, MQTTServiceDependancies dependancies) : base(dependancies, log, dependancies.Cfg.PDU.PollInterval) { } + protected basePublishingService(MQTTServiceDependancies dependancies, ILogger log, int Interval) : base(dependancies, log, Interval) { } /// /// Publish a series of measurements under diff --git a/rPDU2MQTT/Startup/ConfigLoader.cs b/rPDU2MQTT/Startup/ConfigLoader.cs new file mode 100644 index 0000000..7308663 --- /dev/null +++ b/rPDU2MQTT/Startup/ConfigLoader.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +namespace rPDU2MQTT.Startup; + +public static class ConfigLoader +{ + /// + /// This loads configuration from appsettings.json, and appsettings.$ENV.json + /// + /// + /// + public static void Configure(HostBuilderContext context, IConfigurationBuilder config) + { + string baseConfig = "appsettings.json"; + string envSpecificConfig = $"appsettings.{context.HostingEnvironment.EnvironmentName}.json"; + + Console.WriteLine("Loading JSON Configuration"); + Console.WriteLine($"{baseConfig} exists: {File.Exists(baseConfig)}"); + Console.WriteLine($"{envSpecificConfig} exists: {File.Exists(envSpecificConfig)}"); + + //Check for configuration files in the current directory. + config + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile(envSpecificConfig, optional: true, reloadOnChange: true) + .AddEnvironmentVariables(); + } +} diff --git a/rPDU2MQTT/Startup/FindYamlConfig.cs b/rPDU2MQTT/Startup/FindYamlConfig.cs new file mode 100644 index 0000000..569777b --- /dev/null +++ b/rPDU2MQTT/Startup/FindYamlConfig.cs @@ -0,0 +1,78 @@ +using Microsoft.Extensions.DependencyInjection; +using rPDU2MQTT.Classes; +using System.Runtime.InteropServices; +using YamlDotNet.Serialization; + +namespace rPDU2MQTT.Startup; +/// +/// This class, validates a YAML configuration exists, and returns the path. +/// +internal class FindYamlConfig +{ + public static string Find() + { + Console.WriteLine("Attempting to locate configuration file."); + + // Before starting- we need to validate a configuration file exists. + string[] SearchPaths = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) switch + { + true => [Environment.CurrentDirectory], + false => ["/config", Environment.CurrentDirectory], + }; + + string[] FileNames = ["config"]; + + string[] YamlExtensions = [".yaml", ".yml"]; + + var combinations = from path in SearchPaths + from file in FileNames + from extension in YamlExtensions + select Path.Combine(path, $"{file}{extension}"); + + foreach (var file in combinations) + { + if (File.Exists(file)) + return file; + } + + /// At this point, we cannot find a configuration. + /// Print an error to the console, and lets add a sleep / delay + /// before throwing the exception. + /// + + Console.WriteLine("Unable to locate config.yaml. Paths searched:"); + foreach (var file in combinations) + Console.WriteLine($"\t{file}"); + Console.WriteLine("Restarting in 15 seconds."); + + System.Threading.Thread.Sleep(TimeSpan.FromSeconds(15)); + + throw new Exception("Unable to locate config.yaml"); + } + + public static Config GetConfig() + { + var ds = new DeserializerBuilder() + // Ignore case when deserializing + .WithCaseInsensitivePropertyMatching() + // Ignore fields. + .IgnoreFields() + // Ignore any non-required properties missing + .IgnoreUnmatchedProperties() + // Enforce Nullability + .WithEnforceNullability() + // Check for duplicate keys + .WithDuplicateKeyChecking() + // Enforce required attributes + .WithEnforceRequiredMembers(); + + IDeserializer s = ds.Build(); + + using var stream = File.OpenRead(Find()); + using var sr = new StreamReader(stream); + + var cfg = s.Deserialize(sr); + + return cfg; + } +} diff --git a/rPDU2MQTT/Startup/ServiceConfiguration.cs b/rPDU2MQTT/Startup/ServiceConfiguration.cs new file mode 100644 index 0000000..ae59c73 --- /dev/null +++ b/rPDU2MQTT/Startup/ServiceConfiguration.cs @@ -0,0 +1,86 @@ +using HiveMQtt.Client; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using rPDU2MQTT.Classes; +using rPDU2MQTT.Helpers; +using rPDU2MQTT.Services; + +namespace rPDU2MQTT.Startup; + +public static class ServiceConfiguration +{ + public static void Configure(HostBuilderContext context, IServiceCollection services) + { + // While- we can request services when building dependancies- + // Need the configuration DURING service collection initilization- + // Because it determiens which hosted services we want to add. + Config cfg = FindYamlConfig.GetConfig() ?? throw new Exception("Unable to load configuration"); + + // Bind Configuration + services.AddSingleton(cfg); + + // Bind IHiveMQClient + services.AddSingleton((sp) => + { + ThrowError.TestRequiredConfigurationSection(cfg.MQTT, "MQTT"); + ThrowError.TestRequiredConfigurationSection(cfg.MQTT.Connection, "MQTT.Connection"); + ThrowError.TestRequiredConfigurationSection(cfg.MQTT.Connection.Host, "MQTT.Connection.Host"); + ThrowError.TestRequiredConfigurationSection(cfg.MQTT.Connection.Host, "MQTT.Connection.Port"); + + var mqttBuilder = new HiveMQClientOptionsBuilder() + .WithBroker(cfg.MQTT.Connection.Host) + .WithPort(cfg.MQTT.Connection.Port!.Value) + .WithClientId(cfg.MQTT.ClientID ?? "rpdu2mqtt") + .WithAutomaticReconnect(true); + + if (cfg.MQTT.Credentials?.Username is not null) + mqttBuilder.WithUserName(cfg.MQTT.Credentials.Username); + + if (cfg.MQTT.Credentials?.Password is not null) + mqttBuilder.WithPassword(cfg.MQTT.Credentials.Password); + + // Return new client, with options applied. + return new HiveMQClient(mqttBuilder.Build()); + }); + + //Configure Services + services.AddSingleton(); + + // Create HttpClient for PDU. + services.AddHttpClient(client => + { + ThrowError.TestRequiredConfigurationSection(cfg.PDU, "PDU"); + ThrowError.TestRequiredConfigurationSection(cfg.PDU.Connection, "PDU.Connection"); + ThrowError.TestRequiredConfigurationSection(cfg.PDU.Connection.Host, "PDU.Connection.Host"); + UriBuilder uriBuilder = new UriBuilder(); + + uriBuilder.Host = cfg.PDU.Connection.Host; + uriBuilder.Port = cfg.PDU.Connection.Port ?? 80; + + if (!string.IsNullOrEmpty(cfg.PDU.Connection.Scheme)) + uriBuilder.Scheme = cfg.PDU.Connection.Scheme; + else + uriBuilder.Scheme = uriBuilder.Port switch + { + 80 => "http", + 443 => "https", + _ => uriBuilder.Scheme + }; + + client.BaseAddress = uriBuilder.Uri; + client.Timeout = TimeSpan.FromSeconds(cfg.PDU.Connection.TimeoutSecs ?? 15); + }); + + + + services.AddSingleton(); + + // Created hosted services. + services.AddHostedService(); + + if (cfg.HASS.DiscoveryEnabled) + services.AddHostedService(); + else + Console.WriteLine($"Home Assistant Discovery Disabled."); + } +} diff --git a/rPDU2MQTT/appsettings.json b/rPDU2MQTT/appsettings.json index 6c80876..d2267d3 100644 --- a/rPDU2MQTT/appsettings.json +++ b/rPDU2MQTT/appsettings.json @@ -5,134 +5,5 @@ "System.Net.Http.HttpClient": "Warning", "rPDU2MQTT.*": "Debug" } - }, - "Mqtt": { - // Add your MQTT Username and Password here. - "Username": "user", - "Password": "password", - - // This will be the parent topic in MQTT, where keys are published to. - "ParentTopic": "Rack_PDU", - - // This is the client-ID used. - "ClientID": "rpdu2mqtt", - - // Set this to the host / ip of your MQTT server. - "Host": "localhost", - "Port": 1883, - "KeepAlive": 60 - }, - "Pdu": { - // Add your PDU's device ID here. - "DeviceId": "A0AE260C851900C3", - - // Set to the URL, or IP for your PDU. - "Url": "http://your-pdu-ip-or-dns/", - - // This is how often sensors will be published to MQTT (in seconds) - "PollInterval": 5, - - // Timeout for requests to/from the PDU, in seconds. - "Timeout": 5 - }, - // This section allows overriding the generated entity_id, and names for various objects. - // Note- ID / entity_id, will only be set when the entity is created. - // Do not touch or change it throug here after it has been created! Otherwise, you can end up with duplicates, or other issues. - // "Name" can be freely updated, and home assistant will reflect updated names instantly (after the discovery runs) - "Overrides": { - // Allows overriding the generated identifier for the PDU. Leave blank to disable. - // Warning- Do not change after creation! This is the root, unique identifier used, and should not change! - // IF, you change this, you will have a lot of duplicated devices, and entities!!!!!! - "PduID": null, - - // Allows overriding the generated entity name for the PDU. Leave blank to disable. - // This- can be changed at any time, and will update the name of the Device which represents the PDU. - // If null, this will default to the PDU's configured label. - "PduName": "Rack-PDU-1" - }, - "Outlets": { - // The "entity_id" can be overridden, and customized for each outlet. - // By default, if not specified, an entity ID will be automatically generated from the labels configured via the PDU. - // Note- don't change this after initial discovery. Will cause problems. - "ID": { - //"1": "kube02", - //"2": "dell_md1220", - //"3": "outlet_3" - //.. - }, - // The "name" can be overridden, and customized for each outlet. - // This, represents the "display name", which is the human-readable, pretty version. - // By default, if not specified, this will be automatically generated from the labels configured via the PDU. - "Name": { - //"1": "Proxmox: Kube02", - //"2": "Dell: MD1220", - //"3": "Outlet 3 Friendly Name" - //.. - }, - - ///Allows enabling, or disabling specific devices. - // Disabled devices will not be discovered or published to MQTT. - "Enabled": { - "1": true, - "2": true, - "3": true - } - }, - "Measurements": { - // This allows customizing the entity IDs generated for metrics. - // All measurement entity_ids will start with the generated ID for the device they belong to. - // ie, {device_name}_power, device_energy - // Output Format will be {EntityID}_{MeasurementID} - // Both measurement ID, and Name, is based on the data type presented from the PDU. - // This will affect ALL devices and entities, using measurements. - // Note- changing the ID only customizes the suffix. - "ID": { - "apparentpower": null, - "realpower": "power", - "energy": null, - "powerFactor": null, - "current": null, - "voltage": null - }, - - //Customize the names for measurements. - //Output Format will be {Entity Name} {Measurement Name} - "Name": { - "apparentpower": "Apparent Power", - "realpower": "Power", - "energy": "Energy", - "powerFactor": "Power Factor", - "current": "Current", - "voltage": "Voltage" - }, - - //Allow enabling, or disabling publishing specific measurement types. - //This applies to all entity types. - "Enabled": { - "apparentpower": true, - "realpower": true, - "energy": true, - "powerFactor": true, - "current": true, - "voltage": true, - - // These are not enabled- because they don't quite map to anything useful. - "balance": false, - "currentCrestFactor": false - } - }, - - // Ignore this section for now... - "Actions": { - "Enabled": false, - "Username": "actionsUser", - "Password": "actionsPass" - }, - "HomeAssistant": { - "DiscoveryEnabled": true, - "DiscoveryTopic": "homeassistant/discovery", - "DiscoveryInterval": 300, - // Default expireAfter interval applied to all sensors. After this time- the sensor will be marked as unavailable. - "SensorExpireAfterSeconds": 300 } } diff --git a/rPDU2MQTT/config.defaults.yaml b/rPDU2MQTT/config.defaults.yaml new file mode 100644 index 0000000..530fffc --- /dev/null +++ b/rPDU2MQTT/config.defaults.yaml @@ -0,0 +1,145 @@ +Mqtt: + # Optional Credentials. + Credentials: + # Add your MQTT Username and Password here. + Username: "user" + Password: "password" + + # This will be the parent topic in MQTT, where keys are published to. + ParentTopic: "Rack_PDU" + + # This is the client-ID used. + ClientID: "rpdu2mqtt" + + KeepAlive: 60 + + # Connection details for MQTT server + Connection: + # Set this to the host / ip of your MQTT server. + Host: "localhost" + + # Set this to the port of your MQTT server + Port: 1883 + + # Timeout (in seconds) for connection. + Timeout: 15 + +Pdu: + # (Required) Connection details for MQTT server + Connection: + # Sets Scheme used. + # If, not provided, will use http for port 80, and https for port 443. Otherwise, will default to http + Scheme: http / https + + # Set this to the host / ip of your PDU + Host: "localhost" + + # Set this to the port of your PDU + Port: 1883 + + # Timeout (in seconds) for requests. + Timeout: 15 + + # (Optional) Credentials to connect to MQTT server + Credentials: + Username: "actionsUser" + Password: "actionsPass" + + # This is how often sensors will be published to MQTT (in seconds) + PollInterval: 5 + + # Configuration needed to allow write-actions. + # Aka- this enables the ability to toggle switches, or alter configuration. + # If this is false, rPDU2MQTT won't "change" anything on the PDU. + # (this- includes changing the status of a switch, aka, turning it on or off) + # This, defaults to false. + ActionsEnabled: true + + +# This section allows overriding the generated entity_id, name, and enabled/disabled, for various objects. +# Note- ID / entity_id, will only be set when the entity is created. +# Do not touch or change it through here after it has been created! Otherwise, you can end up with duplicates, or other issues. +# "Name" can be freely updated, and home assistant will reflect updated names instantly (after the discovery runs) + +# Device / Entity Hierarchy: +# PDU > Devices > [Entities, Outlets] > Measurements +Overrides: + # Override details about the PDU itself. + PDU: + ID: null, + Name: "Your-PDU" + # Not- sure why you would want to set this to false... But, the option exists.. + Enabled: true + + # Override details regarding devices exposed by the PDU. + Devices: + # Place your serial number here as the key. + A0AE260C851900C3: + ID: + Name: + Enabled: true + + YOUR-SERIAL-NUMBER: + ID: + Name: + Enabled: true + + # Customize individual outlets. + # These are number-based, start at 1. + # "Outlet 1" in the PDU corresponds to 1, for example. + # Note- You can set the Label field in the PDU itself, instead of setting the Name here, as the default "Name" is the Label value from the PDU. + Outlets: + 1: + ID: kube02 + Name: "Proxmox: Kube02" + Enabled: true + 2: + ID: dell_md1220 + Name: "Dell: MD1220" + Enabled: true + + # Empty record. + 3: + ID: + Name: + Enabled: + + # Customize how metrics are sent to services. + # The entity ID used for metrics, is [DEVICE_ID]_[METRIC_TYPE] + # The "ID" field here, will be used instead of "METRIC_TYPE" + # The "Name" field here, maps back to a "Human Readable Version" + # These overrides affect measurements from all entity types (PDU, Devices, Entities, Outlets, etc...) + Measurements: + apparentPower: + ID: + Name: Apparent Power + Enabled: true + realPower: + ID: power + Name: Power + Enabled: true + energy: + ID: energy + Name: Energy + Enabled: true + powerFactor: + Name: Power Factor + Enabled: true + current: + Name: Current + Enabled: true + voltage: + Name: Voltage + Enabled: true + # These measurements, didn't seem very useful, and don't map back to home assistant very well... + currentCrestFactor: + Enabled: false + balance: + Enabled: false + +HomeAssistant: + DiscoveryEnabled: true + DiscoveryTopic: "homeassistant/discovery" + DiscoveryInterval: 300 + # Default expireAfter interval applied to all sensors. After this time- the sensor will be marked as unavailable. + SensorExpireAfterSeconds: 300 diff --git a/rPDU2MQTT/rPDU2MQTT.csproj b/rPDU2MQTT/rPDU2MQTT.csproj index 0f85f1f..ae3004b 100644 --- a/rPDU2MQTT/rPDU2MQTT.csproj +++ b/rPDU2MQTT/rPDU2MQTT.csproj @@ -18,13 +18,14 @@ + - + Always - + Always