Merge remote-tracking branch 'origin/main'

This commit is contained in:
atef 2026-02-03 09:21:22 +01:00
commit 8a2be78c01
39 changed files with 1259 additions and 228 deletions

View File

@ -8,6 +8,7 @@ using InnovEnergy.App.Backend.Relations;
using InnovEnergy.App.Backend.Websockets;
using InnovEnergy.Lib.Utils;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
namespace InnovEnergy.App.Backend;
@ -554,6 +555,7 @@ public class Controller : ControllerBase
if (!mail_success)
{
Db.GetSession(authToken).Delete(newUser);
return StatusCode(500, "Welcome email failed to send");
}
return mail_success ? newUser.HidePassword():Unauthorized();
@ -935,11 +937,19 @@ public class Controller : ControllerBase
[HttpPost(nameof(EditInstallationConfig))]
public async Task<ActionResult<IEnumerable<Object>>> EditInstallationConfig([FromBody] Configuration config, Int64 installationId,Token authToken)
public async Task<ActionResult<IEnumerable<Object>>> EditInstallationConfig([FromBody] Configuration config, Int64 installationId,int product,Token authToken)
{
var session = Db.GetSession(authToken);
Console.WriteLine("CONFIG IS " + config.GetConfigurationString());
string configString = product switch
{
0 => config.GetConfigurationSalimax(), // Salimax
3 => config.GetConfigurationSodistoreMax(), // SodiStoreMax
2 => config.GetConfigurationSodistoreHome(), // SodiStoreHome
_ => config.GetConfigurationString() // fallback
};
Console.WriteLine("CONFIG IS " + configString);
//Send configuration changes
var success = await session.SendInstallationConfig(installationId, config);
@ -947,17 +957,23 @@ public class Controller : ControllerBase
// Record configuration change
if (success)
{
// Create a new UserAction object
// // Update Configuration colum in Installation table
// var installation = Db.GetInstallationById(installationId);
//
// installation.Configuration = JsonConvert.SerializeObject(config);
//
// if (!installation.Apply(Db.Update))
// return StatusCode(500, "Failed to update installation configuration in database");
var action = new UserAction
{
InstallationId = installationId,
Timestamp = DateTime.Now,
Description = config.GetConfigurationString()
Timestamp = DateTime.UtcNow,
Description = configString
};
Console.WriteLine(action.Description);
var actionSuccess = await session.InsertUserAction(action);
return actionSuccess?Ok():Unauthorized();
return actionSuccess ? Ok() : StatusCode(500, "Failed to record Configuration changes in History of Action");
}
return Unauthorized();

View File

@ -2,28 +2,52 @@ namespace InnovEnergy.App.Backend.DataTypes;
public class Configuration
{
public Double MinimumSoC { get; set; }
public Double GridSetPoint { get; set; }
public CalibrationChargeType CalibrationChargeState { get; set; }
public DateTime CalibrationChargeDate { get; set; }
public CalibrationChargeType CalibrationDischargeState { get; set; }
public DateTime CalibrationDischargeDate { get; set; }
public double? MinimumSoC { get; set; }
public double? GridSetPoint { get; set; }
public CalibrationChargeType? CalibrationChargeState { get; set; }
public DateTime? CalibrationChargeDate { get; set; }
public CalibrationChargeType? CalibrationDischargeState { get; set; }
public DateTime? CalibrationDischargeDate { get; set; }
//For sodistoreHome installations
public Double MaximumDischargingCurrent { get; set; }
public Double MaximumChargingCurrent { get; set; }
public Double OperatingPriority { get; set; }
public Double BatteriesCount { get; set; }
public double? MaximumDischargingCurrent { get; set; }
public double? MaximumChargingCurrent { get; set; }
public double? OperatingPriority { get; set; }
public double? BatteriesCount { get; set; }
public double? ClusterNumber { get; set; }
public double? PvNumber { get; set; }
public bool ControlPermission { get; set; }
public double? TimeChargeandDischargePower { get; set; }
public DateTime? StartTimeChargeandDischargeDayandTime { get; set; }
public DateTime? StopTimeChargeandDischargeDayandTime { get; set; }
public String GetConfigurationString()
{
return $"MinimumSoC: {MinimumSoC}, GridSetPoint: {GridSetPoint}, CalibrationChargeState: {CalibrationChargeState}, CalibrationChargeDate: {CalibrationChargeDate}, " +
$"CalibrationDischargeState: {CalibrationDischargeState}, CalibrationDischargeDate: {CalibrationDischargeDate}" +
$"MaximumDischargingCurrent: {MaximumDischargingCurrent}, MaximumChargingCurrent: {MaximumChargingCurrent}, OperatingPriority: {OperatingPriority}" +
$"BatteriesCount: {BatteriesCount}";
$"CalibrationDischargeState: {CalibrationDischargeState}, CalibrationDischargeDate: {CalibrationDischargeDate}, " +
$"MaximumDischargingCurrent: {MaximumDischargingCurrent}, MaximumChargingCurrent: {MaximumChargingCurrent}, OperatingPriority: {OperatingPriority}, " +
$"BatteriesCount: {BatteriesCount}, ClusterNumber: {ClusterNumber}, PvNumber: {PvNumber}, GrowattControlPermission:{ControlPermission}, "+
$"SinexcelTimeChargeandDischargePower: {TimeChargeandDischargePower}, SinexcelStartTimeChargeandDischargeDayandTime: {StartTimeChargeandDischargeDayandTime}, SinexcelStopTimeChargeandDischargeDayandTime: {StopTimeChargeandDischargeDayandTime}";
}
public string GetConfigurationSalimax()
{
return
$"MinimumSoC: {MinimumSoC}, GridSetPoint: {GridSetPoint}, CalibrationChargeState: {CalibrationChargeState}, CalibrationChargeDate: {CalibrationChargeDate}";
}
public string GetConfigurationSodistoreMax()
{
return
$"MinimumSoC: {MinimumSoC}, GridSetPoint: {GridSetPoint}, CalibrationChargeState: {CalibrationChargeState}, CalibrationChargeDate: {CalibrationChargeDate}";
}
public string GetConfigurationSodistoreHome()
{
return $"MinimumSoC: {MinimumSoC}, MaximumDischargingCurrent: {MaximumDischargingCurrent}, MaximumChargingCurrent: {MaximumChargingCurrent}, OperatingPriority: {OperatingPriority}, " +
$"BatteriesCount: {BatteriesCount}, ClusterNumber: {ClusterNumber}, PvNumber: {PvNumber}, GrowattControlPermission:{ControlPermission}, "+
$"SinexcelTimeChargeandDischargePower: {TimeChargeandDischargePower}, SinexcelStartTimeChargeandDischargeDayandTime: {StartTimeChargeandDischargeDayandTime}, SinexcelStopTimeChargeandDischargeDayandTime: {StopTimeChargeandDischargeDayandTime}";
}
}
public enum CalibrationChargeType

View File

@ -45,8 +45,12 @@ public class Installation : TreeNode
public string SerialNumber { get; set; } = "";
public string InverterSN { get; set; } = "";
public string DataloggerSN { get; set; } = "";
public int BatteryClusterNumber { get; set; } = 0;
public int BatteryNumber { get; set; } = 0;
public string BatterySerialNumbers { get; set; } = "";
[Ignore]
public String OrderNumbers { get; set; }
public String VrmLink { get; set; } = "";
public string Configuration { get; set; } = "";
}

View File

@ -296,8 +296,10 @@ public static partial class Db
await user.SendNewUserWelcomeMessage();
return true;
}
catch
catch (Exception ex)
{
Console.WriteLine($"Welcome email failed for {user.Email}");
Console.WriteLine(ex.ToString());
return false;
}
}

View File

