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.App.Backend.Websockets;
using InnovEnergy.Lib.Utils; using InnovEnergy.Lib.Utils;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
namespace InnovEnergy.App.Backend; namespace InnovEnergy.App.Backend;
@ -554,6 +555,7 @@ public class Controller : ControllerBase
if (!mail_success) if (!mail_success)
{ {
Db.GetSession(authToken).Delete(newUser); Db.GetSession(authToken).Delete(newUser);
return StatusCode(500, "Welcome email failed to send");
} }
return mail_success ? newUser.HidePassword():Unauthorized(); return mail_success ? newUser.HidePassword():Unauthorized();
@ -935,11 +937,19 @@ public class Controller : ControllerBase
[HttpPost(nameof(EditInstallationConfig))] [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); var session = Db.GetSession(authToken);
string configString = product switch
{
0 => config.GetConfigurationSalimax(), // Salimax
3 => config.GetConfigurationSodistoreMax(), // SodiStoreMax
2 => config.GetConfigurationSodistoreHome(), // SodiStoreHome
_ => config.GetConfigurationString() // fallback
};
Console.WriteLine("CONFIG IS " + config.GetConfigurationString()); Console.WriteLine("CONFIG IS " + configString);
//Send configuration changes //Send configuration changes
var success = await session.SendInstallationConfig(installationId, config); var success = await session.SendInstallationConfig(installationId, config);
@ -947,17 +957,23 @@ public class Controller : ControllerBase
// Record configuration change // Record configuration change
if (success) 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 var action = new UserAction
{ {
InstallationId = installationId, InstallationId = installationId,
Timestamp = DateTime.Now, Timestamp = DateTime.UtcNow,
Description = config.GetConfigurationString() Description = configString
}; };
Console.WriteLine(action.Description);
var actionSuccess = await session.InsertUserAction(action); 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(); return Unauthorized();

View File

