diff --git a/rPDU2MQTT/Classes/Config.cs b/rPDU2MQTT/Classes/Config.cs index 96988b6..f9e2d7f 100644 --- a/rPDU2MQTT/Classes/Config.cs +++ b/rPDU2MQTT/Classes/Config.cs @@ -5,12 +5,14 @@ namespace rPDU2MQTT.Classes; public class Config { - public Config(IOptionsSnapshot MQTT, IOptionsSnapshot PDU, IOptionsSnapshot HASS, IOptionsSnapshot Overrides) + 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; } @@ -18,4 +20,8 @@ public Config(IOptionsSnapshot MQTT, IOptionsSnapshot PDU public HomeAssistantConfig HASS { get; } public Overrides Overrides { get; } + + public OutletOverrides Outlets { get; } + + public MeasurementOverrides Measurements { get; } } diff --git a/rPDU2MQTT/Classes/PDU.cs b/rPDU2MQTT/Classes/PDU.cs index 02c3fde..9e289c0 100644 --- a/rPDU2MQTT/Classes/PDU.cs +++ b/rPDU2MQTT/Classes/PDU.cs @@ -56,6 +56,9 @@ private void processDevices(Dictionary devices) { foreach (var (key, device) in devices) { + // 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)); @@ -72,8 +75,7 @@ private void processChildDevice(Dictionary entities) where T : Nam 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.Entity_Name = o.GetOverrideOrDefault(k, config.Overrides.OutletID, FormatName: true); - entity.Entity_DisplayName = o.GetOverrideOrDefault(k, config.Overrides.OutletName, FormatName: false); + entity.ApplyOverrides(k.ToString(), config.Outlets); } else { @@ -81,6 +83,9 @@ private void processChildDevice(Dictionary entities) where T : Nam entity.Entity_DisplayName = (entity.Label ?? entity.Name); } + // Remove any disabled measurements. + config.Measurements.RemoveDisabledRecords(entity.Measurements, o => o.Type); + // All measurements will be stored into a sub-key. entity.Measurements.SetParentAndIdentifier(BaseEntity.FromDevice(entity, MqttPath.Measurements)); @@ -95,10 +100,10 @@ private void processMeasurements(Dictionary measurements) where T { // 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); - var suffix = entity.GetOverrideOrDefault(entity.Type, config.Overrides.MeasurementID, entity.Type, true); - entity.Entity_Name = entity.GetEntityName(suffix); - entity.Entity_DisplayName = entity.GetOverrideOrDefault(entity.Type, config.Overrides.MeasurementName, entity.Type, false); + //SInce- ApplyNameOverridesReturnIsValid already set the EntityName, we are just going to append a suffix to it. + entity.Entity_Name = entity.GetEntityName(entity.Entity_Name); } } } diff --git a/rPDU2MQTT/Extensions/EntityWithName_Overrides.cs b/rPDU2MQTT/Extensions/EntityWithName_Overrides.cs index cb8063c..61e9ad5 100644 --- a/rPDU2MQTT/Extensions/EntityWithName_Overrides.cs +++ b/rPDU2MQTT/Extensions/EntityWithName_Overrides.cs @@ -1,4 +1,6 @@ using rPDU2MQTT.Helpers; +using rPDU2MQTT.Interfaces; +using rPDU2MQTT.Models.Config; using rPDU2MQTT.Models.PDU.basePDU; using rPDU2MQTT.Models.PDU.DummyDevices; @@ -6,6 +8,23 @@ 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. /// @@ -16,21 +35,23 @@ public static class EntityWithName_Overrides /// /// /// - public static string GetOverrideOrDefault(this T entity, Key? key, Dictionary Overrides, string Default = null, bool FormatName = false) - where Key : notnull + public static string GetOverrideOrDefault(this T entity, string? key, Dictionary Overrides, string? DefaultValue = null, bool FormatAsName = false) where T : NamedEntity { - string formatIfNeeded(string input) => FormatName switch + string formatIfNeeded(string input) => FormatAsName switch { true => input.FormatName(), false => input }; - if (tryGetValue(Overrides, key, out string val)) - return formatIfNeeded(val); + if (string.IsNullOrEmpty(key)) + throw new NullReferenceException("Key is null"); - if (Default is not null) - return formatIfNeeded(Default); + if (Overrides.TryGetValue(key, out string overrideValue)) + return formatIfNeeded(overrideValue); + + if (!string.IsNullOrEmpty(DefaultValue)) + return formatIfNeeded(DefaultValue); if (entity is EntityWithNameAndLabel entityWithNameAndLabel) return formatIfNeeded(entityWithNameAndLabel.Label ?? entityWithNameAndLabel.Name); @@ -42,21 +63,21 @@ public static string GetOverrideOrDefault(this T entity, Key? key, Dicti /// /// Multi-type lookup. Does case-insensitive compare for strings. /// - /// + /// /// /// /// /// - private static bool tryGetValue(this Dictionary dictionary, T? key, out string Result) - where T : notnull + private static bool tryGetValue(this Dictionary dictionary, TKey? key, out TValue Result, TValue DefaultValue) + where TKey : notnull { if (key is null) { - Result = string.Empty; + Result = DefaultValue; return false; } - if (key is string sKey && dictionary is Dictionary stringDictionary) - return tryGetStringValue(stringDictionary, sKey, out Result); + if (key is string sKey && dictionary is Dictionary stringDictionary) + return caseInsensitiveLookup(stringDictionary, sKey, out Result, DefaultValue); if (dictionary.ContainsKey(key)) { @@ -64,22 +85,46 @@ private static bool tryGetValue(this Dictionary dictionary, T? key return true; } - Result = string.Empty; + Result = DefaultValue; return false; } - private static bool tryGetStringValue(this Dictionary Dictionary, string Key, out string Result) + private static bool caseInsensitiveLookup(this Dictionary Dictionary, string Key, out TValue Result, TValue DefaultValue) { var match = Dictionary.Keys.FirstOrDefault(o => string.Equals(o, Key, StringComparison.OrdinalIgnoreCase)); if (match is null) { - Result = string.Empty; + Result = DefaultValue; return false; } Result = Dictionary[match]; - return !string.IsNullOrWhiteSpace(Result); + + if (Result is null) + return false; + if (Result is string s) + return !string.IsNullOrWhiteSpace(s); + return Result is not null; + } + + /// + /// Sets all properties of . + /// + /// + /// is set to key from device. + /// + /// + /// + /// + public static void SetParentAndIdentifier(this Dictionary Items, IMQTTKey Parent) where T : BaseEntity + { + foreach (var (key, item) in Items) + { + item.Record_Parent = Parent; + item.Record_Key = key; + item.Entity_Identifier = Parent.CreateChildIdentifier(key); + } } } \ No newline at end of file diff --git a/rPDU2MQTT/Extensions/IMQTTKeyExtensions.cs b/rPDU2MQTT/Extensions/IMQTTKeyExtensions.cs index f2e3c59..c791348 100644 --- a/rPDU2MQTT/Extensions/IMQTTKeyExtensions.cs +++ b/rPDU2MQTT/Extensions/IMQTTKeyExtensions.cs @@ -84,22 +84,4 @@ string getObjectID(IMQTTKey? cur) return result.FormatName(); } - /// - /// Sets all properties of . - /// - /// - /// is set to key from device. - /// - /// - /// - /// - public static void SetParentAndIdentifier(this Dictionary Items, IMQTTKey Parent) where T : BaseEntity - { - foreach (var (key, item) in Items) - { - item.Record_Parent = Parent; - item.Record_Key = key; - item.Entity_Identifier = Parent.CreateChildIdentifier(key); - } - } } diff --git a/rPDU2MQTT/Models/Config/Overrides.cs b/rPDU2MQTT/Models/Config/Overrides.cs index 58c8cdc..be07319 100644 --- a/rPDU2MQTT/Models/Config/Overrides.cs +++ b/rPDU2MQTT/Models/Config/Overrides.cs @@ -1,4 +1,6 @@ -namespace rPDU2MQTT.Models.Config; +using System.Text.Json.Serialization; + +namespace rPDU2MQTT.Models.Config; public class Overrides { @@ -11,30 +13,14 @@ public class Overrides /// Allows overriding the generated entity name for the PDU. /// public string? PduName { get; set; } = null; +} - /// - /// Allows overriding the generated "name" for each outlet. - /// - /// - /// This maps to , ie, "object_id" - /// - public Dictionary OutletID { get; set; } = new(); - - /// - /// Allows overriding the "Display Name" for each outlet. - /// - /// - /// This maps to , ie, "name" - /// - public Dictionary OutletName { get; set; } = new(); - - /// - /// Allows overriding the generated Entity ID for measurements. - /// - public Dictionary MeasurementID { get; set; } = new(); +/// +/// Defines overrides for measurements. +/// +public class MeasurementOverrides : TypeOverride { } - /// - /// Allows overriding the generated Entity Name for measurements. - /// - public Dictionary MeasurementName { get; set; } = new(); -} +/// +/// Defines overrides for outlets. +/// +public class OutletOverrides : TypeOverride { } diff --git a/rPDU2MQTT/Models/Config/TypeOverride.cs b/rPDU2MQTT/Models/Config/TypeOverride.cs new file mode 100644 index 0000000..47773bc --- /dev/null +++ b/rPDU2MQTT/Models/Config/TypeOverride.cs @@ -0,0 +1,56 @@ +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/Converters/CaseInsensitiveDictionaryConverter.cs b/rPDU2MQTT/Models/Converters/CaseInsensitiveDictionaryConverter.cs new file mode 100644 index 0000000..0fa06f5 --- /dev/null +++ b/rPDU2MQTT/Models/Converters/CaseInsensitiveDictionaryConverter.cs @@ -0,0 +1,18 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace rPDU2MQTT.Models.Converters; + +public sealed class CaseInsensitiveDictionaryConverter : JsonConverter> +{ + public override Dictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var newDictionary = (Dictionary)JsonSerializer.Deserialize(ref reader, typeToConvert, options); + return new Dictionary(newDictionary, StringComparer.OrdinalIgnoreCase); + } + + public override void Write(Utf8JsonWriter writer, Dictionary value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value, value.GetType(), options); + } +} diff --git a/rPDU2MQTT/Models/PDU/basePDU/BaseEntity.cs b/rPDU2MQTT/Models/PDU/basePDU/BaseEntity.cs index da12b29..4cd98b2 100644 --- a/rPDU2MQTT/Models/PDU/basePDU/BaseEntity.cs +++ b/rPDU2MQTT/Models/PDU/basePDU/BaseEntity.cs @@ -20,8 +20,6 @@ public static DummyEntity FromDevice(IMQTTKey Parent, MqttPath Path) Entity_Identifier = Parent.CreateChildIdentifier(Path.ToJsonString()) }; } - #region IMQTTKey - /// /// /// This should only bet set by @@ -42,6 +40,11 @@ public static DummyEntity FromDevice(IMQTTKey Parent, MqttPath Path) /// [JsonIgnore] public IMQTTKey? Record_Parent { get; set; } - #endregion -} \ No newline at end of file + /// + /// Determine if this entity is enabled, and should be used. + /// + [JsonIgnore] + public bool Entity_Enabled { get; set; } = true; + +} diff --git a/rPDU2MQTT/Program.cs b/rPDU2MQTT/Program.cs index def42b2..5833727 100644 --- a/rPDU2MQTT/Program.cs +++ b/rPDU2MQTT/Program.cs @@ -23,6 +23,8 @@ 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."); diff --git a/rPDU2MQTT/appsettings.json b/rPDU2MQTT/appsettings.json index b460d31..769f2ca 100644 --- a/rPDU2MQTT/appsettings.json +++ b/rPDU2MQTT/appsettings.json @@ -37,12 +37,13 @@ // 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", - + "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. - "OutletID": { + "ID": { //"1": "kube02", //"2": "dell_md1220", //"3": "outlet_3" @@ -51,13 +52,22 @@ // 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. - "OutletName": { + "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 @@ -65,7 +75,7 @@ // 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. - "MeasurementID": { + "ID": { "apparentpower": null, "realpower": "power", "energy": null, @@ -76,13 +86,28 @@ //Customize the names for measurements. //Output Format will be {Entity Name} {Measurement Name} - "MeasurementName": { + "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 useufl. + "balance": false, + "currentCrestFactor": false } }, "Actions": { @@ -91,7 +116,7 @@ "Password": "actionsPass" }, "HomeAssistant": { - "DiscoveryEnabled": false, + "DiscoveryEnabled": true, "DiscoveryTopic": "homeassistant/discovery", "DiscoveryInterval": 300, // Default expireAfter interval applied to all sensors. After this time- the sensor will be marked as unavailable.