@ -1,26 +1,27 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Collector", "App/Collector/Collector.csproj", "{E3A5F3A3-72A5-47CC-85C6-2D8E962A0EC1}"
#
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Collector", "App\Collector\Collector.csproj", "{E3A5F3A3-72A5-47CC-85C6-2D8E962A0EC1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenVpnCertificatesServer", "App/OpenVpnCertificatesServer/OpenVpnCertificatesServer.csproj", "{CF4834CB-91B7-4172-AC13-ECDA8613CD17}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenVpnCertificatesServer", "App\OpenVpnCertificatesServer\OpenVpnCertificatesServer.csproj", "{CF4834CB-91B7-4172-AC13-ECDA8613CD17}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RemoteSupportConsole", "App/RemoteSupportConsole/RemoteSupportConsole.csproj", "{B1268C03-66EB-4486-8BFC-B439225D9D54}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RemoteSupportConsole", "App\RemoteSupportConsole\RemoteSupportConsole.csproj", "{B1268C03-66EB-4486-8BFC-B439225D9D54}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SysTools", "Lib/SysTools/SysTools.csproj", "{4A67D79F-F0C9-4BBC-9601-D5948E6C05D3}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SysTools", "Lib\SysTools\SysTools.csproj", "{4A67D79F-F0C9-4BBC-9601-D5948E6C05D3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebServer", "Lib/WebServer/WebServer.csproj", "{B2627B9F-41DF-44F7-A0D1-CA71FF4A007A}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebServer", "Lib\WebServer\WebServer.csproj", "{B2627B9F-41DF-44F7-A0D1-CA71FF4A007A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EmuMeterDriver", "App/EmuMeterDriver/EmuMeterDriver.csproj", "{F65F33B0-3522-4008-8D1E-47EF8E4C7AC7}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EmuMeterDriver", "App\EmuMeterDriver\EmuMeterDriver.csproj", "{F65F33B0-3522-4008-8D1E-47EF8E4C7AC7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BmsTunnel", "App/BmsTunnel/BmsTunnel.csproj", "{40B45363-BE34-420B-8F87-775EE6EE3513}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BmsTunnel", "App\BmsTunnel\BmsTunnel.csproj", "{40B45363-BE34-420B-8F87-775EE6EE3513}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "App", "App", "{145597B4-3E30-45E6-9F72-4DD43194539A}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Lib", "Lib", "{AD5B98A8-AB7F-4DA2-B66D-5B4E63E7D854}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SaliMax", "App/SaliMax/SaliMax.csproj", "{25073794-D859-4824-9984-194C7E928496}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SaliMax", "App\SaliMax\SaliMax.csproj", "{25073794-D859-4824-9984-194C7E928496}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StatusApi", "Lib/StatusApi/StatusApi.csproj", "{9D17E78C-8A70-43DB-A619-DC12D20D023D}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StatusApi", "Lib\StatusApi\StatusApi.csproj", "{9D17E78C-8A70-43DB-A619-DC12D20D023D}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Devices", "Devices", "{4931A385-24DC-4E78-BFF4-356F8D6D5183}"
EndProject
@ -30,35 +31,35 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Victron", "Victron", "{BD8C
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Trumpf", "Trumpf", "{DDDBEFD0-5DEA-4C7C-A9F2-FDB4636CF092}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TruConvertAc", "Lib/Devices/Trumpf/TruConvertAc/TruConvertAc.csproj", "{1F4B445E-459E-44CD-813E-6D725EBB81E8}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TruConvertAc", "Lib\Devices\Trumpf\TruConvertAc\TruConvertAc.csproj", "{1F4B445E-459E-44CD-813E-6D725EBB81E8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TruConvertDc", "Lib/Devices/Trumpf/TruConvertDc/TruConvertDc.csproj", "{F6F29829-C31A-4994-A698-E441BEA631C6}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TruConvertDc", "Lib\Devices\Trumpf\TruConvertDc\TruConvertDc.csproj", "{F6F29829-C31A-4994-A698-E441BEA631C6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DBus", "Lib/Protocols/DBus/DBus.csproj", "{8C3C620A-087D-4DD6-B493-A47FC643F8DC}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DBus", "Lib\Protocols\DBus\DBus.csproj", "{8C3C620A-087D-4DD6-B493-A47FC643F8DC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modbus", "Lib/Protocols/Modbus/Modbus.csproj", "{E4AE6A33-0DEB-48EB-9D57-C0C7C63FC267}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modbus", "Lib\Protocols\Modbus\Modbus.csproj", "{E4AE6A33-0DEB-48EB-9D57-C0C7C63FC267}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VeDBus", "Lib/Victron/VeDBus/VeDBus.csproj", "{50B26E29-1B99-4D07-BCA5-359CD550BBAA}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VeDBus", "Lib\Victron\VeDBus\VeDBus.csproj", "{50B26E29-1B99-4D07-BCA5-359CD550BBAA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VictronVRM", "Lib/Victron/VictronVRM/VictronVRM.csproj", "{FE05DF69-B5C7-4C2E-8FB9-7776441A7622}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VictronVRM", "Lib\Victron\VictronVRM\VictronVRM.csproj", "{FE05DF69-B5C7-4C2E-8FB9-7776441A7622}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ampt", "Lib/Devices/AMPT/Ampt.csproj", "{77AF3A64-2878-4150-BCD0-F16530783165}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ampt", "Lib\Devices\AMPT\Ampt.csproj", "{77AF3A64-2878-4150-BCD0-F16530783165}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Battery48TL", "Lib/Devices/Battery48TL/Battery48TL.csproj", "{1C3F443A-B339-4B08-80E6-8A84817FFEC9}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Battery48TL", "Lib\Devices\Battery48TL\Battery48TL.csproj", "{1C3F443A-B339-4B08-80E6-8A84817FFEC9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EmuMeter", "Lib/Devices/EmuMeter/EmuMeter.csproj", "{152A4168-F612-493C-BBEA-8EB26E6E2D34}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EmuMeter", "Lib\Devices\EmuMeter\EmuMeter.csproj", "{152A4168-F612-493C-BBEA-8EB26E6E2D34}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Utils", "Lib/Utils/Utils.csproj", "{89A3E29C-4E57-47FE-A800-12AC68418264}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Utils", "Lib\Utils\Utils.csproj", "{89A3E29C-4E57-47FE-A800-12AC68418264}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Adam6060", "Lib/Devices/Adam6060/Adam6060.csproj", "{4AFDB799-E6A4-4DCA-8B6D-8C0F98398461}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Adam6060", "Lib\Devices\Adam6060\Adam6060.csproj", "{4AFDB799-E6A4-4DCA-8B6D-8C0F98398461}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Channels", "Lib/Channels/Channels.csproj", "{AF7E8DCA-8D48-498E-AB3D-208061B244DC}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Channels", "Lib\Channels\Channels.csproj", "{AF7E8DCA-8D48-498E-AB3D-208061B244DC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Backend", "App/Backend/Backend.csproj", "{A56F58C2-B265-435B-A985-53B4D6F49B1A}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Backend", "App\Backend\Backend.csproj", "{A56F58C2-B265-435B-A985-53B4D6F49B1A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Units", "Lib/Units/Units.csproj", "{C04FB6DA-23C6-46BB-9B21-8F4FBA32FFF7}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Units", "Lib\Units\Units.csproj", "{C04FB6DA-23C6-46BB-9B21-8F4FBA32FFF7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SystemControl", "Lib/Devices/Trumpf/SystemControl/SystemControl.csproj", "{B816BB44-E97E-4E02-B80A-BEDB5B923A96}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SystemControl", "Lib\Devices\Trumpf\SystemControl\SystemControl.csproj", "{B816BB44-E97E-4E02-B80A-BEDB5B923A96}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Meta", "Meta", "{AED84693-C389-44C9-B2C0-ACB560189CF2}"
ProjectSection(SolutionItems) = preProject
@ -87,8 +88,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Doepke", "Lib\Devices\Doepk
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amax5070", "Lib\Devices\Amax5070\Amax5070.csproj", "{09E280B0-43D3-47BD-AF15-CF4FCDD24FE6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SofarInverter", "Lib\Devices\SofarInverter\SofarInverter.csproj", "{2C7F3D89-402B-43CB-988E-8D2D853BEF44}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SchneiderMeterDriver", "App\SchneiderMeterDriver\SchneiderMeterDriver.csproj", "{2E7E7657-3A53-4B62-8927-FE9A082B81DE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Battery250UP", "Lib\Devices\Battery250UP\Battery250UP.csproj", "{F2967439-A590-4D5E-9208-1B973C83AA1C}"
@ -109,7 +108,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SinexcelCommunication", "Ap
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sinexcel 12K TL", "Sinexcel 12K TL\Sinexcel 12K TL.csproj", "{28C16B43-E498-40DB-8ACF-D7F2A88A402F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -248,10 +246,6 @@ Global
{09E280B0-43D3-47BD-AF15-CF4FCDD24FE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{09E280B0-43D3-47BD-AF15-CF4FCDD24FE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{09E280B0-43D3-47BD-AF15-CF4FCDD24FE6}.Release|Any CPU.Build.0 = Release|Any CPU
{2C7F3D89-402B-43CB-988E-8D2D853BEF44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2C7F3D89-402B-43CB-988E-8D2D853BEF44}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2C7F3D89-402B-43CB-988E-8D2D853BEF44}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2C7F3D89-402B-43CB-988E-8D2D853BEF44}.Release|Any CPU.Build.0 = Release|Any CPU
{2E7E7657-3A53-4B62-8927-FE9A082B81DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2E7E7657-3A53-4B62-8927-FE9A082B81DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2E7E7657-3A53-4B62-8927-FE9A082B81DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -331,7 +325,6 @@ Global
{73B97F6E-2BDC-40DA-84A7-7FB0264387D6} = {AD5B98A8-AB7F-4DA2-B66D-5B4E63E7D854}
{C2B14CD4-1BCA-4933-96D9-92F40EACD2B9} = {4931A385-24DC-4E78-BFF4-356F8D6D5183}
{09E280B0-43D3-47BD-AF15-CF4FCDD24FE6} = {4931A385-24DC-4E78-BFF4-356F8D6D5183}
{2C7F3D89-402B-43CB-988E-8D2D853BEF44} = {4931A385-24DC-4E78-BFF4-356F8D6D5183}
{2E7E7657-3A53-4B62-8927-FE9A082B81DE} = {145597B4-3E30-45E6-9F72-4DD43194539A}
{F2967439-A590-4D5E-9208-1B973C83AA1C} = {4931A385-24DC-4E78-BFF4-356F8D6D5183}
{1045AC74-D4D8-4581-AAE3-575DF26060E6} = {4931A385-24DC-4E78-BFF4-356F8D6D5183}

View File

@ -1,35 +1,52 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using MailKit.Net.Smtp;
using MailKit.Security;
using MimeKit;
namespace InnovEnergy.Lib.Mailer;
public static class Mailer
{
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
public static async Task Send(String recipientName, String recipientEmailAddress, String subject, String body)
public static async Task Send(string recipientName, string recipientEmailAddress, string subject, string body)
{
var config = await ReadMailerConfig();
Console.WriteLine("=============== SMTP CONFIG LOADED ==============");
Console.WriteLine($"Config full path: {Path.GetFullPath(MailerConfig.DefaultFile)}");
Console.WriteLine($"SMTP host: {config!.SmtpServerUrl}");
Console.WriteLine($"SMTP port: {config.SmtpPort}");
Console.WriteLine($"SMTP username: {config.SmtpUsername}");
Console.WriteLine($"Sender: {config.SenderName} <{config.SenderAddress}>");
Console.WriteLine("==================================================");
var from = new MailboxAddress(config!.SenderName, config.SenderAddress);
var to = new MailboxAddress(recipientName, recipientEmailAddress);
var msg = new MimeMessage
var msg = new MimeMessage
{
From = { from },
To = { to },
Subject = subject,
Body = new TextPart { Text = body }
Body = new TextPart("plain") { Text = body }
};
using var smtp = new SmtpClient();
await smtp.ConnectAsync(config.SmtpServerUrl, config.SmtpPort, false);
await smtp.AuthenticateAsync(config.SmtpUsername, config.SmtpPassword);
await smtp.SendAsync(msg);
await smtp.DisconnectAsync(true);
try
{
await smtp.ConnectAsync(config.SmtpServerUrl, config.SmtpPort, SecureSocketOptions.StartTls);
await smtp.AuthenticateAsync(config.SmtpUsername, config.SmtpPassword);
await smtp.SendAsync(msg);
await smtp.DisconnectAsync(true);
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
throw; // keep while testing
}
}
[RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.DeserializeAsync<TValue>(Stream, JsonSerializerOptions, CancellationToken)")]

View File

@ -1 +0,0 @@
UEsDBBQAAAAIAGZRWVqEoBx0dAAAAJUAAAAJAAAAZGF0YS5qc29ubcqxCsIwEADQf7k5HrlL7pJ0VAeXguAXhBJqlhbSgIr474o4Oj54TxjrclknGJiRDIz5/pUntAaOdZuuuc11mfe599Ie5/VWGgw71oDRBSIXOYmqioHD3yqaUNSKEFGyEryBU8n9836BRB1qDJocO2/Zv95QSwECFAMUAAAACABmUVlahKAcdHQAAACVAAAACQAAAAAAAAAAAAAAgAEAAAAAZGF0YS5qc29uUEsFBgAAAAABAAEANwAAAJsAAAAAAA==

View File

@ -1 +0,0 @@
UEsDBBQAAAAIAGZRWVqEoBx0dAAAAJUAAAAJAAAAZGF0YS5qc29ubcqxCsIwEADQf7k5HrlL7pJ0VAeXguAXhBJqlhbSgIr474o4Oj54TxjrclknGJiRDIz5/pUntAaOdZuuuc11mfe599Ie5/VWGgw71oDRBSIXOYmqioHD3yqaUNSKEFGyEryBU8n9836BRB1qDJocO2/Zv95QSwECFAMUAAAACABmUVlahKAcdHQAAACVAAAACQAAAAAAAAAAAAAAgAEAAAAAZGF0YS5qc29uUEsFBgAAAAABAAEANwAAAJsAAAAAAA==

View File

@ -0,0 +1,40 @@
#!/bin/sh -e
mount -o remount,rw /
# Redirect all output to a log file
exec > /data/log/rc.local.log 2>&1
# Set root password non-interactively
echo "Setting root password..."
echo "root:salidomo" | /usr/sbin/chpasswd
# Check the exit status of chpasswd
if [ $? -eq 0 ]; then
echo "Root password set successfully."
else
echo "Failed to set root password."
fi
# Remove existing timezone link (if it exists)
if [ -L /etc/localtime ]; then
echo "Removing existing timezone link..."
rm /etc/localtime
fi
# Create a symbolic link to the desired timezone
echo "Creating symbolic link to timezone..."
ln -sf /usr/share/zoneinfo/Europe/Paris /etc/localtime
# Set VPN service symlink
echo "Creating symbolic link to VPN service..."
find /data/innovenergy/openvpn -type f -exec chmod 777 {} \;
vpn_service_path="/data/innovenergy/openvpn/service"
vpn_symlink_path="/service/openvpn"
ln -s "$vpn_service_path" "$vpn_symlink_path"
# # Set EmuMeter service symlink
# echo "Creating symbolic link to EmuMeter service..."
# EmuMeter_service_path="/data/EmuMeter/service"
# EmuMeter_symlink_path="/service/EmuMeter"
# ln -s "$EmuMeter_service_path" "$EmuMeter_symlink_path"
exit 0

BIN
firmware/EmuMeter/EmuMeter Executable file

Binary file not shown.

View File

@ -0,0 +1,3 @@
#!/bin/sh
exec 2>&1
exec multilog t s25000 n4 /var/log/EmuMeter

Binary file not shown.

3
firmware/EmuMeter/service/run Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
exec 2>&1
exec softlimit -d 100000000 -s 1000000 -a 100000000 /data/EmuMeter/EmuMeter

View File

Binary file not shown.

View File

@ -0,0 +1,131 @@
#!/bin/bash
set -euo pipefail
# === Settings ===
SERVER="91.92.155.224"
REMOTE_PATH="/home/ubuntu/Sodistorehome/release-package.tar.gz"
TARGET_DIR="$HOME/SodiStoreHome"
SCRIPT_DIR="$TARGET_DIR/scripts"
MODPOLL_DIR="$TARGET_DIR/ModpollDir"
# Change this if your config file has a different name (e.g., config.json, config.yaml, etc.)
CONFIG_FILE="config.json"
SERVICE_NAME="SodiStoreHome.service"
SERVICE_PATH="/etc/systemd/system/$SERVICE_NAME"
echo "📦 Downloading release package from server..."
mkdir -p "$TARGET_DIR"
scp -i ~/.ssh/InnovEnergy.pem.priv "ubuntu@$SERVER:$REMOTE_PATH" "$TARGET_DIR/release-package.tar.gz"
echo "📂 Extracting package..."
tar -xzf "$TARGET_DIR/release-package.tar.gz" -C "$TARGET_DIR"
echo "📁 Ensuring directories exist..."
mkdir -p "$SCRIPT_DIR" "$MODPOLL_DIR" \
"$TARGET_DIR/csvFile" "$TARGET_DIR/FailedUploads" "$TARGET_DIR/JsonLogDirectory"
echo "📥 Placing files (no bin/; put under $TARGET_DIR)"
# Stuff that used to go to bin/ now goes directly under $TARGET_DIR
if [ -f "$TARGET_DIR/libSystem.IO.Ports.Native.so" ]; then
echo " - libSystem.IO.Ports.Native.so -> $TARGET_DIR/"
# already in place after extract; nothing to do
fi
if [ -f "$TARGET_DIR/GrowattCommunication" ]; then
echo " - Setting executable on GrowattCommunication"
chmod +x "$TARGET_DIR/GrowattCommunication"
fi
# modpoll stays in ModpollDir
if [ -f "$TARGET_DIR/modpoll" ]; then
echo " - Moving modpoll -> $MODPOLL_DIR/"
mv -f "$TARGET_DIR/modpoll" "$MODPOLL_DIR/"
chmod +x "$MODPOLL_DIR/modpoll"
fi
# Move Python files to scripts/ (unchanged behavior)
shopt -s nullglob
PY_FILES=( "$TARGET_DIR"/*.py )
if [ ${#PY_FILES[@]} -gt 0 ]; then
echo " - Moving Python files -> $SCRIPT_DIR/"
mv -f "${PY_FILES[@]}" "$SCRIPT_DIR/"
fi
shopt -u nullglob
# 2) Place systemd service under /etc/systemd/system/
if [ -f "$TARGET_DIR/$SERVICE_NAME" ]; then
echo "🛠️ Installing systemd service to $SERVICE_PATH (requires sudo)..."
sudo install -m 0644 "$TARGET_DIR/$SERVICE_NAME" "$SERVICE_PATH"
echo " - Reloading systemd daemon..."
sudo systemctl daemon-reload
# Enable but don't start automatically; comment out if you don't want this:
if ! systemctl is-enabled --quiet "$SERVICE_NAME"; then
echo " - Enabling service $SERVICE_NAME"
sudo systemctl enable "$SERVICE_NAME"
fi
else
echo "⚠️ WARNING: $SERVICE_NAME not found in $TARGET_DIR. Skipping service install."
fi
# 3) Place the config file under $TARGET_DIR
if [ -f "$TARGET_DIR/$CONFIG_FILE" ]; then
echo "📝 Config file already in $TARGET_DIR: $CONFIG_FILE"
elif [ -f "$TARGET_DIR/config" ]; then
echo " - Moving 'config' -> $TARGET_DIR/$CONFIG_FILE"
mv -f "$TARGET_DIR/config" "$TARGET_DIR/$CONFIG_FILE"
else
echo "⚠️ WARNING: Config file '$CONFIG_FILE' not found in extracted package."
echo " If the filename differs, set CONFIG_FILE accordingly at the top of this script."
fi
# 4) csvFile/, FailedUploads/, JsonLogDirectory/ were created earlier.
# 5) Place log, start, stop, restart under $TARGET_DIR and make them executable
for f in log start stop restart; do
if [ -f "$TARGET_DIR/$f" ]; then
echo " - Ensuring $f is in $TARGET_DIR and executable"
chmod +x "$TARGET_DIR/$f"
elif [ -f "$TARGET_DIR/$f.sh" ]; then
echo " - Moving $f.sh -> $TARGET_DIR/$f and making executable"
mv -f "$TARGET_DIR/$f.sh" "$TARGET_DIR/$f"
chmod +x "$TARGET_DIR/$f"
else
echo "⚠️ NOTE: '$f' script not found in extracted package."
fi
done
# --- ModbusTCP Integration ---
echo "Installing systemd Modbus TCP service..."
MODBUS_TARGET_DIR="$TARGET_DIR/ModbusTCP"
sudo cp "$MODBUS_TARGET_DIR/ModbusTCP.service" /etc/systemd/system/
echo "Preparing Python virtual environment..."
cd "$TARGET_DIR"
python3 -m venv venv
source venv/bin/activate
pip install watchdog
pip install pymodbus==2.5.3
pip install pyinstaller
deactivate
echo "Granting permission to bind port 502..."
sudo setcap 'cap_net_bind_service=+ep' "$MODBUS_TARGET_DIR/dist/modbus_tcp_server"
# echo "Enabling ModbusTCP systemd service..."
sudo systemctl daemon-reload
sudo systemctl enable --now ModbusTCP.service
# Remove existing timezone link (if it exists)
if [ -L /etc/localtime ]; then
echo "Removing existing timezone link..."
sudo rm /etc/localtime
fi
# Create a symbolic link to the desired timezone
echo "Creating symbolic link to timezone..."
sudo ln -sf /usr/share/zoneinfo/Europe/Paris /etc/localtime
# Starting the SodistoreHome service
sudo systemctl restart SodiStoreHome.service

View File

@ -0,0 +1,125 @@
#!/bin/bash
# Raspberry Pi OS Customization Setup Script
set -e
############################### Change Raspberry Pi Hostname ##################################
if [ -z "$1" ]; then
echo "Usage: $0 DEVICE_NUMBER"
exit 1
fi
DEVICE_NUM=$(printf "%04d" "$1")
NEW_HOSTNAME="sodistorehome${DEVICE_NUM}"
NEW_USER="inesco"
NEW_PASS="Sodistore0918425"
SSID="inesco"
WIFI_PASS="inesco25"
echo "Creating user $NEW_USER..."
if id "$NEW_USER" &>/dev/null; then
echo "User $NEW_USER already exists"
else
sudo useradd -m -s /bin/bash "$NEW_USER"
echo "${NEW_USER}:${NEW_PASS}" | sudo chpasswd
sudo usermod -aG sudo "$NEW_USER"
fi
echo "Setting static hostname to $NEW_HOSTNAME..."
sudo hostnamectl --static set-hostname "$NEW_HOSTNAME"
# Update /etc/hosts for hostname resolution
if grep -q "^127.0.1.1" /etc/hosts; then
sudo sed -i "s/^127.0.1.1.*/127.0.1.1\t$NEW_HOSTNAME/" /etc/hosts
else
echo "127.0.1.1 $NEW_HOSTNAME" | sudo tee -a /etc/hosts
fi
echo "Disabling default 'pi' user (if exists)..."
if id pi &>/dev/null; then
sudo passwd -l pi
else
echo "User 'pi' does not exist, skipping disabling."
fi
echo "Configuring Wi-Fi..."
sudo tee /etc/wpa_supplicant/wpa_supplicant.conf > /dev/null <<EOF
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
country=US
network={
ssid="$SSID"
psk="$WIFI_PASS"
key_mgmt=WPA-PSK
}
EOF
sudo chmod 600 /etc/wpa_supplicant/wpa_supplicant.conf
echo "OS customization complete. Device: $NEW_HOSTNAME"
######################### Cpoy Privates Keys of Gitea Server ###########################
SSH_DIR="$HOME/.ssh"
KEY_SRC="./key"
echo "Creating $SSH_DIR if it doesn't exist..."
mkdir -p "$SSH_DIR"
chmod 700 "$SSH_DIR"
echo "Copying keys from $KEY_SRC to $SSH_DIR..."
cp -r "$KEY_SRC/"* "$SSH_DIR/"
echo "Setting permissions for files in $SSH_DIR..."
chmod 600 "$SSH_DIR/"*
echo "Private key copying complete."
if [ -z "$1" ]; then
echo "Usage: $0 DEVICE_NUMBER"
echo "Example: $0 3"
exit 1
fi
######################### Set Up VPN ###########################
DEVICENAME="sodistorehome${DEVICE_NUM}"
PASSWORD="MwBRbQb3QaX7l9XIaakq"
echo "Using device name: $NEW_HOSTNAME"
echo "Updating system..."
sudo apt update
sudo apt install -y openvpn bridge-utils
echo "Enabling SSH..."
sudo systemctl start ssh
sudo systemctl enable ssh
echo "Downloading VPN certificate..."
wget "https://salidomo.innovenergy.ch/get_cert?name=${NEW_HOSTNAME}&pw=${PASSWORD}" -O openvpn.tar
echo "Moving certificate to /etc/openvpn/client/..."
sudo mkdir -p /etc/openvpn/client
sudo mv openvpn.tar /etc/openvpn/client/
echo "Extracting certificate..."
sudo tar -xvf /etc/openvpn/client/openvpn.tar -C /etc/openvpn/client/
echo "Enabling VPN service..."
sudo systemctl start openvpn-client@innovenergy.service
sudo systemctl enable openvpn-client@innovenergy.service
echo "VPN setup complete. Checking interface:"
ip -br addr
######################### Copy and run install_release.sh ###########################
HOME_DIR="$HOME"
echo "Copying install_release.sh to $HOME_DIR"
cp install_release.sh "$HOME_DIR"
bash "$HOME_DIR/install_release.sh"
######################### End ###########################
echo "🎉🎉🎉 Okay :) We made it!!! 🌟💪 Great Job, Team! 🚀🔥🏆🙌✨💫🎯"
echo " Please document the following VPN address:"
cat /etc/openvpn/client/installation-ip

View File

@ -168,7 +168,7 @@ function BatteryView(props: BatteryViewProps) {
<Grid container>
<Routes>
<Route
path={routes.mainstats + '*'}
path={routes.mainstats + '/*'}
element={
<MainStats
s3Credentials={props.s3Credentials}

View File

@ -166,7 +166,7 @@ function BatteryViewSalidomo(props: BatteryViewProps) {
<Grid container>
<Routes>
<Route
path={routes.mainstats + '*'}
path={routes.mainstats + '/*'}
element={
<MainStatsSalidomo
s3Credentials={props.s3Credentials}

View File

@ -18,11 +18,13 @@ import { FormattedMessage } from 'react-intl';
import { I_S3Credentials } from '../../../interfaces/S3Types';
import routes from '../../../Resources/routes.json';
import CircularProgress from '@mui/material/CircularProgress';
import { I_Installation } from 'src/interfaces/InstallationTypes';
interface BatteryViewSodioHomeProps {
values: JSONRecordData;
s3Credentials: I_S3Credentials;
installationId: number;
installation: I_Installation;
connected: boolean;
}
@ -33,16 +35,24 @@ function BatteryViewSodioHome(props: BatteryViewSodioHomeProps) {
const currentLocation = useLocation();
const navigate = useNavigate();
const inverter = (props.values as any)?.InverterRecord;
const batteryClusterNumber = props.installation.batteryClusterNumber;
const sortedBatteryView = inverter
? Array.from({ length: batteryClusterNumber }, (_, i) => {
const index = i + 1; // Battery1, Battery2, ...
const sortedBatteryView =
props.values != null &&
props.values?.AcDcGrowatt?.BatteriesRecords?.Batteries
? Object.entries(props.values.AcDcGrowatt.BatteriesRecords.Batteries)
.map(([BatteryId, battery]) => {
return { BatteryId, battery }; // Here we return an object with the id and device
})
.sort((a, b) => parseInt(b.BatteryId) - parseInt(a.BatteryId))
: [];
return {
BatteryId: String(index),
battery: {
Voltage: inverter[`Battery${index}Voltage`],
Current: inverter[`Battery${index}Current`],
Power: inverter[`Battery${index}Power`],
Soc: inverter[`Battery${index}Soc`],
Soh: inverter[`Battery${index}Soh`],
}
};
})
: [];
const [loading, setLoading] = useState(sortedBatteryView.length == 0);
const handleMainStatsButton = () => {
@ -157,7 +167,7 @@ function BatteryViewSodioHome(props: BatteryViewSodioHomeProps) {
{/*<Grid container>*/}
{/* <Routes>*/}
{/* <Route*/}
{/* path={routes.mainstats + '*'}*/}
{/* path={routes.mainstats + '/*'}*/}
{/* element={*/}
{/* <MainStats*/}
{/* s3Credentials={props.s3Credentials}*/}
@ -224,8 +234,8 @@ function BatteryViewSodioHome(props: BatteryViewSodioHomeProps) {
<TableCell align="center">Current</TableCell>
<TableCell align="center">SoC</TableCell>
<TableCell align="center">SoH</TableCell>
<TableCell align="center">Daily Charge Energy</TableCell>
<TableCell align="center">Daily Discharge Energy</TableCell>
{/*<TableCell align="center">Daily Charge Energy</TableCell>*/}
{/*<TableCell align="center">Daily Discharge Energy</TableCell>*/}
</TableRow>
</TableHead>
<TableBody>
@ -261,12 +271,14 @@ function BatteryViewSodioHome(props: BatteryViewSodioHomeProps) {
sx={{
width: '13%',
textAlign: 'center',
backgroundColor: '#32CD32',
color: 'black'
backgroundColor:
battery.Voltage < 32 || battery.Voltage > 63
? '#FF033E'
: '#32CD32',
color: 'inherit'
}}
>
{battery.Voltage + ' ' + 'V'}
{battery.Voltage + ' V'}
</TableCell>
<TableCell
sx={{
@ -282,7 +294,12 @@ function BatteryViewSodioHome(props: BatteryViewSodioHomeProps) {
sx={{
width: '8%',
textAlign: 'center',
backgroundColor: '#32CD32',
backgroundColor:
battery.Soc > 20
? '#32CD32'
: battery.Soc >= 10
? '#ffbf00'
: '#FF033E',
color: 'inherit'
}}
>
@ -292,32 +309,37 @@ function BatteryViewSodioHome(props: BatteryViewSodioHomeProps) {
sx={{
width: '8%',
textAlign: 'center',
backgroundColor: '#32CD32',
backgroundColor:
battery.Soh ==100
? '#32CD32'
: battery.Soc >= 90
? '#ffbf00'
: '#FF033E',
color: 'inherit'
}}
>
{battery.Soh + ' %'}
</TableCell>
<TableCell
sx={{
width: '15%',
textAlign: 'center',
backgroundColor: '#32CD32',
color: 'inherit'
}}
>
{battery.DailyChargeEnergy + ' Wh'}
</TableCell>
<TableCell
sx={{
width: '15%',
textAlign: 'center',
backgroundColor: '#32CD32',
color: 'inherit'
}}
>
{battery.DailyDischargeEnergy + ' Wh'}
</TableCell>
{/*<TableCell*/}
{/* sx={{*/}
{/* width: '15%',*/}
{/* textAlign: 'center',*/}
{/* backgroundColor: '#32CD32',*/}
{/* color: 'inherit'*/}
{/* }}*/}
{/*>*/}
{/* {battery.DailyChargeEnergy + ' Wh'}*/}
{/*</TableCell>*/}
{/*<TableCell*/}
{/* sx={{*/}
{/* width: '15%',*/}
{/* textAlign: 'center',*/}
{/* backgroundColor: '#32CD32',*/}
{/* color: 'inherit'*/}
{/* }}*/}
{/*>*/}
{/* {battery.DailyDischargeEnergy + ' Wh'}*/}
{/*</TableCell>*/}
</TableRow>
))}
</TableBody>

View File

@ -169,7 +169,7 @@ function Configuration(props: ConfigurationProps) {
setLoading(true);
const res = await axiosConfig
.post(
`/EditInstallationConfig?installationId=${props.id}`,
`/EditInstallationConfig?installationId=${props.id}&product=${product}`,
configurationToSend
)
.catch((err) => {
@ -404,32 +404,32 @@ function Configuration(props: ConfigurationProps) {
}
})}
/>
{product === 3 && (
<Tab
value="discharge"
label="Calibration Discharge"
sx={(theme) => ({
flex: 2,
fontWeight: 'bold',
borderRadius: 2,
textTransform: 'none',
color:
activeTab === 'discharge'
? 'white'
: theme.palette.text.primary,
bgcolor:
activeTab === 'discharge'
? theme.palette.primary.main
: 'transparent',
'&:hover': {
bgcolor:
activeTab === 'discharge'
? theme.palette.primary.dark
: '#eee'
}
})}
/>
)}
{/*{product === 3 && (*/}
{/* <Tab*/}
{/* value="discharge"*/}
{/* label="Calibration Discharge"*/}
{/* sx={(theme) => ({*/}
{/* flex: 2,*/}
{/* fontWeight: 'bold',*/}
{/* borderRadius: 2,*/}
{/* textTransform: 'none',*/}
{/* color:*/}
{/* activeTab === 'discharge'*/}
{/* ? 'white'*/}
{/* : theme.palette.text.primary,*/}
{/* bgcolor:*/}
{/* activeTab === 'discharge'*/}
{/* ? theme.palette.primary.main*/}
{/* : 'transparent',*/}
{/* '&:hover': {*/}
{/* bgcolor:*/}
{/* activeTab === 'discharge'*/}
{/* ? theme.palette.primary.dark*/}
{/* : '#eee'*/}
{/* }*/}
{/* })}*/}
{/* />*/}
{/*)}*/}
</Tabs>
</Box>

View File

@ -337,6 +337,22 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
/>
</div>
<div>
<TextField
label={
<FormattedMessage
id="batteryClusterNumber"
defaultMessage="Battery Cluster Number"
/>
}
name="batteryClusterNumber"
value={formValues.batteryClusterNumber}
onChange={handleChange}
variant="outlined"
fullWidth
/>
</div>
<div>
<TextField
label={
@ -361,7 +377,7 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
name="s3writesecretkey"
value={
formValues.s3BucketId +
'-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e'
'-e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa'
}
variant="outlined"
fullWidth

View File

@ -416,7 +416,7 @@ function Installation(props: singleInstallationProps) {
/>
<Route
path={routes.batteryview + '*'}
path={routes.batteryview + '/*'}
element={
<BatteryView
values={values}
@ -428,7 +428,7 @@ function Installation(props: singleInstallationProps) {
></Route>
<Route
path={routes.pvview + '*'}
path={routes.pvview + '/*'}
element={
<PvView values={values} connected={connected}></PvView>
}

View File

@ -18,7 +18,7 @@ function InstallationSearch(props: installationSearchProps) {
return (
<Route
key={installation.id}
path={routes.installation + installation.id + '*'}
path={routes.installation + installation.id + '/*'}
element={
<Installation
key={installation.id}
@ -92,7 +92,7 @@ function InstallationSearch(props: installationSearchProps) {
// return (
// <Route
// key={installation.id}
// path={routes.installation + installation.id + '*'}
// path={routes.installation + installation.id + '/*'}
// element={
// <Installation
// key={installation.id}

View File

@ -329,6 +329,16 @@ export interface JSONRecordData {
MaximumDischargingCurrent: number;
OperatingPriority: string;
BatteriesCount: number;
ClusterNumber: number;
PvNumber: number;
//For SodistoerHome-Growatt:
ControlPermission:boolean;
//For SodistoerHome-Sinexcel: TimeChargeDischarge mode
TimeChargeandDischargePower?: number;
StartTimeChargeandDischargeDayandTime?: Date | null;
StopTimeChargeandDischargeDayandTime?: Date | null;
};
DcDc: {
@ -429,6 +439,23 @@ export interface JSONRecordData {
};
};
// For SodistoreHome
InverterRecord: {
GridPower:number;
Battery1Power:number;
Battery1Soc:number;
Battery1Soh:number;
Battery1Voltage:number;
Battery1Current:number;
Battery2Power:number;
Battery2Soc:number;
Battery2Voltage:number;
Battery2Current:number;
Battery2Soh:number;
PvPower:number;
ConsumptionPower:number;
};
AcDcGrowatt: {
AcChargeEnable: number;
ActivePowerPercentDerating: number;
@ -457,6 +484,7 @@ export interface JSONRecordData {
BatteryOperatingMode: string;
BatteryType: number;
ChargeCutoffSoc: number;
ConsumptionPower:number;
ControlPermession: number;
DischargeCutoffSoc: number;
EmsCommunicationFailureTime: number;
@ -495,6 +523,7 @@ export interface JSONRecordData {
SystemOperatingMode: string;
TotalEnergyToGrid: number;
TotalEnergyToUser: number;
TotalPvPower: number;
VppProtocolVerNumber: number;
};
@ -614,6 +643,16 @@ export type ConfigurationValues = {
maximumChargingCurrent: number;
operatingPriority: number;
batteriesCount: number;
clusterNumber: number;
PvNumber: number;
//For sodistoreHome-Growatt:
controlPermission:boolean;
// For sodistoreHome-Sinexcel: TimeChargeDischarge mode
timeChargeandDischargePower?: number;
startTimeChargeandDischargeDayandTime?: Date | null;
stopTimeChargeandDischargeDayandTime?: Date | null;
};
//
// export interface Pv {
@ -1040,7 +1079,17 @@ export const getHighestConnectionValue = (values: JSONRecordData) => {
'PvOnDc.Dc.Power',
'DcDc.Dc.Link.Power',
'LoadOnDc.Power',
'Battery.Dc.Power'
'Battery.Dc.Power',
'AcDcGrowatt.MeterPower',
'AcDcGrowatt.TotalPvPower',
'AcDcGrowatt.BatteriesRecords.Power',
'AcDcGrowatt.BatteriesRecords.TotalChargeEnergy',
'AcDcGrowatt.ConsumptionPower',
'InverterRecord.GridPower',
'InverterRecord.PvPower',
'InverterRecord.Battery1Power',
'InverterRecord.Battery2Power',
'InverterRecord.ConsumptionPower'
];
// Helper function to safely get a value from a nested path

View File

@ -361,7 +361,7 @@ function SalidomoInstallation(props: singleInstallationProps) {
/>
<Route
path={routes.batteryview + '*'}
path={routes.batteryview + '/*'}
element={
<BatteryViewSalidomo
values={values}

View File

@ -18,7 +18,7 @@ function InstallationSearch(props: installationSearchProps) {
return (
<Route
key={installation.id}
path={routes.installation + installation.id + '*'}
path={routes.installation + installation.id + '/*'}
element={
<SalidomoInstallation
key={installation.id}

View File

@ -55,7 +55,7 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
routes.installation +
`${installationID}` +
'/' +
routes.batteryview,
routes.live,
{
replace: true
}

View File

@ -25,6 +25,7 @@ import { fetchDataJson } from '../Installations/fetchData';
import { FetchResult } from '../../../dataCache/dataCache';
import BatteryViewSodioHome from '../BatteryView/BatteryViewSodioHome';
import SodistoreHomeConfiguration from './SodistoreHomeConfiguration';
import TopologySodistoreHome from '../Topology/TopologySodistoreHome';
interface singleInstallationProps {
current_installation?: I_Installation;
@ -297,6 +298,37 @@ function SodioHomeInstallation(props: singleInstallationProps) {
{props.current_installation.name}
</Typography>
</div>
{currentTab == 'live' && values && (
<div style={{ display: 'flex', alignItems: 'center' }}>
<Typography
fontWeight="bold"
color="text.primary"
noWrap
sx={{
marginTop: '0px',
marginBottom: '10px',
fontSize: '14px'
}}
>
<FormattedMessage id="mode" defaultMessage="Mode:" />
</Typography>
<Typography
fontWeight="bold"
color="orange"
noWrap
sx={{
marginTop: '0px',
marginBottom: '10px',
marginLeft: '85px',
fontSize: '14px'
}}
>
{values.Config.OperatingPriority}
</Typography>
</div>
)}
<div style={{ display: 'flex', alignItems: 'center' }}>
<Typography
fontWeight="bold"
@ -434,22 +466,23 @@ function SodioHomeInstallation(props: singleInstallationProps) {
<Route
path={routes.live}
element={
<div></div>
// <Topology
// values={values}
// connected={connected}
// loading={loading}
// ></Topology>
<TopologySodistoreHome
values={values}
connected={connected}
loading={loading}
batteryClusterNumber={props.current_installation.batteryClusterNumber}
></TopologySodistoreHome>
}
/>
<Route
path={routes.batteryview + '*'}
path={routes.batteryview + '/*'}
element={
<BatteryViewSodioHome
values={values}
s3Credentials={s3Credentials}
installationId={props.current_installation.id}
installation={props.current_installation}
connected={connected}
></BatteryViewSodioHome>
}
@ -474,6 +507,7 @@ function SodioHomeInstallation(props: singleInstallationProps) {
<SodistoreHomeConfiguration
values={values}
id={props.current_installation.id}
installation={props.current_installation}
></SodistoreHomeConfiguration>
}
/>

View File

@ -83,7 +83,7 @@ function InstallationSearch(props: installationSearchProps) {
return (
<Route
key={installation.id}
path={routes.installation + installation.id + '*'}
path={routes.installation + installation.id + '/*'}
element={
<SodioHomeInstallation
key={installation.id}

View File

@ -24,10 +24,18 @@ import MenuItem from '@mui/material/MenuItem';
import axiosConfig from '../../../Resources/axiosConfig';
import { UserContext } from '../../../contexts/userContext';
import { ProductIdContext } from '../../../contexts/ProductIdContextProvider';
import { I_Installation } from 'src/interfaces/InstallationTypes';
import { LocalizationProvider } from '@mui/x-date-pickers';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import {DateTimePicker } from '@mui/x-date-pickers';
import dayjs from 'dayjs';
import Switch from '@mui/material/Switch';
import FormControlLabel from '@mui/material/FormControlLabel';
interface SodistoreHomeConfigurationProps {
values: JSONRecordData;
id: number;
installation: I_Installation;
}
function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
@ -35,11 +43,22 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
return null;
}
const OperatingPriorityOptions = [
'LoadPriority',
'BatteryPriority',
'GridPriority'
];
const device = props.installation.device;
const OperatingPriorityOptions =
device === 3 // Growatt
? ['LoadPriority', 'BatteryPriority', 'GridPriority']
: device === 4 // Sinexcel
? [
'SpontaneousSelfUse',
'TimeChargeDischarge',
// 'TimeOfUsePowerPrice',
// 'DisasterStandby',
// 'ManualControl',
'PvPriorityCharging',
// 'PrioritySellElectricity'
]
: [];
const [errors, setErrors] = useState({
minimumSoC: false,
@ -69,7 +88,21 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
operatingPriority: OperatingPriorityOptions.indexOf(
props.values.Config.OperatingPriority
),
batteriesCount: props.values.Config.BatteriesCount
batteriesCount: props.values.Config.BatteriesCount,
clusterNumber: props.values.Config.ClusterNumber??1,
PvNumber: props.values.Config.PvNumber??0,
timeChargeandDischargePower: props.values.Config?.TimeChargeandDischargePower ?? 0, // default 0 W
startTimeChargeandDischargeDayandTime:
props.values.Config?.StartTimeChargeandDischargeDayandTime
? dayjs(props.values.Config.StartTimeChargeandDischargeDayandTime).toDate()
: null,
stopTimeChargeandDischargeDayandTime:
props.values.Config?.StopTimeChargeandDischargeDayandTime
? dayjs(props.values.Config.StopTimeChargeandDischargeDayandTime).toDate()
: null,
// controlPermission: props.values.Config.ControlPermission??false,
controlPermission: String(props.values.Config.ControlPermission).toLowerCase() === "true",
});
const handleOperatingPriorityChange = (event) => {
@ -81,20 +114,53 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
});
};
// Add time validation function
const validateTimeOnly = () => {
if (formValues.startTimeChargeandDischargeDayandTime &&
formValues.stopTimeChargeandDischargeDayandTime) {
const startHours = formValues.startTimeChargeandDischargeDayandTime.getHours();
const startMinutes = formValues.startTimeChargeandDischargeDayandTime.getMinutes();
const stopHours = formValues.stopTimeChargeandDischargeDayandTime.getHours();
const stopMinutes = formValues.stopTimeChargeandDischargeDayandTime.getMinutes();
const startTimeInMinutes = startHours * 60 + startMinutes;
const stopTimeInMinutes = stopHours * 60 + stopMinutes;
if (startTimeInMinutes >= stopTimeInMinutes) {
setDateSelectionError('Stop time must be later than start time');
setErrorDateModalOpen(true);
return false;
}
}
return true;
};
const handleSubmit = async (e) => {
// console.log('asked for', dayjs(formValues.calibrationChargeDate));
if (!validateTimeOnly()) {
return;
}
const configurationToSend: Partial<ConfigurationValues> = {
minimumSoC: formValues.minimumSoC,
maximumDischargingCurrent: formValues.maximumDischargingCurrent,
maximumChargingCurrent: formValues.maximumChargingCurrent,
operatingPriority: formValues.operatingPriority,
batteriesCount:formValues.batteriesCount
batteriesCount:formValues.batteriesCount,
clusterNumber:formValues.clusterNumber,
PvNumber:formValues.PvNumber,
timeChargeandDischargePower: formValues.timeChargeandDischargePower,
startTimeChargeandDischargeDayandTime: formValues.startTimeChargeandDischargeDayandTime
? new Date(formValues.startTimeChargeandDischargeDayandTime.getTime() - formValues.startTimeChargeandDischargeDayandTime.getTimezoneOffset() * 60000)
: null,
stopTimeChargeandDischargeDayandTime: formValues.stopTimeChargeandDischargeDayandTime
? new Date(formValues.stopTimeChargeandDischargeDayandTime.getTime() - formValues.stopTimeChargeandDischargeDayandTime.getTimezoneOffset() * 60000)
: null,
controlPermission:formValues.controlPermission
};
setLoading(true);
const res = await axiosConfig
.post(
`/EditInstallationConfig?installationId=${props.id}`,
`/EditInstallationConfig?installationId=${props.id}&product=${product}`,
configurationToSend
)
.catch((err) => {
@ -113,34 +179,40 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
const handleChange = (e) => {
const { name, value } = e.target;
switch (name) {
case 'minimumSoC':
if (
/[^0-9.]/.test(value) ||
isNaN(parseFloat(value)) ||
parseFloat(value) > 100
) {
SetErrorForField(name, true);
} else {
SetErrorForField(name, false);
}
break;
case 'gridSetPoint':
if (/[^0-9.]/.test(value) || isNaN(parseFloat(value))) {
SetErrorForField(name, true);
} else {
SetErrorForField(name, false);
}
break;
if (name === 'minimumSoC') {
const numValue = parseFloat(value);
default:
break;
// invalid characters or not a number
if (/[^0-9.]/.test(value) || isNaN(numValue)) {
SetErrorForField(name, 'Invalid number format');
} else {
const minsocRanges = {
3: { min: 10, max: 30 },
4: { min: 5, max: 100 },
};
const { min, max } = minsocRanges[device] || { min: 10, max: 30 };
if (numValue < min || numValue > max) {
SetErrorForField(name, `Value should be between ${min}-${max}%`);
} else {
// ✅ valid → clear error
SetErrorForField(name, '');
}
}
}
setFormValues({
...formValues,
setFormValues(prev => ({
...prev,
[name]: value,
}));
};
const handleTimeChargeDischargeChange = (name: string, value: any) => {
setFormValues((prev) => ({
...prev,
[name]: value
});
}));
};
const handleOkOnErrorDateModal = () => {
@ -209,6 +281,34 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
autoComplete="off"
>
<>
{device === 3 && (
<div style={{ marginBottom: '5px' }}>
<FormControlLabel
labelPlacement="start"
control={
<Switch
name="controlPermission"
checked={Boolean(formValues.controlPermission)}
onChange={(e) =>
setFormValues((prev) => ({
...prev,
controlPermission: e.target.checked,
}))
}
sx={{ transform: "scale(1.4)", marginLeft: "15px" }}
/>
}
label={
<FormattedMessage
id="controlPermission"
defaultMessage="Control Permission"
/>
}
/>
</div>
)}
<div style={{ marginBottom: '5px' }}>
<TextField
label={
@ -224,28 +324,73 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
/>
</div>
<div style={{ marginBottom: '5px' }}>
<TextField
label={
<FormattedMessage
id="minimum_soc "
defaultMessage="Minimum SoC (%)"
{device === 4 && (
<>
<div style={{ marginBottom: '5px' }}>
<TextField
label={
<FormattedMessage
id="clusterNumber"
defaultMessage="Cluster Number"
/>
}
name="clusterNumber"
value={formValues.clusterNumber}
onChange={handleChange}
fullWidth
/>
}
</div>
<div style={{ marginBottom: '5px' }}>
<TextField
label={
<FormattedMessage
id="PvNumber"
defaultMessage="PV Number"
/>
}
name="PvNumber"
value={formValues.PvNumber}
onChange={handleChange}
fullWidth
/>
</div>
</>
)}
<div style={{ marginBottom: '5px' }}>
{/*<TextField*/}
{/* label={*/}
{/* <FormattedMessage*/}
{/* id="minimum_soc "*/}
{/* defaultMessage="Minimum SoC (%)"*/}
{/* />*/}
{/* }*/}
{/* name="minimumSoC"*/}
{/* value={formValues.minimumSoC}*/}
{/* onChange={handleChange}*/}
{/* helperText={*/}
{/* errors.minimumSoC ? (*/}
{/* <span style={{ color: 'red' }}>*/}
{/* Value should be between {device === 4 ? '5100' : '1030'}%*/}
{/* </span>*/}
{/* ) : (*/}
{/* ''*/}
{/* )*/}
{/* }*/}
{/* fullWidth*/}
{/*/>*/}
<TextField
label="Minimum SoC (%)"
name="minimumSoC"
value={formValues.minimumSoC}
onChange={handleChange}
helperText={
errors.minimumSoC ? (
<span style={{ color: 'red' }}>
Value should be between 0-100%
</span>
) : (
''
)
}
error={Boolean(errors.minimumSoC)}
helperText={errors.minimumSoC}
fullWidth
/>
</div>
<div style={{ marginBottom: '5px' }}>
@ -307,6 +452,91 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
</div>
</>
{/* --- Sinexcel + TimeChargeDischarge --- */}
{device === 4 &&
OperatingPriorityOptions[formValues.operatingPriority] ===
'TimeChargeDischarge' && (
<>
{/* Power input*/}
<div style={{ marginBottom: '5px' }}>
<TextField
label="Power (W)"
name="timeChargeandDischargePower"
value={formValues.timeChargeandDischargePower}
onChange={(e) =>
handleTimeChargeDischargeChange(e.target.name, e.target.value)
}
helperText="Enter a positive or negative power value"
fullWidth
/>
</div>
{/* Start DateTime */}
<div style={{ marginBottom: '5px' }}>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DateTimePicker
ampm={false}
label="Start Date and Time (Start Time < Stop Time)"
value={
formValues.startTimeChargeandDischargeDayandTime
? dayjs(formValues.startTimeChargeandDischargeDayandTime)
: null
}
onChange={(newValue) =>
setFormValues((prev) => ({
...prev,
startTimeChargeandDischargeDayandTime: newValue
? newValue.toDate()
: null,
}))
}
renderInput={(params) => (
<TextField
{...params}
sx={{
marginTop: 2,
width: '100%',
}}
/>
)}
/>
</LocalizationProvider>
</div>
{/* Stop DateTime */}
<div style={{ marginBottom: '5px' }}>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DateTimePicker
ampm={false}
label="Stop Date and Time (Start Time < Stop Time)"
value={
formValues.stopTimeChargeandDischargeDayandTime
? dayjs(formValues.stopTimeChargeandDischargeDayandTime)
: null
}
onChange={(newValue) =>
setFormValues((prev) => ({
...prev,
stopTimeChargeandDischargeDayandTime: newValue
? newValue.toDate()
: null,
}))
}
renderInput={(params) => (
<TextField
{...params}
sx={{
marginTop: 2,
width: '100%',
}}
/>
)}
/>
</LocalizationProvider>
</div>
</>
)}
<div
style={{
display: 'flex',

View File

@ -237,6 +237,22 @@ function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros)
/>
</div>
<div>
<TextField
label={
<FormattedMessage
id="batteryClusterNumber"
defaultMessage="Battery Cluster Number"
/>
}
name="batteryClusterNumber"
value={formValues.batteryClusterNumber}
onChange={handleChange}
variant="outlined"
fullWidth
/>
</div>
<div>
<TextField
label={

View File

@ -96,10 +96,10 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
const singleInstallationTabs =
currentUser.userType == UserType.admin
? [
// {
// value: 'live',
// label: <FormattedMessage id="live" defaultMessage="Live" />
// },
{
value: 'live',
label: <FormattedMessage id="live" defaultMessage="Live" />
},
{
value: 'batteryview',
label: (
@ -155,10 +155,10 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
}
]
: [
// {
// value: 'live',
// label: <FormattedMessage id="live" defaultMessage="Live" />
// },
{
value: 'live',
label: <FormattedMessage id="live" defaultMessage="Live" />
},
// {
// value: 'overview',
// label: <FormattedMessage id="overview" defaultMessage="Overview" />
@ -186,10 +186,10 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
value: 'tree',
icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" />
},
// {
// value: 'live',
// label: <FormattedMessage id="live" defaultMessage="Live" />
// },
{
value: 'live',
label: <FormattedMessage id="live" defaultMessage="Live" />
},
{
value: 'batteryview',
label: (
@ -256,10 +256,10 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
value: 'tree',
icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" />
},
// {
// value: 'live',
// label: <FormattedMessage id="live" defaultMessage="Live" />
// },
{
value: 'live',
label: <FormattedMessage id="live" defaultMessage="Live" />
},
{
value: 'overview',
label: <FormattedMessage id="overview" defaultMessage="Overview" />

View File

@ -0,0 +1,290 @@
import React, { useContext, useState } from 'react';
import {
CircularProgress,
Container,
Grid,
Switch,
Typography
} from '@mui/material';
import TopologyColumn from './topologyColumn';
import {
getAmount,
getHighestConnectionValue,
JSONRecordData
} from '../Log/graph.util';
import { ProductIdContext } from '../../../contexts/ProductIdContextProvider';
interface TopologySodistoreHomeProps {
values: JSONRecordData;
connected: boolean;
loading: boolean;
batteryClusterNumber:number;
}
function TopologySodistoreHome(props: TopologySodistoreHomeProps) {
if (props.values === null && props.connected == true) {
return null;
}
const highestConnectionValue =
props.values != null ? getHighestConnectionValue(props.values) : 0;
const { product, setProduct } = useContext(ProductIdContext);
const [showValues, setShowValues] = useState(false);
const handleSwitch = () => () => {
setShowValues(!showValues);
};
const isMobile = window.innerWidth <= 1490;
const totalBatteryPower: number = Number(
props.values && props.values.InverterRecord
? Array.from({ length: props.batteryClusterNumber }).reduce(
(sum: number, _, index) => {
const i = index + 1;
const rawPower =
props.values.InverterRecord[`Battery${i}Power`] as unknown;
const power = Number(rawPower) || 0;
return sum + power;
},
0
)
: 0
);
const pvPower =
props.values?.InverterRecord?.PvPower ??
['PvPower1', 'PvPower2', 'PvPower3', 'PvPower4']
.map((key) => props.values?.InverterRecord?.[key] ?? 0)
.reduce((sum, val) => sum + val, 0);
return (
<Container maxWidth="xl" style={{ backgroundColor: 'white' }}>
<Grid container>
{!props.connected && !props.loading && (
<Container
maxWidth="xl"
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
height: '70vh'
}}
>
<CircularProgress size={60} style={{ color: '#ffc04d' }} />
<Typography
variant="body2"
style={{ color: 'black', fontWeight: 'bold' }}
mt={2}
>
Unable to communicate with the installation
</Typography>
<Typography variant="body2" style={{ color: 'black' }}>
Please wait or refresh the page
</Typography>
</Container>
)}
{props.connected && (
<>
<Grid
item
xs={12}
md={12}
style={{
marginTop: '10px',
height: '20px',
display: 'flex',
flexDirection: 'row',
alignItems: 'right',
justifyContent: 'right'
}}
>
{/*<div>*/}
{/* <Typography sx={{ marginTop: '5px', marginRight: '20px' }}>*/}
{/* Display Values*/}
{/* </Typography>*/}
{/* <Switch*/}
{/* edge="start"*/}
{/* color="secondary"*/}
{/* onChange={handleSwitch()}*/}
{/* sx={{*/}
{/* '& .MuiSwitch-thumb': {*/}
{/* backgroundColor: 'orange'*/}
{/* },*/}
{/* marginLeft: '20px'*/}
{/* }}*/}
{/* />*/}
{/*</div>*/}
</Grid>
<Grid
item
xs={12}
md={12}
style={{
height: isMobile ? '550px' : '600px',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center'
}}
>
<TopologyColumn
centerBox={{
title: 'Grid',
data: props.values?.InverterRecord
? [
{
value: props.values.InverterRecord.GridPower,
unit: 'W'
}
]
: undefined,
connected:
true
}}
centerConnection={{
orientation: 'horizontal',
data: props.values?.InverterRecord
? {
value: props.values.InverterRecord.GridPower,
unit: 'W'
}
: undefined,
amount: props.values?.InverterRecord
? getAmount(
highestConnectionValue,
props.values.InverterRecord.GridPower
)
: 0,
showValues: showValues
}}
isLast={false}
isFirst={true}
/>
{/*-------------------------------------------------------------------------------------------------------------------------------------------------------------*/}
<TopologyColumn
topBox={{
title: 'PV',
data: props.values?.InverterRecord
? [
{
value: pvPower,
unit: 'W'
}
]
: undefined,
connected: true
}}
topConnection={{
orientation: 'vertical',
position: 'top',
data: props.values?.InverterRecord
? {
value: pvPower,
unit: 'W'
}
: undefined,
amount: props.values?.InverterRecord ? getAmount(highestConnectionValue, pvPower) : 0,
showValues: showValues
}}
centerBox={{
title: 'Inverter',
data: props.values?.InverterRecord
? [
{
value: 0,
unit: 'W'
}
]
: undefined,
connected: true
}}
centerConnection={{
orientation: 'horizontal',
data: props.values?.InverterRecord
? {
value: totalBatteryPower,
unit: 'W'
}
: undefined,
amount: props.values?.InverterRecord
? getAmount(highestConnectionValue, totalBatteryPower)
: 0,
showValues: showValues
}}
bottomBox={{
title: 'Loads',
data: props.values?.InverterRecord
? [
{
value: props.values.InverterRecord.ConsumptionPower,
unit: 'W'
}
]
: undefined,
connected:true
}}
bottomConnection={{
orientation: 'vertical',
position: 'bottom',
data: props.values?.InverterRecord
? {
value: props.values.InverterRecord.ConsumptionPower,
unit: 'W'
}
: undefined,
amount: props.values?.InverterRecord
? getAmount(
highestConnectionValue,
props.values.InverterRecord.ConsumptionPower
)
: 0,
showValues: showValues
}}
isLast={false}
isFirst={false}
/>
{/*-------------------------------------------------------------------------------------------------------------------------------------------------------------*/}
{Array.from({ length: props.batteryClusterNumber }).map((_, index) => {
const i = index + 1; // battery cluster index starting from 1
return (
<TopologyColumn
key={i}
centerBox={{
title: `Battery C${i}`,
data: props.values.InverterRecord
? [
{
value: props.values.InverterRecord[`Battery${i}Soc`],
unit: '%'
},
{
value: props.values.InverterRecord[`Battery${i}Power`],
unit: 'W'
}
]
: undefined,
connected: true
}}
isFirst={false}
isLast={true}
/>
);
})}
</Grid>
</>
)}
</Grid>
</Container>
);
}
export default TopologySodistoreHome;

View File

@ -39,8 +39,8 @@ function formatPower(value, unit) {
const roundedValue = value.toFixed(1);
//Filter all values less than 100 Watts
if (magnitude === 0 && value < 100 && unit === 'W') {
//Filter all values less than 1 Watts(why?)
if (magnitude === 0 && value < 1 && unit === 'W') {
//console.log('DROP THIS VALUE ' + value);
return 0;
}
@ -70,11 +70,15 @@ function TopologyBox(props: TopologyBoxProps) {
props.title === 'Battery'
? '165px'
: props.title === 'AC Loads' ||
props.title === 'DC Loads' ||
props.title === 'Pv Inverter' ||
props.title === 'Pv DC-DC'
? '100px'
: '150px',
props.title === 'DC Loads' ||
props.title === 'Pv Inverter' ||
props.title === 'Pv DC-DC' ||
props.title === 'PV' ||
props.title === 'Loads'
? '100px'
: props.title === 'Inverter'
? '150px'
: '150px',
backgroundColor: !props.data
? 'darkgrey'
: props.title === 'Grid Bus' ||
@ -134,6 +138,17 @@ function TopologyBox(props: TopologyBoxProps) {
/>
)}
{props.data && props.title === 'Inverter' && (
<img
src={inverterImage}
style={{
width: '40px',
height: '40px',
color: 'orange'
}}
/>
)}
{props.data && props.title === 'DC Link' && (
<PowerInputIcon
style={{
@ -183,7 +198,8 @@ function TopologyBox(props: TopologyBoxProps) {
marginTop: '4px'
}}
>
{(props.title === 'Pv Inverter' ||
{(props.title === 'PV' ||
props.title === 'Pv Inverter' ||
props.title === 'Pv DC-DC') && (
<SolarPowerIcon
style={{
@ -207,7 +223,7 @@ function TopologyBox(props: TopologyBoxProps) {
}}
></BatteryCharging60Icon>
)}
{(props.title === 'AC Loads' || props.title === 'DC Loads') && (
{(props.title === 'AC Loads' || props.title === 'DC Loads' ||props.title === 'Loads') && (
<OutletIcon
style={{
fontSize: 30,

View File

@ -78,7 +78,7 @@ function InstallationTree() {
return (
<Route
key={installation.id}
path={routes.installation + installation.id + '*'}
path={routes.installation + installation.id + '/*'}
element={
installation.product == 0 || installation.product == 3 ? (
<Installation

View File

@ -614,6 +614,7 @@ export const transformInputToAggregatedDataJson = async (
}
const results = await Promise.all(timestampPromises);
console.log("Fetched aggregated daily results:", results);
currentDay = start_date;
for (let i = 0; i < results.length; i++) {

View File

@ -15,6 +15,7 @@ export interface I_Installation extends I_S3Credentials {
information: string;
inverterSN: string;
dataloggerSN: string;
batteryClusterNumber: number;
parentId: number;
s3WriteKey: string;
s3WriteSecret: string;