Innovenergy_trunk/csharp/app/VenusLogger/Program.cs

272 lines
10 KiB
C#

using System.Collections.Immutable;
using System.Globalization;
using System.Net.Sockets;
using System.Reactive.Linq;
using Amazon;
using Amazon.S3;
using Amazon.S3.Model;
using Google.Protobuf;
using InnovEnergy.Lib.Protocols.DBus;
using InnovEnergy.Lib.Protocols.DBus.Transport;
using InnovEnergy.Lib.Utils;
using InnovEnergy.Lib.Victron.VeDBus;
using InnovEnergy.VenusLogger.Parsers;
using InnovEnergy.WireFormat;
using InnovEnergy.WireFormat.VictronV1;
using CodedOutputStream = Google.Protobuf.CodedOutputStream;
namespace InnovEnergy.VenusLogger;
using Props = ImmutableDictionary<String, VeProperty>;
using Services = IReadOnlyList<ServiceProperties>;
public static class Program
{
const Int32 SamplePeriodSecs = 2;
// rm -f $(pwd)/remote_dbus.sock ; ssh -nNT -L $(pwd)/remote_dbus.sock:/var/run/dbus/system_bus_socket root@10.2.1.6
public static void Main()
{
Console.WriteLine("starting...");
var serviceUrl = "https://sos-ch-dk-2.exo.io/";
var keyName = "EXO5ead8a3e6a908014025edc5c";
var secret = "xhCJ6gth-MS8ED4Qij3P7K12TfmkiqncCg70HxSGAe0";
var bucket = "graber-zurich";
var dispatch = CreateS3Dispatcher(serviceUrl, keyName, secret, bucket, SamplePeriodSecs);
var ep = new UnixDomainSocketEndPoint("/home/eef/remote_dbus.sock");
var auth = AuthenticationMethod.ExternalAsRoot();
var bus = new Bus(ep, auth);
var dbus = new DBusConnection(bus);
var batteries = dbus.ObserveVeService(VeService.IsBatteryServiceName);
var inverter = dbus.ObserveVeService(VeService.IsInverterServiceName);
var pvInverters = dbus.ObserveVeService(VeService.IsPvInverterServiceName);
var mppts = dbus.ObserveVeService(VeService.IsSolarChargerServiceName);
var grid = dbus.ObserveVeService(VeService.IsGridServiceName);
var generator = dbus.ObserveVeService(VeService.IsGeneratorServiceName);
var settings = dbus.ObserveVeService(VeService.IsSettingsServiceName);
var services = Observable
.CombineLatest(batteries, inverter, pvInverters, mppts, grid, generator, settings)
.Select(EnumerableUtils.Flatten)
.Select(Enumerable.ToList)
.OfType<Services>();
var interval = TimeSpan.FromSeconds(SamplePeriodSecs);
Console.WriteLine("start sampling");
services.Sample(interval)
.Select(ParsePayload)
.Skip(1)
.SelectMany(dispatch)
.Wait();
}
private static IObservable<IReadOnlyList<ServiceProperties>> ObserveVeService(this DBusConnection dbus,
Func<String, Boolean> selector)
{
return dbus
.ObservePropertiesOfServices(selector)
.StartWith(Array.Empty<ServiceProperties>());
}
private static String GetInfo(PutObjectResponse response)
{
return DateTime
.Now
.ToUniversalTime()
.ToString(CultureInfo.CurrentCulture.DateTimeFormat.SortableDateTimePattern)
.Replace('T', ' ')
.Replace('-', '/') + " " + response.HttpStatusCode;
}
private static Func<IMessage, Task<PutObjectResponse>> CreateS3Dispatcher(String serviceUrl,
String keyName,
String secret,
String bucket,
Int32 timeResolutionSecs = 2)
{
var buffer = new Byte[1024];
var config = new AmazonS3Config
{
RegionEndpoint = RegionEndpoint.EUWest3, // can be whatever
ServiceURL = serviceUrl
};
var s3Client = new AmazonS3Client(keyName, secret, config);
return Dispatch;
Task<PutObjectResponse> Dispatch(IMessage payload)
{
var now = DateTimeOffset.UtcNow;
var timestamp = now.ToUnixTimeSeconds() / timeResolutionSecs * timeResolutionSecs;
var request = new PutObjectRequest
{
BucketName = bucket,
Key = timestamp.ToString(),
ContentType = "application/octet-stream",
InputStream = WriteToStream(payload),
AutoCloseStream = false
};
return s3Client.PutObjectAsync(request);
MemoryStream WriteToStream(IMessage data)
{
try
{
var outputStream = new CodedOutputStream(buffer);
data.WriteTo(outputStream);
outputStream.Flush();
Console.WriteLine($"{now}: Writing {timestamp}: {outputStream.Position} bytes");
return new MemoryStream(buffer, 0, outputStream.Position.ConvertTo<Int32>());
}
catch (CodedOutputStream.OutOfSpaceException)
{
buffer = new Byte[buffer.Length * 2];
return WriteToStream(data);
}
}
}
}
public static Payload ParsePayload(Services services)
{
var grid = services.GetGrid();
var generator = services.GetGenerator(); // TODO: generator
var battery = services.GetBattery();
var pvOnDc = services.GetPvCharger();
var pvOnAcIn = services.GetPvInverter(AcBusType.AcIn1); // TODO: acIn2
var pvOnAcOut = services.GetPvInverter(AcBusType.AcOut);
var settings = services.GetSettings();
var (inverterAcIn, inverterAcOut, inverterDc) = services.GetInverterBusses();
var acInBus = CreateAcInBus(grid, pvOnAcIn, inverterAcIn);
var acOutBus = CreateAcOutBus(settings, pvOnAcOut, inverterAcOut, acInBus);
var dcBus = CreateDcBus(settings, pvOnDc, inverterDc, battery);
var losses = CalcInverterLosses(acOutBus, dcBus);
var inverter = new Device
{
Type = DeviceType.Inverter,
Devices = { acOutBus, dcBus, losses }
};
//Console.WriteLine(inverter);
return new Payload { VictronTopologyV1 = new VictronTopologyV1 { Root = inverter } };
}
private static Device CalcInverterLosses(Maybe<Device> acBus, Maybe<Device> dcBus)
{
var acPower = acBus.SelectMany(ac => ac.Phases).Sum(p => p.Power);
var dcPower = -dcBus.SelectMany(dc => dc.Phases).Sum(p => p.Power);
//var losses = Math.Max(0, acPower - dcPower);
var losses = dcPower - acPower;
return new Device
{
Type = DeviceType.Losses,
Phases = { new Phase { Power = losses } }
};
}
private static String Debug(Maybe<Device> device)
{
var phases = device.SelectMany(d => d.Phases);
var p = phases.Sum(p => p.Power) + "W ";
return phases
.Select(p => " | " + p.Power.ToString().PadRight(6).Substring(0, 6) + "W")
.Aggregate(p.PadRight(20) ,(a, b) => a + b);
}
private static Maybe<Device> CreateAcInBus(Maybe<Device> grid, Maybe<Device> pvOnAcIn, Maybe<Device> inverterAcIn)
{
if (!grid.HasValue && !pvOnAcIn.HasValue)
return null;
var loadOnAcIn = Devices
.EquivalentDevice(DeviceType.AcLoad, inverterAcIn, pvOnAcIn, grid)
.Select(Devices.ReversePhases);
// loadOnAcIn.SelectMany(l => l.Phases)
// .ForEach(p =>
// {
// p.Current = Math.Min(0, p.Current); // current in load cannot be positive
// p.Power = Math.Min(0, p.Power); // power of load cannot be positive
// });
return Devices.Combine(DeviceType.AcInBus, loadOnAcIn, pvOnAcIn, grid);
}
private static Maybe<Device> CreateAcOutBus(VenusSettings settings, Maybe<Device> pvOnAcOut, Maybe<Device> inverterAcOut, Maybe<Device> acInBus)
{
if (!settings.HasAcOutBus || !pvOnAcOut.HasValue)
return acInBus;
var loadOnAcOut = Devices
.EquivalentDevice(DeviceType.AcLoad, inverterAcOut, pvOnAcOut)
.Select(Devices.ReversePhases);
// loadOnAcOut.SelectMany(l => l.Phases)
// .ForEach(p =>
// {
// p.Current = Math.Min(0, p.Current); // current in load cannot be positive
// p.Power = Math.Min(0, p.Power); // power of load cannot be positive
// });
return Devices.Combine(DeviceType.AcOutBus, loadOnAcOut, pvOnAcOut, acInBus);
}
private static Maybe<Device> CreateDcBus(VenusSettings settings, Maybe<Device> pvOnDc, Maybe<Device> inverterDc, Maybe<Device> battery)
{
if (!pvOnDc.HasValue && !settings.HasDcSystem)
return battery;
// if (!settings.HasDcSystem)
// return Devices.Combine(DeviceType.DcBus, pvOnDc, battery);
var loadOnDc = Devices
.EquivalentDevice(DeviceType.DcLoad, inverterDc, pvOnDc, battery)
// .Select(Devices.ReversePhases);
;
// TODO: no way to measure battery heater power yet
// if there is no DC-system declared:
// we assume that the missing (EquivalentDevice) power is consumed by the Battery Heater
//
// if there is a DC-system declared:
// lump the battery heater and the other devices into a DC load
if (settings.HasDcSystem)
return Devices.Combine(DeviceType.DcBus, loadOnDc, pvOnDc, battery);
loadOnDc.ForEach(l => l.Type = DeviceType.BatteryHeater);
battery.ForEach(b => b.Devices.Add(loadOnDc));
return Devices.Combine(DeviceType.DcBus, pvOnDc, battery);
}
}