Felicity-Inverter-Monitor/src/Client/Pages/Index.razor
2025-03-13 10:13:25 +05:30

266 lines
11 KiB
Plaintext

@page "/"
@using InverterMon.Shared.Models
@using System.Text.Json
@implements IDisposable
<PageTitle>Dashboard</PageTitle>
<Loader Enabled=@(status is null)/>
@if (status is not null)
{
<div class="container">
@if (status?.GridUsageWatts > 100)
{
<div class="row">
<div class="col-sm-12">
<div class="card">
<h5 class="card-header">
<span class="oi oi-power-standby" aria-hidden="true"></span>
Grid Usage
</h5>
<div class="card-body pt-0">
<div class="container text-center fw-bold p-0">
<div class="row bg-light rounded">
<div class="col">
<div class="fs-1 text-danger">@status?.GridUsageWatts</div>
W
</div>
</div>
</div>
</div>
</div>
</div>
</div>
}
<div class="row mt-2">
<div class="col-sm-6">
<div class="card">
<h5 class="card-header">
<span class="oi oi-lightbulb"></span>
<span>Output Load</span>
</h5>
<div class="card-body pt-0">
<div class="container text-center fw-bold p-0">
<div class="row bg-light rounded">
<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>
</div>
<div class="col">
<div class="fs-1 text-danger">@status?.LoadWatts</div>
W
</div>
</div>
<div class="row">
<div class="col">
<div class="fs-5 text-muted">@RoundToWholeNumber(status?.OutputVoltage)</div>
<span class="text-body">
V
</span>
</div>
<div class="col">
<div class="row h-100 align-content-center">
<span class="fs-6 text-muted">
@(status?.WorkingMode)
</span>
</div>
</div>
<div class="col">
<div class="fs-5 text-muted">@RoundToOneDecimal(status?.LoadCurrent)</div>
A
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="card mt-2 mt-sm-0">
<h5 class="card-header">
<span class="oi oi-sun" aria-hidden="true"/>
<span>Solar Power</span>
</h5>
<div class="card-body pt-0">
<div class="container text-center fw-bold p-0">
<div class="row bg-light rounded">
<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>
</div>
<div class="col">
<div class="fs-1 text-success">@status?.PVInputWatt</div>
W
</div>
</div>
<div class="row">
<div class="col">
<div class="fs-5 text-muted">@RoundToWholeNumber(status?.PVInputVoltage)</div>
<span class="text-body">
V
</span>
</div>
<div class="col">
<div class="fs-5 text-muted">@RoundToOneDecimal(status?.PVInputCurrent)</div>
A
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="container pt-2">
<div class="row">
<div class="col-sm-12">
<div class="card">
<h5 class="card-header align-self-center w-100">
<span class="oi oi-battery-empty" aria-hidden="true"/>
Battery
</h5>
<div class="card-body p-0 m-0">
<div class="container text-center m-0 p-0">
<div class="row m-0 p-0">
<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>
</div>
</div>
<div class="row mt-2">
<div class="col-4">
<span class="fs-6 fw-bold text-muted charge-discharge">Charging</span>
<div class="fw-bold">
<div class="fs-1 text-danger">@status?.BatteryChargeWatts</div>
W
<div class="fw-bold">
<div class="fs-5 text-muted">@RoundToOneDecimal(status?.BatteryChargeCurrent)</div>
A
</div>
</div>
</div>
<div class="col-4">
<div class="container bg-light rounded-1 p-1 m-0 fw-bold text-muted fs-1">
<div class="fs-4 voltage">Voltage</div>
<div class="text-black charge-discharge">@RoundToOneDecimal(status?.BatteryVoltage)</div>
<div class="fs-4 text-black-50">V</div>
<div class="fs-6 bg-white text-dark">@GetCRate() C</div>
</div>
</div>
<div class="col-4">
<span class="fs-6 fw-bold text-muted charge-discharge">Discharging</span>
<div class="fw-bold">
<div class="fs-1 text-success">@status?.BatteryDischargeWatts</div>
W
<div class="fw-bold">
<div class="fs-5 text-muted">@status?.BatteryDischargeCurrent</div>
A
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
}
@code{
private static event Action<InverterStatus?>? onStatusUpdated;
private static event Action? onStatusRetrievalError;
private static InverterStatus? status;
protected override void OnInitialized()
{
onStatusUpdated += UpdateState;
onStatusRetrievalError += NullifyStatus;
}
private void NullifyStatus()
{
status = null;
StateHasChanged();
}
private void UpdateState(InverterStatus? s)
{
status = s;
StateHasChanged();
}
public void Dispose()
{
onStatusUpdated -= UpdateState;
onStatusRetrievalError -= NullifyStatus;
}
private static double RoundToWholeNumber(double? val)
=> Math.Round(val ?? 0, 0);
private static double RoundToOneDecimal(double? val)
=> Math.Round(val ?? 0, 1);
public static async Task StartStatusStreaming(string basePath)
{
//note: only reason we have a full-time stream download is because there's a bug in
// blazor-wasm that doesn't close the fetch http requests when streaming is involved.
// 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();
client.BaseAddress = new(basePath);
client.Timeout = TimeSpan.FromSeconds(5);
var retryDelay = 1000;
while (true)
{
try
{
using var request = new HttpRequestMessage(HttpMethod.Get, "api/status");
request.SetBrowserResponseStreamingEnabled(true);
using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();
using var stream = await response.Content.ReadAsStreamAsync();
await foreach (var s in
JsonSerializer.DeserializeAsyncEnumerable<InverterStatus>(
stream,
new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
DefaultBufferSize = 64
}))
{
onStatusUpdated?.Invoke(s);
retryDelay = 1000;
}
}
catch (Exception)
{
onStatusRetrievalError?.Invoke();
await Task.Delay(retryDelay);
retryDelay += 500;
}
}
}
private static double GetCRate()
{
if (status?.BatteryChargeCRate > 0)
return Math.Round(status.BatteryChargeCRate, 2);
if (status?.BatteryDischargeCRate > 0)
return Math.Round(status.BatteryDischargeCRate, 2);
return 0;
}
}