272 lines
10 KiB
C#
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);
|
|||
|
}
|
|||
|
|
|||
|
}
|