Skip to content

Commit

Permalink
Round 2 (#24)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
XtremeOwnageDotCom authored Sep 1, 2024
1 parent 9f542b1 commit 7dfe106
Show file tree
Hide file tree
Showing 37 changed files with 841 additions and 633 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -363,3 +363,4 @@ MigrationBackup/
FodyWeavers.xsd

/rPDU2MQTT/appsettings.Development.json
/rPDU2MQTT/config.yaml
27 changes: 0 additions & 27 deletions rPDU2MQTT/Classes/Config.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

namespace rPDU2MQTT.Classes;

public class ServiceDependancies
/// <summary>
/// Just a helper class, to reduce the constructor size when passing these dependanceies.
/// </summary>
public class MQTTServiceDependancies
{
public ServiceDependancies(IHiveMQClient mqtt, Config cfg, PDU pdu)
public MQTTServiceDependancies(IHiveMQClient mqtt, Config cfg, PDU pdu)
{
Mqtt = mqtt;
Cfg = cfg;
Expand Down
102 changes: 56 additions & 46 deletions rPDU2MQTT/Classes/PDU.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -33,79 +34,88 @@ public async Task<RootData> 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<string, Device> devices)
private async Task processRecursive<TEntity, TParent>([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<T>(Dictionary<string, T> 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<string, Measurement, string> 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<T>(Dictionary<string, T> measurements) where T : Measurement
private async Task processRecursive<TKey, TEntity, TParent>(Dictionary<TKey, TEntity> 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);
}
}
166 changes: 79 additions & 87 deletions rPDU2MQTT/Extensions/EntityWithName_Overrides.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// This method will both apply any overides specified for entity_id, and name. And, it will return if this entity is enabled or not.
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <typeparam name="Key"></typeparam>
/// <param name="entity"></param>
/// <param name="key"></param>
/// <param name="Overrides"></param>
/// <returns></returns>
public static void ApplyOverrides<TEntity>(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;
}

/// <summary>
/// This calculates the name of an entity, based on a collection of overrides.
/// Sets the <see cref="NamedEntity.Entity_Name"/>, <see cref="NamedEntity.Entity_DisplayName"/>, and <see cref="BaseEntity.Entity_Enabled"/> properties
/// based on the provided <paramref name="overrides"/> or default functions.
/// </summary>
/// <remarks>
/// Suppose you could always do String1 ?? String2 ?? String3 ?? "Default.
/// But- this was funner. also- this checks for empty/whitespace strings.
/// </remarks>
/// <param name="entity"></param>
/// <param name="Key"></param>
/// <param name="Overrides"></param>
public static string GetOverrideOrDefault<T>(this T entity, string? key, Dictionary<string, string> Overrides, string? DefaultValue = null, bool FormatAsName = false)
where T : NamedEntity
/// <typeparam name="TKey">The type of the key used in the dictionary of entities.</typeparam>
/// <typeparam name="TEntity">The type of the entity in the dictionary, which must be a <see cref="NamedEntity"/>.</typeparam>
/// <param name="entities">The dictionary of entities to update.</param>
/// <param name="overrides">The overrides object that may contain specific override values for the entities.</param>
/// <param name="DefaultNameFunc">A function that provides the default name for an entity based on the key and the entity itself.</param>
/// <param name="DefaultDisplayNameFunc">A function that provides the default display name for an entity based on the key and the entity itself. If null, it defaults to <paramref name="DefaultNameFunc"/>.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="DefaultNameFunc"/> is null.</exception>
/// <exception cref="Exception">Thrown when unable to determine the entity ID or name, or if any other unexpected error occurs during processing.</exception>
public static void SetEntityNameAndEnabled<TKey, TEntity>([DisallowNull] this Dictionary<TKey, TEntity> entities, [DisallowNull] Overrides overrides, [DisallowNull] Func<TKey, TEntity, string> DefaultNameFunc, [DisallowNull] Func<TKey, TEntity, string> 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;
}
}
}

/// <summary>
/// Multi-type lookup. Does case-insensitive compare for strings.
/// Prune disabled items from dictionary.
/// </summary>
/// <typeparam name="TKey"></typeparam>
/// <param name="dictionary"></param>
/// <param name="key"></param>
/// <param name="Result"></param>
/// <returns></returns>
private static bool tryGetValue<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, TKey? key, out TValue Result, TValue DefaultValue)
where TKey : notnull
/// <typeparam name="TEntity"></typeparam>
/// <param name="entities"></param>
public static void PruneDisabled<TKey, TEntity>([DisallowNull] this Dictionary<TKey, TEntity> entities)
where TKey : notnull
where TEntity : notnull, NamedEntity
{
if (key is null)
{
Result = DefaultValue;
return false;
}
if (key is string sKey && dictionary is Dictionary<string, TValue> stringDictionary)
return caseInsensitiveLookup<TValue>(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<TValue>(this Dictionary<string, TValue> Dictionary, string Key, out TValue Result, TValue DefaultValue)
/// <summary>
/// Set a prefix for all contained entities. (This- is used to prefix measurements, with the parent's ID.)
/// </summary>
/// <typeparam name="TKey"></typeparam>
/// <typeparam name="TEntity"></typeparam>
/// <param name="entities"></param>
/// <param name="Prefix"></param>
public static void SetEntityNamePrefix<TKey, TEntity>([DisallowNull] this Dictionary<TKey, TEntity> 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}";
}


/// <summary>
/// Sets all properties of <see cref="IMQTTKey"/>.
/// </summary>
/// <remarks>
/// <see cref="IMQTTKey.Record_Key"/> is set to key from device.
/// </remarks>
/// <typeparam name="T"></typeparam>
/// <typeparam name="TEntity"></typeparam>
/// <param name="Items"></param>
/// <param name="Parent"></param>
public static void SetParentAndIdentifier<T>(this Dictionary<string, T> Items, IMQTTKey Parent) where T : BaseEntity
public static void SetParentAndIdentifier<TKey, TEntity>(this Dictionary<TKey, TEntity> Items, IMQTTKey Parent, [DisallowNull] Func<TKey, TEntity, string>? 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);
}
}

Expand Down
Loading

0 comments on commit 7dfe106

Please sign in to comment.