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; using Services = IReadOnlyList; 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(); var interval = TimeSpan.FromSeconds(SamplePeriodSecs); Console.WriteLine("start sampling"); services.Sample(interval) .Select(ParsePayload) .Skip(1) .SelectMany(dispatch) .Wait(); } private static IObservable> ObserveVeService(this DBusConnection dbus, Func selector) { return dbus .ObservePropertiesOfServices(selector) .StartWith(Array.Empty()); } private static String GetInfo(PutObjectResponse response) { return DateTime .Now .ToUniversalTime() .ToString(CultureInfo.CurrentCulture.DateTimeFormat.SortableDateTimePattern) .Replace('T', ' ') .Replace('-', '/') + " " + response.HttpStatusCode; } private static Func> 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 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()); } 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 acBus, Maybe 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) { 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 CreateAcInBus(Maybe grid, Maybe pvOnAcIn, Maybe 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 CreateAcOutBus(VenusSettings settings, Maybe pvOnAcOut, Maybe inverterAcOut, Maybe 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 CreateDcBus(VenusSettings settings, Maybe pvOnDc, Maybe inverterDc, Maybe 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); } }