set settings

This commit is contained in:
djnitehawk 2025-03-12 21:21:13 +05:30 committed by Dĵ ΝιΓΞΗΛψΚ
parent b642498a60
commit ef7fff7302
15 changed files with 226 additions and 174 deletions

View File

@ -7,7 +7,7 @@
<PageTitle>JK BMS Status</PageTitle>
<Loader Enabled=@(status is null) />
<Loader Enabled=@(status is null)/>
@if (status is not null)
{
@ -50,7 +50,7 @@
@if (status.AvgCurrentAmps == 0)
{
<div class="fs-5 m-0 p-0 text-muted">
Holding<br />Voltage
Holding<br/>Voltage
</div>
}
@if (status.IsWarning)
@ -140,7 +140,8 @@
protected override async Task OnInitializedAsync()
{
var st = await Js.LoadStateAsync<ClientSettings>();
if(st is null)
if (st is null)
{
await Js.SaveStateAsync(state);
}
@ -168,6 +169,7 @@
{
return status!.GetTimeString();
}
return $"{status!.TimeHrs} Hrs {status.TimeMins} Mins";
}
@ -175,10 +177,12 @@
{
if (state.ShowCapacityKwh)
{
var avlCap = Math.Round((status!.AvailableCapacity * status.PackNominalVoltage) / 1000, 1);
var packCap = Math.Round((status!.PackCapacity * status.PackNominalVoltage) / 1000, 1);
var avlCap = Math.Round(status!.AvailableCapacity * status.PackNominalVoltage / 1000, 1);
var packCap = Math.Round(status!.PackCapacity * status.PackNominalVoltage / 1000, 1);
return $"{avlCap} kWh / {packCap} kWh";
}
return $"{Math.Round(status!.AvailableCapacity, 1)} Ah / {status!.PackCapacity} Ah";
}
@ -207,11 +211,9 @@
// and it leads to a new stream download being created everytime a page is initialized.
// which leads to a memory leak/ connection exhaustion.
using var client = new HttpClient
{
BaseAddress = new(basePath),
Timeout = TimeSpan.FromSeconds(5)
};
using var client = new HttpClient();
client.BaseAddress = new(basePath);
client.Timeout = TimeSpan.FromSeconds(5);
var retryDelay = 1000;

View File

@ -265,7 +265,7 @@
@code{
private CurrentSettings? settings;
private Button currentButton = Button.None;
private Button? currentButton;
private bool isSuccess;
protected override async Task OnInitializedAsync()
@ -274,7 +274,7 @@
StateHasChanged();
}
private async Task SetChargePriority(string priority)
private async Task SetChargePriority(byte priority)
{
isSuccess = false;
@ -297,7 +297,7 @@
break;
default:
currentButton = Button.None;
currentButton = null;
break;
}
@ -310,7 +310,7 @@
}
}
private async Task SetOutputPriority(string priority)
private async Task SetOutputPriority(byte priority)
{
isSuccess = false;
@ -319,7 +319,7 @@
OutputPriority.SolarFirst => Button.OpSolarFirst,
OutputPriority.SolarBatteryUtility => Button.OpSolarBatteryUtility,
OutputPriority.UtilityFirst => Button.OpUtilityFirst,
_ => Button.None
_ => null
};
if (await Http.GetStringAsync($"api/settings/set-setting/{Setting.OutputPriority}/{priority}") == "true")
@ -332,7 +332,7 @@
private async Task SetVoltage(Setting setting)
{
isSuccess = false;
decimal value = 0;
double value = 0;
switch (setting)
{
@ -362,19 +362,19 @@
break;
default:
currentButton = Button.None;
currentButton = null;
break;
}
;
if (await Http.GetStringAsync($"api/settings/set-setting/{setting}/{value:00.0}") == "true")
if (await Http.GetStringAsync($"api/settings/set-setting/{setting}/{value}") == "true")
{
isSuccess = true;
}
}
private async Task SetSetting(Setting settingName, string value)
private async Task SetSetting(Setting settingName, byte value)
{
if (await Http.GetStringAsync($"api/settings/set-setting/{settingName}/{value}") == "true")
{
@ -382,7 +382,7 @@
}
}
private void UpdateLocalSetting(Setting settingName, string value)
private void UpdateLocalSetting(Setting settingName, byte value)
{
switch (settingName)
{
@ -411,7 +411,7 @@
isSuccess = false;
await Http.PostAsJsonAsync("api/settings/set-system-spec", settings!.SystemSpec);
isSuccess = true;
currentButton = Button.None;
currentButton = null;
}
private string Spinner(Button button)
@ -424,7 +424,7 @@
? "visually-hidden"
: "";
private string Success(Button button, string currentValue, string settingValue)
private string Success(Button button, byte currentValue, byte settingValue)
=> (currentButton == button && isSuccess) || currentValue == settingValue
? "oi oi-circle-check text-success"
: "";
@ -432,16 +432,15 @@
private string Sanitize(string value)
=> value.StartsWith("0") ? value[1..] : value;
private enum Button
private enum Button : byte
{
None = 0,
ChOnlySolar = 1,
ChSolarFirst = 2,
ChSolarAndUtility = 3,
ChUtilityFirst = 4,
OpUtilityFirst = 5,
OpSolarFirst = 6,
OpSolarBatteryUtility = 7,
OpUtilityFirst = 1, //never change this value
OpSolarFirst = 2, //never change this value
OpSolarBatteryUtility = 3, //never change this value
ChOnlySolar = 4,
ChSolarFirst = 5,
ChSolarAndUtility = 6,
ChUtilityFirst = 7,
UpdateUserSettings = 8,
BackToGridVoltage = 9,
BackToBattery = 10,

View File

@ -6,7 +6,7 @@ namespace InverterMon.Server.Endpoints.GetStatus;
public class Endpoint : EndpointWithoutRequest<object>
{
public FelicitySolarInverter Inverter { get; set; }
public FelicitySolarInverter Inverter { get; set; } = null!;
public override void Configure()
{
@ -28,25 +28,25 @@ public class Endpoint : EndpointWithoutRequest<object>
async IAsyncEnumerable<InverterStatus> GetDataStream([EnumeratorCancellation] CancellationToken c)
{
var blank = new InverterStatus();
while (!c.IsCancellationRequested)
{
if (Env.IsDevelopment())
{
var status = new InverterStatus();
status.OutputVoltage = Random.Shared.Next(240);
status.LoadWatts = Random.Shared.Next(3500);
status.LoadPercentage = Random.Shared.Next(100);
status.BatteryVoltage = Random.Shared.Next(24);
status.BatteryChargeCurrent = Random.Shared.Next(20);
status.BatteryDischargeCurrent = Random.Shared.Next(300);
status.HeatSinkTemperature = Random.Shared.Next(300);
status.PVInputCurrent = Random.Shared.Next(300);
status.PVInputVoltage = Random.Shared.Next(300);
status.PVInputWatt = Random.Shared.Next(1000);
status.PV_MaxCapacity = 1000;
status.BatteryCapacity = 100;
var status = new InverterStatus
{
OutputVoltage = Random.Shared.Next(240),
LoadWatts = Random.Shared.Next(3500),
LoadPercentage = Random.Shared.Next(100),
BatteryVoltage = Random.Shared.Next(24),
BatteryChargeCurrent = Random.Shared.Next(20),
BatteryDischargeCurrent = Random.Shared.Next(300),
HeatSinkTemperature = Random.Shared.Next(300),
PVInputCurrent = Random.Shared.Next(300),
PVInputVoltage = Random.Shared.Next(300),
PVInputWatt = Random.Shared.Next(1000),
PV_MaxCapacity = 1000,
BatteryCapacity = 100
};
yield return status;
}

View File

@ -4,6 +4,11 @@ using InverterMon.Shared.Models;
namespace InverterMon.Server.Endpoints.PVLog.GetPVForDay;
public class Request
{
public int DayNumber { get; set; }
}
public class Endpoint : Endpoint<Request, PVDay>
{
public Database Db { get; set; }
@ -21,7 +26,7 @@ public class Endpoint : Endpoint<Request, PVDay>
if (pvDay is null)
{
await SendNotFoundAsync();
await SendNotFoundAsync(c);
return;
}
@ -34,8 +39,6 @@ public class Endpoint : Endpoint<Request, PVDay>
TotalWattHours = Random.Shared.Next(3000)
};
//pvDay.AllocateBuckets(6, 18);
for (var i = 0; i < 97; i++)
pvDay.WattPeaks.Add(i.ToString(), Random.Shared.Next(2000));
}

View File

@ -1,6 +0,0 @@
namespace InverterMon.Server.Endpoints.PVLog.GetPVForDay;
public class Request
{
public int DayNumber { get; set; }
}

View File

@ -1,4 +1,5 @@
using InverterMon.Server.Persistence.Settings;
using InverterMon.Server.InverterService;
using InverterMon.Server.Persistence.Settings;
using InverterMon.Shared.Models;
namespace InverterMon.Server.Endpoints.Settings.GetSettingValues;
@ -6,6 +7,7 @@ namespace InverterMon.Server.Endpoints.Settings.GetSettingValues;
public class Endpoint : EndpointWithoutRequest<CurrentSettings>
{
public UserSettings UserSettings { get; set; }
public FelicitySolarInverter Inverter { get; set; }
public override void Configure()
{
@ -15,29 +17,48 @@ public class Endpoint : EndpointWithoutRequest<CurrentSettings>
public override async Task HandleAsync(CancellationToken c)
{
//todo: get values from inverter and send to client
if (Env.IsDevelopment())
{
var res = new CurrentSettings
{
BackToBatteryVoltage = 48.1,
BackToGridVoltage = 48.2,
FloatChargeVoltage = 48.3,
ChargePriority = ChargePriority.OnlySolar,
DischargeCuttOffVoltage = 48.4,
BulkChargeVoltage = 48.5,
MaxACChargeCurrent = 10,
MaxCombinedChargeCurrent = 20,
OutputPriority = OutputPriority.SolarFirst,
SystemSpec = UserSettings.ToSystemSpec()
};
await SendAsync(res, cancellation: c);
// var cmd = new GetSettings();
// cmd.Result.SystemSpec = UserSettings.ToSystemSpec();
//
// if (Env.IsDevelopment())
// {
// cmd.Result.ChargePriority = "03";
// cmd.Result.MaxACChargeCurrent = "10";
// cmd.Result.MaxCombinedChargeCurrent = "020";
// cmd.Result.OutputPriority = "02";
// cmd.Result.BulkChargeVoltage = 27.1m;
// await SendAsync(cmd.Result);
// return;
// }
//
// Queue.AddCommands(cmd);
//
// await cmd.WhileProcessing(c);
//
// if (cmd.IsComplete)
// await SendAsync(cmd.Result);
// else
// ThrowError("Unable to read settings in a timely manner!");
return;
}
try
{
var data = Inverter.ReadSettings();
var res = new CurrentSettings
{
BackToBatteryVoltage = data.BatteryBackToDischargeVoltage,
BackToGridVoltage = data.BatteryBackToChargeVoltage,
BulkChargeVoltage = data.BatteryCvChargingVoltage,
ChargePriority = data.ChargingSourcePriority,
DischargeCuttOffVoltage = data.BatteryCutOffVoltage,
FloatChargeVoltage = data.BatteryFloatingChargingVoltage,
MaxACChargeCurrent = data.MaxAcChargingCurrent,
MaxCombinedChargeCurrent = data.MaxChargingCurrent,
OutputPriority = data.OutputSourcePriority,
SystemSpec = UserSettings.ToSystemSpec()
};
await SendAsync(res, cancellation: c);
}
catch (Exception e)
{
Logger.LogError("Unable to read settings from inverter. Details: [{msg}]", e.Message);
ThrowError("Unable to read settings from inverter!");
}
}
}

View File

@ -1,20 +1,33 @@
namespace InverterMon.Server.Endpoints.Settings.SetSettingValue;
using InverterMon.Server.InverterService;
namespace InverterMon.Server.Endpoints.Settings.SetSettingValue;
public class Endpoint : Endpoint<Shared.Models.SetSetting, bool>
{
public FelicitySolarInverter Inverter { get; set; }
public override void Configure()
{
Get("settings/set-setting/{Command}/{Value}");
Get("settings/set-setting/{Setting}/{Value}");
AllowAnonymous();
}
public override async Task HandleAsync(Shared.Models.SetSetting r, CancellationToken c)
{
//todo: set settings using inveter
if (Env.IsDevelopment())
{
await SendAsync(true, cancellation: c);
// var cmd = new InverterService.Commands.SetSetting(r.Command, r.Value);
// Queue.AddCommands(cmd);
// await cmd.WhileProcessing(c);
// await SendAsync(cmd.Result);
return;
}
try
{
Inverter.SetSetting(r.Setting, r.Value);
}
catch
{
await SendAsync(false, cancellation: c);
}
}
}

View File

@ -1,8 +0,0 @@
// using InverterMon.Shared.Models;
//
// namespace InverterMon.Server.InverterService;
//
// public class CurrentStatus
// {
// public InverterStatus Result { get; set; }
// }

View File

@ -5,20 +5,6 @@ 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; }
@ -32,21 +18,32 @@ sealed class SettingsData
public byte MaxAcChargingCurrent { get; set; }
}
[SuppressMessage("Performance", "CA1822:Mark members as static")]
[SuppressMessage("Performance", "CA1822:Mark members as static"),
SuppressMessage("ReSharper", "MemberCanBeMadeStatic.Global")]
public sealed class FelicitySolarInverter
{
public InverterStatus Status { get; private set; }
public InverterStatus Status { get; } = new();
const byte SlaveAddress = 0x01;
static SerialPort _serialPort = null!;
internal void Connect(string portName)
internal bool Connect(string portName)
{
lock (_lock)
{
_serialPort = new(portName, 2400, Parity.None, 8, StopBits.One);
try
{
_serialPort.Open();
}
catch
{
return false;
}
}
return true;
}
static byte[]? _cachedStatusFrame;
@ -60,11 +57,8 @@ public sealed class FelicitySolarInverter
{
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
@ -103,7 +97,7 @@ public sealed class FelicitySolarInverter
return settings;
}
internal void SetSetting(Setting setting, float value)
internal void SetSetting(Setting setting, double value)
{
ushort registerAddress;
@ -154,7 +148,23 @@ public sealed class FelicitySolarInverter
throw new ArgumentException("Invalid setting!");
}
WriteSingleRegister(registerAddress, (ushort)value);
var settingValue = (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)(settingValue >> 8);
frame[5] = (byte)(settingValue & 0xFF);
var crc = CalculateCrc(frame, 6);
frame[6] = (byte)(crc & 0xFF);
frame[7] = (byte)(crc >> 8);
SendModbusRequest(frame);
}
internal void Close()
@ -212,25 +222,6 @@ public sealed class FelicitySolarInverter
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)

View File

@ -3,16 +3,35 @@ using InverterMon.Server.Persistence.Settings;
namespace InverterMon.Server.InverterService;
class StatusRetriever(Database db, FelicitySolarInverter inverter, UserSettings userSettings) : BackgroundService
class StatusRetriever(Database db, FelicitySolarInverter inverter, UserSettings userSettings, IConfiguration config, ILogger<StatusRetriever> log)
: BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken c)
{
var port = config["LaunchSettings:DeviceAddress"] ?? throw new ArgumentException("Device address not specified in appsettings.json file!");
while (!inverter.Connect(port))
{
log.LogCritical("Unable to connect to the inverter at [{port}]. Retrying in 5 seconds...", port);
await Task.Delay(5000);
}
while (!c.IsCancellationRequested)
{
inverter.Status.BatteryCapacity = userSettings.BatteryCapacity;
inverter.Status.PV_MaxCapacity = userSettings.PV_MaxCapacity;
//todo: get data from inverter and map to CurrentStatus.Result
try
{
inverter.UpdateStatus();
}
catch (Exception e)
{
log.LogError("Error while reading inverter status data! Details: [{msg}]", e.Message);
await Task.Delay(2000);
continue;
}
db.UpdateTodaysPvGeneration(c);

View File

@ -20,6 +20,7 @@ bld.WebHost.ConfigureKestrel(o => o.Listen(IPAddress.Any, port));
bld.Services
.AddSingleton<UserSettings>()
.AddSingleton<Database>()
.AddSingleton<FelicitySolarInverter>()
.AddSingleton<JkBms>();
if (!bld.Environment.IsDevelopment())

View File

@ -2,8 +2,8 @@
public static class ChargePriority
{
public const string SolarFirst = "1";
public const string SolarAndUtility = "2";
public const string OnlySolar = "3";
public const string UtilityFirst = "0";
public const byte SolarFirst = 1;
public const byte SolarAndUtility = 2;
public const byte OnlySolar = 3;
public const byte UtilityFirst = 0;
}

View File

@ -2,39 +2,56 @@
public class CurrentSettings
{
public string ChargePriority { get; set; } = "000";
public string OutputPriority { get; set; } = "000";
public string MaxACChargeCurrent { get; set; } = "000";
public string MaxCombinedChargeCurrent { get; set; } = "000";
public byte ChargePriority { get; set; }
public byte OutputPriority { get; set; }
public byte MaxACChargeCurrent { get; set; }
public byte MaxCombinedChargeCurrent { get; set; }
decimal _backToGrid;
public decimal BackToGridVoltage { get => _backToGrid; set => _backToGrid = RoundToHalfPoints(value); }
double _backToGrid;
decimal _dischargeCuttOff;
public decimal DischargeCuttOffVoltage { get => _dischargeCuttOff; set => _dischargeCuttOff = RoundToOneDecimalPoint(value); }
public double BackToGridVoltage
{
get => _backToGrid;
set => _backToGrid = value; //RoundToHalfPoints(value);
}
decimal _bulkVoltage;
public decimal BulkChargeVoltage
double _dischargeCuttOff;
public double DischargeCuttOffVoltage
{
get => _dischargeCuttOff;
set => _dischargeCuttOff = value; // RoundToOneDecimalPoint(value);
}
double _bulkVoltage;
public double BulkChargeVoltage
{
get => _bulkVoltage;
set => _bulkVoltage = RoundToOneDecimalPoint(value < _floatVoltage ? _floatVoltage : value);
set => _bulkVoltage = value; // RoundToOneDecimalPoint(value < _floatVoltage ? _floatVoltage : value);
}
decimal _floatVoltage;
public decimal FloatChargeVoltage
double _floatVoltage;
public double FloatChargeVoltage
{
get => _floatVoltage;
set => _floatVoltage = RoundToOneDecimalPoint(value > _bulkVoltage ? _bulkVoltage : value);
set => _floatVoltage = value; // RoundToOneDecimalPoint(value > _bulkVoltage ? _bulkVoltage : value);
}
decimal _backToBattery;
public decimal BackToBatteryVoltage { get => _backToBattery; set => _backToBattery = RoundToHalfPoints(value); }
double _backToBattery;
public double BackToBatteryVoltage
{
get => _backToBattery;
set => _backToBattery = value; //RoundToHalfPoints(value);
}
public SystemSpec SystemSpec { get; set; } = new();
static decimal RoundToHalfPoints(decimal value)
=> Math.Round(value * 2, MidpointRounding.AwayFromZero) / 2;
// static double RoundToHalfPoints(double value)
// => Math.Round(value * 2, MidpointRounding.AwayFromZero) / 2;
static decimal RoundToOneDecimalPoint(decimal value)
=> Math.Round(value, 1, MidpointRounding.AwayFromZero);
// static double RoundToOneDecimalPoint(double value)
// => Math.Round(value, 1, MidpointRounding.AwayFromZero);
}

View File

@ -2,7 +2,7 @@
public static class OutputPriority
{
public const string SolarFirst = "1";
public const string SolarBatteryUtility = "2";
public const string UtilityFirst = "0";
public const byte SolarFirst = 1;
public const byte SolarBatteryUtility = 2;
public const byte UtilityFirst = 0;
}

View File

@ -2,6 +2,6 @@
public class SetSetting
{
public string Command { get; set; }
public string Value { get; set; }
public Setting Setting { get; set; }
public double Value { get; set; }
}