wip: inverter service
This commit is contained in:
parent
7fca1d1cfb
commit
b642498a60
@ -45,7 +45,8 @@
|
|||||||
<div class="container text-center fw-bold p-0">
|
<div class="container text-center fw-bold p-0">
|
||||||
<div class="row bg-light rounded">
|
<div class="row bg-light rounded">
|
||||||
<div class="progress p-0" style="height:2px;">
|
<div class="progress p-0" style="height:2px;">
|
||||||
<span class="progress-bar" role="progressbar" style="width: @RoundToWholeNumber(status?.LoadPercentage)%" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100"></span>
|
<span class="progress-bar" role="progressbar" style="width: @RoundToWholeNumber(status?.LoadPercentage)%" aria-valuenow="25"
|
||||||
|
aria-valuemin="0" aria-valuemax="100"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="fs-1 text-danger">@status?.LoadWatts</div>
|
<div class="fs-1 text-danger">@status?.LoadWatts</div>
|
||||||
@ -86,7 +87,8 @@
|
|||||||
<div class="container text-center fw-bold p-0">
|
<div class="container text-center fw-bold p-0">
|
||||||
<div class="row bg-light rounded">
|
<div class="row bg-light rounded">
|
||||||
<div class="progress p-0" style="height:2px;">
|
<div class="progress p-0" style="height:2px;">
|
||||||
<span class="progress-bar" role="progressbar" style="width: @status?.PVPotential%" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100"></span>
|
<span class="progress-bar" role="progressbar" style="width: @status?.PVPotential%" aria-valuenow="25" aria-valuemin="0"
|
||||||
|
aria-valuemax="100"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="fs-1 text-success">@status?.PVInputWatt</div>
|
<div class="fs-1 text-success">@status?.PVInputWatt</div>
|
||||||
@ -124,7 +126,8 @@
|
|||||||
<div class="container text-center m-0 p-0">
|
<div class="container text-center m-0 p-0">
|
||||||
<div class="row m-0 p-0">
|
<div class="row m-0 p-0">
|
||||||
<div class="progress p-0" style="height:2px;">
|
<div class="progress p-0" style="height:2px;">
|
||||||
<span class="progress-bar" role="progressbar" style="width: @status?.BatteryDischargePotential%" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100"></span>
|
<span class="progress-bar" role="progressbar" style="width: @status?.BatteryDischargePotential%" aria-valuenow="25"
|
||||||
|
aria-valuemin="0" aria-valuemax="100"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mt-2">
|
<div class="row mt-2">
|
||||||
@ -196,10 +199,10 @@
|
|||||||
onStatusRetrievalError -= NullifyStatus;
|
onStatusRetrievalError -= NullifyStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static decimal RoundToWholeNumber(decimal? val)
|
private static double RoundToWholeNumber(double? val)
|
||||||
=> Math.Round(val ?? 0, 0);
|
=> Math.Round(val ?? 0, 0);
|
||||||
|
|
||||||
private static decimal RoundToOneDecimal(decimal? val)
|
private static double RoundToOneDecimal(double? val)
|
||||||
=> Math.Round(val ?? 0, 1);
|
=> Math.Round(val ?? 0, 1);
|
||||||
|
|
||||||
private static string TemperatureCss()
|
private static string TemperatureCss()
|
||||||
@ -259,7 +262,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static decimal GetCRate()
|
private static double GetCRate()
|
||||||
{
|
{
|
||||||
if (status?.BatteryChargeCRate > 0)
|
if (status?.BatteryChargeCRate > 0)
|
||||||
return Math.Round(status.BatteryChargeCRate, 2);
|
return Math.Round(status.BatteryChargeCRate, 2);
|
||||||
|
|||||||
@ -6,7 +6,7 @@ namespace InverterMon.Server.Endpoints.GetStatus;
|
|||||||
|
|
||||||
public class Endpoint : EndpointWithoutRequest<object>
|
public class Endpoint : EndpointWithoutRequest<object>
|
||||||
{
|
{
|
||||||
public CurrentStatus CurrentStatus { get; set; }
|
public FelicitySolarInverter Inverter { get; set; }
|
||||||
|
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
{
|
{
|
||||||
@ -34,7 +34,7 @@ public class Endpoint : EndpointWithoutRequest<object>
|
|||||||
{
|
{
|
||||||
if (Env.IsDevelopment())
|
if (Env.IsDevelopment())
|
||||||
{
|
{
|
||||||
var status = CurrentStatus.Result;
|
var status = new InverterStatus();
|
||||||
status.OutputVoltage = Random.Shared.Next(240);
|
status.OutputVoltage = Random.Shared.Next(240);
|
||||||
status.LoadWatts = Random.Shared.Next(3500);
|
status.LoadWatts = Random.Shared.Next(3500);
|
||||||
status.LoadPercentage = Random.Shared.Next(100);
|
status.LoadPercentage = Random.Shared.Next(100);
|
||||||
@ -51,7 +51,7 @@ public class Endpoint : EndpointWithoutRequest<object>
|
|||||||
yield return status;
|
yield return status;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
yield return CurrentStatus.Result;
|
yield return Inverter.Status;
|
||||||
|
|
||||||
await Task.Delay(1000, c);
|
await Task.Delay(1000, c);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,12 +41,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Remove="appsettings.Development.json"/>
|
|
||||||
<None Remove="InverterMon-log.db"/>
|
|
||||||
<None Remove="InverterMon.db"/>
|
|
||||||
<None Remove="InverterService\protocol.pdf"/>
|
|
||||||
<None Include="../changelog.md" Link="changelog.md"/>
|
<None Include="../changelog.md" Link="changelog.md"/>
|
||||||
<None Remove="Properties\PublishProfiles\FolderProfile.pubxml"/>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@ -1,8 +1,8 @@
|
|||||||
using InverterMon.Shared.Models;
|
// using InverterMon.Shared.Models;
|
||||||
|
//
|
||||||
namespace InverterMon.Server.InverterService;
|
// namespace InverterMon.Server.InverterService;
|
||||||
|
//
|
||||||
public class CurrentStatus
|
// public class CurrentStatus
|
||||||
{
|
// {
|
||||||
public InverterStatus Result { get; set; }
|
// public InverterStatus Result { get; set; }
|
||||||
}
|
// }
|
||||||
325
src/Server/InverterService/FelicityInverter.cs
Normal file
325
src/Server/InverterService/FelicityInverter.cs
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
using System.Buffers;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO.Ports;
|
||||||
|
using InverterMon.Shared.Models;
|
||||||
|
|
||||||
|
namespace InverterMon.Server.InverterService;
|
||||||
|
|
||||||
|
// sealed class StatusData
|
||||||
|
// {
|
||||||
|
// public int WorkingMode { get; set; }
|
||||||
|
// public int BatteryChargingStage { get; set; }
|
||||||
|
// public double BatteryVoltage { get; set; }
|
||||||
|
// public int BatteryCurrent { get; set; }
|
||||||
|
// public int BatteryPower { get; set; }
|
||||||
|
// public double AcOutputVoltage { get; set; }
|
||||||
|
// public int AcOutputActivePower { get; set; }
|
||||||
|
// public int LoadPercentage { get; set; }
|
||||||
|
// public double PvInputVoltage { get; set; }
|
||||||
|
// public int PvInputPower { get; set; }
|
||||||
|
// }
|
||||||
|
|
||||||
|
sealed class SettingsData
|
||||||
|
{
|
||||||
|
public double BatteryCutOffVoltage { get; set; }
|
||||||
|
public double BatteryCvChargingVoltage { get; set; }
|
||||||
|
public double BatteryFloatingChargingVoltage { get; set; }
|
||||||
|
public double BatteryBackToChargeVoltage { get; set; }
|
||||||
|
public double BatteryBackToDischargeVoltage { get; set; }
|
||||||
|
public byte OutputSourcePriority { get; set; }
|
||||||
|
public byte ChargingSourcePriority { get; set; }
|
||||||
|
public byte MaxChargingCurrent { get; set; }
|
||||||
|
public byte MaxAcChargingCurrent { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("Performance", "CA1822:Mark members as static")]
|
||||||
|
public sealed class FelicitySolarInverter
|
||||||
|
{
|
||||||
|
public InverterStatus Status { get; private set; }
|
||||||
|
|
||||||
|
const byte SlaveAddress = 0x01;
|
||||||
|
static SerialPort _serialPort = null!;
|
||||||
|
|
||||||
|
internal void Connect(string portName)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_serialPort = new(portName, 2400, Parity.None, 8, StopBits.One);
|
||||||
|
_serialPort.Open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static byte[]? _cachedStatusFrame;
|
||||||
|
|
||||||
|
// The status registers we need are located between 0x1101 and 0x112A.
|
||||||
|
// Total registers to read = (0x112A - 0x1101 + 1)
|
||||||
|
const ushort StatusStartAddress = 0x1101;
|
||||||
|
const ushort StatusRegisterCount = 0x112A - 0x1101 + 1; // 42 registers
|
||||||
|
|
||||||
|
internal void UpdateStatus()
|
||||||
|
{
|
||||||
|
var regs = ReadRegisters(StatusStartAddress, StatusRegisterCount);
|
||||||
|
|
||||||
|
// var status = new StatusData
|
||||||
|
// {
|
||||||
|
// WorkingMode = regs[0], // 0x1101: Working mode (offset 0)
|
||||||
|
// BatteryChargingStage = regs[1], // 0x1102: Battery charging stage (offset 1)
|
||||||
|
// };
|
||||||
|
|
||||||
|
Status.BatteryVoltage = regs[7] / 100.0; // 0x1108: Battery voltage (offset 0x1108 - 0x1101 = 7)
|
||||||
|
Status.BatteryDischargeCurrent = regs[8]; // 0x1109: Battery current (offset 8) -- signed value
|
||||||
|
Status.BatteryChargeCurrent = regs[8]; // 0x1109: Battery current (offset 8) -- signed value
|
||||||
|
Status.BatteryDischargeWatts = regs[9]; // 0x110A: Battery power (offset 9) -- signed value
|
||||||
|
Status.BatteryChargeWatts = regs[9]; // 0x110A: Battery power (offset 9) -- signed value
|
||||||
|
Status.OutputVoltage = regs[16] / 10.0; // 0x1111: AC output voltage (offset 0x1111 - 0x1101 = 16)
|
||||||
|
Status.LoadWatts = regs[29]; // 0x111E: AC output active power (offset 0x111E - 0x1101 = 29)
|
||||||
|
Status.LoadPercentage = regs[31]; // 0x1120: Load percentage (offset 0x1120 - 0x1101 = 31)
|
||||||
|
Status.PVInputVoltage = regs[37] / 10.0; // 0x1126: PV input voltage (offset 0x1126 - 0x1101 = 37)
|
||||||
|
Status.PVInputWatt = regs[41]; // 0x112A: PV input power (offset 0x112A - 0x1101 = 41) -- signed value
|
||||||
|
}
|
||||||
|
|
||||||
|
// The settings registers we need are located between 0x211F and 0x2159.
|
||||||
|
// Total registers to read = (0x2159 - 0x211F + 1)
|
||||||
|
const ushort SettingsStartAddress = 0x211F;
|
||||||
|
const ushort SettingsRegisterCount = 0x2159 - 0x211F + 1; // 59 registers
|
||||||
|
|
||||||
|
internal SettingsData ReadSettings()
|
||||||
|
{
|
||||||
|
var regs = ReadRegisters(SettingsStartAddress, SettingsRegisterCount);
|
||||||
|
|
||||||
|
var settings = new SettingsData
|
||||||
|
{
|
||||||
|
BatteryCutOffVoltage = regs[0] / 10.0, // 0x211F: Battery cut-off voltage (offset 0)
|
||||||
|
BatteryCvChargingVoltage = regs[3] / 10.0, // 0x2122: Battery C.V charging voltage (offset = 0x2122 - 0x211F = 3)
|
||||||
|
BatteryFloatingChargingVoltage = regs[4] / 10.0, // 0x2123: Battery floating charging voltage (offset = 4)
|
||||||
|
OutputSourcePriority = (byte)regs[11], // 0x212A: Output source priority (offset = 0x212A - 0x211F = 11)
|
||||||
|
ChargingSourcePriority = (byte)regs[13], // 0x212C: Charging source priority (offset = 0x212C - 0x211F = 13)
|
||||||
|
MaxChargingCurrent = (byte)regs[15], // 0x212E: Max charging current (offset = 15)
|
||||||
|
MaxAcChargingCurrent = (byte)regs[17], // 0x2130: Max AC charging current (offset = 17)
|
||||||
|
BatteryBackToChargeVoltage = regs[55] / 10.0, // 0x2156: Battery back to charge voltage (offset = 0x2156 - 0x211F = 55)
|
||||||
|
BatteryBackToDischargeVoltage = regs[58] / 10.0 // 0x2159: Battery back to discharge voltage (offset = 0x2159 - 0x211F = 58)
|
||||||
|
};
|
||||||
|
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void SetSetting(Setting setting, float value)
|
||||||
|
{
|
||||||
|
ushort registerAddress;
|
||||||
|
|
||||||
|
switch (setting)
|
||||||
|
{
|
||||||
|
case Setting.DischargeCutOff:
|
||||||
|
registerAddress = 0x211F;
|
||||||
|
value *= 10; // scale volts to register value
|
||||||
|
|
||||||
|
break;
|
||||||
|
case Setting.BulkVoltage:
|
||||||
|
registerAddress = 0x2122;
|
||||||
|
value *= 10;
|
||||||
|
|
||||||
|
break;
|
||||||
|
case Setting.FloatVoltage:
|
||||||
|
registerAddress = 0x2123;
|
||||||
|
value *= 10;
|
||||||
|
|
||||||
|
break;
|
||||||
|
case Setting.BackToGrid:
|
||||||
|
registerAddress = 0x2156;
|
||||||
|
value *= 10;
|
||||||
|
|
||||||
|
break;
|
||||||
|
case Setting.BackToBattery:
|
||||||
|
registerAddress = 0x2159;
|
||||||
|
value *= 10;
|
||||||
|
|
||||||
|
break;
|
||||||
|
case Setting.OutputPriority:
|
||||||
|
registerAddress = 0x212A; // No scaling needed for priority values (0,1,2 etc.)
|
||||||
|
|
||||||
|
break;
|
||||||
|
case Setting.ChargePriority:
|
||||||
|
registerAddress = 0x212C;
|
||||||
|
|
||||||
|
break;
|
||||||
|
case Setting.CombinedChargeCurrent:
|
||||||
|
registerAddress = 0x212E; // Value in amperes (1A per unit)
|
||||||
|
|
||||||
|
break;
|
||||||
|
case Setting.UtilityChargeCurrent:
|
||||||
|
registerAddress = 0x2130;
|
||||||
|
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentException("Invalid setting!");
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteSingleRegister(registerAddress, (ushort)value);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void Close()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (_serialPort.IsOpen)
|
||||||
|
_serialPort.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static short[] ReadRegisters(ushort startAddress, ushort numberOfPoints)
|
||||||
|
{
|
||||||
|
// Build Modbus request frame:
|
||||||
|
// [Slave Address][Function Code 0x03][Start Address Hi][Start Address Lo][Quantity Hi][Quantity Lo][CRC Lo][CRC Hi]
|
||||||
|
|
||||||
|
byte[] frame;
|
||||||
|
|
||||||
|
var statusRequest = startAddress == StatusStartAddress && numberOfPoints == StatusRegisterCount;
|
||||||
|
|
||||||
|
if (statusRequest && _cachedStatusFrame is not null)
|
||||||
|
frame = _cachedStatusFrame;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
frame = new byte[8];
|
||||||
|
frame[0] = SlaveAddress;
|
||||||
|
frame[1] = 0x03;
|
||||||
|
frame[2] = (byte)(startAddress >> 8);
|
||||||
|
frame[3] = (byte)(startAddress & 0xFF);
|
||||||
|
frame[4] = (byte)(numberOfPoints >> 8);
|
||||||
|
frame[5] = (byte)(numberOfPoints & 0xFF);
|
||||||
|
var crc = CalculateCrc(frame, 6);
|
||||||
|
frame[6] = (byte)(crc & 0xFF);
|
||||||
|
frame[7] = (byte)(crc >> 8);
|
||||||
|
|
||||||
|
if (statusRequest)
|
||||||
|
_cachedStatusFrame = frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = SendModbusRequest(frame);
|
||||||
|
|
||||||
|
// Expected response structure:
|
||||||
|
// [Slave Address][Function Code][Byte Count][Data...][CRC Lo][CRC Hi]
|
||||||
|
|
||||||
|
int byteCount = response[2];
|
||||||
|
var expectedDataBytes = numberOfPoints * 2;
|
||||||
|
|
||||||
|
if (byteCount != expectedDataBytes)
|
||||||
|
throw new InvalidDataException("Unexpected byte count in response!");
|
||||||
|
|
||||||
|
var registers = new short[numberOfPoints];
|
||||||
|
for (var i = 0; i < numberOfPoints; i++)
|
||||||
|
registers[i] = (short)((response[3 + i * 2] << 8) | response[3 + i * 2 + 1]);
|
||||||
|
|
||||||
|
return registers;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void WriteSingleRegister(ushort registerAddress, ushort value)
|
||||||
|
{
|
||||||
|
// Build request frame:
|
||||||
|
// [Slave Address][Function Code 0x06][Register Address Hi][Register Address Lo]
|
||||||
|
// [Value Hi][Value Lo][CRC Lo][CRC Hi]
|
||||||
|
var frame = new byte[8];
|
||||||
|
frame[0] = SlaveAddress;
|
||||||
|
frame[1] = 0x06;
|
||||||
|
frame[2] = (byte)(registerAddress >> 8);
|
||||||
|
frame[3] = (byte)(registerAddress & 0xFF);
|
||||||
|
frame[4] = (byte)(value >> 8);
|
||||||
|
frame[5] = (byte)(value & 0xFF);
|
||||||
|
var crc = CalculateCrc(frame, 6);
|
||||||
|
frame[6] = (byte)(crc & 0xFF);
|
||||||
|
frame[7] = (byte)(crc >> 8);
|
||||||
|
|
||||||
|
SendModbusRequest(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
static readonly object _lock = new();
|
||||||
|
|
||||||
|
static byte[] SendModbusRequest(byte[] request)
|
||||||
|
{
|
||||||
|
lock (_lock) //prevent concurrent access
|
||||||
|
{
|
||||||
|
_serialPort.DiscardInBuffer();
|
||||||
|
_serialPort.DiscardOutBuffer();
|
||||||
|
|
||||||
|
var oldTimeout = _serialPort.ReadTimeout;
|
||||||
|
var buffer = ArrayPool<byte>.Shared.Rent(256);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_serialPort.ReadTimeout = 1000;
|
||||||
|
_serialPort.Write(request, 0, request.Length);
|
||||||
|
|
||||||
|
var totalBytesRead = ReadBytes(buffer, 0, 3); // Read fixed header (3 bytes)
|
||||||
|
|
||||||
|
if ((buffer[1] & 0x80) != 0) // Handle Modbus exceptions (function code with high bit set)
|
||||||
|
totalBytesRead += ReadBytes(buffer, 3, 2); // Error response: read remaining 2 bytes (CRC)
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Calculate remaining bytes based on function code
|
||||||
|
var bytesToRead = GetRemainingBytes(buffer[1], buffer[2]);
|
||||||
|
totalBytesRead += ReadBytes(buffer, 3, bytesToRead);
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = new byte[totalBytesRead];
|
||||||
|
Buffer.BlockCopy(buffer, 0, response, 0, totalBytesRead);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(buffer);
|
||||||
|
_serialPort.ReadTimeout = oldTimeout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ReadBytes(byte[] b, int offset, int count)
|
||||||
|
{
|
||||||
|
var bytesRead = 0;
|
||||||
|
|
||||||
|
while (bytesRead < count)
|
||||||
|
{
|
||||||
|
var read = _serialPort.Read(b, offset + bytesRead, count - bytesRead);
|
||||||
|
|
||||||
|
if (read == 0)
|
||||||
|
throw new TimeoutException("No data received");
|
||||||
|
|
||||||
|
bytesRead += read;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytesRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int GetRemainingBytes(byte functionCode, byte byteCount)
|
||||||
|
{
|
||||||
|
return functionCode switch
|
||||||
|
{
|
||||||
|
0x03 => 2 + byteCount, // Read holding registers
|
||||||
|
0x06 => 4, // Write single register (fixed 4 bytes)
|
||||||
|
0x10 => 4, // Write multiple registers (fixed 4 bytes)
|
||||||
|
_ => throw new NotSupportedException($"Function code 0x{functionCode:X2} not supported")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static ushort CalculateCrc(byte[] data, int length)
|
||||||
|
{
|
||||||
|
ushort crc = 0xFFFF;
|
||||||
|
|
||||||
|
for (var pos = 0; pos < length; pos++)
|
||||||
|
{
|
||||||
|
crc ^= data[pos];
|
||||||
|
|
||||||
|
for (var i = 0; i < 8; i++)
|
||||||
|
{
|
||||||
|
if ((crc & 0x0001) != 0)
|
||||||
|
{
|
||||||
|
crc >>= 1;
|
||||||
|
crc ^= 0xA001;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
crc >>= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,18 +3,18 @@ using InverterMon.Server.Persistence.Settings;
|
|||||||
|
|
||||||
namespace InverterMon.Server.InverterService;
|
namespace InverterMon.Server.InverterService;
|
||||||
|
|
||||||
class StatusRetriever(Database db, CurrentStatus currentStatus, UserSettings userSettings) : BackgroundService
|
class StatusRetriever(Database db, FelicitySolarInverter inverter, UserSettings userSettings) : BackgroundService
|
||||||
{
|
{
|
||||||
protected override async Task ExecuteAsync(CancellationToken c)
|
protected override async Task ExecuteAsync(CancellationToken c)
|
||||||
{
|
{
|
||||||
while (!c.IsCancellationRequested)
|
while (!c.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
currentStatus.Result.BatteryCapacity = userSettings.BatteryCapacity;
|
inverter.Status.BatteryCapacity = userSettings.BatteryCapacity;
|
||||||
currentStatus.Result.PV_MaxCapacity = userSettings.PV_MaxCapacity;
|
inverter.Status.PV_MaxCapacity = userSettings.PV_MaxCapacity;
|
||||||
|
|
||||||
//todo: get data from inverter and map to CurrentStatus.Result
|
//todo: get data from inverter and map to CurrentStatus.Result
|
||||||
|
|
||||||
_ = db.UpdateTodaysPvGeneration(currentStatus, c);
|
db.UpdateTodaysPvGeneration(c);
|
||||||
|
|
||||||
await Task.Delay(2000);
|
await Task.Delay(2000);
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
src/Server/InverterService/protocol doc/modbus-protocol.pdf
Normal file
BIN
src/Server/InverterService/protocol doc/modbus-protocol.pdf
Normal file
Binary file not shown.
@ -8,15 +8,15 @@ namespace InverterMon.Server.Persistence;
|
|||||||
public class Database
|
public class Database
|
||||||
{
|
{
|
||||||
readonly LiteDatabase _db;
|
readonly LiteDatabase _db;
|
||||||
readonly CurrentStatus _currentStatus;
|
readonly FelicitySolarInverter _inverter;
|
||||||
readonly UserSettings _settings;
|
readonly UserSettings _settings;
|
||||||
readonly ILiteCollection<PVGeneration> _pvGenCollection;
|
readonly ILiteCollection<PVGeneration> _pvGenCollection;
|
||||||
readonly ILiteCollection<UserSettings> _usrSettingsCollection;
|
readonly ILiteCollection<UserSettings> _usrSettingsCollection;
|
||||||
PVGeneration? _today;
|
PVGeneration? _today;
|
||||||
|
|
||||||
public Database(IHostApplicationLifetime lifetime, CurrentStatus status, UserSettings settings)
|
public Database(IHostApplicationLifetime lifetime, FelicitySolarInverter inverter, UserSettings settings)
|
||||||
{
|
{
|
||||||
_currentStatus = status;
|
_inverter = inverter;
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
_db = new("InverterMon.db") { CheckpointSize = 0 };
|
_db = new("InverterMon.db") { CheckpointSize = 0 };
|
||||||
lifetime.ApplicationStopping.Register(() => _db?.Dispose());
|
lifetime.ApplicationStopping.Register(() => _db?.Dispose());
|
||||||
@ -36,18 +36,18 @@ public class Database
|
|||||||
.SingleOrDefault();
|
.SingleOrDefault();
|
||||||
|
|
||||||
if (_today is not null)
|
if (_today is not null)
|
||||||
_currentStatus.Result.RestorePVWattHours(_today.TotalWattHours);
|
_inverter.Status.RestorePVWattHours(_today.TotalWattHours);
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_today = new() { Id = todayDayNumber };
|
_today = new() { Id = todayDayNumber };
|
||||||
_today.SetTotalWattHours(0);
|
_today.SetTotalWattHours(0);
|
||||||
_currentStatus.Result.RestorePVWattHours(0);
|
_inverter.Status.RestorePVWattHours(0);
|
||||||
_pvGenCollection.Insert(_today);
|
_pvGenCollection.Insert(_today);
|
||||||
_db.Checkpoint();
|
_db.Checkpoint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateTodaysPvGeneration(CurrentStatus cmd, CancellationToken c)
|
public void UpdateTodaysPvGeneration(CancellationToken c)
|
||||||
{
|
{
|
||||||
var hourNow = DateTime.Now.Hour;
|
var hourNow = DateTime.Now.Hour;
|
||||||
|
|
||||||
@ -58,13 +58,13 @@ public class Database
|
|||||||
|
|
||||||
if (_today?.Id == todayDayNumber)
|
if (_today?.Id == todayDayNumber)
|
||||||
{
|
{
|
||||||
_today.SetWattPeaks(cmd.Result.PVInputWatt);
|
_today.SetWattPeaks(_inverter.Status.PVInputWatt);
|
||||||
_today.SetTotalWattHours(cmd.Result.PVInputWattHour);
|
_today.SetTotalWattHours(_inverter.Status.PVInputWattHour);
|
||||||
_pvGenCollection.Update(_today);
|
_pvGenCollection.Update(_today);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
cmd.Result.ResetPVWattHourAccumulation(); //it's a new day. start accumulation from scratch.
|
_inverter.Status.ResetPVWattHourAccumulation(); //it's a new day. start accumulation from scratch.
|
||||||
_today = new() { Id = todayDayNumber };
|
_today = new() { Id = todayDayNumber };
|
||||||
_today.SetTotalWattHours(0);
|
_today.SetTotalWattHours(0);
|
||||||
_pvGenCollection.Insert(_today);
|
_pvGenCollection.Insert(_today);
|
||||||
|
|||||||
@ -4,7 +4,7 @@ public class PVGeneration
|
|||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public Dictionary<string, int> WattPeaks { get; set; } = new();
|
public Dictionary<string, int> WattPeaks { get; set; } = new();
|
||||||
public decimal TotalWattHours { get; set; }
|
public double TotalWattHours { get; set; }
|
||||||
|
|
||||||
public void SetWattPeaks(int newValue)
|
public void SetWattPeaks(int newValue)
|
||||||
{
|
{
|
||||||
@ -19,7 +19,7 @@ public class PVGeneration
|
|||||||
WattPeaks[key] = newValue;
|
WattPeaks[key] = newValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetTotalWattHours(decimal totalWattHours)
|
public void SetTotalWattHours(double totalWattHours)
|
||||||
{
|
{
|
||||||
TotalWattHours = totalWattHours;
|
TotalWattHours = totalWattHours;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,6 @@ _ = int.TryParse(bld.Configuration["LaunchSettings:WebPort"] ?? "80", out var po
|
|||||||
bld.WebHost.ConfigureKestrel(o => o.Listen(IPAddress.Any, port));
|
bld.WebHost.ConfigureKestrel(o => o.Listen(IPAddress.Any, port));
|
||||||
|
|
||||||
bld.Services
|
bld.Services
|
||||||
.AddSingleton<CurrentStatus>()
|
|
||||||
.AddSingleton<UserSettings>()
|
.AddSingleton<UserSettings>()
|
||||||
.AddSingleton<Database>()
|
.AddSingleton<Database>()
|
||||||
.AddSingleton<JkBms>();
|
.AddSingleton<JkBms>();
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -2,9 +2,7 @@
|
|||||||
"LaunchSettings": {
|
"LaunchSettings": {
|
||||||
"DeviceAddress": "/dev/ttyUSB1",
|
"DeviceAddress": "/dev/ttyUSB1",
|
||||||
"JkBmsAddress": "/dev/ttyUSB0",
|
"JkBmsAddress": "/dev/ttyUSB0",
|
||||||
"WebPort": 80,
|
"WebPort": 80
|
||||||
"TroubleMode": "no",
|
|
||||||
"MppSolarPath": "/usr/local/bin/mpp-solar"
|
|
||||||
},
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
|
|||||||
@ -8,16 +8,16 @@ public class InverterStatus
|
|||||||
public int BatteryCapacity { get; set; } = 100;
|
public int BatteryCapacity { get; set; } = 100;
|
||||||
|
|
||||||
[JsonPropertyName("b")]
|
[JsonPropertyName("b")]
|
||||||
public decimal BatteryChargeCRate => BatteryChargeCurrent == 0 ? 0 : Convert.ToDecimal(BatteryChargeCurrent) / BatteryCapacity;
|
public double BatteryChargeCRate => BatteryChargeCurrent == 0 ? 0 : Convert.ToDouble(BatteryChargeCurrent) / BatteryCapacity;
|
||||||
|
|
||||||
[JsonPropertyName("c")]
|
[JsonPropertyName("c")]
|
||||||
public int BatteryChargeCurrent { get; set; }
|
public int BatteryChargeCurrent { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("d")]
|
[JsonPropertyName("d")]
|
||||||
public int BatteryChargeWatts => BatteryChargeCurrent == 0 ? 0 : Convert.ToInt32(BatteryChargeCurrent * BatteryVoltage);
|
public int BatteryChargeWatts { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("e")]
|
[JsonPropertyName("e")]
|
||||||
public decimal BatteryDischargeCRate => BatteryDischargeCurrent == 0 ? 0 : Convert.ToDecimal(BatteryDischargeCurrent) / BatteryCapacity;
|
public double BatteryDischargeCRate => BatteryDischargeCurrent == 0 ? 0 : Convert.ToDouble(BatteryDischargeCurrent) / BatteryCapacity;
|
||||||
|
|
||||||
[JsonPropertyName("f")]
|
[JsonPropertyName("f")]
|
||||||
public int BatteryDischargeCurrent { get; set; }
|
public int BatteryDischargeCurrent { get; set; }
|
||||||
@ -26,37 +26,37 @@ public class InverterStatus
|
|||||||
public int BatteryDischargePotential => BatteryDischargeCurrent > 0 ? Convert.ToInt32(Convert.ToDouble(BatteryDischargeCurrent) / BatteryCapacity * 100) : 0;
|
public int BatteryDischargePotential => BatteryDischargeCurrent > 0 ? Convert.ToInt32(Convert.ToDouble(BatteryDischargeCurrent) / BatteryCapacity * 100) : 0;
|
||||||
|
|
||||||
[JsonPropertyName("h")]
|
[JsonPropertyName("h")]
|
||||||
public int BatteryDischargeWatts => BatteryDischargeCurrent == 0 ? 0 : Convert.ToInt32(BatteryDischargeCurrent * BatteryVoltage);
|
public int BatteryDischargeWatts { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("i")]
|
[JsonPropertyName("i")]
|
||||||
public decimal BatteryVoltage { get; set; }
|
public double BatteryVoltage { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("j")]
|
[JsonPropertyName("j")]
|
||||||
public int GridUsageWatts => GridVoltage < 10 ? 0 : LoadWatts + BatteryChargeWatts - (PVInputWatt + BatteryDischargeWatts);
|
public int GridUsageWatts => GridVoltage < 10 ? 0 : LoadWatts + BatteryChargeWatts - (PVInputWatt + BatteryDischargeWatts);
|
||||||
|
|
||||||
[JsonPropertyName("k")]
|
[JsonPropertyName("k")]
|
||||||
public decimal GridVoltage { get; set; }
|
public double GridVoltage { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("l")]
|
[JsonPropertyName("l")]
|
||||||
public int HeatSinkTemperature { get; set; }
|
public int HeatSinkTemperature { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("m")]
|
[JsonPropertyName("m")]
|
||||||
public decimal LoadCurrent => LoadWatts == 0 ? 0 : LoadWatts / OutputVoltage;
|
public double LoadCurrent => LoadWatts == 0 ? 0 : LoadWatts / OutputVoltage;
|
||||||
|
|
||||||
[JsonPropertyName("n")]
|
[JsonPropertyName("n")]
|
||||||
public decimal LoadPercentage { get; set; }
|
public int LoadPercentage { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("o")]
|
[JsonPropertyName("o")]
|
||||||
public int LoadWatts { get; set; }
|
public int LoadWatts { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("p")]
|
[JsonPropertyName("p")]
|
||||||
public decimal OutputVoltage { get; set; }
|
public double OutputVoltage { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("q")]
|
[JsonPropertyName("q")]
|
||||||
public decimal PVInputCurrent { get; set; }
|
public double PVInputCurrent { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("r")]
|
[JsonPropertyName("r")]
|
||||||
public decimal PVInputVoltage { get; set; }
|
public double PVInputVoltage { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("s")]
|
[JsonPropertyName("s")]
|
||||||
public int PVInputWatt
|
public int PVInputWatt
|
||||||
@ -69,13 +69,13 @@ public class InverterStatus
|
|||||||
|
|
||||||
pvInputWatt = value;
|
pvInputWatt = value;
|
||||||
var interval = (DateTime.Now - pvInputWattHourLastComputed).TotalSeconds;
|
var interval = (DateTime.Now - pvInputWattHourLastComputed).TotalSeconds;
|
||||||
PVInputWattHour += value / (3600 / Convert.ToDecimal(interval));
|
PVInputWattHour += value / (3600 / Convert.ToDouble(interval));
|
||||||
pvInputWattHourLastComputed = DateTime.Now;
|
pvInputWattHourLastComputed = DateTime.Now;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonPropertyName("t")]
|
[JsonPropertyName("t")]
|
||||||
public decimal PVInputWattHour { get; private set; }
|
public double PVInputWattHour { get; private set; }
|
||||||
|
|
||||||
[JsonPropertyName("u")]
|
[JsonPropertyName("u")]
|
||||||
public int PV_MaxCapacity { get; set; }
|
public int PV_MaxCapacity { get; set; }
|
||||||
@ -86,7 +86,7 @@ public class InverterStatus
|
|||||||
int pvInputWatt;
|
int pvInputWatt;
|
||||||
DateTime pvInputWattHourLastComputed;
|
DateTime pvInputWattHourLastComputed;
|
||||||
|
|
||||||
public void RestorePVWattHours(decimal accruedWattHours)
|
public void RestorePVWattHours(double accruedWattHours)
|
||||||
{
|
{
|
||||||
PVInputWattHour = accruedWattHours;
|
PVInputWattHour = accruedWattHours;
|
||||||
pvInputWattHourLastComputed = DateTime.Now;
|
pvInputWattHourLastComputed = DateTime.Now;
|
||||||
|
|||||||
@ -6,7 +6,7 @@ public class PVDay
|
|||||||
{
|
{
|
||||||
public int DayNumber { get; set; }
|
public int DayNumber { get; set; }
|
||||||
public string DayName { get; set; }
|
public string DayName { get; set; }
|
||||||
public decimal TotalKiloWattHours { get; set; }
|
public double TotalKiloWattHours { get; set; }
|
||||||
public IEnumerable<WattPeak> WattPeaks { get; set; }
|
public IEnumerable<WattPeak> WattPeaks { get; set; }
|
||||||
public int GraphTickCount { get; set; }
|
public int GraphTickCount { get; set; }
|
||||||
public int[] GraphRange { get; set; }
|
public int[] GraphRange { get; set; }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user