Skip to content

Commit

Permalink
Add support for Flume devices.
Browse files Browse the repository at this point in the history
Also:

* Established a pattern to maintain specialized behavior for specific device types (see GenericDevice, FlumeDevice)
* Device attributes can now be accessed and used like a Dictionary by casting to IDictionary
* Refactored to consolidate behavior of single-device and all-device queries
* Other minor refactorings
  • Loading branch information
aholmes committed Sep 10, 2022
1 parent 584316c commit 1a9ce14
Show file tree
Hide file tree
Showing 5 changed files with 398 additions and 104 deletions.
138 changes: 130 additions & 8 deletions hubitat2prom/HubitatDevice/DeviceSummaryAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
using AttributeValue = OneOf.OneOf<string, string[], int?, double?, OneOf.OneOf<double, string>?>;
using System.Dynamic;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Collections;

namespace hubitat2prom.HubitatDevice;

Expand Down Expand Up @@ -37,7 +39,7 @@ namespace hubitat2prom.HubitatDevice;
/// </example>
/// </summary>
[Serializable]
public class DeviceSummaryAttributes : DynamicObject
public class DeviceSummaryAttributes : DynamicObject, IDictionary<string, AttributeValue?>
{
/// <summary>
/// Return the name and value of each property in this class instance.
Expand Down Expand Up @@ -65,13 +67,27 @@ public class DeviceSummaryAttributes : DynamicObject
}

private Lazy<Dictionary<string, PropertyInfo>> typedPropertiesLazy
=> new Lazy<Dictionary<string, PropertyInfo>>(
() => this.GetType().GetProperties(
BindingFlags.Public
| BindingFlags.Instance
| BindingFlags.DeclaredOnly
).ToDictionary(propertyInfo => propertyInfo.Name)
);
{
get
{
// https://stackoverflow.com/a/39244835
var interfaceMethods = this.GetType().GetInterfaces()
.Select(@interface => this.GetType().GetInterfaceMap(@interface))
.SelectMany(interfaceMapping => interfaceMapping.TargetMethods);

return new Lazy<Dictionary<string, PropertyInfo>>(
() => this.GetType().GetProperties(
BindingFlags.Public
| BindingFlags.Instance
| BindingFlags.DeclaredOnly
)
.Where(propertyInfo => !propertyInfo.GetAccessors(true).Any(
methodInfo => interfaceMethods.Contains(methodInfo))
)
.ToDictionary(propertyInfo => propertyInfo.Name)
);
}
}
private Dictionary<string, PropertyInfo> typedProperties => typedPropertiesLazy.Value;
private Lazy<Dictionary<string, object>> typedPropertiesAsObjectDictionary
=> new Lazy<Dictionary<string, object>>(() => typedProperties.ToDictionary(kvp => kvp.Key, kvp => (object)kvp.Value));
Expand Down Expand Up @@ -128,6 +144,112 @@ public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, o
return true;
}

public void Add(string key, AttributeValue? value)
{
if (typedProperties.ContainsKey(key))
{
try
{
value.Value.Switch(
@string => typedProperties[key].SetValue(this, @string),
stringArray => typedProperties[key].SetValue(this, stringArray),
@int => typedProperties[key].SetValue(this, @int),
@double => typedProperties[key].SetValue(this, @double),
oneOfDoubleString =>
{
var propertyType = typedProperties[key].PropertyType;
if (propertyType == typeof(double?) || propertyType == typeof(double))
{
typedProperties[key].SetValue(this, oneOfDoubleString.Value.AsT0);
}
else if (propertyType == typeof(string))
{
typedProperties[key].SetValue(this, oneOfDoubleString.Value.AsT1);
}
else
{
typedProperties[key].SetValue(this, oneOfDoubleString);
}
}
);
}
catch (Exception e)
{
System.Diagnostics.Debug.WriteLine(e.Message);
}
}
else
{
dynamicProperties[key] = value;
}
}
public bool ContainsKey(string key)
=> typedProperties.ContainsKey(key) || dynamicProperties.ContainsKey(key);

public bool Remove(string key)
=> throw new NotImplementedException();

public bool TryGetValue(string key, [MaybeNullWhen(false)] out AttributeValue? value)
{
if (typedProperties.ContainsKey(key))
{
value = (AttributeValue)typedProperties[key].GetValue(this);
return true;
}
else
{
var success = dynamicProperties.TryGetValue(key, out object attributeValue);
value = (AttributeValue)attributeValue;
return success;
}
}

public void Add(KeyValuePair<string, AttributeValue?> item)
{
Add(item.Key, item.Value);
}

public void Clear()
{
dynamicProperties.Clear();
}

public bool Contains(KeyValuePair<string, AttributeValue?> item)
=> dynamicProperties.Contains(new KeyValuePair<string, object>(item.Key, item.Value));

public void CopyTo(KeyValuePair<string, AttributeValue?>[] array, int arrayIndex)
=> throw new NotImplementedException();

public bool Remove(KeyValuePair<string, AttributeValue?> item)
=> throw new NotImplementedException();

IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}

public ICollection<string> Keys => this.properties.Select(o => o.Key).ToArray();

public ICollection<AttributeValue?> Values => this.Select(o => o.Value).ToArray();

public int Count => this.properties.Count();

public bool IsReadOnly => false;

public AttributeValue? this[string key]
{
get
{
if (!this.TryGetValue(key, out AttributeValue? value)) throw new KeyNotFoundException(key);
return value;

}
set
{
Add(key, value);
}
}

