Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
3deee7bbac
|
@ -881,7 +881,10 @@ import random
|
||||||
logging.debug('finished update cycle\n')
|
logging.debug('finished update cycle\n')
|
||||||
return True'''
|
return True'''
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
def update(modbus, batteries, dbus, signals, csv_signals):
|
def update(modbus, batteries, dbus, signals, csv_signals):
|
||||||
|
global start_time
|
||||||
# type: (Modbus, Iterable[Battery], DBus, Iterable[Signal]) -> bool
|
# type: (Modbus, Iterable[Battery], DBus, Iterable[Signal]) -> bool
|
||||||
"""
|
"""
|
||||||
Main update function
|
Main update function
|
||||||
|
@ -910,7 +913,11 @@ def update(modbus, batteries, dbus, signals, csv_signals):
|
||||||
#print(update_state_from_dictionaries(current_warnings, current_alarms))
|
#print(update_state_from_dictionaries(current_warnings, current_alarms))
|
||||||
status_message, alarms_number_list, warnings_number_list = update_state_from_dictionaries(current_warnings, current_alarms, node_numbers)
|
status_message, alarms_number_list, warnings_number_list = update_state_from_dictionaries(current_warnings, current_alarms, node_numbers)
|
||||||
publish_values(dbus, signals, statuses)
|
publish_values(dbus, signals, statuses)
|
||||||
|
elapsed_time = time.time() - start_time
|
||||||
|
if elapsed_time >= 30:
|
||||||
create_csv_files(csv_signals, statuses, node_numbers, alarms_number_list, warnings_number_list)
|
create_csv_files(csv_signals, statuses, node_numbers, alarms_number_list, warnings_number_list)
|
||||||
|
start_time = time.time()
|
||||||
|
print(f"Elapsed time: {elapsed_time:.2f} seconds")
|
||||||
logging.debug('finished update cycle\n')
|
logging.debug('finished update cycle\n')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -975,7 +982,7 @@ def get_installation_name(file_path):
|
||||||
return file.read().strip()
|
return file.read().strip()
|
||||||
|
|
||||||
def manage_csv_files(directory_path, max_files=20):
|
def manage_csv_files(directory_path, max_files=20):
|
||||||
csv_files = [f for f in os.listdir(directory_path)]
|
csv_files = [f for f in os.listdir(directory_path) if os.path.isfile(os.path.join(directory_path, f))]
|
||||||
csv_files.sort(key=lambda x: os.path.getctime(os.path.join(directory_path, x)))
|
csv_files.sort(key=lambda x: os.path.getctime(os.path.join(directory_path, x)))
|
||||||
# Remove oldest files if exceeds maximum
|
# Remove oldest files if exceeds maximum
|
||||||
while len(csv_files) > max_files:
|
while len(csv_files) > max_files:
|
||||||
|
|
|
@ -18,7 +18,7 @@ public static class Config
|
||||||
|
|
||||||
public static readonly TimeSpan UpdatePeriod = TimeSpan.FromSeconds(1);
|
public static readonly TimeSpan UpdatePeriod = TimeSpan.FromSeconds(1);
|
||||||
|
|
||||||
public static readonly IReadOnlyList<Signal> Signals = new Signal[]
|
public static readonly IReadOnlyList<Signal> Signals = new List<Signal>
|
||||||
{
|
{
|
||||||
// new(s => s..CurrentL1, "/Ac/L1/Current", "0.0 A"),
|
// new(s => s..CurrentL1, "/Ac/L1/Current", "0.0 A"),
|
||||||
// new(s => s..CurrentL2, "/Ac/L2/Current", "0.0 A"),
|
// new(s => s..CurrentL2, "/Ac/L2/Current", "0.0 A"),
|
||||||
|
@ -30,10 +30,10 @@ public static class Config
|
||||||
// new(s => s.Ac.L3.Voltage, "/Ac/L3/Voltage", "0.0 A"),
|
// new(s => s.Ac.L3.Voltage, "/Ac/L3/Voltage", "0.0 A"),
|
||||||
// new(s => (s.Ac.L1.Voltage + s.Ac.L2.Voltage + s.Ac.L3.Voltage) / 3.0m, "/Ac/Voltage", "0.0 A"),
|
// new(s => (s.Ac.L1.Voltage + s.Ac.L2.Voltage + s.Ac.L3.Voltage) / 3.0m, "/Ac/Voltage", "0.0 A"),
|
||||||
|
|
||||||
new(s => s.ActivePowerL1, "/Ac/L1/Power", "0 W"),
|
new Signal(s => s.ActivePowerL1, "/Ac/L1/Power", "0 W"),
|
||||||
new(s => s.ActivePowerL2, "/Ac/L2/Power", "0 W"),
|
new Signal(s => s.ActivePowerL2, "/Ac/L2/Power", "0 W"),
|
||||||
new(s => s.ActivePowerL3, "/Ac/L3/Power", "0 W"),
|
new Signal(s => s.ActivePowerL3, "/Ac/L3/Power", "0 W"),
|
||||||
//new(s => (s.ActivePowerL1 + s.ActivePowerL2 + s.ActivePowerL3), "/Ac/Power", "0 W"),
|
new Signal(s => s.ActivePowerL1 + s.ActivePowerL2 + s.ActivePowerL3, "/Ac/Power", "0 W"),
|
||||||
|
|
||||||
// new(s => s.EnergyImportL123, "Ac/Energy/Forward", "0.00 kWh"),
|
// new(s => s.EnergyImportL123, "Ac/Energy/Forward", "0.00 kWh"),
|
||||||
// new(s => s.EnergyExportL123, "Ac/Energy/Reverse", "0.00 kWh"),
|
// new(s => s.EnergyExportL123, "Ac/Energy/Reverse", "0.00 kWh"),
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Reactive.Linq;
|
/*using System.Reactive.Linq;
|
||||||
using InnovEnergy.Lib.Devices.IEM3kGridMeter;
|
using InnovEnergy.Lib.Devices.IEM3kGridMeter;
|
||||||
using InnovEnergy.Lib.Protocols.DBus;
|
using InnovEnergy.Lib.Protocols.DBus;
|
||||||
using InnovEnergy.Lib.Protocols.Modbus.Clients;
|
using InnovEnergy.Lib.Protocols.Modbus.Clients;
|
||||||
|
@ -84,4 +84,72 @@ public static class SchneiderMeterDriver
|
||||||
Console.WriteLine($"Connecting to DBus {bus}");
|
Console.WriteLine($"Connecting to DBus {bus}");
|
||||||
return properties.PublishOnDBus(bus, Config.BusName);
|
return properties.PublishOnDBus(bus, Config.BusName);
|
||||||
}
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Reactive.Linq;
|
||||||
|
using InnovEnergy.Lib.Devices.IEM3kGridMeter;
|
||||||
|
using InnovEnergy.Lib.Protocols.DBus;
|
||||||
|
using InnovEnergy.Lib.Protocols.Modbus.Clients;
|
||||||
|
using InnovEnergy.Lib.Utils;
|
||||||
|
using InnovEnergy.Lib.Victron.VeDBus;
|
||||||
|
|
||||||
|
namespace InnovEnergy.App.SchneiderDriver
|
||||||
|
{
|
||||||
|
public static class SchneiderMeterDriver
|
||||||
|
{
|
||||||
|
public static Task<Exception> Run(string hostName, Bus dbusAddress)
|
||||||
|
{
|
||||||
|
return Run(hostName, ModbusTcpClient.DefaultPort, dbusAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task<Exception> Run(string hostName, ushort port, Bus dbusAddress)
|
||||||
|
{
|
||||||
|
var schneider = new Iem3KGridMeterDevice(hostName, port, Config.ModbusNodeId);
|
||||||
|
var schneiderStatus = Observable
|
||||||
|
.Interval(Config.UpdatePeriod)
|
||||||
|
.Select(_ =>
|
||||||
|
{
|
||||||
|
var status = schneider.Read();
|
||||||
|
if (status == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Failed to read data from Iem3KGridMeterDevice");
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
})
|
||||||
|
.Where(status => status != null) // Ignore null readings
|
||||||
|
.Publish();
|
||||||
|
|
||||||
|
var poller = schneiderStatus.Connect();
|
||||||
|
var properties = Config.DefaultProperties;
|
||||||
|
|
||||||
|
var signals = Config
|
||||||
|
.Signals
|
||||||
|
.Select(signal => schneiderStatus.Select(signal.ToVeProperty))
|
||||||
|
.Merge()
|
||||||
|
.Do(p => properties.Set(p));
|
||||||
|
|
||||||
|
schneiderStatus.Subscribe(_ => properties.Set("/Connected", 1));
|
||||||
|
|
||||||
|
var dbus = schneiderStatus
|
||||||
|
.Skip(1)
|
||||||
|
.Take(1)
|
||||||
|
.SelectMany(_ => PublishPropertiesOnDBus(properties, dbusAddress));
|
||||||
|
|
||||||
|
return await signals
|
||||||
|
.MergeErrors(dbus)
|
||||||
|
.Finally(poller.Dispose)
|
||||||
|
.SelectErrors();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Task<Exception> PublishPropertiesOnDBus(VeProperties properties, Bus bus)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Connecting to DBus {bus}");
|
||||||
|
return properties.PublishOnDBus(bus, Config.BusName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using InnovEnergy.Lib.Devices.IEM3kGridMeter;
|
/*using InnovEnergy.Lib.Devices.IEM3kGridMeter;
|
||||||
using InnovEnergy.Lib.Protocols.DBus.Protocol.DataTypes;
|
using InnovEnergy.Lib.Protocols.DBus.Protocol.DataTypes;
|
||||||
using InnovEnergy.Lib.Victron.VeDBus;
|
using InnovEnergy.Lib.Victron.VeDBus;
|
||||||
|
|
||||||
|
@ -6,11 +6,38 @@ namespace InnovEnergy.App.SchneiderDriver;
|
||||||
|
|
||||||
|
|
||||||
// TODO: Does not compile
|
// TODO: Does not compile
|
||||||
public record Signal(Func<Iem3KGridMeterRegisters, Object> Source, ObjectPath Path, String Format = "")
|
public record Signal(Func<Iem3KGridMeterRegisters, object> Source, ObjectPath Path, string Format = "")
|
||||||
{
|
{
|
||||||
public VeProperty ToVeProperty(Iem3KGridMeterRegisters status)
|
public VeProperty ToVeProperty(Iem3KGridMeterRegisters status)
|
||||||
{
|
{
|
||||||
|
var value = Source(status);
|
||||||
|
return new VeProperty(Path, value, string.Format($"{{0:{Format}}}", value));
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
using InnovEnergy.Lib.Devices.IEM3kGridMeter;
|
||||||
|
using InnovEnergy.Lib.Protocols.DBus.Protocol.DataTypes;
|
||||||
|
using InnovEnergy.Lib.Victron.VeDBus;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace InnovEnergy.App.SchneiderDriver
|
||||||
|
{
|
||||||
|
public record Signal(Func<Iem3KGridMeterRegisters, object> Source, ObjectPath Path, string Format = "")
|
||||||
|
{
|
||||||
|
public VeProperty ToVeProperty(Iem3KGridMeterRegisters status)
|
||||||
|
{
|
||||||
|
if (status == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Status is null for path: {Path}");
|
||||||
|
// Return a default VeProperty if status is null
|
||||||
|
return new VeProperty(Path, default, String.Format($"{{0:{Format}}}", default));
|
||||||
|
}
|
||||||
|
|
||||||
var value = Source(status);
|
var value = Source(status);
|
||||||
return new VeProperty(Path, value, String.Format($"{{0:{Format}}}", value));
|
return new VeProperty(Path, value, String.Format($"{{0:{Format}}}", value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
namespace InnovEnergy.App.SchniederDriver;
|
|
||||||
|
|
||||||
public static class Utils
|
|
||||||
{
|
|
||||||
public static IEnumerable<T> TryWhere<T>(this IEnumerable<T> src, Func<T, Boolean> predicate)
|
|
||||||
{
|
|
||||||
foreach (var e in src)
|
|
||||||
{
|
|
||||||
var ok = false;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
ok = predicate(e);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// ignored
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ok)
|
|
||||||
yield return e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IEnumerable<R> TrySelect<T,R>(this IEnumerable<T> src, Func<T, R> map)
|
|
||||||
{
|
|
||||||
foreach (var e in src)
|
|
||||||
{
|
|
||||||
var ok = false;
|
|
||||||
var result = default(R);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
result = map(e);
|
|
||||||
ok = true;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// ignored
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ok)
|
|
||||||
yield return result!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
exec 2>&1
|
|
||||||
exec multilog t s25000 n4 /var/log/EmuMeter
|
|
|
@ -1,3 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
exec 2>&1
|
|
||||||
exec softlimit -d 100000000 -s 1000000 -a 100000000 /opt/innovenergy/EmuMeter/EmuMeter
|
|
|
@ -1,4 +1,4 @@
|
||||||
using InnovEnergy.Lib.Protocols.Modbus.Channels;
|
/*using InnovEnergy.Lib.Protocols.Modbus.Channels;
|
||||||
using InnovEnergy.Lib.Protocols.Modbus.Clients;
|
using InnovEnergy.Lib.Protocols.Modbus.Clients;
|
||||||
using InnovEnergy.Lib.Protocols.Modbus.Slaves;
|
using InnovEnergy.Lib.Protocols.Modbus.Slaves;
|
||||||
using InnovEnergy.Lib.Utils;
|
using InnovEnergy.Lib.Utils;
|
||||||
|
@ -49,4 +49,83 @@ public class Iem3KGridMeterDevice: ModbusDevice<Iem3KGridMeterRegisters>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}*/
|
||||||
|
|
||||||
|
using InnovEnergy.Lib.Protocols.Modbus.Channels;
|
||||||
|
using InnovEnergy.Lib.Protocols.Modbus.Clients;
|
||||||
|
using InnovEnergy.Lib.Protocols.Modbus.Slaves;
|
||||||
|
using InnovEnergy.Lib.Utils;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace InnovEnergy.Lib.Devices.IEM3kGridMeter
|
||||||
|
{
|
||||||
|
public class Iem3KGridMeterDevice : ModbusDevice<Iem3KGridMeterRegisters>
|
||||||
|
{
|
||||||
|
private readonly string _hostname;
|
||||||
|
private readonly ushort _port;
|
||||||
|
private readonly byte _slaveId;
|
||||||
|
|
||||||
|
public Iem3KGridMeterDevice(string hostname, ushort port = 502, byte slaveId = 1)
|
||||||
|
: this(new TcpChannel(hostname, port), slaveId)
|
||||||
|
{
|
||||||
|
_hostname = hostname ?? throw new ArgumentNullException(nameof(hostname));
|
||||||
|
_port = port;
|
||||||
|
_slaveId = slaveId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Iem3KGridMeterDevice(TcpChannel channel, byte slaveId = 1)
|
||||||
|
: base(new ModbusTcpClient(channel, slaveId))
|
||||||
|
{
|
||||||
|
_hostname = channel.Host;
|
||||||
|
_port = channel.Port;
|
||||||
|
_slaveId = slaveId;
|
||||||
|
Console.WriteLine($"Initializing Iem3KGridMeterDevice with channel: {channel.Host}:{channel.Port}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Iem3KGridMeterDevice(ModbusClient client)
|
||||||
|
: base(client)
|
||||||
|
{
|
||||||
|
if (client is ModbusTcpClient tcpClient)
|
||||||
|
{
|
||||||
|
_hostname = tcpClient.Channel.Host;
|
||||||
|
_port = tcpClient.Channel.Port;
|
||||||
|
_slaveId = tcpClient.SlaveId;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Invalid client type", nameof(client));
|
||||||
|
}
|
||||||
|
Console.WriteLine("Initializing Iem3KGridMeterDevice with ModbusClient");
|
||||||
|
}
|
||||||
|
|
||||||
|
public new Iem3KGridMeterRegisters? Read()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Attempting to read data from {_hostname}:{_port} with slaveId {_slaveId}");
|
||||||
|
return base.Read();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed to read data from {nameof(Iem3KGridMeterDevice)}: {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public new void Write(Iem3KGridMeterRegisters registers)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
base.Write(registers);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed to write data to {nameof(Iem3KGridMeterDevice)}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,19 @@
|
||||||
namespace InnovEnergy.Lib.Protocols.Modbus.Channels;
|
//namespace InnovEnergy.Lib.Protocols.Modbus.Channels;
|
||||||
|
|
||||||
public abstract class Channel
|
/*public abstract class Channel
|
||||||
{
|
{
|
||||||
public abstract IReadOnlyList<Byte> Read(Int32 nBytes);
|
public abstract IReadOnlyList<Byte> Read(Int32 nBytes);
|
||||||
public abstract void Write(IReadOnlyList<Byte> bytes);
|
public abstract void Write(IReadOnlyList<Byte> bytes);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace InnovEnergy.Lib.Protocols.Modbus.Channels
|
||||||
|
{
|
||||||
|
public abstract class Channel
|
||||||
|
{
|
||||||
|
public abstract IReadOnlyList<byte> Read(int nBytes);
|
||||||
|
public abstract void Write(IReadOnlyList<byte> bytes);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -4,7 +4,7 @@ using System.Reactive.Linq;
|
||||||
using CliWrap;
|
using CliWrap;
|
||||||
using InnovEnergy.Lib.Utils;
|
using InnovEnergy.Lib.Utils;
|
||||||
|
|
||||||
namespace InnovEnergy.Lib.Protocols.Modbus.Channels;
|
/*namespace InnovEnergy.Lib.Protocols.Modbus.Channels;
|
||||||
|
|
||||||
public record RemoteSerialConnection
|
public record RemoteSerialConnection
|
||||||
(
|
(
|
||||||
|
@ -207,4 +207,93 @@ public class RemoteSerialChannel : ConnectionChannel<TcpChannel>
|
||||||
{
|
{
|
||||||
connection.Write(data);
|
connection.Write(data);
|
||||||
}
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO.Ports;
|
||||||
|
using CliWrap;
|
||||||
|
using InnovEnergy.Lib.Utils;
|
||||||
|
|
||||||
|
namespace InnovEnergy.Lib.Protocols.Modbus.Channels
|
||||||
|
{
|
||||||
|
public class RemoteSerialChannel : ConnectionChannel<TcpChannel>, IDisposable
|
||||||
|
{
|
||||||
|
private readonly Command _Command;
|
||||||
|
private readonly TcpChannel _TcpChannel;
|
||||||
|
|
||||||
|
const string SsDir = "/opt/victronenergy/serial-starter";
|
||||||
|
const string KillTasks = "kill $!";
|
||||||
|
|
||||||
|
private CancellationTokenSource _CancellationTokenSource = new CancellationTokenSource();
|
||||||
|
|
||||||
|
private CommandTask<CommandResult>? _CommandTask;
|
||||||
|
|
||||||
|
public RemoteSerialChannel(SshHost host, string tty, int baudRate, Parity parity, int dataBits, int stopBits)
|
||||||
|
{
|
||||||
|
const int port = 6855;
|
||||||
|
|
||||||
|
tty = tty.EnsureStartsWith("/dev/");
|
||||||
|
|
||||||
|
var configureTty = ConfigureTty(tty, baudRate, parity, stopBits, dataBits);
|
||||||
|
|
||||||
|
var stopTty = $"{SsDir}/stop-tty.sh {tty}";
|
||||||
|
var startTty = $"{SsDir}/start-tty.sh {tty}";
|
||||||
|
|
||||||
|
var socat = $"socat TCP-LISTEN:{port},nodelay {tty},raw";
|
||||||
|
|
||||||
|
var script = $"{configureTty} && {socat}";
|
||||||
|
|
||||||
|
_Command = host.Command.AppendArgument(script);
|
||||||
|
_TcpChannel = new TcpChannel(host.HostName, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ConfigureTty(string tty, int baudRate, Parity parity, int stopBits, int dataBits)
|
||||||
|
{
|
||||||
|
var oParity = parity switch
|
||||||
|
{
|
||||||
|
Parity.Even => "parenb -parodd",
|
||||||
|
Parity.Odd => "parenb parodd",
|
||||||
|
Parity.None => "-parenb",
|
||||||
|
_ => throw new NotImplementedException()
|
||||||
|
};
|
||||||
|
|
||||||
|
var oStopBits = stopBits switch
|
||||||
|
{
|
||||||
|
1 => "-cstopb",
|
||||||
|
2 => "cstopb",
|
||||||
|
_ => throw new NotImplementedException()
|
||||||
|
};
|
||||||
|
|
||||||
|
var oDataBits = "cs" + dataBits;
|
||||||
|
|
||||||
|
return $"stty -F {tty} {baudRate} {oDataBits} {oStopBits} {oParity}";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override TcpChannel Open()
|
||||||
|
{
|
||||||
|
return _TcpChannel;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Close(TcpChannel connection)
|
||||||
|
{
|
||||||
|
_CancellationTokenSource.Cancel();
|
||||||
|
connection.Dispose();
|
||||||
|
_CancellationTokenSource = new CancellationTokenSource();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IReadOnlyList<byte> Read(TcpChannel connection, int nBytes)
|
||||||
|
{
|
||||||
|
return connection.Read(nBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Write(TcpChannel connection, IReadOnlyList<byte> data)
|
||||||
|
{
|
||||||
|
connection.Write(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Close(_TcpChannel);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -2,7 +2,7 @@ using System.Net.Sockets;
|
||||||
using InnovEnergy.Lib.Protocols.Modbus.Protocol;
|
using InnovEnergy.Lib.Protocols.Modbus.Protocol;
|
||||||
using InnovEnergy.Lib.Utils.Net;
|
using InnovEnergy.Lib.Utils.Net;
|
||||||
|
|
||||||
namespace InnovEnergy.Lib.Protocols.Modbus.Channels;
|
/*namespace InnovEnergy.Lib.Protocols.Modbus.Channels;
|
||||||
|
|
||||||
public class TcpChannel : ConnectionChannel<TcpClient>
|
public class TcpChannel : ConnectionChannel<TcpClient>
|
||||||
{
|
{
|
||||||
|
@ -82,4 +82,104 @@ public class TcpChannel : ConnectionChannel<TcpClient>
|
||||||
var array = data.ToArray();
|
var array = data.ToArray();
|
||||||
tcpClient.GetStream().Write(array, 0, array.Length);
|
tcpClient.GetStream().Write(array, 0, array.Length);
|
||||||
}
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
|
||||||
|
namespace InnovEnergy.Lib.Protocols.Modbus.Channels
|
||||||
|
{
|
||||||
|
public class TcpChannel : Channel, IDisposable
|
||||||
|
{
|
||||||
|
public string Host { get; }
|
||||||
|
public ushort Port { get; }
|
||||||
|
|
||||||
|
private const int TimeoutMs = 500; // TODO: parametrize
|
||||||
|
private Socket? Socket { get; set; }
|
||||||
|
private byte[] Buffer { get; }
|
||||||
|
|
||||||
|
public TcpChannel(string hostname, ushort port)
|
||||||
|
{
|
||||||
|
Host = hostname ?? throw new ArgumentNullException(nameof(hostname));
|
||||||
|
Port = port;
|
||||||
|
Buffer = new byte[8192]; // Buffer size can be adjusted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override IReadOnlyList<byte> Read(int nBytes)
|
||||||
|
{
|
||||||
|
if (Socket == null)
|
||||||
|
throw new InvalidOperationException("Socket is not connected.");
|
||||||
|
|
||||||
|
var buffer = new byte[nBytes];
|
||||||
|
int bytesRead = 0;
|
||||||
|
|
||||||
|
while (bytesRead < nBytes)
|
||||||
|
{
|
||||||
|
var read = Socket.Receive(buffer, bytesRead, nBytes - bytesRead, SocketFlags.None);
|
||||||
|
if (read == 0)
|
||||||
|
throw new Exception("Socket closed.");
|
||||||
|
|
||||||
|
bytesRead += read;
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(IReadOnlyList<byte> bytes)
|
||||||
|
{
|
||||||
|
if (Socket == null)
|
||||||
|
throw new InvalidOperationException("Socket is not connected.");
|
||||||
|
|
||||||
|
Socket.Send(bytes.ToArray(), SocketFlags.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Connect()
|
||||||
|
{
|
||||||
|
if (Socket != null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Socket = new Socket(SocketType.Stream, ProtocolType.Tcp)
|
||||||
|
{
|
||||||
|
Blocking = true,
|
||||||
|
NoDelay = true,
|
||||||
|
LingerState = new LingerOption(false, 0),
|
||||||
|
ReceiveTimeout = TimeoutMs,
|
||||||
|
SendTimeout = TimeoutMs
|
||||||
|
};
|
||||||
|
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
cts.CancelAfter(TimeoutMs);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Socket.ConnectAsync(Host, Port).Wait(TimeoutMs);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Socket = null;
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Disconnect()
|
||||||
|
{
|
||||||
|
if (Socket == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Socket.Close();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Socket = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ namespace InnovEnergy.Lib.Protocols.Modbus.Clients;
|
||||||
using UInt16s = IReadOnlyCollection<UInt16>;
|
using UInt16s = IReadOnlyCollection<UInt16>;
|
||||||
using Booleans = IReadOnlyCollection<Boolean>;
|
using Booleans = IReadOnlyCollection<Boolean>;
|
||||||
|
|
||||||
public class ModbusTcpClient : ModbusClient
|
/*public class ModbusTcpClient : ModbusClient
|
||||||
{
|
{
|
||||||
public const UInt16 DefaultPort = 502;
|
public const UInt16 DefaultPort = 502;
|
||||||
private UInt16 _Id;
|
private UInt16 _Id;
|
||||||
|
@ -184,4 +184,171 @@ public class ModbusTcpClient : ModbusClient
|
||||||
return new MbData(rxFrm.RegistersRead.RawData, readAddress, Endian);
|
return new MbData(rxFrm.RegistersRead.RawData, readAddress, Endian);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}*/
|
||||||
|
|
||||||
|
public class ModbusTcpClient : ModbusClient
|
||||||
|
{
|
||||||
|
public const ushort DefaultPort = 502;
|
||||||
|
private ushort _Id;
|
||||||
|
public TcpChannel Channel { get; }
|
||||||
|
|
||||||
|
public ModbusTcpClient(TcpChannel channel, byte slaveId) : base(channel, slaveId)
|
||||||
|
{
|
||||||
|
Channel = channel;
|
||||||
|
Channel.Connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ushort NextId() => unchecked(++_Id);
|
||||||
|
|
||||||
|
public override MbData ReadCoils(ushort readAddress, ushort nValues)
|
||||||
|
{
|
||||||
|
var id = NextId(); // TODO: check response id
|
||||||
|
|
||||||
|
var cmd = new ReadCoilsCommandFrame(SlaveId, readAddress, nValues);
|
||||||
|
var hdr = new MbapHeader(id, cmd.Data.Count);
|
||||||
|
var frm = new ModbusTcpFrame(hdr, cmd);
|
||||||
|
|
||||||
|
Channel.Write(frm.Data);
|
||||||
|
|
||||||
|
var hData = Channel.Read(MbapHeader.Size).ToArray();
|
||||||
|
var rxHdr = new MbapHeader(hData);
|
||||||
|
|
||||||
|
var rxFrm = Channel
|
||||||
|
.Read(rxHdr.FrameLength)
|
||||||
|
.ToArray()
|
||||||
|
.Apply(ReadCoilsResponseFrame.Parse)
|
||||||
|
.Apply(cmd.VerifyResponse);
|
||||||
|
|
||||||
|
return new MbData(rxFrm.Coils.RawData, readAddress, Endian);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override MbData ReadDiscreteInputs(ushort readAddress, ushort nValues)
|
||||||
|
{
|
||||||
|
var id = NextId(); // TODO: check response id
|
||||||
|
|
||||||
|
var cmd = new ReadDiscreteInputsCommandFrame(SlaveId, readAddress, nValues);
|
||||||
|
var hdr = new MbapHeader(id, cmd.Data.Count);
|
||||||
|
var frm = new ModbusTcpFrame(hdr, cmd);
|
||||||
|
|
||||||
|
Channel.Write(frm.Data);
|
||||||
|
|
||||||
|
var hData = Channel.Read(MbapHeader.Size).ToArray();
|
||||||
|
var rxHdr = new MbapHeader(hData);
|
||||||
|
|
||||||
|
var rxFrm = Channel
|
||||||
|
.Read(rxHdr.FrameLength)
|
||||||
|
.ToArray()
|
||||||
|
.Apply(ReadDiscreteInputsResponseFrame.Parse)
|
||||||
|
.Apply(cmd.VerifyResponse);
|
||||||
|
|
||||||
|
return new MbData(rxFrm.Inputs.RawData, readAddress, Endian);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override MbData ReadInputRegisters(ushort readAddress, ushort nValues)
|
||||||
|
{
|
||||||
|
var id = NextId(); // TODO: check response id
|
||||||
|
|
||||||
|
var cmd = new ReadInputRegistersCommandFrame(SlaveId, readAddress, nValues);
|
||||||
|
var hdr = new MbapHeader(id, cmd.Data.Count);
|
||||||
|
var frm = new ModbusTcpFrame(hdr, cmd);
|
||||||
|
|
||||||
|
Channel.Write(frm.Data);
|
||||||
|
|
||||||
|
var hData = Channel.Read(MbapHeader.Size).ToArray();
|
||||||
|
var rxHdr = new MbapHeader(hData);
|
||||||
|
|
||||||
|
var rxFrm = Channel
|
||||||
|
.Read(rxHdr.FrameLength)
|
||||||
|
.ToArray()
|
||||||
|
.Apply(ReadInputRegistersResponseFrame.Parse)
|
||||||
|
.Apply(cmd.VerifyResponse);
|
||||||
|
|
||||||
|
return new MbData(rxFrm.RegistersRead.RawData, readAddress, Endian);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override MbData ReadHoldingRegisters(ushort readAddress, ushort nValues)
|
||||||
|
{
|
||||||
|
var id = NextId(); // TODO: check response id
|
||||||
|
|
||||||
|
var cmd = new ReadHoldingRegistersCommandFrame(SlaveId, readAddress, nValues);
|
||||||
|
var hdr = new MbapHeader(id, cmd.Data.Count);
|
||||||
|
var frm = new ModbusTcpFrame(hdr, cmd);
|
||||||
|
|
||||||
|
Channel.Write(frm.Data);
|
||||||
|
|
||||||
|
var hData = Channel.Read(MbapHeader.Size).ToArray();
|
||||||
|
var rxHdr = new MbapHeader(hData);
|
||||||
|
|
||||||
|
var rxFrm = Channel
|
||||||
|
.Read(rxHdr.FrameLength)
|
||||||
|
.ToArray()
|
||||||
|
.Apply(ReadHoldingRegistersResponseFrame.Parse)
|
||||||
|
.Apply(cmd.VerifyResponse);
|
||||||
|
|
||||||
|
return new MbData(rxFrm.RegistersRead.RawData, readAddress, Endian);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ushort WriteCoils(ushort writeAddress, Booleans coils)
|
||||||
|
{
|
||||||
|
var id = NextId(); // TODO: check response id
|
||||||
|
var cmd = new WriteCoilsCommandFrame(SlaveId, writeAddress, coils);
|
||||||
|
var hdr = new MbapHeader(id, cmd.Data.Count);
|
||||||
|
var frm = new ModbusTcpFrame(hdr, cmd);
|
||||||
|
|
||||||
|
Channel.Write(frm.Data);
|
||||||
|
|
||||||
|
var hData = Channel.Read(MbapHeader.Size).ToArray();
|
||||||
|
var rxHdr = new MbapHeader(hData);
|
||||||
|
|
||||||
|
var rxFrm = Channel
|
||||||
|
.Read(rxHdr.FrameLength)
|
||||||
|
.ToArray()
|
||||||
|
.Apply(WriteCoilsResponseFrame.Parse)
|
||||||
|
.Apply(cmd.VerifyResponse);
|
||||||
|
|
||||||
|
return rxFrm.NbWritten;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ushort WriteRegisters(ushort writeAddress, UInt16s values)
|
||||||
|
{
|
||||||
|
var id = NextId(); // TODO: check response id
|
||||||
|
var cmd = new WriteRegistersCommandFrame(SlaveId, writeAddress, values);
|
||||||
|
var hdr = new MbapHeader(id, cmd.Data.Count);
|
||||||
|
var frm = new ModbusTcpFrame(hdr, cmd);
|
||||||
|
|
||||||
|
Channel.Write(frm.Data);
|
||||||
|
|
||||||
|
var hData = Channel.Read(MbapHeader.Size).ToArray();
|
||||||
|
var rxHdr = new MbapHeader(hData);
|
||||||
|
|
||||||
|
var rxFrm = Channel
|
||||||
|
.Read(rxHdr.FrameLength)
|
||||||
|
.ToArray()
|
||||||
|
.Apply(WriteRegistersResponseFrame.Parse)
|
||||||
|
.Apply(cmd.VerifyResponse);
|
||||||
|
|
||||||
|
return rxFrm.NbWritten;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override MbData ReadWriteRegisters(ushort readAddress, ushort nbToRead, ushort writeAddress, UInt16s registersToWrite)
|
||||||
|
{
|
||||||
|
var id = NextId(); // TODO: check response id
|
||||||
|
|
||||||
|
var cmd = new ReadWriteRegistersCommandFrame(SlaveId, readAddress, nbToRead, writeAddress, registersToWrite);
|
||||||
|
|
||||||
|
var hdr = new MbapHeader(id, cmd.Data.Count);
|
||||||
|
var frm = new ModbusTcpFrame(hdr, cmd);
|
||||||
|
|
||||||
|
Channel.Write(frm.Data);
|
||||||
|
|
||||||
|
var hData = Enumerable.ToArray(Channel.Read(MbapHeader.Size));
|
||||||
|
var rxHdr = new MbapHeader(hData);
|
||||||
|
|
||||||
|
var fData = Enumerable.ToArray(Channel.Read(rxHdr.FrameLength));
|
||||||
|
var rxFrm = fData
|
||||||
|
.Apply(ReadWriteRegistersResponseFrame.Parse)
|
||||||
|
.Apply(cmd.VerifyResponse);
|
||||||
|
|
||||||
|
return new MbData(rxFrm.RegistersRead.RawData, readAddress, Endian);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
using InnovEnergy.Lib.Protocols.Modbus.Channels;
|
using InnovEnergy.Lib.Protocols.Modbus.Channels;
|
||||||
using InnovEnergy.Lib.Protocols.Modbus.Clients;
|
using InnovEnergy.Lib.Protocols.Modbus.Clients;
|
||||||
|
|
||||||
namespace InnovEnergy.Lib.Protocols.Modbus.Slaves;
|
/*namespace InnovEnergy.Lib.Protocols.Modbus.Slaves;
|
||||||
|
|
||||||
public static class ModbusSlave
|
public static class ModbusSlave
|
||||||
{
|
{
|
||||||
|
@ -52,4 +52,58 @@ public static class ModbusSlave
|
||||||
return new ModbusDevice<T>(modbusClient);
|
return new ModbusDevice<T>(modbusClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}*/
|
||||||
|
|
||||||
|
using InnovEnergy.Lib.Protocols.Modbus.Channels;
|
||||||
|
using InnovEnergy.Lib.Protocols.Modbus.Clients;
|
||||||
|
|
||||||
|
namespace InnovEnergy.Lib.Protocols.Modbus.Slaves
|
||||||
|
{
|
||||||
|
public static class ModbusSlave
|
||||||
|
{
|
||||||
|
public static Func<byte, ModbusTcpClient> ModbusTcp(this Channel channel)
|
||||||
|
{
|
||||||
|
ModbusTcpClient SlaveId(byte slaveId) => new ModbusTcpClient((TcpChannel)channel, slaveId);
|
||||||
|
return SlaveId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Func<byte, ModbusRtuClient> ModbusRtu(this Channel channel)
|
||||||
|
{
|
||||||
|
ModbusRtuClient SlaveId(byte slaveId) => new ModbusRtuClient(channel, slaveId);
|
||||||
|
return SlaveId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Func<byte, ModbusTcpClient> ModbusTcp<R>(this Channel channel) where R : notnull, new()
|
||||||
|
{
|
||||||
|
ModbusTcpClient SlaveId(byte slaveId)
|
||||||
|
{
|
||||||
|
return new ModbusTcpClient((TcpChannel)channel, slaveId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SlaveId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Func<byte, ModbusRtuClient> ModbusRtu<R>(this Channel channel) where R : notnull, new()
|
||||||
|
{
|
||||||
|
ModbusRtuClient SlaveId(byte slaveId) => new ModbusRtuClient(channel, slaveId);
|
||||||
|
return SlaveId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ModbusDevice<T> TcpSlave<T>(this Channel channel, byte slaveId) where T : notnull, new()
|
||||||
|
{
|
||||||
|
var client = new ModbusTcpClient((TcpChannel)channel, slaveId);
|
||||||
|
return new ModbusDevice<T>(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ModbusDevice<T> RtuSlave<T>(this Channel channel, byte slaveId) where T : notnull, new()
|
||||||
|
{
|
||||||
|
var client = new ModbusRtuClient(channel, slaveId);
|
||||||
|
return new ModbusDevice<T>(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ModbusDevice<T> Slave<T>(this ModbusClient modbusClient) where T : notnull, new()
|
||||||
|
{
|
||||||
|
return new ModbusDevice<T>(modbusClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -492,8 +492,9 @@ def reset_batteries(modbus, batteries):
|
||||||
|
|
||||||
alive = True # global alive flag, watchdog_task clears it, update_task sets it
|
alive = True # global alive flag, watchdog_task clears it, update_task sets it
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
def create_update_task(modbus, service, batteries):
|
def create_update_task(modbus, service, batteries):
|
||||||
|
global start_time
|
||||||
# type: (Modbus, DBusService, Iterable[Battery]) -> Callable[[],bool]
|
# type: (Modbus, DBusService, Iterable[Battery]) -> Callable[[],bool]
|
||||||
"""
|
"""
|
||||||
Creates an update task which runs the main update function
|
Creates an update task which runs the main update function
|
||||||
|
@ -510,8 +511,7 @@ def create_update_task(modbus, service, batteries):
|
||||||
|
|
||||||
def update_task():
|
def update_task():
|
||||||
# type: () -> bool
|
# type: () -> bool
|
||||||
|
global alive, start_time
|
||||||
global alive
|
|
||||||
|
|
||||||
logging.debug('starting update cycle')
|
logging.debug('starting update cycle')
|
||||||
|
|
||||||
|
@ -535,7 +535,11 @@ def create_update_task(modbus, service, batteries):
|
||||||
|
|
||||||
publish_values_on_dbus(service, _signals, statuses)
|
publish_values_on_dbus(service, _signals, statuses)
|
||||||
|
|
||||||
|
elapsed_time = time.time() - start_time
|
||||||
|
if elapsed_time >= 30:
|
||||||
create_csv_files(csv_signals, statuses, node_numbers, alarms_number_list, warnings_number_list)
|
create_csv_files(csv_signals, statuses, node_numbers, alarms_number_list, warnings_number_list)
|
||||||
|
start_time = time.time()
|
||||||
|
print("Elapsed time: {:.2f} seconds".format(elapsed_time))
|
||||||
|
|
||||||
upload_status_to_innovenergy(_socket, statuses)
|
upload_status_to_innovenergy(_socket, statuses)
|
||||||
|
|
||||||
|
@ -548,7 +552,7 @@ def create_update_task(modbus, service, batteries):
|
||||||
return update_task
|
return update_task
|
||||||
|
|
||||||
def manage_csv_files(directory_path, max_files=20):
|
def manage_csv_files(directory_path, max_files=20):
|
||||||
csv_files = [f for f in os.listdir(directory_path)]
|
csv_files = [f for f in os.listdir(directory_path) if os.path.isfile(os.path.join(directory_path, f))]
|
||||||
csv_files.sort(key=lambda x: os.path.getctime(os.path.join(directory_path, x)))
|
csv_files.sort(key=lambda x: os.path.getctime(os.path.join(directory_path, x)))
|
||||||
# Remove oldest files if exceeds maximum
|
# Remove oldest files if exceeds maximum
|
||||||
while len(csv_files) > max_files:
|
while len(csv_files) > max_files:
|
||||||
|
|
|
@ -863,7 +863,10 @@ def update_for_testing(modbus, batteries, dbus, signals, csv_signals):
|
||||||
logging.debug('finished update cycle\n')
|
logging.debug('finished update cycle\n')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
def update(modbus, batteries, dbus, signals, csv_signals):
|
def update(modbus, batteries, dbus, signals, csv_signals):
|
||||||
|
global start_time
|
||||||
# type: (Modbus, Iterable[Battery], DBus, Iterable[Signal]) -> bool
|
# type: (Modbus, Iterable[Battery], DBus, Iterable[Signal]) -> bool
|
||||||
"""
|
"""
|
||||||
Main update function
|
Main update function
|
||||||
|
@ -891,7 +894,11 @@ def update(modbus, batteries, dbus, signals, csv_signals):
|
||||||
current_alarms[signal_name] = value
|
current_alarms[signal_name] = value
|
||||||
print(update_state_from_dictionaries(current_warnings, current_alarms))
|
print(update_state_from_dictionaries(current_warnings, current_alarms))
|
||||||
publish_values(dbus, signals, statuses)
|
publish_values(dbus, signals, statuses)
|
||||||
|
elapsed_time = time.time() - start_time
|
||||||
|
if elapsed_time >= 30:
|
||||||
create_csv_files(csv_signals, statuses, node_numbers)
|
create_csv_files(csv_signals, statuses, node_numbers)
|
||||||
|
start_time = time.time()
|
||||||
|
print("Elapsed time {:.2f} seconds".format(elapsed_time))
|
||||||
logging.debug('finished update cycle\n')
|
logging.debug('finished update cycle\n')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -956,7 +963,7 @@ def get_installation_name(file_path):
|
||||||
return file.read().strip()
|
return file.read().strip()
|
||||||
|
|
||||||
def manage_csv_files(directory_path, max_files=20):
|
def manage_csv_files(directory_path, max_files=20):
|
||||||
csv_files = [f for f in os.listdir(directory_path)]
|
csv_files = [f for f in os.listdir(directory_path) if os.path.isfile(os.path.join(directory_path, f))]
|
||||||
csv_files.sort(key=lambda x: os.path.getctime(os.path.join(directory_path, x)))
|
csv_files.sort(key=lambda x: os.path.getctime(os.path.join(directory_path, x)))
|
||||||
# Remove oldest files if exceeds maximum
|
# Remove oldest files if exceeds maximum
|
||||||
while len(csv_files) > max_files:
|
while len(csv_files) > max_files:
|
||||||
|
|
Loading…
Reference in New Issue