@ -2,28 +2,52 @@ namespace InnovEnergy.App.Backend.DataTypes;
public class Configuration public class Configuration
{ {
public Double MinimumSoC { get; set; } public double? MinimumSoC { get; set; }
public Double GridSetPoint { get; set; } public double? GridSetPoint { get; set; }
public CalibrationChargeType CalibrationChargeState { get; set; } public CalibrationChargeType? CalibrationChargeState { get; set; }
public DateTime CalibrationChargeDate { get; set; } public DateTime? CalibrationChargeDate { get; set; }
public CalibrationChargeType CalibrationDischargeState { get; set; } public CalibrationChargeType? CalibrationDischargeState { get; set; }
public DateTime CalibrationDischargeDate { get; set; } public DateTime? CalibrationDischargeDate { get; set; }
//For sodistoreHome installations public double? MaximumDischargingCurrent { get; set; }
public double? MaximumChargingCurrent { get; set; }
public Double MaximumDischargingCurrent { get; set; } public double? OperatingPriority { get; set; }
public Double MaximumChargingCurrent { get; set; } public double? BatteriesCount { get; set; }
public Double OperatingPriority { get; set; } public double? ClusterNumber { get; set; }
public Double BatteriesCount { 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() public String GetConfigurationString()
{ {
return $"MinimumSoC: {MinimumSoC}, GridSetPoint: {GridSetPoint}, CalibrationChargeState: {CalibrationChargeState}, CalibrationChargeDate: {CalibrationChargeDate}, " + return $"MinimumSoC: {MinimumSoC}, GridSetPoint: {GridSetPoint}, CalibrationChargeState: {CalibrationChargeState}, CalibrationChargeDate: {CalibrationChargeDate}, " +
$"CalibrationDischargeState: {CalibrationDischargeState}, CalibrationDischargeDate: {CalibrationDischargeDate}" + $"CalibrationDischargeState: {CalibrationDischargeState}, CalibrationDischargeDate: {CalibrationDischargeDate}, " +
$"MaximumDischargingCurrent: {MaximumDischargingCurrent}, MaximumChargingCurrent: {MaximumChargingCurrent}, OperatingPriority: {OperatingPriority}" + $"MaximumDischargingCurrent: {MaximumDischargingCurrent}, MaximumChargingCurrent: {MaximumChargingCurrent}, OperatingPriority: {OperatingPriority}, " +
$"BatteriesCount: {BatteriesCount}"; $"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 public enum CalibrationChargeType

View File

@ -45,8 +45,12 @@ public class Installation : TreeNode
public string SerialNumber { get; set; } = ""; public string SerialNumber { get; set; } = "";
public string InverterSN { get; set; } = ""; public string InverterSN { get; set; } = "";
public string DataloggerSN { 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] [Ignore]
public String OrderNumbers { get; set; } public String OrderNumbers { get; set; }
public String VrmLink { 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(); await user.SendNewUserWelcomeMessage();
return true; return true;
} }
catch catch (Exception ex)
{ {
Console.WriteLine($"Welcome email failed for {user.Email}");
Console.WriteLine(ex.ToString());
return false; return false;
} }
} }

View File

@ -1,26 +1,27 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 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 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 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 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 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 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 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 EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "App", "App", "{145597B4-3E30-45E6-9F72-4DD43194539A}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "App", "App", "{145597B4-3E30-45E6-9F72-4DD43194539A}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Lib", "Lib", "{AD5B98A8-AB7F-4DA2-B66D-5B4E63E7D854}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Lib", "Lib", "{AD5B98A8-AB7F-4DA2-B66D-5B4E63E7D854}"
EndProject 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 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 EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Devices", "Devices", "{4931A385-24DC-4E78-BFF4-356F8D6D5183}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Devices", "Devices", "{4931A385-24DC-4E78-BFF4-356F8D6D5183}"
EndProject EndProject
@ -30,35 +31,35 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Victron", "Victron", "{BD8C
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Trumpf", "Trumpf", "{DDDBEFD0-5DEA-4C7C-A9F2-FDB4636CF092}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Trumpf", "Trumpf", "{DDDBEFD0-5DEA-4C7C-A9F2-FDB4636CF092}"
EndProject 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Meta", "Meta", "{AED84693-C389-44C9-B2C0-ACB560189CF2}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Meta", "Meta", "{AED84693-C389-44C9-B2C0-ACB560189CF2}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
@ -87,8 +88,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Doepke", "Lib\Devices\Doepk
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amax5070", "Lib\Devices\Amax5070\Amax5070.csproj", "{09E280B0-43D3-47BD-AF15-CF4FCDD24FE6}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amax5070", "Lib\Devices\Amax5070\Amax5070.csproj", "{09E280B0-43D3-47BD-AF15-CF4FCDD24FE6}"
EndProject 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}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SchneiderMeterDriver", "App\SchneiderMeterDriver\SchneiderMeterDriver.csproj", "{2E7E7657-3A53-4B62-8927-FE9A082B81DE}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Battery250UP", "Lib\Devices\Battery250UP\Battery250UP.csproj", "{F2967439-A590-4D5E-9208-1B973C83AA1C}" 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 EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sinexcel 12K TL", "Sinexcel 12K TL\Sinexcel 12K TL.csproj", "{28C16B43-E498-40DB-8ACF-D7F2A88A402F}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sinexcel 12K TL", "Sinexcel 12K TL\Sinexcel 12K TL.csproj", "{28C16B43-E498-40DB-8ACF-D7F2A88A402F}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{09E280B0-43D3-47BD-AF15-CF4FCDD24FE6}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{2E7E7657-3A53-4B62-8927-FE9A082B81DE}.Debug|Any CPU.Build.0 = 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 {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} {73B97F6E-2BDC-40DA-84A7-7FB0264387D6} = {AD5B98A8-AB7F-4DA2-B66D-5B4E63E7D854}
{C2B14CD4-1BCA-4933-96D9-92F40EACD2B9} = {4931A385-24DC-4E78-BFF4-356F8D6D5183} {C2B14CD4-1BCA-4933-96D9-92F40EACD2B9} = {4931A385-24DC-4E78-BFF4-356F8D6D5183}
{09E280B0-43D3-47BD-AF15-CF4FCDD24FE6} = {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} {2E7E7657-3A53-4B62-8927-FE9A082B81DE} = {145597B4-3E30-45E6-9F72-4DD43194539A}
{F2967439-A590-4D5E-9208-1B973C83AA1C} = {4931A385-24DC-4E78-BFF4-356F8D6D5183} {F2967439-A590-4D5E-9208-1B973C83AA1C} = {4931A385-24DC-4E78-BFF4-356F8D6D5183}
{1045AC74-D4D8-4581-AAE3-575DF26060E6} = {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.Diagnostics.CodeAnalysis;
using System.Text.Json; using System.Text.Json;
using MailKit.Net.Smtp; using MailKit.Net.Smtp;
using MailKit.Security;
using MimeKit; using MimeKit;
namespace InnovEnergy.Lib.Mailer; namespace InnovEnergy.Lib.Mailer;
public static class 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>")] [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(); 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 from = new MailboxAddress(config!.SenderName, config.SenderAddress);
var to = new MailboxAddress(recipientName, recipientEmailAddress); var to = new MailboxAddress(recipientName, recipientEmailAddress);
var msg = new MimeMessage var msg = new MimeMessage
{ {
From = { from }, From = { from },
To = { to }, To = { to },
Subject = subject, Subject = subject,
Body = new TextPart { Text = body } Body = new TextPart("plain") { Text = body }
}; };
using var smtp = new SmtpClient(); using var smtp = new SmtpClient();
await smtp.ConnectAsync(config.SmtpServerUrl, config.SmtpPort, false); try
await smtp.AuthenticateAsync(config.SmtpUsername, config.SmtpPassword); {
await smtp.SendAsync(msg); await smtp.ConnectAsync(config.SmtpServerUrl, config.SmtpPort, SecureSocketOptions.StartTls);
await smtp.DisconnectAsync(true); 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)")] [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> <Grid container>
<Routes> <Routes>
<Route <Route
path={routes.mainstats + '*'} path={routes.mainstats + '/*'}
element={ element={
<MainStats <MainStats
s3Credentials={props.s3Credentials} s3Credentials={props.s3Credentials}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -329,6 +329,16 @@ export interface JSONRecordData {
MaximumDischargingCurrent: number; MaximumDischargingCurrent: number;
OperatingPriority: string; OperatingPriority: string;
BatteriesCount: number; 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: { 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: { AcDcGrowatt: {
AcChargeEnable: number; AcChargeEnable: number;
ActivePowerPercentDerating: number; ActivePowerPercentDerating: number;
@ -457,6 +484,7 @@ export interface JSONRecordData {
BatteryOperatingMode: string; BatteryOperatingMode: string;
BatteryType: number; BatteryType: number;
ChargeCutoffSoc: number; ChargeCutoffSoc: number;
ConsumptionPower:number;
ControlPermession: number; ControlPermession: number;
DischargeCutoffSoc: number; DischargeCutoffSoc: number;
EmsCommunicationFailureTime: number; EmsCommunicationFailureTime: number;
@ -495,6 +523,7 @@ export interface JSONRecordData {
SystemOperatingMode: string; SystemOperatingMode: string;
TotalEnergyToGrid: number; TotalEnergyToGrid: number;
TotalEnergyToUser: number; TotalEnergyToUser: number;
TotalPvPower: number;
VppProtocolVerNumber: number; VppProtocolVerNumber: number;
}; };
@ -614,6 +643,16 @@ export type ConfigurationValues = {
maximumChargingCurrent: number; maximumChargingCurrent: number;
operatingPriority: number; operatingPriority: number;
batteriesCount: 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 { // export interface Pv {
@ -1040,7 +1079,17 @@ export const getHighestConnectionValue = (values: JSONRecordData) => {
'PvOnDc.Dc.Power', 'PvOnDc.Dc.Power',
'DcDc.Dc.Link.Power', 'DcDc.Dc.Link.Power',
'LoadOnDc.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 // Helper function to safely get a value from a nested path

View File

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

View File

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

View File

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

View File

@ -25,6 +25,7 @@ import { fetchDataJson } from '../Installations/fetchData';
import { FetchResult } from '../../../dataCache/dataCache'; import { FetchResult } from '../../../dataCache/dataCache';
import BatteryViewSodioHome from '../BatteryView/BatteryViewSodioHome'; import BatteryViewSodioHome from '../BatteryView/BatteryViewSodioHome';
import SodistoreHomeConfiguration from './SodistoreHomeConfiguration'; import SodistoreHomeConfiguration from './SodistoreHomeConfiguration';
import TopologySodistoreHome from '../Topology/TopologySodistoreHome';
interface singleInstallationProps { interface singleInstallationProps {
current_installation?: I_Installation; current_installation?: I_Installation;
@ -297,6 +298,37 @@ function SodioHomeInstallation(props: singleInstallationProps) {
{props.current_installation.name} {props.current_installation.name}
</Typography> </Typography>
</div> </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' }}> <div style={{ display: 'flex', alignItems: 'center' }}>
<Typography <Typography
fontWeight="bold" fontWeight="bold"
@ -434,22 +466,23 @@ function SodioHomeInstallation(props: singleInstallationProps) {
<Route <Route
path={routes.live} path={routes.live}
element={ element={
<div></div> <TopologySodistoreHome
// <Topology values={values}
// values={values} connected={connected}
// connected={connected} loading={loading}
// loading={loading} batteryClusterNumber={props.current_installation.batteryClusterNumber}
// ></Topology> ></TopologySodistoreHome>
} }
/> />
<Route <Route
path={routes.batteryview + '*'} path={routes.batteryview + '/*'}
element={ element={
<BatteryViewSodioHome <BatteryViewSodioHome
values={values} values={values}
s3Credentials={s3Credentials} s3Credentials={s3Credentials}
installationId={props.current_installation.id} installationId={props.current_installation.id}
installation={props.current_installation}
connected={connected} connected={connected}
></BatteryViewSodioHome> ></BatteryViewSodioHome>
} }
@ -474,6 +507,7 @@ function SodioHomeInstallation(props: singleInstallationProps) {
<SodistoreHomeConfiguration <SodistoreHomeConfiguration
values={values} values={values}
id={props.current_installation.id} id={props.current_installation.id}
installation={props.current_installation}
></SodistoreHomeConfiguration> ></SodistoreHomeConfiguration>
} }
/> />

View File

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

View File

@ -24,10 +24,18 @@ import MenuItem from '@mui/material/MenuItem';
import axiosConfig from '../../../Resources/axiosConfig'; import axiosConfig from '../../../Resources/axiosConfig';
import { UserContext } from '../../../contexts/userContext'; import { UserContext } from '../../../contexts/userContext';
import { ProductIdContext } from '../../../contexts/ProductIdContextProvider'; 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 { interface SodistoreHomeConfigurationProps {
values: JSONRecordData; values: JSONRecordData;
id: number; id: number;
installation: I_Installation;
} }
function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) { function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
@ -35,11 +43,22 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
return null; return null;
} }
const OperatingPriorityOptions = [ const device = props.installation.device;
'LoadPriority',
'BatteryPriority', const OperatingPriorityOptions =
'GridPriority' device === 3 // Growatt
]; ? ['LoadPriority', 'BatteryPriority', 'GridPriority']
: device === 4 // Sinexcel
? [
'SpontaneousSelfUse',
'TimeChargeDischarge',
// 'TimeOfUsePowerPrice',
// 'DisasterStandby',
// 'ManualControl',
'PvPriorityCharging',
// 'PrioritySellElectricity'
]
: [];
const [errors, setErrors] = useState({ const [errors, setErrors] = useState({
minimumSoC: false, minimumSoC: false,
@ -69,7 +88,21 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
operatingPriority: OperatingPriorityOptions.indexOf( operatingPriority: OperatingPriorityOptions.indexOf(
props.values.Config.OperatingPriority 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) => { 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) => { const handleSubmit = async (e) => {
// console.log('asked for', dayjs(formValues.calibrationChargeDate)); if (!validateTimeOnly()) {
return;
}
const configurationToSend: Partial<ConfigurationValues> = { const configurationToSend: Partial<ConfigurationValues> = {
minimumSoC: formValues.minimumSoC, minimumSoC: formValues.minimumSoC,
maximumDischargingCurrent: formValues.maximumDischargingCurrent, maximumDischargingCurrent: formValues.maximumDischargingCurrent,
maximumChargingCurrent: formValues.maximumChargingCurrent, maximumChargingCurrent: formValues.maximumChargingCurrent,
operatingPriority: formValues.operatingPriority, 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); setLoading(true);
const res = await axiosConfig const res = await axiosConfig
.post( .post(
`/EditInstallationConfig?installationId=${props.id}`, `/EditInstallationConfig?installationId=${props.id}&product=${product}`,
configurationToSend configurationToSend
) )
.catch((err) => { .catch((err) => {
@ -113,34 +179,40 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
const handleChange = (e) => { const handleChange = (e) => {
const { name, value } = e.target; const { name, value } = e.target;
switch (name) { if (name === 'minimumSoC') {
case 'minimumSoC': const numValue = parseFloat(value);
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;
default: // invalid characters or not a number
break; 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({ setFormValues(prev => ({
...formValues, ...prev,
[name]: value,
}));
};
const handleTimeChargeDischargeChange = (name: string, value: any) => {
setFormValues((prev) => ({
...prev,
[name]: value [name]: value
}); }));
}; };
const handleOkOnErrorDateModal = () => { const handleOkOnErrorDateModal = () => {
@ -209,6 +281,34 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
autoComplete="off" 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' }}> <div style={{ marginBottom: '5px' }}>
<TextField <TextField
label={ label={
@ -224,28 +324,73 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
/> />
</div> </div>
<div style={{ marginBottom: '5px' }}> {device === 4 && (
<TextField <>
label={ <div style={{ marginBottom: '5px' }}>
<FormattedMessage <TextField
id="minimum_soc " label={
defaultMessage="Minimum SoC (%)" <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" name="minimumSoC"
value={formValues.minimumSoC} value={formValues.minimumSoC}
onChange={handleChange} onChange={handleChange}
helperText={ error={Boolean(errors.minimumSoC)}
errors.minimumSoC ? ( helperText={errors.minimumSoC}
<span style={{ color: 'red' }}>
Value should be between 0-100%
</span>
) : (
''
)
}
fullWidth fullWidth
/> />
</div> </div>
<div style={{ marginBottom: '5px' }}> <div style={{ marginBottom: '5px' }}>
@ -307,6 +452,91 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
</div> </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 <div
style={{ style={{
display: 'flex', display: 'flex',

View File

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

View File

@ -96,10 +96,10 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
const singleInstallationTabs = const singleInstallationTabs =
currentUser.userType == UserType.admin currentUser.userType == UserType.admin
? [ ? [
// { {
// value: 'live', value: 'live',
// label: <FormattedMessage id="live" defaultMessage="Live" /> label: <FormattedMessage id="live" defaultMessage="Live" />
// }, },
{ {
value: 'batteryview', value: 'batteryview',
label: ( label: (
@ -155,10 +155,10 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
} }
] ]
: [ : [
// { {
// value: 'live', value: 'live',
// label: <FormattedMessage id="live" defaultMessage="Live" /> label: <FormattedMessage id="live" defaultMessage="Live" />
// }, },
// { // {
// value: 'overview', // value: 'overview',
// label: <FormattedMessage id="overview" defaultMessage="Overview" /> // label: <FormattedMessage id="overview" defaultMessage="Overview" />
@ -186,10 +186,10 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
value: 'tree', value: 'tree',
icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" /> icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" />
}, },
// { {
// value: 'live', value: 'live',
// label: <FormattedMessage id="live" defaultMessage="Live" /> label: <FormattedMessage id="live" defaultMessage="Live" />
// }, },
{ {
value: 'batteryview', value: 'batteryview',
label: ( label: (
@ -256,10 +256,10 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
value: 'tree', value: 'tree',
icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" /> icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" />
}, },
// { {
// value: 'live', value: 'live',
// label: <FormattedMessage id="live" defaultMessage="Live" /> label: <FormattedMessage id="live" defaultMessage="Live" />
// }, },
{ {
value: 'overview', value: 'overview',
label: <FormattedMessage id="overview" defaultMessage="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); const roundedValue = value.toFixed(1);
//Filter all values less than 100 Watts //Filter all values less than 1 Watts(why?)
if (magnitude === 0 && value < 100 && unit === 'W') { if (magnitude === 0 && value < 1 && unit === 'W') {
//console.log('DROP THIS VALUE ' + value); //console.log('DROP THIS VALUE ' + value);
return 0; return 0;
} }
@ -70,11 +70,15 @@ function TopologyBox(props: TopologyBoxProps) {
props.title === 'Battery' props.title === 'Battery'
? '165px' ? '165px'
: props.title === 'AC Loads' || : props.title === 'AC Loads' ||
props.title === 'DC Loads' || props.title === 'DC Loads' ||
props.title === 'Pv Inverter' || props.title === 'Pv Inverter' ||
props.title === 'Pv DC-DC' props.title === 'Pv DC-DC' ||
? '100px' props.title === 'PV' ||
: '150px', props.title === 'Loads'
? '100px'
: props.title === 'Inverter'
? '150px'
: '150px',
backgroundColor: !props.data backgroundColor: !props.data
? 'darkgrey' ? 'darkgrey'
: props.title === 'Grid Bus' || : 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' && ( {props.data && props.title === 'DC Link' && (
<PowerInputIcon <PowerInputIcon
style={{ style={{
@ -183,7 +198,8 @@ function TopologyBox(props: TopologyBoxProps) {
marginTop: '4px' marginTop: '4px'
}} }}
> >
{(props.title === 'Pv Inverter' || {(props.title === 'PV' ||
props.title === 'Pv Inverter' ||
props.title === 'Pv DC-DC') && ( props.title === 'Pv DC-DC') && (
<SolarPowerIcon <SolarPowerIcon
style={{ style={{
@ -207,7 +223,7 @@ function TopologyBox(props: TopologyBoxProps) {
}} }}
></BatteryCharging60Icon> ></BatteryCharging60Icon>
)} )}
{(props.title === 'AC Loads' || props.title === 'DC Loads') && ( {(props.title === 'AC Loads' || props.title === 'DC Loads' ||props.title === 'Loads') && (
<OutletIcon <OutletIcon
style={{ style={{
fontSize: 30, fontSize: 30,

View File

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

View File

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

View File

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