public string dataType { get; set; }
public string[] values { get; set; }
public string syncStatus { get; set; }
Expand Down
23 changes: 23 additions & 0 deletions hubitat2prom/HubitatDevice/DeviceTypes/DeviceType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using AttributeValue = OneOf.OneOf<string, string[], int?, double?, OneOf.OneOf<double, string>?>;
using hubitat2prom.HubitatDevice;

namespace hubitat2prom.PrometheusExporter.DeviceTypes;

public abstract class DeviceType
{
protected const double MISSING_VALUE_DEFAULT = 0d;

protected DeviceType() { }
public abstract double ExtractMetric(string attributeName, AttributeValue attributeValue);

public static DeviceType CreateDeviceType(DeviceSummary deviceSummary)
{
switch(deviceSummary.type)
{
case "Flume Device":
return new FlumeDevice();
default:
return new GenericDevice();
}
}
}
88 changes: 88 additions & 0 deletions hubitat2prom/HubitatDevice/DeviceTypes/FlumeDevice.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using AttributeValue = OneOf.OneOf<string, string[], int?, double?, OneOf.OneOf<double, string>?>;

namespace hubitat2prom.PrometheusExporter.DeviceTypes;


public class FlumeDevice: GenericDevice
{
public FlumeDevice() { }

public override double ExtractMetric(string attributeName, AttributeValue attributeValue)
{
// We can return if the value type is not T0 (string)
// because that is the only type this method extracts
if (!attributeValue.IsT0) return base.ExtractMetric(attributeName, attributeValue);
var attributeStringValue = attributeValue.AsT0;
double value;
switch (attributeName)
{
case "commstatus":
if (TryGetCommStatus(attributeStringValue, out value)) return value;
break;
case "flowstatus":
if (TryGetFlowStatus(attributeStringValue, out value)) return value;
break;
case "presence":
if (TryGetPresence(attributeStringValue, out value)) return value;
break;
case "water":
if (TryGetWater(attributeStringValue, out value)) return value;
break;
default:
System.Diagnostics.Debug.WriteLine($"Unknown attribute \"{attributeName}\" from Flume device.");
break;
}

return MISSING_VALUE_DEFAULT;
}

private static bool TryGetCommStatus(string commStatus, out double value)
{
switch (commStatus)
{
case "good": value = 0; return true;
case "unknown": value = 1; return true;
case "error": value = 2; return true;
}

value = -1;
return false;
}

private static bool TryGetFlowStatus(string flowStatus, out double value)
{
switch (flowStatus)
{
case "stopped": value = 0; return true;
case "running": value = 1; return true;
case "monitoring": value = 2; return true;
}

value = -1;
return false;
}

private static bool TryGetPresence(string presence, out double value)
{
switch (presence)
{
case "not present": value = 0; return true;
case "present": value = 1; return true;
}

value = -1;
return false;
}

private static bool TryGetWater(string water, out double value)
{
switch (water)
{
case "dry": value = 0; return true;
case "wet": value = 1; return true;
}

value = -1;
return false;
}
}
86 changes: 86 additions & 0 deletions hubitat2prom/HubitatDevice/DeviceTypes/GenericDevice.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using AttributeValue = OneOf.OneOf<string, string[], int?, double?, OneOf.OneOf<double, string>?>;

namespace hubitat2prom.PrometheusExporter.DeviceTypes;

public class GenericDevice: DeviceType
{
public GenericDevice() { }

public override double ExtractMetric(string attributeName, AttributeValue attributeValue)
{
double value;
var metricValue = attributeValue.Match(
@string =>
{
var name = attributeName.ToLowerInvariant();
if (name == "switch") return @string == "on" ? 1 : 0;
if (name == "power" && TryGetPower(@string, out value)) return value;
if (name == "thermostatoperatingstate" && TryGetThermostatOperatingState(@string, out value)) return value;
if (name == "thermostatmode" && TryGetThermostatMode(@string, out value)) return value;
return MISSING_VALUE_DEFAULT;
},
stringArray => MISSING_VALUE_DEFAULT,
nullableInt => nullableInt ?? MISSING_VALUE_DEFAULT,
nullableDouble => nullableDouble ?? MISSING_VALUE_DEFAULT,
nullableOneOfDoubleString => nullableOneOfDoubleString.HasValue
? nullableOneOfDoubleString.Value.Match(
@double => @double,
@string => MISSING_VALUE_DEFAULT
)
: MISSING_VALUE_DEFAULT
);

return metricValue;
}

private static bool TryGetPower(string power, out double value)
{
if (power == "off")
{
value = 0;
return true;
}

if (power == "on")
{
value = 1;
return true;
}

value = -1;
return false;
}

private static bool TryGetThermostatOperatingState(string thermostatOperatingState, out double value)
{
switch (thermostatOperatingState)
{
case "heating": value = 0; return true;
case "pending cool": value = 1; return true;
case "pending heat": value = 2; return true;
case "vent economizer": value = 3; return true;
case "idle": value = 4; return true;
case "cooling": value = 5; return true;
case "fan only": value = 6; return true;
}

value = -1;
return false;
}

private static bool TryGetThermostatMode(string thermostatMode, out double value)
{
switch (thermostatMode)
{
case "auto": value = 0; return true;
case "off": value = 1; return true;
case "heat": value = 2; return true;
case "emergency heat": value = 3; return true;
case "cool": value = 4; return true;
}

value = -1;
return false;
}
}
Loading

0 comments on commit 1a9ce14

Please sign in to comment.