Innovenergy_trunk/csharp/lib/Victron/VeDBus/VeDBus.cs

234 lines
9.5 KiB
C#
Raw Normal View History

2023-02-16 12:57:06 +00:00
using System.Collections.Immutable;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using InnovEnergy.Lib.Protocols.DBus;
using InnovEnergy.Lib.Protocols.DBus.Daemon;
using InnovEnergy.Lib.Protocols.DBus.Protocol.DataTypes;
using InnovEnergy.Lib.Utils;
namespace InnovEnergy.Lib.Victron.VeDBus;
using VeProps = ImmutableDictionary<String, VeProperty>;
public record struct ServiceProperties(String ServiceName, VeProps Properties);
public static class VeDBus
{
public static IObservable<VeProps> ObservePropertiesOfService(this DBusConnection connection,
String serviceName)
{
return connection
.ObservePropertiesOfServices(busName => busName == serviceName)
.SelectMany(s => s.Take(1))
.Select(s => s.Properties);
}
public static IObservable<IReadOnlyList<ServiceProperties>> ObservePropertiesOfServices(this DBusConnection connection,
Func<String, Boolean> serviceSelector)
{
// subscription management could probably be done using a combination of GroupBy and Switch.
// however I dont understand GroupBy :(
var services = new Dictionary<String, (IObservable<VeProps> props, IDisposable subscription)>();
return connection
.Services
.Where(c => serviceSelector(c.BusNameOrId))
.Where(c => c.Change != ServiceChange.NoChange)
.Select(ObserveProps)
.Switch()
.OnDispose(DisposeSubscriptions);
IObservable<IReadOnlyList<ServiceProperties>> ObserveProps(ServiceChanged serviceChanged)
{
var (change, serviceName, _, _) = serviceChanged;
lock (services)
{
if (services.TryGetValue(serviceName, out var sv))
{
sv.subscription.Dispose();
services.Remove(serviceName);
}
if (change is ServiceChange.BusNameAdded or ServiceChange.IdAdded)
{
var props = connection
.ObserveAllProperties(serviceName)
.Replay(1);
services.Add(serviceName, (props, props.Connect()));
}
var observeProps = services.Select(kv => kv.Value.props).ToList(); // ToList is important!
var serviceNames = services.Select(kv => kv.Key).ToList(); // ToList is important!
return Observable
.CombineLatest(observeProps)
.Select(props => Enumerable
.Zip(serviceNames, props, (s, ps) => new ServiceProperties(s, ps))
.ToList());
}
}
void DisposeSubscriptions()
{
lock (services)
{
foreach (var service in services.Values)
service.subscription.Dispose();
services.Clear();
}
}
}
public static IObservable<VeProperty> ObservePropertiesChanged(this DBusConnection connection,
String service ,
ObjectPath? objectPath = default)
{
// some VeServices dont set the interface (!) so don't use it in the filter
return connection.ObserveSignalMessages(sender: service,
objectPath: objectPath,
member: VeDBusApi.PropertiesChanged)
.TrySelect(VeProperty.Decode);
}
private static IObservable<VeProps> ObserveAllProperties(this DBusConnection connection,
String serviceName)
{
var init = connection
.GetAllProperties(serviceName)
.Catch(Observable.Empty<VeProperty>())
.Aggregate(VeProps.Empty, UpdateItem); // produces a single aggregated dict
return init
.Select(ObserveChanges)
.Switch();
VeProps UpdateItem(VeProps props, VeProperty prop) => props.SetItem(prop.ObjectPath, prop);
IObservable<VeProps> ObserveChanges(VeProps initialProps)
{
return connection
.ObservePropertiesChanged(serviceName)
.Scan(initialProps, UpdateItem)
.StartWith(initialProps); // not sure if necessary
}
}
public static IObservable<VeProperty> GetAllProperties(this DBusConnection connection, String serviceName)
{
var texts = connection.GetAllTexts(serviceName) .ToObservable();
var values = connection.GetAllValues(serviceName).ToObservable();
return Observable
.Zip(values, texts, Merge)
.SelectMany(ps => ps);
static IEnumerable<VeProperty> Merge(IReadOnlyDictionary<String, Object> values,
IReadOnlyDictionary<String, String> texts)
{
return from kv in values
let path = kv.Key
let value = kv.Value
let text = texts.TryGetValue(path, out var txt) ? txt : value.ToString()
select new VeProperty(path, value, text);
}
}
public static async Task<IReadOnlyDictionary<String, Object>> GetAllValues(this DBusConnection connection, String destination)
{
var result = await connection.CallMethod<Object>(destination: destination,
@interface: VeDBusApi.Interface,
objectPath: ObjectPath.Root,
member: VeDBusApi.GetValue);
while (result is Variant v)
result = v.Value;
return result is IReadOnlyDictionary<String, Variant> variantDict
? variantDict.ToDictionary(kv => kv.Key, kv => kv.Value.Value)
: new Dictionary<String, Object>();
}
public static async Task<IReadOnlyDictionary<String, String>> GetAllTexts(this DBusConnection connection, String destination)
{
var result = await connection.CallMethod<Object>(destination: destination,
@interface: VeDBusApi.Interface,
objectPath: ObjectPath.Root,
member: VeDBusApi.GetText);
while (result is Variant v)
result = v.Value;
return result as IReadOnlyDictionary<String, String> ?? new Dictionary<String, String>();
}
public static async Task<T> GetValue<T>(this DBusConnection connection, String destination, ObjectPath objectPath)
{
var value = await connection.GetValue(destination, objectPath);
return (T)value;
}
public static async Task<Object> GetValue(this DBusConnection connection, String destination, ObjectPath objectPath)
{
var variant = await connection.CallMethod<Variant>(destination: destination,
@interface: VeDBusApi.Interface,
objectPath: objectPath,
member: VeDBusApi.GetValue);
return variant.Value;
}
public static Task<String> GetText(this DBusConnection connection, String destination, ObjectPath objectPath)
{
return connection.CallMethod<String>(destination: destination,
@interface: VeDBusApi.Interface,
objectPath: objectPath,
member: VeDBusApi.GetText);
}
public static async Task<Boolean> SetValue<T>(this DBusConnection connection, String destination, ObjectPath objectPath, T value)
{
var setValue = await connection.CallMethod<Int32>(destination: destination,
@interface: VeDBusApi.Interface,
objectPath: objectPath,
member: VeDBusApi.SetValue,
payload: value!.Variant());
return setValue == 0;
}
public static void BroadcastPropertiesChanged(this DBusConnection connection, ObjectPath path, Object value, String text)
{
var veProp = new VeProperty(path, value, text);
connection.BroadcastPropertiesChanged(veProp);
}
public static void BroadcastPropertiesChanged(this DBusConnection connection, VeProperty property)
{
var veProp = new Dictionary<String, Variant>
{
{ "Value", property.Value.Variant() },
{ "Text" , property.Text.Variant() }
};
connection.BroadcastSignal(VeDBusApi.Interface,
property.ObjectPath,
VeDBusApi.PropertiesChanged,
veProp);
}
}