diff --git a/src/Client/Pages/Index.razor b/src/Client/Pages/Index.razor index 98abadf..e4ecc56 100644 --- a/src/Client/Pages/Index.razor +++ b/src/Client/Pages/Index.razor @@ -45,7 +45,8 @@
- +
@status?.LoadWatts
@@ -86,7 +87,8 @@
- +
@status?.PVInputWatt
@@ -124,7 +126,8 @@
- +
@@ -196,10 +199,10 @@ onStatusRetrievalError -= NullifyStatus; } - private static decimal RoundToWholeNumber(decimal? val) + private static double RoundToWholeNumber(double? val) => Math.Round(val ?? 0, 0); - private static decimal RoundToOneDecimal(decimal? val) + private static double RoundToOneDecimal(double? val) => Math.Round(val ?? 0, 1); private static string TemperatureCss() @@ -259,7 +262,7 @@ } } - private static decimal GetCRate() + private static double GetCRate() { if (status?.BatteryChargeCRate > 0) return Math.Round(status.BatteryChargeCRate, 2); diff --git a/src/Server/Endpoints/GetStatus/Endpoint.cs b/src/Server/Endpoints/GetStatus/Endpoint.cs index 8c77358..d395650 100644 --- a/src/Server/Endpoints/GetStatus/Endpoint.cs +++ b/src/Server/Endpoints/GetStatus/Endpoint.cs @@ -6,7 +6,7 @@ namespace InverterMon.Server.Endpoints.GetStatus; public class Endpoint : EndpointWithoutRequest { - public CurrentStatus CurrentStatus { get; set; } + public FelicitySolarInverter Inverter { get; set; } public override void Configure() { @@ -34,7 +34,7 @@ public class Endpoint : EndpointWithoutRequest { if (Env.IsDevelopment()) { - var status = CurrentStatus.Result; + var status = new InverterStatus(); status.OutputVoltage = Random.Shared.Next(240); status.LoadWatts = Random.Shared.Next(3500); status.LoadPercentage = Random.Shared.Next(100); @@ -51,7 +51,7 @@ public class Endpoint : EndpointWithoutRequest yield return status; } else - yield return CurrentStatus.Result; + yield return Inverter.Status; await Task.Delay(1000, c); } diff --git a/src/Server/InverterMon.Server.csproj b/src/Server/InverterMon.Server.csproj index b8eca2e..d9d09d1 100644 --- a/src/Server/InverterMon.Server.csproj +++ b/src/Server/InverterMon.Server.csproj @@ -41,12 +41,7 @@ - - - - - \ No newline at end of file diff --git a/src/Server/InverterService/CurrentStatus.cs b/src/Server/InverterService/CurrentStatus.cs index 7909c54..8829d67 100644 --- a/src/Server/InverterService/CurrentStatus.cs +++ b/src/Server/InverterService/CurrentStatus.cs @@ -1,8 +1,8 @@ -using InverterMon.Shared.Models; - -namespace InverterMon.Server.InverterService; - -public class CurrentStatus -{ - public InverterStatus Result { get; set; } -} \ No newline at end of file +// using InverterMon.Shared.Models; +// +// namespace InverterMon.Server.InverterService; +// +// public class CurrentStatus +// { +// public InverterStatus Result { get; set; } +// } \ No newline at end of file diff --git a/src/Server/InverterService/FelicityInverter.cs b/src/Server/InverterService/FelicityInverter.cs new file mode 100644 index 0000000..2a0b9f1 --- /dev/null +++ b/src/Server/InverterService/FelicityInverter.cs @@ -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.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.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; + } +} \ No newline at end of file diff --git a/src/Server/InverterService/StatusRetriever.cs b/src/Server/InverterService/StatusRetriever.cs index 8de9d1c..757fef8 100644 --- a/src/Server/InverterService/StatusRetriever.cs +++ b/src/Server/InverterService/StatusRetriever.cs @@ -3,18 +3,18 @@ using InverterMon.Server.Persistence.Settings; 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) { while (!c.IsCancellationRequested) { - currentStatus.Result.BatteryCapacity = userSettings.BatteryCapacity; - currentStatus.Result.PV_MaxCapacity = userSettings.PV_MaxCapacity; + inverter.Status.BatteryCapacity = userSettings.BatteryCapacity; + inverter.Status.PV_MaxCapacity = userSettings.PV_MaxCapacity; //todo: get data from inverter and map to CurrentStatus.Result - _ = db.UpdateTodaysPvGeneration(currentStatus, c); + db.UpdateTodaysPvGeneration(c); await Task.Delay(2000); } diff --git a/src/Server/InverterService/protocol doc/modbus-protocol.pdf b/src/Server/InverterService/protocol doc/modbus-protocol.pdf new file mode 100644 index 0000000..8445054 Binary files /dev/null and b/src/Server/InverterService/protocol doc/modbus-protocol.pdf differ diff --git a/src/Server/Persistance/Database.cs b/src/Server/Persistance/Database.cs index 2e86e8d..199e31c 100644 --- a/src/Server/Persistance/Database.cs +++ b/src/Server/Persistance/Database.cs @@ -8,15 +8,15 @@ namespace InverterMon.Server.Persistence; public class Database { readonly LiteDatabase _db; - readonly CurrentStatus _currentStatus; + readonly FelicitySolarInverter _inverter; readonly UserSettings _settings; readonly ILiteCollection _pvGenCollection; readonly ILiteCollection _usrSettingsCollection; PVGeneration? _today; - public Database(IHostApplicationLifetime lifetime, CurrentStatus status, UserSettings settings) + public Database(IHostApplicationLifetime lifetime, FelicitySolarInverter inverter, UserSettings settings) { - _currentStatus = status; + _inverter = inverter; _settings = settings; _db = new("InverterMon.db") { CheckpointSize = 0 }; lifetime.ApplicationStopping.Register(() => _db?.Dispose()); @@ -36,18 +36,18 @@ public class Database .SingleOrDefault(); if (_today is not null) - _currentStatus.Result.RestorePVWattHours(_today.TotalWattHours); + _inverter.Status.RestorePVWattHours(_today.TotalWattHours); else { _today = new() { Id = todayDayNumber }; _today.SetTotalWattHours(0); - _currentStatus.Result.RestorePVWattHours(0); + _inverter.Status.RestorePVWattHours(0); _pvGenCollection.Insert(_today); _db.Checkpoint(); } } - public async Task UpdateTodaysPvGeneration(CurrentStatus cmd, CancellationToken c) + public void UpdateTodaysPvGeneration(CancellationToken c) { var hourNow = DateTime.Now.Hour; @@ -58,13 +58,13 @@ public class Database if (_today?.Id == todayDayNumber) { - _today.SetWattPeaks(cmd.Result.PVInputWatt); - _today.SetTotalWattHours(cmd.Result.PVInputWattHour); + _today.SetWattPeaks(_inverter.Status.PVInputWatt); + _today.SetTotalWattHours(_inverter.Status.PVInputWattHour); _pvGenCollection.Update(_today); } 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.SetTotalWattHours(0); _pvGenCollection.Insert(_today); diff --git a/src/Server/Persistance/PVGen/PVGeneration.cs b/src/Server/Persistance/PVGen/PVGeneration.cs index dfaf137..c857c1e 100644 --- a/src/Server/Persistance/PVGen/PVGeneration.cs +++ b/src/Server/Persistance/PVGen/PVGeneration.cs @@ -4,7 +4,7 @@ public class PVGeneration { public int Id { get; set; } public Dictionary WattPeaks { get; set; } = new(); - public decimal TotalWattHours { get; set; } + public double TotalWattHours { get; set; } public void SetWattPeaks(int newValue) { @@ -19,7 +19,7 @@ public class PVGeneration WattPeaks[key] = newValue; } - public void SetTotalWattHours(decimal totalWattHours) + public void SetTotalWattHours(double totalWattHours) { TotalWattHours = totalWattHours; } diff --git a/src/Server/Program.cs b/src/Server/Program.cs index d000ab0..516ca48 100644 --- a/src/Server/Program.cs +++ b/src/Server/Program.cs @@ -18,7 +18,6 @@ _ = int.TryParse(bld.Configuration["LaunchSettings:WebPort"] ?? "80", out var po bld.WebHost.ConfigureKestrel(o => o.Listen(IPAddress.Any, port)); bld.Services - .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton(); diff --git a/src/Server/appsettings.Development.json b/src/Server/appsettings.Development.json deleted file mode 100644 index 0c208ae..0000000 --- a/src/Server/appsettings.Development.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - } -} diff --git a/src/Server/appsettings.json b/src/Server/appsettings.json index 9dec519..3e4674a 100644 --- a/src/Server/appsettings.json +++ b/src/Server/appsettings.json @@ -2,9 +2,7 @@ "LaunchSettings": { "DeviceAddress": "/dev/ttyUSB1", "JkBmsAddress": "/dev/ttyUSB0", - "WebPort": 80, - "TroubleMode": "no", - "MppSolarPath": "/usr/local/bin/mpp-solar" + "WebPort": 80 }, "Logging": { "LogLevel": { diff --git a/src/Shared/Models/InverterStatus.cs b/src/Shared/Models/InverterStatus.cs index 74c3bbc..d35b429 100644 --- a/src/Shared/Models/InverterStatus.cs +++ b/src/Shared/Models/InverterStatus.cs @@ -8,16 +8,16 @@ public class InverterStatus public int BatteryCapacity { get; set; } = 100; [JsonPropertyName("b")] - public decimal BatteryChargeCRate => BatteryChargeCurrent == 0 ? 0 : Convert.ToDecimal(BatteryChargeCurrent) / BatteryCapacity; + public double BatteryChargeCRate => BatteryChargeCurrent == 0 ? 0 : Convert.ToDouble(BatteryChargeCurrent) / BatteryCapacity; [JsonPropertyName("c")] public int BatteryChargeCurrent { get; set; } [JsonPropertyName("d")] - public int BatteryChargeWatts => BatteryChargeCurrent == 0 ? 0 : Convert.ToInt32(BatteryChargeCurrent * BatteryVoltage); + public int BatteryChargeWatts { get; set; } [JsonPropertyName("e")] - public decimal BatteryDischargeCRate => BatteryDischargeCurrent == 0 ? 0 : Convert.ToDecimal(BatteryDischargeCurrent) / BatteryCapacity; + public double BatteryDischargeCRate => BatteryDischargeCurrent == 0 ? 0 : Convert.ToDouble(BatteryDischargeCurrent) / BatteryCapacity; [JsonPropertyName("f")] 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; [JsonPropertyName("h")] - public int BatteryDischargeWatts => BatteryDischargeCurrent == 0 ? 0 : Convert.ToInt32(BatteryDischargeCurrent * BatteryVoltage); + public int BatteryDischargeWatts { get; set; } [JsonPropertyName("i")] - public decimal BatteryVoltage { get; set; } + public double BatteryVoltage { get; set; } [JsonPropertyName("j")] public int GridUsageWatts => GridVoltage < 10 ? 0 : LoadWatts + BatteryChargeWatts - (PVInputWatt + BatteryDischargeWatts); [JsonPropertyName("k")] - public decimal GridVoltage { get; set; } + public double GridVoltage { get; set; } [JsonPropertyName("l")] public int HeatSinkTemperature { get; set; } [JsonPropertyName("m")] - public decimal LoadCurrent => LoadWatts == 0 ? 0 : LoadWatts / OutputVoltage; + public double LoadCurrent => LoadWatts == 0 ? 0 : LoadWatts / OutputVoltage; [JsonPropertyName("n")] - public decimal LoadPercentage { get; set; } + public int LoadPercentage { get; set; } [JsonPropertyName("o")] public int LoadWatts { get; set; } [JsonPropertyName("p")] - public decimal OutputVoltage { get; set; } + public double OutputVoltage { get; set; } [JsonPropertyName("q")] - public decimal PVInputCurrent { get; set; } + public double PVInputCurrent { get; set; } [JsonPropertyName("r")] - public decimal PVInputVoltage { get; set; } + public double PVInputVoltage { get; set; } [JsonPropertyName("s")] public int PVInputWatt @@ -69,13 +69,13 @@ public class InverterStatus pvInputWatt = value; var interval = (DateTime.Now - pvInputWattHourLastComputed).TotalSeconds; - PVInputWattHour += value / (3600 / Convert.ToDecimal(interval)); + PVInputWattHour += value / (3600 / Convert.ToDouble(interval)); pvInputWattHourLastComputed = DateTime.Now; } } [JsonPropertyName("t")] - public decimal PVInputWattHour { get; private set; } + public double PVInputWattHour { get; private set; } [JsonPropertyName("u")] public int PV_MaxCapacity { get; set; } @@ -86,7 +86,7 @@ public class InverterStatus int pvInputWatt; DateTime pvInputWattHourLastComputed; - public void RestorePVWattHours(decimal accruedWattHours) + public void RestorePVWattHours(double accruedWattHours) { PVInputWattHour = accruedWattHours; pvInputWattHourLastComputed = DateTime.Now; diff --git a/src/Shared/Models/PVDay.cs b/src/Shared/Models/PVDay.cs index b901192..8fe04b2 100644 --- a/src/Shared/Models/PVDay.cs +++ b/src/Shared/Models/PVDay.cs @@ -6,7 +6,7 @@ public class PVDay { public int DayNumber { get; set; } public string DayName { get; set; } - public decimal TotalKiloWattHours { get; set; } + public double TotalKiloWattHours { get; set; } public IEnumerable WattPeaks { get; set; } public int GraphTickCount { get; set; } public int[] GraphRange { get; set; }