Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
73880f0737
|
|
@ -77,14 +77,18 @@ The script dynamically generates headers for the output CSV file based on the ke
|
|||
extracted data, providing a clear and understandable format for subsequent analysis. The headers correspond to the keys used for data extraction, making
|
||||
it easy to identify and analyze the extracted data.
|
||||
|
||||
|
||||
4)Advanced Data Processing Capabilities:
|
||||
|
||||
Booleans as Numbers: The --booleans_as_numbers flag allows users to convert boolean values (True/False) into numeric representations (1/0). This feature
|
||||
i) Booleans as Numbers: The --booleans_as_numbers flag allows users to convert boolean values (True/False) into numeric representations (1/0). This feature
|
||||
is particularly useful for analytical tasks that require numerical data processing.
|
||||
|
||||
ii) Sampling Stepsize: The --sampling_stepsize parameter enables users to define the granularity of the time range for data extraction. By specifying the number
|
||||
of 1 minute intervals, users can adjust the sampling interval, allowing for flexible data retrieval based on time.
|
||||
Example Command:
|
||||
|
||||
python3 extractS3data.py 1749062721 1749106001 --keys AcDc/SystemControl/ResetAlarmsAndWarnings,AcDc/Devices/1/Status/Ac/L1/Voltage --bucket-number 12 --product_name=SodistoreMax
|
||||
python3 extractS3data.py 1749062721 1749106001 --keys AcDc/SystemControl/ResetAlarmsAndWarnings,AcDc/Devices/1/Status/Ac/L1/Voltage --bucket-number 12 --product_name=SodistoreMax --sampling_stepsize 2 --booleans_as_numbers
|
||||
|
||||
This command extracts data for AcDc/SystemControl/ResetAlarmsAndWarnings and AcDc/Devices/1/Status/Ac/L1/Voltage keys from bucket number 12, between the specified timestamps, with boolean values converted to numbers.
|
||||
The script will fetch data in 2 minutes intervals
|
||||
|
||||
|
|
|
|||
|
|
@ -18,14 +18,15 @@ def extract_timestamp(filename):
|
|||
except ValueError:
|
||||
return 0
|
||||
|
||||
import subprocess
|
||||
|
||||
def list_files_in_range(start_timestamp, end_timestamp, sampling_stepsize,product_type,bucket_number):
|
||||
if product_type == "Salimax" or product_type=="SodistoreMax":
|
||||
def list_files_in_range(start_timestamp, end_timestamp, sampling_stepsize, product_type, bucket_number):
|
||||
if product_type in ["Salimax", "SodistoreMax"]:
|
||||
hash = "3e5b3069-214a-43ee-8d85-57d72000c19d"
|
||||
elif product_type == "Salidomo":
|
||||
hash = "c0436b6a-d276-4cd8-9c44-1eae86cf5d0e"
|
||||
else:
|
||||
raise ValueError("Invalid product type option. Use Salimax or Salidomo or SodistoreMax")
|
||||
raise ValueError("Invalid product type option.")
|
||||
|
||||
# Find common prefix
|
||||
common_prefix = ""
|
||||
|
|
@ -43,20 +44,31 @@ def list_files_in_range(start_timestamp, end_timestamp, sampling_stepsize,produc
|
|||
output = subprocess.check_output(s3cmd_command, shell=True, text=True)
|
||||
files = [line.split()[-1] for line in output.strip().split("\n") if line.strip()]
|
||||
filenames = []
|
||||
count=0
|
||||
|
||||
for f in files:
|
||||
name = f.split("/")[-1] # e.g., 1748802020.json
|
||||
timestamp_str = name.split(".")[0] # extract '1748802020'
|
||||
if timestamp_str.isdigit() and int(timestamp_str) <= int(end_timestamp):
|
||||
filenames.append(name)
|
||||
else:
|
||||
break
|
||||
name = f.split("/")[-1]
|
||||
timestamp_str = name.split(".")[0]
|
||||
|
||||
if timestamp_str.isdigit():
|
||||
timestamp = int(timestamp_str)
|
||||
|
||||
|
||||
if start_timestamp <= timestamp <= end_timestamp :
|
||||
if count % sampling_stepsize == 0:
|
||||
filenames.append(name)
|
||||
count += 1
|
||||
|
||||
|
||||
|
||||
print(filenames)
|
||||
return filenames
|
||||
|
||||
except subprocess.CalledProcessError:
|
||||
print(f"No files found for prefix {common_prefix}")
|
||||
return []
|
||||
|
||||
|
||||
def get_nested_value(data, key_path):
|
||||
try:
|
||||
for key in key_path:
|
||||
|
|
@ -151,7 +163,7 @@ def download_files(bucket_number, filenames_to_download, product_type):
|
|||
print(f"Files with prefix '{filename}' downloaded successfully.")
|
||||
decompress_file(os.path.join(output_directory, filename), output_directory)
|
||||
except subprocess.CalledProcessError as e:
|
||||
# print(f"Error downloading files: {e}")
|
||||
print(f"Error downloading files: {e}")
|
||||
continue
|
||||
else:
|
||||
print(f"File '{filename}.json' already exists locally. Skipping download.")
|
||||
|
|
@ -187,7 +199,7 @@ def get_last_component(path):
|
|||
path_without_slashes = path.replace('/', '')
|
||||
return path_without_slashes
|
||||
|
||||
def download_and_process_files(bucket_number, start_timestamp, end_timestamp, sampling_stepsize, keys, booleans_as_numbers, exact_match, product_type):
|
||||
def download_and_process_files(bucket_number, start_timestamp, end_timestamp, sampling_stepsize, keys, booleans_as_numbers, product_type):
|
||||
output_directory = f"S3cmdData_{bucket_number}"
|
||||
|
||||
#if os.path.exists(output_directory):
|
||||
|
|
@ -200,7 +212,7 @@ def download_and_process_files(bucket_number, start_timestamp, end_timestamp, sa
|
|||
filenames_to_check = list_files_in_range(start_timestamp, end_timestamp, sampling_stepsize,product_type,bucket_number)
|
||||
existing_files = [filename for filename in filenames_to_check if os.path.exists(os.path.join(output_directory, f"{filename}.json"))]
|
||||
files_to_download = set(filenames_to_check) - set(existing_files)
|
||||
print(files_to_download)
|
||||
#print(files_to_download)
|
||||
|
||||
#if os.listdir(output_directory):
|
||||
# print("Files already exist in the local folder. Skipping download.")
|
||||
|
|
@ -231,9 +243,8 @@ def main():
|
|||
parser.add_argument('end_timestamp', type=int, help='The end timestamp for the range (even number)')
|
||||
parser.add_argument('--keys', type=parse_keys, required=True, help='The part to match from each CSV file, can be a single key or a comma-separated list of keys')
|
||||
parser.add_argument('--bucket-number', type=int, required=True, help='The number of the bucket to download from')
|
||||
parser.add_argument('--sampling_stepsize', type=int, required=False, default=1, help='The number of 2sec intervals, which define the length of the sampling interval in S3 file retrieval')
|
||||
parser.add_argument('--sampling_stepsize', type=int, required=False, default=1, help='The number of 1 minute intervals, which define the length of the sampling interval in S3 file retrieval')
|
||||
parser.add_argument('--booleans_as_numbers', action="store_true", required=False, help='If key used, then booleans are converted to numbers [0/1], if key not used, then booleans maintained as text [False/True]')
|
||||
parser.add_argument('--exact_match', action="store_true", required=False, help='If key used, then key has to match exactly "=", else it is enough that key is found "in" text')
|
||||
parser.add_argument('--product_name', required=True, help='Use Salimax, Salidomo or SodistoreMax')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
|
@ -243,14 +254,13 @@ def main():
|
|||
bucket_number = args.bucket_number
|
||||
sampling_stepsize = args.sampling_stepsize
|
||||
booleans_as_numbers = args.booleans_as_numbers
|
||||
exact_match = args.exact_match
|
||||
# new arg for product type
|
||||
product_type = args.product_name
|
||||
|
||||
if start_timestamp >= end_timestamp:
|
||||
print("Error: start_timestamp must be smaller than end_timestamp.")
|
||||
return
|
||||
download_and_process_files(bucket_number, start_timestamp, end_timestamp, sampling_stepsize, keys, booleans_as_numbers, exact_match, product_type)
|
||||
download_and_process_files(bucket_number, start_timestamp, end_timestamp, sampling_stepsize, keys, booleans_as_numbers, product_type)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
|||
|
|
@ -344,6 +344,18 @@ public class Controller : ControllerBase
|
|||
.Select(u => u.HidePassword())
|
||||
.ToList();
|
||||
}
|
||||
|
||||
[HttpGet(nameof(GetInstallationsTheUserHasAccess))]
|
||||
public ActionResult<IEnumerable<Object>> GetInstallationsTheUserHasAccess(Int64 userId, Token authToken)
|
||||
{
|
||||
var user = Db.GetUserById(userId);
|
||||
if (user == null)
|
||||
return Unauthorized();
|
||||
|
||||
|
||||
|
||||
return user.AccessibleInstallations().ToList();
|
||||
}
|
||||
|
||||
[HttpGet(nameof(GetUsersWithInheritedAccessToInstallation))]
|
||||
public ActionResult<IEnumerable<Object>> GetUsersWithInheritedAccessToInstallation(Int64 id, Token authToken)
|
||||
|
|
@ -926,10 +938,12 @@ public class Controller : ControllerBase
|
|||
public async Task<ActionResult<IEnumerable<Object>>> EditInstallationConfig([FromBody] Configuration config, Int64 installationId,Token authToken)
|
||||
{
|
||||
var session = Db.GetSession(authToken);
|
||||
|
||||
Console.WriteLine("CONFIG IS " + config.GetConfigurationString());
|
||||
|
||||
// Send configuration changes
|
||||
var success = await session.SendInstallationConfig(installationId, config);
|
||||
|
||||
|
||||
// Record configuration change
|
||||
if (success)
|
||||
{
|
||||
|
|
@ -940,6 +954,7 @@ public class Controller : ControllerBase
|
|||
Timestamp = DateTime.Now,
|
||||
Description = config.GetConfigurationString()
|
||||
};
|
||||
Console.WriteLine(action.Description);
|
||||
|
||||
var actionSuccess = await session.InsertUserAction(action);
|
||||
return actionSuccess?Ok():Unauthorized();
|
||||
|
|
@ -1020,7 +1035,7 @@ public class Controller : ControllerBase
|
|||
|
||||
Db.DeleteUserPassword(user);
|
||||
|
||||
return Redirect($"https://monitor.innov.energy/?username={user.Email}&reset=true"); // TODO: move to settings file
|
||||
return Redirect($"https://monitor.inesco.energy/?username={user.Email}&reset=true"); // TODO: move to settings file
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -413,6 +413,8 @@ public static class ExoCmd
|
|||
byte[] data = Encoding.UTF8.GetBytes(JsonSerializer.Serialize<Configuration>(config));
|
||||
udpClient.Send(data, data.Length, installation.VpnIp, port);
|
||||
|
||||
Console.WriteLine(config.GetConfigurationString());
|
||||
|
||||
//Console.WriteLine($"Sent UDP message to {installation.VpnIp}:{port}: {config}");
|
||||
Console.WriteLine($"Sent UDP message to {installation.VpnIp}:{port}"+" GridSetPoint is "+config.GridSetPoint +" and MinimumSoC is "+config.MinimumSoC);
|
||||
|
||||
|
|
@ -435,15 +437,6 @@ public static class ExoCmd
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
|
||||
//var s3Region = new S3Region($"https://{installation.S3Region}.{installation.S3Provider}", S3Credentials!);
|
||||
//var url = s3Region.Bucket(installation.BucketName()).Path("config.json");
|
||||
//return await url.PutObject(config);
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -7,12 +7,9 @@ namespace InnovEnergy.App.Backend.DataTypes.Methods;
|
|||
|
||||
public static class InstallationMethods
|
||||
{
|
||||
private static readonly String BucketNameSalt =
|
||||
// Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == ""
|
||||
// ? "stage" :"3e5b3069-214a-43ee-8d85-57d72000c19d";
|
||||
"3e5b3069-214a-43ee-8d85-57d72000c19d";
|
||||
|
||||
private static readonly String BucketNameSalt = "3e5b3069-214a-43ee-8d85-57d72000c19d";
|
||||
private static readonly String SalidomoBucketNameSalt = "c0436b6a-d276-4cd8-9c44-1eae86cf5d0e";
|
||||
private static readonly String SodioHomeBucketNameSalt = "e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa";
|
||||
|
||||
public static String BucketName(this Installation installation)
|
||||
{
|
||||
|
|
@ -20,6 +17,11 @@ public static class InstallationMethods
|
|||
{
|
||||
return $"{installation.S3BucketId}-{BucketNameSalt}";
|
||||
}
|
||||
|
||||
if (installation.Product == (int)ProductType.SodioHome)
|
||||
{
|
||||
return $"{installation.S3BucketId}-{SodioHomeBucketNameSalt}";
|
||||
}
|
||||
|
||||
return $"{installation.S3BucketId}-{SalidomoBucketNameSalt}";
|
||||
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@ public static class SessionMethods
|
|||
|
||||
}
|
||||
|
||||
if (installation.Product == (int)ProductType.SodiStoreMax)
|
||||
if (installation.Product == (int)ProductType.SodiStoreMax || installation.Product == (int)ProductType.SodioHome)
|
||||
{
|
||||
return user is not null
|
||||
&& user.UserType != 0
|
||||
|
|
@ -244,15 +244,15 @@ public static class SessionMethods
|
|||
&& await installation.CreateBucket()
|
||||
&& await installation.RenewS3Credentials();
|
||||
}
|
||||
|
||||
if (installation.Product == (int)ProductType.SodioHome)
|
||||
{
|
||||
return user is not null
|
||||
&& user.UserType != 0
|
||||
&& user.HasAccessToParentOf(installation)
|
||||
&& Db.Create(installation);
|
||||
}
|
||||
|
||||
//
|
||||
// if (installation.Product == (int)ProductType.SodioHome)
|
||||
// {
|
||||
// return user is not null
|
||||
// && user.UserType != 0
|
||||
// && user.HasAccessToParentOf(installation)
|
||||
// && Db.Create(installation);
|
||||
// }
|
||||
//
|
||||
|
||||
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -229,6 +229,9 @@ public static class UserMethods
|
|||
|
||||
public static Task SendEmail(this User user, String subject, String body)
|
||||
{
|
||||
|
||||
Console.WriteLine(user.Name);
|
||||
Console.WriteLine(subject);
|
||||
return Mailer.Send(user.Name, user.Email, subject, body);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -284,6 +284,7 @@ public static partial class Db
|
|||
}
|
||||
catch
|
||||
{
|
||||
Console.WriteLine("return false");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"SmtpServerUrl" : "mail.agenturserver.de",
|
||||
"SmtpUsername" : "p518526p69",
|
||||
"SmtpPassword" : "i;b*xqm4iB5uhl",
|
||||
"SmtpServerUrl" : "smtp.gmail.com",
|
||||
"SmtpUsername" : "angelis@inesco.energy",
|
||||
"SmtpPassword" : "huvu pkqd kakz hqtm ",
|
||||
"SmtpPort" : 587,
|
||||
"SenderName" : "InnovEnergy",
|
||||
"SenderAddress" : "noreply@innov.energy"
|
||||
"SenderName" : "Inesco Energy",
|
||||
"SenderAddress" : "noreply@inesco.energy"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,9 +28,8 @@ public static class Program
|
|||
|
||||
RabbitMqManager.InitializeEnvironment();
|
||||
RabbitMqManager.StartRabbitMqConsumer().SupressAwaitWarning();
|
||||
WebsocketManager.MonitorSalimaxInstallationTable().SupressAwaitWarning();
|
||||
WebsocketManager.MonitorSalidomoInstallationTable().SupressAwaitWarning();
|
||||
WebsocketManager.MonitorSodistoreInstallationTable().SupressAwaitWarning();
|
||||
|
||||
WebsocketManager.MonitorInstallationTable().SupressAwaitWarning();
|
||||
|
||||
|
||||
// Task.Run(() => DeleteOldDataFromS3.DeleteOldData());
|
||||
|
|
|
|||
|
|
@ -49,6 +49,10 @@ public class Session : Relation<String, Int64>
|
|||
AccessToSalidomo = user.AccessibleInstallations(product: (int)ProductType.Salidomo).ToList().Count > 0;
|
||||
AccessToSodistoreMax = user.AccessibleInstallations(product: (int)ProductType.SodiStoreMax).ToList().Count > 0;
|
||||
AccessToSodioHome = user.AccessibleInstallations(product: (int)ProductType.SodioHome).ToList().Count > 0;
|
||||
|
||||
Console.WriteLine("salimax" +user.AccessibleInstallations(product: (int)ProductType.Salimax).ToList().Count);
|
||||
Console.WriteLine("AccessToSodistoreMax" +user.AccessibleInstallations(product: (int)ProductType.SodiStoreMax).ToList().Count);
|
||||
Console.WriteLine("sodio" + user.AccessibleInstallations(product: (int)ProductType.SodioHome).ToList().Count);
|
||||
}
|
||||
|
||||
private static String CreateToken()
|
||||
|
|
|
|||
|
|
@ -13,101 +13,41 @@ public static class WebsocketManager
|
|||
{
|
||||
public static Dictionary<Int64, InstallationInfo> InstallationConnections = new Dictionary<Int64, InstallationInfo>();
|
||||
|
||||
//Every 1 minute, check the timestamp of the latest received message for every installation.
|
||||
//If the difference between the two timestamps is more than two minutes, we consider this Salimax installation unavailable.
|
||||
public static async Task MonitorSalimaxInstallationTable()
|
||||
public static async Task MonitorInstallationTable()
|
||||
{
|
||||
while (true){
|
||||
lock (InstallationConnections){
|
||||
// Console.WriteLine("MONITOR SALIMAX INSTALLATIONS\n");
|
||||
foreach (var installationConnection in InstallationConnections){
|
||||
|
||||
if (installationConnection.Value.Product==(int)ProductType.Salimax && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2)){
|
||||
|
||||
// Console.WriteLine("Installation ID is "+installationConnection.Key);
|
||||
// Console.WriteLine("installationConnection.Value.Timestamp is "+installationConnection.Value.Timestamp);
|
||||
while (true)
|
||||
{
|
||||
lock (InstallationConnections)
|
||||
{
|
||||
Console.WriteLine("Monitoring installation table...");
|
||||
foreach (var installationConnection in InstallationConnections)
|
||||
{
|
||||
Console.WriteLine("installationConnection ID is " + installationConnection.Key + "latest timestamp is" +installationConnection.Value.Timestamp + "product is "+ installationConnection.Value.Product
|
||||
+ "and time diff is "+ (DateTime.Now - installationConnection.Value.Timestamp));
|
||||
|
||||
if ((installationConnection.Value.Product == (int)ProductType.Salimax && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2)) ||
|
||||
(installationConnection.Value.Product == (int)ProductType.Salidomo && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(60)) ||
|
||||
(installationConnection.Value.Product == (int)ProductType.SodioHome && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2)) ||
|
||||
(installationConnection.Value.Product == (int)ProductType.SodiStoreMax && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2))
|
||||
)
|
||||
{
|
||||
|
||||
Console.WriteLine("Installation ID is " + installationConnection.Key);
|
||||
Console.WriteLine("installationConnection.Value.Timestamp is " + installationConnection.Value.Timestamp);
|
||||
// Console.WriteLine("diff is "+(DateTime.Now-installationConnection.Value.Timestamp));
|
||||
|
||||
installationConnection.Value.Status = (int)StatusType.Offline;
|
||||
Installation installation = Db.Installations.FirstOrDefault(f => f.Product == (int)ProductType.Salimax && f.Id == installationConnection.Key);
|
||||
installation.Status = (int)StatusType.Offline;
|
||||
installation.Apply(Db.Update);
|
||||
if (installationConnection.Value.Connections.Count > 0){InformWebsocketsForInstallation(installationConnection.Key);}
|
||||
}
|
||||
}
|
||||
// Console.WriteLine("FINISHED MONITORING SALIMAX INSTALLATIONS\n");
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromMinutes(1));
|
||||
}
|
||||
}
|
||||
|
||||
//Every 1 minute, check the timestamp of the latest received message for every installation.
|
||||
//If the difference between the two timestamps is more than 1 hour, we consider this Salidomo installation unavailable.
|
||||
public static async Task MonitorSalidomoInstallationTable()
|
||||
{
|
||||
while (true){
|
||||
//Console.WriteLine("TRY TO LOCK FOR MONITOR SALIDOMO INSTALLATIONS\n");
|
||||
lock (InstallationConnections){
|
||||
//Console.WriteLine("MONITOR SALIDOMO INSTALLATIONS\n");
|
||||
foreach (var installationConnection in InstallationConnections)
|
||||
{
|
||||
//Console.WriteLine("Installation ID is "+installationConnection.Key);
|
||||
// if (installationConnection.Value.Product == (int)ProductType.Salidomo && (DateTime.Now - installationConnection.Value.Timestamp) < TimeSpan.FromMinutes(60)){
|
||||
// Console.WriteLine("Installation ID is "+installationConnection.Key + " diff is "+(DateTime.Now-installationConnection.Value.Timestamp));
|
||||
// }
|
||||
|
||||
if (installationConnection.Value.Product==(int)ProductType.Salidomo && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(60))
|
||||
{
|
||||
|
||||
//Console.WriteLine("installationConnection.Value.Timestamp is "+installationConnection.Value.Timestamp);
|
||||
//Console.WriteLine("timestamp now is is "+(DateTime.Now));
|
||||
|
||||
Installation installation = Db.Installations.FirstOrDefault(f => f.Product == (int)ProductType.Salidomo && f.Id == installationConnection.Key);
|
||||
//Console.WriteLine("Installation ID is "+installation.Name + " diff is "+(DateTime.Now-installationConnection.Value.Timestamp));
|
||||
installationConnection.Value.Status = (int)StatusType.Offline;
|
||||
Installation installation = Db.Installations.FirstOrDefault(f => f.Product == installationConnection.Value.Product && f.Id == installationConnection.Key);
|
||||
installation.Status = (int)StatusType.Offline;
|
||||
installation.Apply(Db.Update);
|
||||
|
||||
installationConnection.Value.Status = (int)StatusType.Offline;
|
||||
if (installationConnection.Value.Connections.Count > 0){InformWebsocketsForInstallation(installationConnection.Key);}
|
||||
//else{Console.WriteLine("NONE IS CONNECTED TO THAT INSTALLATION-------------------------------------------------------------");}
|
||||
if (installationConnection.Value.Connections.Count > 0)
|
||||
{
|
||||
InformWebsocketsForInstallation(installationConnection.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
//Console.WriteLine("FINISHED WITH UPDATING\n");
|
||||
}
|
||||
await Task.Delay(TimeSpan.FromMinutes(1));
|
||||
}
|
||||
}
|
||||
|
||||
//Every 1 minute, check the timestamp of the latest received message for every installation.
|
||||
//If the difference between the two timestamps is more than two minutes, we consider this Sodistore installation unavailable.
|
||||
public static async Task MonitorSodistoreInstallationTable()
|
||||
{
|
||||
while (true){
|
||||
//Console.WriteLine("TRY TO LOCK FOR MONITOR SODISTORE INSTALLATIONS\n");
|
||||
lock (InstallationConnections){
|
||||
//Console.WriteLine("MONITOR SODISTORE INSTALLATIONS\n");
|
||||
foreach (var installationConnection in InstallationConnections)
|
||||
{
|
||||
|
||||
if (installationConnection.Value.Product==(int)ProductType.SodiStoreMax && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2))
|
||||
{
|
||||
|
||||
//Console.WriteLine("installationConnection.Value.Timestamp is "+installationConnection.Value.Timestamp);
|
||||
//Console.WriteLine("timestamp now is is "+(DateTime.Now));
|
||||
|
||||
Installation installation = Db.Installations.FirstOrDefault(f => f.Product == (int)ProductType.SodiStoreMax && f.Id == installationConnection.Key);
|
||||
//Console.WriteLine("Installation ID is "+installation.Name + " diff is "+(DateTime.Now-installationConnection.Value.Timestamp));
|
||||
installation.Status = (int)StatusType.Offline;
|
||||
installation.Apply(Db.Update);
|
||||
|
||||
installationConnection.Value.Status = (int)StatusType.Offline;
|
||||
if (installationConnection.Value.Connections.Count > 0){InformWebsocketsForInstallation(installationConnection.Key);}
|
||||
//else{Console.WriteLine("NONE IS CONNECTED TO THAT INSTALLATION-------------------------------------------------------------");}
|
||||
}
|
||||
}
|
||||
//Console.WriteLine("FINISHED WITH UPDATING\n");
|
||||
}
|
||||
await Task.Delay(TimeSpan.FromMinutes(1));
|
||||
}
|
||||
}
|
||||
|
|
@ -140,6 +80,7 @@ public static class WebsocketManager
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
public static async Task HandleWebSocketConnection(WebSocket currentWebSocket)
|
||||
{
|
||||
var buffer = new byte[4096];
|
||||
|
|
@ -155,6 +96,7 @@ public static class WebsocketManager
|
|||
|
||||
var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
|
||||
var installationIds = JsonSerializer.Deserialize<int[]>(message);
|
||||
Console.WriteLine("Received Websocket message: " + message);
|
||||
|
||||
//This is a ping message to keep the connection alive, reply with a pong
|
||||
if (installationIds[0] == -1)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,271 @@
|
|||
using System.Text;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DataCollectorWebApp;
|
||||
|
||||
public class LoginResponse
|
||||
{
|
||||
public string Token { get; set; }
|
||||
public object User { get; set; } // or a User class if needed
|
||||
public bool AccessToSalimax { get; set; }
|
||||
public bool AccessToSalidomo { get; set; }
|
||||
public bool AccessToSodiohome { get; set; }
|
||||
public bool AccessToSodistoreMax { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public class Installation
|
||||
{
|
||||
//Each installation has 2 roles, a read role and a write role.
|
||||
//There are 2 keys per role a public key and a secret
|
||||
//Product can be 0 or 1, 0 for Salimax, 1 for Salidomo
|
||||
public String Name { get; set; }
|
||||
public String Location { get; set; }
|
||||
public String Region { get; set; } = "";
|
||||
public String Country { get; set; } = "";
|
||||
public String VpnIp { get; set; } = "";
|
||||
public String InstallationName { get; set; } = "";
|
||||
|
||||
public String S3Region { get; set; } = "sos-ch-dk-2";
|
||||
public String S3Provider { get; set; } = "exo.io";
|
||||
public String S3WriteKey { get; set; } = "";
|
||||
public String S3Key { get; set; } = "";
|
||||
public String S3WriteSecret { get; set; } = "";
|
||||
public String S3Secret { get; set; } = "";
|
||||
public int S3BucketId { get; set; } = 0;
|
||||
public String ReadRoleId { get; set; } = "";
|
||||
public String WriteRoleId { get; set; } = "";
|
||||
public Boolean TestingMode { get; set; } = false;
|
||||
public int Status { get; set; } = -1;
|
||||
public int Product { get; set; } = 0;
|
||||
public int Device { get; set; } = 0;
|
||||
public string SerialNumber { get; set; } = "";
|
||||
|
||||
public String OrderNumbers { get; set; }
|
||||
public String VrmLink { get; set; } = "";
|
||||
}
|
||||
|
||||
[Controller]
|
||||
public class InstallationsController : Controller
|
||||
{
|
||||
|
||||
|
||||
|
||||
[HttpGet]
|
||||
[Route("/Installations")]
|
||||
[Produces("text/html")]
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
const string HtmlHeader = @"
|
||||
<html>
|
||||
<head>
|
||||
<title>Inesco Energy Installations Overview</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
margin: 2rem;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
th, td {
|
||||
padding: 0.75rem;
|
||||
border: 1px solid rgb(200, 200, 200);
|
||||
text-align: left;
|
||||
}
|
||||
thead th {
|
||||
background-color: #f0f0f0;
|
||||
font-weight: bold;
|
||||
border-bottom: 2px solid #EB9486;
|
||||
}
|
||||
tbody tr:nth-child(odd) {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
a {
|
||||
color: #0645AD;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.status-circle {
|
||||
display: inline-block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 50%;
|
||||
margin-left: 50%;
|
||||
}
|
||||
.status-0 { background-color: #4CAF50; } /* Green */
|
||||
.status-1 { background-color: #FF9800; } /* Orange */
|
||||
.status-2 { background-color: #F44336; } /* Red */
|
||||
|
||||
.centered {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.offline-symbol {
|
||||
font-size: 1.2rem;
|
||||
color: #F44336; /* Gray */
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 50%;
|
||||
margin-left: 50%;
|
||||
}
|
||||
|
||||
|
||||
.status-cell {
|
||||
text-align: center; /* center horizontally */
|
||||
vertical-align: middle; /* center vertically */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Installation Overview</h1>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class=""centered"">Name</th>
|
||||
<th class=""centered"">Product</th>
|
||||
<th class=""centered"">Location</th>
|
||||
<th class=""centered"">VPN IP</th>
|
||||
<th class=""centered"">Status</th>
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
";
|
||||
|
||||
const string HtmlFooter = @"
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
";
|
||||
|
||||
|
||||
|
||||
string GetProductName(int productId)
|
||||
{
|
||||
return productId switch
|
||||
{
|
||||
0 => "Salimax",
|
||||
1 => "Salidomo",
|
||||
2 => "SodioHome",
|
||||
3 => "SodistoreMax",
|
||||
};
|
||||
}
|
||||
|
||||
string GetStatusHtml(int status)
|
||||
{
|
||||
if (status == -1)
|
||||
{
|
||||
return "<span class='offline-symbol' title='Offline'>×</span>";
|
||||
}
|
||||
|
||||
|
||||
var statusClass = $"status-{status}";
|
||||
var title = status switch
|
||||
{
|
||||
0 => "Online",
|
||||
1 => "Warning",
|
||||
2 => "Error",
|
||||
_ => "Unknown"
|
||||
};
|
||||
|
||||
return $"<span class='status-circle {statusClass}' title='{title}'></span>";
|
||||
}
|
||||
|
||||
|
||||
string BuildRowHtml(Installation i) => $@"
|
||||
<tr>
|
||||
<td>{i.Name}</td>
|
||||
<td>{GetProductName(i.Product)}</td>
|
||||
<td>{i.Location}</td>
|
||||
<td>{i.VpnIp}</td>
|
||||
<td>{GetStatusHtml(i.Status)}</td>
|
||||
</tr>
|
||||
";
|
||||
|
||||
var installations = await FetchInstallationsFromApi();
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(HtmlHeader);
|
||||
|
||||
foreach (var i in installations)
|
||||
{
|
||||
sb.Append(BuildRowHtml(i));
|
||||
}
|
||||
|
||||
sb.Append(HtmlFooter);
|
||||
return Content(sb.ToString(), "text/html");
|
||||
}
|
||||
|
||||
|
||||
public async Task<List<Installation>?> FetchInstallationsFromApi()
|
||||
{
|
||||
|
||||
var username = "baumgartner@innov.energy";
|
||||
var password = "1234";
|
||||
|
||||
using var http = new HttpClient { BaseAddress = new Uri("https://monitor.inesco.energy/api/") };
|
||||
|
||||
// Step 1: Login
|
||||
var loginResponse = await http.PostAsync($"Login?username={username}&password={password}", null);
|
||||
if (!loginResponse.IsSuccessStatusCode)
|
||||
{
|
||||
Console.WriteLine("Login failed with status code {StatusCode}", loginResponse.StatusCode);
|
||||
return null;
|
||||
}
|
||||
|
||||
var loginData = await loginResponse.Content.ReadFromJsonAsync<LoginResponse>();
|
||||
if (loginData?.Token is null)
|
||||
{
|
||||
Console.WriteLine("Login succeeded but token was missing");
|
||||
return null;
|
||||
}
|
||||
|
||||
var token = loginData.Token;
|
||||
Console.WriteLine($"Token: {token}");
|
||||
var installations = new List<Installation>();
|
||||
|
||||
var getInstallationsRequestResponse = await http.GetAsync($"GetAllInstallationsFromProduct?product=0&authToken={token}");
|
||||
|
||||
var newInstallations= await getInstallationsRequestResponse.Content.ReadFromJsonAsync<List<Installation>>();
|
||||
if (newInstallations != null)
|
||||
{
|
||||
installations.AddRange(newInstallations);
|
||||
}
|
||||
|
||||
getInstallationsRequestResponse = await http.GetAsync($"GetAllInstallationsFromProduct?product=1&authToken={token}");
|
||||
newInstallations= await getInstallationsRequestResponse.Content.ReadFromJsonAsync<List<Installation>>();
|
||||
if (newInstallations != null)
|
||||
{
|
||||
installations.AddRange(newInstallations);
|
||||
}
|
||||
|
||||
getInstallationsRequestResponse = await http.GetAsync($"GetAllInstallationsFromProduct?product=2&authToken={token}");
|
||||
newInstallations= await getInstallationsRequestResponse.Content.ReadFromJsonAsync<List<Installation>>();
|
||||
if (newInstallations != null)
|
||||
{
|
||||
installations.AddRange(newInstallations);
|
||||
}
|
||||
|
||||
getInstallationsRequestResponse = await http.GetAsync($"GetAllInstallationsFromProduct?product=3&authToken={token}");
|
||||
newInstallations= await getInstallationsRequestResponse.Content.ReadFromJsonAsync<List<Installation>>();
|
||||
if (newInstallations != null)
|
||||
{
|
||||
installations.AddRange(newInstallations);
|
||||
}
|
||||
|
||||
|
||||
//Console.WriteLine("Installations retrieved ",installations);
|
||||
return installations;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.OpenApi" Version="1.6.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
using Microsoft.AspNetCore.Builder;
|
||||
|
||||
namespace InnovEnergy.App.DataCollectorWebApp;
|
||||
|
||||
public static class Program
|
||||
{
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
Console.WriteLine("Starting DataCollectorWebApp");
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddControllers();
|
||||
var app = builder.Build();
|
||||
|
||||
app.MapControllers();
|
||||
await app.RunAsync();
|
||||
}
|
||||
}
|
||||
|
|
@ -103,6 +103,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GrowattCommunication", "App
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WITGrowatt4-15K", "Lib\Devices\WITGrowatt4-15K\WITGrowatt4-15K.csproj", "{44DD9E5E-2AD3-4579-A47D-7A40FD28D369}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataCollectorWebApp", "DataCollectorWebApp\DataCollectorWebApp.csproj", "{6069D487-DBAB-4253-BFA1-CF994B84BE49}"
|
||||
EndProject
|
||||
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
|
|
@ -274,6 +276,10 @@ Global
|
|||
{44DD9E5E-2AD3-4579-A47D-7A40FD28D369}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{44DD9E5E-2AD3-4579-A47D-7A40FD28D369}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{44DD9E5E-2AD3-4579-A47D-7A40FD28D369}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6069D487-DBAB-4253-BFA1-CF994B84BE49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6069D487-DBAB-4253-BFA1-CF994B84BE49}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6069D487-DBAB-4253-BFA1-CF994B84BE49}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6069D487-DBAB-4253-BFA1-CF994B84BE49}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{CF4834CB-91B7-4172-AC13-ECDA8613CD17} = {145597B4-3E30-45E6-9F72-4DD43194539A}
|
||||
|
|
@ -321,5 +327,6 @@ Global
|
|||
{39B83793-49DB-4940-9C25-A7F944607407} = {145597B4-3E30-45E6-9F72-4DD43194539A}
|
||||
{DC0BE34A-368F-46DC-A081-70C9A1EFE9C0} = {145597B4-3E30-45E6-9F72-4DD43194539A}
|
||||
{44DD9E5E-2AD3-4579-A47D-7A40FD28D369} = {4931A385-24DC-4E78-BFF4-356F8D6D5183}
|
||||
{6069D487-DBAB-4253-BFA1-CF994B84BE49} = {145597B4-3E30-45E6-9F72-4DD43194539A}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
|
|
|||
|
|
@ -5191,7 +5191,7 @@
|
|||
"order": 5,
|
||||
"width": 0,
|
||||
"height": 0,
|
||||
"format": "<a href=\"https://monitor.innov.energy/salidomo_installations/list/\" target=\"_blank\" class=\"button\"> Battery Monitor </a>\n",
|
||||
"format": "<a href=\"https://monitor.inesco.energy/salidomo_installations/list/\" target=\"_blank\" class=\"button\"> Battery Monitor </a>\n",
|
||||
"storeOutMessages": true,
|
||||
"fwdInMessages": true,
|
||||
"resendOnRefresh": true,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
npm run build && rsync -rv .* ubuntu@194.182.190.208:~/frontend/ && ssh ubuntu@194.182.190.208 'sudo cp -rf ~/frontend/build/* /var/www/html/monitor.innov.energy/html/' && ssh ubuntu@194.182.190.208 'sudo npm install -g serve'
|
||||
npm run build && rsync -rv .* ubuntu@194.182.190.208:~/frontend/ && ssh ubuntu@194.182.190.208 'sudo cp -rf ~/frontend/build/* /var/www/html/monitor.inesco.energy/html/' && ssh ubuntu@194.182.190.208 'sudo npm install -g serve'
|
||||
|
||||
|
||||
#npm run build && rsync -rv .* ubuntu@91.92.154.141:~/frontend/ && ssh ubuntu@91.92.154.141 'sudo cp -rf ~/frontend/build/* /var/www/html/monitor.innov.energy/html/' && ssh ubuntu@91.92.154.141 'sudo npm install -g serve'
|
||||
#npm run build && rsync -rv .* ubuntu@91.92.154.141:~/frontend/ && ssh ubuntu@91.92.154.141 'sudo cp -rf ~/frontend/build/* /var/www/html/monitor.inesco.energy/html/' && ssh ubuntu@91.92.154.141 'sudo npm install -g serve'
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
npm run build && rsync -rv .* ubuntu@91.92.154.141:~/frontend/ && ssh ubuntu@91.92.154.141 'sudo cp -rf ~/frontend/build/* /var/www/html/stage.innov.energy/html/' && ssh ubuntu@91.92.154.141 'sudo npm install -g serve'
|
||||
npm run build && rsync -rv .* ubuntu@91.92.154.141:~/frontend/ && ssh ubuntu@91.92.154.141 'sudo cp -rf ~/frontend/build/* /var/www/html/stage.inesco.energy/html/' && ssh ubuntu@91.92.154.141 'sudo npm install -g serve'
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 6.2 KiB |
|
|
@ -2,7 +2,7 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<link href="%PUBLIC_URL%/favicon.png" rel="shortcut icon"/>
|
||||
<link href="%PUBLIC_URL%/Logo.svg" rel="shortcut icon"/>
|
||||
<meta
|
||||
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
||||
name="viewport"
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
href="https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,400&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<title>InnovEnergy</title>
|
||||
<title>Inesco Energy</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ function App() {
|
|||
path={routes.sodiohome_installations + '*'}
|
||||
element={
|
||||
<AccessContextProvider>
|
||||
<SodioHomeInstallationTabs />
|
||||
<SodioHomeInstallationTabs product={2} />
|
||||
</AccessContextProvider>
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 6.2 KiB |
|
|
@ -1,12 +1,12 @@
|
|||
import axios from 'axios';
|
||||
|
||||
export const axiosConfigWithoutToken = axios.create({
|
||||
baseURL: 'https://monitor.innov.energy/api'
|
||||
baseURL: 'https://monitor.inesco.energy/api'
|
||||
//baseURL: 'http://127.0.0.1:7087/api'
|
||||
});
|
||||
|
||||
const axiosConfig = axios.create({
|
||||
baseURL: 'https://monitor.innov.energy/api'
|
||||
baseURL: 'https://monitor.inesco.energy/api'
|
||||
//baseURL: 'http://127.0.0.1:7087/api'
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ function Footer() {
|
|||
>
|
||||
<Box>
|
||||
<Typography variant="subtitle1">
|
||||
© 2024 - InnovEnergy AG
|
||||
© 2025 - Inesco Energy AG
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography
|
||||
|
|
@ -29,11 +29,11 @@ function Footer() {
|
|||
>
|
||||
Crafted by{' '}
|
||||
<Link
|
||||
href="https://www.innov.energy/"
|
||||
href="https://www.inesco.energy/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
InnovEnergy AG
|
||||
Inesco Energy AG
|
||||
</Link>
|
||||
</Typography>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ import {
|
|||
Typography,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import innovenergyLogo from 'src/Resources/innoveng_logo_on_orange.png';
|
||||
import inescologo from 'src/Resources/Logo.svg';
|
||||
|
||||
import { UserContext } from 'src/contexts/userContext';
|
||||
import { TokenContext } from 'src/contexts/tokenContext';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
|
|
@ -76,8 +77,8 @@ function ForgotPassword() {
|
|||
<Container maxWidth="xl" sx={{ pt: 2 }}>
|
||||
<Grid container>
|
||||
<Grid item xs={3} container justifyContent="flex-start" mb={2}>
|
||||
<a href="https://monitor.innov.energy/">
|
||||
<img src={innovenergyLogo} alt="innovenergy logo" height="100" />
|
||||
<a href="https://monitor.inesco.energy/">
|
||||
<img src={inescologo} alt="inesco logo" height="100" />
|
||||
</a>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
|
@ -100,7 +101,7 @@ function ForgotPassword() {
|
|||
transform: 'translate(-50%, -50%)'
|
||||
}}
|
||||
>
|
||||
<Avatar sx={{ m: 1, bgcolor: '#ffc04d' }}>
|
||||
<Avatar sx={{ m: 1, bgcolor: '#00b33c' }}>
|
||||
<LockOutlinedIcon />
|
||||
</Avatar>
|
||||
<Typography component="h1" variant="h5">
|
||||
|
|
@ -134,15 +135,15 @@ function ForgotPassword() {
|
|||
}}
|
||||
/>
|
||||
|
||||
{loading && <CircularProgress sx={{ color: '#ffc04d' }} />}
|
||||
{loading && <CircularProgress sx={{ color: '#00b33c' }} />}
|
||||
|
||||
<Button
|
||||
sx={{
|
||||
mt: 3,
|
||||
mb: 2,
|
||||
textTransform: 'none',
|
||||
bgcolor: '#ffc04d',
|
||||
'&:hover': { bgcolor: '#f7b34d' }
|
||||
bgcolor: '#00b33c',
|
||||
'&:hover': { bgcolor: '#009933' }
|
||||
}}
|
||||
variant="contained"
|
||||
fullWidth={true}
|
||||
|
|
@ -181,9 +182,9 @@ function ForgotPassword() {
|
|||
sx={{
|
||||
marginTop: 2,
|
||||
textTransform: 'none',
|
||||
bgcolor: '#ffc04d',
|
||||
bgcolor: '#00b33c',
|
||||
color: '#111111',
|
||||
'&:hover': { bgcolor: '#f7b34d' }
|
||||
'&:hover': { bgcolor: '#009933' }
|
||||
}}
|
||||
onClick={() => setErrorModalOpen(false)}
|
||||
>
|
||||
|
|
@ -221,9 +222,9 @@ function ForgotPassword() {
|
|||
sx={{
|
||||
marginTop: 2,
|
||||
textTransform: 'none',
|
||||
bgcolor: '#ffc04d',
|
||||
bgcolor: '#00b33c',
|
||||
color: '#111111',
|
||||
'&:hover': { bgcolor: '#f7b34d' }
|
||||
'&:hover': { bgcolor: '#009933' }
|
||||
}}
|
||||
onClick={handleReturn}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ import {
|
|||
Typography,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import innovenergyLogo from 'src/Resources/innoveng_logo_on_orange.png';
|
||||
import inescologo from 'src/Resources/Logo.svg';
|
||||
|
||||
import axiosConfig from 'src/Resources/axiosConfig';
|
||||
import { UserContext } from 'src/contexts/userContext';
|
||||
import { TokenContext } from 'src/contexts/tokenContext';
|
||||
|
|
@ -73,8 +74,8 @@ function ResetPassword() {
|
|||
<Container maxWidth="xl" sx={{ pt: 2 }}>
|
||||
<Grid container>
|
||||
<Grid item xs={3} container justifyContent="flex-start" mb={2}>
|
||||
<a href="https://monitor.innov.energy/">
|
||||
<img src={innovenergyLogo} alt="innovenergy logo" height="100" />
|
||||
<a href="https://monitor.inesco.energy/">
|
||||
<img src={inescologo} alt="inesco logo" height="100" />
|
||||
</a>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
|
@ -97,7 +98,7 @@ function ResetPassword() {
|
|||
transform: 'translate(-50%, -50%)'
|
||||
}}
|
||||
>
|
||||
<Avatar sx={{ m: 1, bgcolor: '#ffc04d' }}>
|
||||
<Avatar sx={{ m: 1, bgcolor: '#00b33c' }}>
|
||||
<LockOutlinedIcon />
|
||||
</Avatar>
|
||||
<Typography component="h1" variant="h5">
|
||||
|
|
@ -137,7 +138,7 @@ function ResetPassword() {
|
|||
/>
|
||||
|
||||
{loading && (
|
||||
<CircularProgress sx={{ color: '#ffc04d', marginLeft: '170px' }} />
|
||||
<CircularProgress sx={{ color: '#00b33c', marginLeft: '170px' }} />
|
||||
)}
|
||||
|
||||
{password != verifypassword && (
|
||||
|
|
@ -155,8 +156,8 @@ function ResetPassword() {
|
|||
mt: 3,
|
||||
mb: 2,
|
||||
textTransform: 'none',
|
||||
bgcolor: '#ffc04d',
|
||||
'&:hover': { bgcolor: '#f7b34d' }
|
||||
bgcolor: '#00b33c',
|
||||
'&:hover': { bgcolor: '#009933' }
|
||||
}}
|
||||
variant="contained"
|
||||
fullWidth={true}
|
||||
|
|
@ -195,9 +196,9 @@ function ResetPassword() {
|
|||
sx={{
|
||||
marginTop: 2,
|
||||
textTransform: 'none',
|
||||
bgcolor: '#ffc04d',
|
||||
bgcolor: '#00b33c',
|
||||
color: '#111111',
|
||||
'&:hover': { bgcolor: '#f7b34d' }
|
||||
'&:hover': { bgcolor: '#009933' }
|
||||
}}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ import {
|
|||
Typography,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import innovenergyLogo from 'src/Resources/innoveng_logo_on_orange.png';
|
||||
import inescologo from 'src/Resources/Logo.svg';
|
||||
|
||||
import axiosConfig from 'src/Resources/axiosConfig';
|
||||
import { UserContext } from 'src/contexts/userContext';
|
||||
import { TokenContext } from 'src/contexts/tokenContext';
|
||||
|
|
@ -74,8 +75,8 @@ function SetNewPassword() {
|
|||
<Container maxWidth="xl" sx={{ pt: 2 }}>
|
||||
<Grid container>
|
||||
<Grid item xs={3} container justifyContent="flex-start" mb={2}>
|
||||
<a href="https://monitor.innov.energy/">
|
||||
<img src={innovenergyLogo} alt="innovenergy logo" height="100" />
|
||||
<a href="https://monitor.inesco.energy/">
|
||||
<img src={inescologo} alt="inesco logo" height="100" />
|
||||
</a>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
|
@ -98,7 +99,7 @@ function SetNewPassword() {
|
|||
transform: 'translate(-50%, -50%)'
|
||||
}}
|
||||
>
|
||||
<Avatar sx={{ m: 1, bgcolor: '#ffc04d' }}>
|
||||
<Avatar sx={{ m: 1, bgcolor: '#00b33c' }}>
|
||||
<LockOutlinedIcon />
|
||||
</Avatar>
|
||||
<Typography component="h1" variant="h5">
|
||||
|
|
@ -138,7 +139,7 @@ function SetNewPassword() {
|
|||
/>
|
||||
|
||||
{loading && (
|
||||
<CircularProgress sx={{ color: '#ffc04d', marginLeft: '0px' }} />
|
||||
<CircularProgress sx={{ color: '#00b33c', marginLeft: '0px' }} />
|
||||
)}
|
||||
|
||||
{password != verifypassword && (
|
||||
|
|
@ -156,8 +157,8 @@ function SetNewPassword() {
|
|||
mt: 3,
|
||||
mb: 2,
|
||||
textTransform: 'none',
|
||||
bgcolor: '#ffc04d',
|
||||
'&:hover': { bgcolor: '#f7b34d' }
|
||||
bgcolor: '#00b33c',
|
||||
'&:hover': { bgcolor: '#009933' }
|
||||
}}
|
||||
variant="contained"
|
||||
fullWidth={true}
|
||||
|
|
@ -196,9 +197,9 @@ function SetNewPassword() {
|
|||
sx={{
|
||||
marginTop: 2,
|
||||
textTransform: 'none',
|
||||
bgcolor: '#ffc04d',
|
||||
bgcolor: '#00b33c',
|
||||
color: '#111111',
|
||||
'&:hover': { bgcolor: '#f7b34d' }
|
||||
'&:hover': { bgcolor: '#009933' }
|
||||
}}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import {
|
|||
import Avatar from '@mui/material/Avatar';
|
||||
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
|
||||
import Link from '@mui/material/Link';
|
||||
import innovenergyLogo from 'src/Resources/innoveng_logo_on_orange.png';
|
||||
import inescologo from 'src/Resources/Logo.svg';
|
||||
import { axiosConfigWithoutToken } from 'src/Resources/axiosConfig';
|
||||
import Cookies from 'universal-cookie';
|
||||
import { UserContext } from 'src/contexts/userContext';
|
||||
|
|
@ -93,6 +93,8 @@ function Login() {
|
|||
navigate(routes.installations);
|
||||
} else if (response.data.accessToSalidomo) {
|
||||
navigate(routes.salidomo_installations);
|
||||
} else if (response.data.accessToSodistoreMax) {
|
||||
navigate(routes.sodistore_installations);
|
||||
} else {
|
||||
navigate(routes.sodiohome_installations);
|
||||
}
|
||||
|
|
@ -113,8 +115,8 @@ function Login() {
|
|||
<Container maxWidth="xl" sx={{ pt: 2 }} className="login">
|
||||
<Grid container>
|
||||
<Grid item xs={3} container justifyContent="flex-start" mb={2}>
|
||||
<a href="https://monitor.innov.energy/">
|
||||
<img src={innovenergyLogo} alt="innovenergy logo" height="100" />
|
||||
<a href="https://monitor.inesco.energy/">
|
||||
<img src={inescologo} alt="inescologo" height="100" />
|
||||
</a>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
|
@ -137,7 +139,7 @@ function Login() {
|
|||
transform: 'translate(-50%, -50%)'
|
||||
}}
|
||||
>
|
||||
<Avatar sx={{ m: 1, bgcolor: '#ffc04d' }}>
|
||||
<Avatar sx={{ m: 1, bgcolor: '#00b33c' }}>
|
||||
<LockOutlinedIcon />
|
||||
</Avatar>
|
||||
<Typography component="h1" variant="h5">
|
||||
|
|
@ -193,7 +195,7 @@ function Login() {
|
|||
checked={rememberMe}
|
||||
onChange={handleRememberMeChange}
|
||||
icon={<CheckBoxOutlineBlankIcon style={{ color: 'grey' }} />}
|
||||
checkedIcon={<CheckBoxIcon style={{ color: '#ffc04d' }} />}
|
||||
checkedIcon={<CheckBoxIcon style={{ color: '#00b33c' }} />}
|
||||
style={{ marginLeft: -175 }}
|
||||
/>
|
||||
}
|
||||
|
|
@ -204,8 +206,8 @@ function Login() {
|
|||
sx={{
|
||||
mb: 2,
|
||||
textTransform: 'none',
|
||||
bgcolor: '#ffc04d',
|
||||
'&:hover': { bgcolor: '#f7b34d' }
|
||||
bgcolor: '#00b33c',
|
||||
'&:hover': { bgcolor: '#009933' }
|
||||
}}
|
||||
variant="contained"
|
||||
fullWidth={true}
|
||||
|
|
@ -218,7 +220,7 @@ function Login() {
|
|||
{loading && (
|
||||
<CircularProgress
|
||||
sx={{
|
||||
color: '#ffc04d',
|
||||
color: '#009933',
|
||||
marginLeft: '20px'
|
||||
}}
|
||||
/>
|
||||
|
|
@ -253,9 +255,9 @@ function Login() {
|
|||
sx={{
|
||||
marginTop: 2,
|
||||
textTransform: 'none',
|
||||
bgcolor: '#ffc04d',
|
||||
bgcolor: '#00b33c',
|
||||
color: '#111111',
|
||||
'&:hover': { bgcolor: '#f7b34d' }
|
||||
'&:hover': { bgcolor: '#009933' }
|
||||
}}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ function BatteryView(props: BatteryViewProps) {
|
|||
|
||||
const currentLocation = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { product, setProduct } = useContext(ProductIdContext);
|
||||
|
||||
const sortedBatteryView =
|
||||
props.values != null && props.values?.Battery?.Devices
|
||||
? Object.entries(props.values.Battery.Devices)
|
||||
|
|
@ -58,8 +60,6 @@ function BatteryView(props: BatteryViewProps) {
|
|||
navigate(routes.mainstats);
|
||||
};
|
||||
|
||||
const { product, setProduct } = useContext(ProductIdContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (sortedBatteryView.length == 0) {
|
||||
setLoading(true);
|
||||
|
|
@ -232,7 +232,7 @@ function BatteryView(props: BatteryViewProps) {
|
|||
<TableCell align="center">Battery</TableCell>
|
||||
<TableCell align="center">Firmware</TableCell>
|
||||
<TableCell align="center">Power</TableCell>
|
||||
<TableCell align="center">Voltage</TableCell>
|
||||
<TableCell align="center">Battery Voltage</TableCell>
|
||||
<TableCell align="center">SoC</TableCell>
|
||||
<TableCell align="center">Temperature</TableCell>
|
||||
{product === 0 ? (
|
||||
|
|
@ -293,7 +293,7 @@ function BatteryView(props: BatteryViewProps) {
|
|||
</TableCell>
|
||||
<TableCell
|
||||
sx={{
|
||||
width: '10%',
|
||||
width: '14%',
|
||||
textAlign: 'center',
|
||||
|
||||
backgroundColor:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,333 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Container,
|
||||
Grid,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import { JSONRecordData } from '../Log/graph.util';
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
import Button from '@mui/material/Button';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { I_S3Credentials } from '../../../interfaces/S3Types';
|
||||
import routes from '../../../Resources/routes.json';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
|
||||
interface BatteryViewSodioHomeProps {
|
||||
values: JSONRecordData;
|
||||
s3Credentials: I_S3Credentials;
|
||||
installationId: number;
|
||||
connected: boolean;
|
||||
}
|
||||
|
||||
function BatteryViewSodioHome(props: BatteryViewSodioHomeProps) {
|
||||
if (props.values === null && props.connected == true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentLocation = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
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))
|
||||
: [];
|
||||
|
||||
console.log('battery view', sortedBatteryView);
|
||||
const [loading, setLoading] = useState(sortedBatteryView.length == 0);
|
||||
const handleMainStatsButton = () => {
|
||||
navigate(routes.mainstats);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (sortedBatteryView.length == 0) {
|
||||
setLoading(true);
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [sortedBatteryView]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!props.connected && (
|
||||
<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>
|
||||
)}
|
||||
{loading && props.connected && (
|
||||
<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}
|
||||
>
|
||||
Battery service is not available at the moment
|
||||
</Typography>
|
||||
<Typography variant="body2" style={{ color: 'black' }}>
|
||||
Please wait or refresh the page
|
||||
</Typography>
|
||||
</Container>
|
||||
)}
|
||||
|
||||
{!loading && props.connected && (
|
||||
<Container maxWidth="xl">
|
||||
<Grid container>
|
||||
<Grid
|
||||
item
|
||||
xs={6}
|
||||
md={6}
|
||||
sx={{
|
||||
display:
|
||||
!currentLocation.pathname.includes('detailed_view') &&
|
||||
!currentLocation.pathname.includes('mainstats')
|
||||
? 'block'
|
||||
: 'none'
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{
|
||||
marginTop: '20px',
|
||||
backgroundColor: '#808080',
|
||||
color: '#000000',
|
||||
'&:hover': { bgcolor: '#f7b34d' }
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="battery_view"
|
||||
defaultMessage="Battery View"
|
||||
/>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleMainStatsButton}
|
||||
sx={{
|
||||
marginTop: '20px',
|
||||
marginLeft: '20px',
|
||||
backgroundColor: '#ffc04d',
|
||||
color: '#000000',
|
||||
'&:hover': { bgcolor: '#f7b34d' }
|
||||
}}
|
||||
>
|
||||
<FormattedMessage id="main_stats" defaultMessage="Main Stats" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/*<Grid container>*/}
|
||||
{/* <Routes>*/}
|
||||
{/* <Route*/}
|
||||
{/* path={routes.mainstats + '*'}*/}
|
||||
{/* element={*/}
|
||||
{/* <MainStats*/}
|
||||
{/* s3Credentials={props.s3Credentials}*/}
|
||||
{/* id={props.installationId}*/}
|
||||
{/* ></MainStats>*/}
|
||||
{/* }*/}
|
||||
{/* />*/}
|
||||
{/* {product === 0*/}
|
||||
{/* ? Object.entries(props.values.Battery.Devices).map(*/}
|
||||
{/* ([BatteryId, battery]) => (*/}
|
||||
{/* <Route*/}
|
||||
{/* key={routes.detailed_view + BatteryId}*/}
|
||||
{/* path={routes.detailed_view + BatteryId}*/}
|
||||
{/* element={*/}
|
||||
{/* <DetailedBatteryView*/}
|
||||
{/* batteryId={Number(BatteryId)}*/}
|
||||
{/* s3Credentials={props.s3Credentials}*/}
|
||||
{/* batteryData={battery}*/}
|
||||
{/* installationId={props.installationId}*/}
|
||||
{/* productNum={product}*/}
|
||||
{/* ></DetailedBatteryView>*/}
|
||||
{/* }*/}
|
||||
{/* />*/}
|
||||
{/* )*/}
|
||||
{/* )*/}
|
||||
{/* : Object.entries(props.values.Battery.Devices).map(*/}
|
||||
{/* ([BatteryId, battery]) => (*/}
|
||||
{/* <Route*/}
|
||||
{/* key={routes.detailed_view + BatteryId}*/}
|
||||
{/* path={routes.detailed_view + BatteryId}*/}
|
||||
{/* element={*/}
|
||||
{/* <DetailedBatteryViewSodistore*/}
|
||||
{/* batteryId={Number(BatteryId)}*/}
|
||||
{/* s3Credentials={props.s3Credentials}*/}
|
||||
{/* batteryData={battery}*/}
|
||||
{/* installationId={props.installationId}*/}
|
||||
{/* productNum={product}*/}
|
||||
{/* ></DetailedBatteryViewSodistore>*/}
|
||||
{/* }*/}
|
||||
{/* />*/}
|
||||
{/* )*/}
|
||||
{/* )}*/}
|
||||
{/* </Routes>*/}
|
||||
{/*</Grid>*/}
|
||||
|
||||
<TableContainer
|
||||
component={Paper}
|
||||
sx={{
|
||||
marginTop: '20px',
|
||||
marginBottom: '20px',
|
||||
display:
|
||||
!currentLocation.pathname.includes('detailed_view') &&
|
||||
!currentLocation.pathname.includes('mainstats')
|
||||
? 'block'
|
||||
: 'none'
|
||||
}}
|
||||
>
|
||||
<Table sx={{ minWidth: 650 }} aria-label="simple table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell align="center">Battery</TableCell>
|
||||
<TableCell align="center">Power</TableCell>
|
||||
<TableCell align="center">Battery Voltage</TableCell>
|
||||
<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>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{sortedBatteryView.map(({ BatteryId, battery }) => (
|
||||
<TableRow
|
||||
key={BatteryId}
|
||||
style={{
|
||||
height: '10px'
|
||||
}}
|
||||
>
|
||||
<TableCell
|
||||
component="th"
|
||||
scope="row"
|
||||
align="center"
|
||||
sx={{ fontWeight: 'bold' }}
|
||||
>
|
||||
<Link
|
||||
style={{ color: 'black' }}
|
||||
to={routes.detailed_view + BatteryId}
|
||||
>
|
||||
{'Battery Cluster ' + BatteryId}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{
|
||||
width: '13%',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{battery.Power + ' ' + 'W'}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{
|
||||
width: '13%',
|
||||
textAlign: 'center',
|
||||
|
||||
backgroundColor: '#32CD32',
|
||||
color: 'black'
|
||||
}}
|
||||
>
|
||||
{battery.Voltage + ' ' + 'V'}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{
|
||||
width: '13%',
|
||||
textAlign: 'center',
|
||||
backgroundColor: '#32CD32',
|
||||
color: 'inherit'
|
||||
}}
|
||||
>
|
||||
{battery.Current + ' A'}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{
|
||||
width: '8%',
|
||||
textAlign: 'center',
|
||||
backgroundColor: '#32CD32',
|
||||
color: 'inherit'
|
||||
}}
|
||||
>
|
||||
{battery.Soc + ' %'}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{
|
||||
width: '8%',
|
||||
textAlign: 'center',
|
||||
backgroundColor: '#32CD32',
|
||||
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>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Container>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default BatteryViewSodioHome;
|
||||
|
|
@ -169,7 +169,7 @@ function DetailedBatteryViewSodistore(
|
|||
align="left"
|
||||
sx={{ fontWeight: 'bold' }}
|
||||
>
|
||||
Total Battery Voltage
|
||||
Bus Voltage
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
|
|
@ -183,6 +183,29 @@ function DetailedBatteryViewSodistore(
|
|||
' V'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableCell
|
||||
component="th"
|
||||
scope="row"
|
||||
align="left"
|
||||
sx={{ fontWeight: 'bold' }}
|
||||
>
|
||||
Battery Voltage
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
sx={{
|
||||
width: '6ch',
|
||||
whiteSpace: 'nowrap',
|
||||
paddingRight: '12px'
|
||||
}}
|
||||
>
|
||||
{props.batteryData.BatteryDeligreenDataRecord
|
||||
.TotalBatteryVoltage + ' V'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableCell
|
||||
component="th"
|
||||
|
|
@ -349,25 +372,6 @@ function DetailedBatteryViewSodistore(
|
|||
{props.batteryData.BatteryDeligreenDataRecord.Soh + ' %'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableCell
|
||||
component="th"
|
||||
scope="row"
|
||||
align="left"
|
||||
sx={{ fontWeight: 'bold' }}
|
||||
>
|
||||
Port Voltage
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
sx={{
|
||||
width: '6ch',
|
||||
whiteSpace: 'nowrap',
|
||||
paddingRight: '12px'
|
||||
}}
|
||||
></TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
|
|
|||
|
|
@ -11,10 +11,13 @@ import {
|
|||
InputLabel,
|
||||
Modal,
|
||||
Select,
|
||||
Tab,
|
||||
Tabs,
|
||||
TextField,
|
||||
Typography,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import Button from '@mui/material/Button';
|
||||
|
|
@ -29,6 +32,7 @@ import { UserContext } from '../../../contexts/userContext';
|
|||
|
||||
import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
|
||||
import { TimePicker } from '@mui/lab';
|
||||
import { ProductIdContext } from '../../../contexts/ProductIdContextProvider';
|
||||
|
||||
interface ConfigurationProps {
|
||||
values: JSONRecordData;
|
||||
|
|
@ -40,6 +44,9 @@ function Configuration(props: ConfigurationProps) {
|
|||
return null;
|
||||
}
|
||||
|
||||
//console.log(props.values.Config);
|
||||
const [activeTab, setActiveTab] = useState<'charge' | 'discharge'>('charge');
|
||||
|
||||
const CalibrationChargeOptions = [
|
||||
'Repetitive Calibration',
|
||||
'Additional Calibration',
|
||||
|
|
@ -84,11 +91,12 @@ function Configuration(props: ConfigurationProps) {
|
|||
const [isErrorDateModalOpen, setErrorDateModalOpen] = useState(false);
|
||||
const context = useContext(UserContext);
|
||||
const { currentUser, setUser } = context;
|
||||
const { product, setProduct } = useContext(ProductIdContext);
|
||||
|
||||
const [formValues, setFormValues] = useState<ConfigurationValues>({
|
||||
minimumSoC: props.values.Config.MinSoc,
|
||||
gridSetPoint: (props.values.Config.GridSetPoint as number) / 1000,
|
||||
CalibrationChargeState: CalibrationChargeOptionsController.indexOf(
|
||||
calibrationChargeState: CalibrationChargeOptionsController.indexOf(
|
||||
props.values.Config.ForceCalibrationChargeState.toString()
|
||||
),
|
||||
calibrationChargeDate:
|
||||
|
|
@ -100,9 +108,27 @@ function Configuration(props: ConfigurationProps) {
|
|||
.toDate()
|
||||
: dayjs(props.values.Config.DayAndTimeForAdditionalCalibration)
|
||||
// .add(localOffset, 'minute')
|
||||
.toDate()
|
||||
.toDate(),
|
||||
|
||||
...(product === 3 && {
|
||||
calibrationDischargeState: CalibrationChargeOptionsController.indexOf(
|
||||
props.values.Config.ForceCalibrationDischargeState.toString()
|
||||
),
|
||||
calibrationDischargeDate:
|
||||
CalibrationChargeOptionsController.indexOf(
|
||||
props.values.Config.ForceCalibrationDischargeState.toString()
|
||||
) == 0
|
||||
? dayjs(
|
||||
props.values.Config.DownDayAndTimeForRepetitiveCalibration
|
||||
).toDate()
|
||||
: dayjs(
|
||||
props.values.Config.DownDayAndTimeForAdditionalCalibration
|
||||
).toDate()
|
||||
})
|
||||
});
|
||||
|
||||
// console.log(formValues);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
if (
|
||||
CalibrationChargeOptionsController.indexOf(
|
||||
|
|
@ -116,7 +142,7 @@ function Configuration(props: ConfigurationProps) {
|
|||
setErrorDateModalOpen(true);
|
||||
return;
|
||||
} else if (
|
||||
formValues.CalibrationChargeState === 1 &&
|
||||
formValues.calibrationChargeState === 1 &&
|
||||
dayjs(formValues.calibrationChargeDate).isBefore(dayjs())
|
||||
) {
|
||||
//console.log('asked for', dayjs(formValues.calibrationChargeDate));
|
||||
|
|
@ -128,13 +154,17 @@ function Configuration(props: ConfigurationProps) {
|
|||
const configurationToSend: ConfigurationValues = {
|
||||
minimumSoC: formValues.minimumSoC,
|
||||
gridSetPoint: formValues.gridSetPoint,
|
||||
CalibrationChargeState: formValues.CalibrationChargeState,
|
||||
calibrationChargeState: formValues.calibrationChargeState,
|
||||
calibrationChargeDate: dayjs
|
||||
.utc(formValues.calibrationChargeDate)
|
||||
.add(localOffset, 'minute')
|
||||
.toDate(),
|
||||
calibrationDischargeState: formValues.calibrationDischargeState,
|
||||
calibrationDischargeDate: dayjs
|
||||
.utc(formValues.calibrationDischargeDate)
|
||||
.add(localOffset, 'minute')
|
||||
.toDate()
|
||||
};
|
||||
// console.log('will send ', dayjs(formValues.calibrationChargeDate));
|
||||
|
||||
setLoading(true);
|
||||
const res = await axiosConfig
|
||||
|
|
@ -169,6 +199,15 @@ function Configuration(props: ConfigurationProps) {
|
|||
});
|
||||
};
|
||||
|
||||
const handleConfirmDischarge = (newDate) => {
|
||||
//console.log('non adapted day is ', newDate);
|
||||
//console.log('adapted day is ', dayjs.utc(newDate).toDate());
|
||||
setFormValues({
|
||||
...formValues,
|
||||
['calibrationDischargeDate']: dayjs(newDate).toDate()
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectedCalibrationChargeDay = (event) => {
|
||||
const selectedDay = daysInWeek.indexOf(event.target.value);
|
||||
const currentDate = dayjs();
|
||||
|
|
@ -184,10 +223,25 @@ function Configuration(props: ConfigurationProps) {
|
|||
});
|
||||
};
|
||||
|
||||
const handleSelectedCalibrationDisChargeDay = (event) => {
|
||||
const selectedDay = daysInWeek.indexOf(event.target.value);
|
||||
const currentDate = dayjs();
|
||||
let difference = selectedDay - currentDate.day();
|
||||
if (difference < 0) {
|
||||
difference += 7;
|
||||
}
|
||||
|
||||
const adjustedDate = currentDate.add(difference, 'day');
|
||||
setFormValues({
|
||||
...formValues,
|
||||
['calibrationDischargeDate']: adjustedDate.toDate()
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectedCalibrationChargeChange = (event) => {
|
||||
setFormValues({
|
||||
...formValues,
|
||||
['CalibrationChargeState']: CalibrationChargeOptions.indexOf(
|
||||
['calibrationChargeState']: CalibrationChargeOptions.indexOf(
|
||||
event.target.value
|
||||
),
|
||||
['calibrationChargeDate']:
|
||||
|
|
@ -201,6 +255,23 @@ function Configuration(props: ConfigurationProps) {
|
|||
});
|
||||
};
|
||||
|
||||
const handleSelectedCalibrationDisChargeChange = (event) => {
|
||||
setFormValues({
|
||||
...formValues,
|
||||
['calibrationDischargeState']: CalibrationChargeOptions.indexOf(
|
||||
event.target.value
|
||||
),
|
||||
['calibrationDischargeDate']:
|
||||
CalibrationChargeOptions.indexOf(event.target.value) == 0
|
||||
? dayjs(props.values.Config.DownDayAndTimeForRepetitiveCalibration)
|
||||
// .add(localOffset, 'minute')
|
||||
.toDate()
|
||||
: dayjs(props.values.Config.DownDayAndTimeForAdditionalCalibration)
|
||||
// .add(localOffset, 'minute')
|
||||
.toDate()
|
||||
});
|
||||
};
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
|
||||
|
|
@ -284,7 +355,84 @@ function Configuration(props: ConfigurationProps) {
|
|||
</Box>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
<Grid item xs={12} md={12}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
mt: 2,
|
||||
mb: 2
|
||||
}}
|
||||
>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={(e, newValue) => setActiveTab(newValue)}
|
||||
textColor="inherit"
|
||||
TabIndicatorProps={{ style: { display: 'none' } }} // hide default underline
|
||||
sx={{
|
||||
bgcolor: '#f5f5f5',
|
||||
borderRadius: 2,
|
||||
p: 0.5,
|
||||
boxShadow: 1,
|
||||
width: 500,
|
||||
height: 47
|
||||
}}
|
||||
>
|
||||
<Tab
|
||||
value="charge"
|
||||
label="Calibration Charge"
|
||||
sx={(theme) => ({
|
||||
flex: 2,
|
||||
fontWeight: 'bold',
|
||||
borderRadius: 2,
|
||||
textTransform: 'none',
|
||||
color:
|
||||
activeTab === 'charge'
|
||||
? 'white'
|
||||
: theme.palette.text.primary,
|
||||
bgcolor:
|
||||
activeTab === 'charge'
|
||||
? theme.palette.primary.main
|
||||
: 'transparent',
|
||||
|
||||
'&:hover': {
|
||||
bgcolor:
|
||||
activeTab === 'charge'
|
||||
? 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>
|
||||
|
||||
<CardContent>
|
||||
<Box
|
||||
component="form"
|
||||
|
|
@ -294,98 +442,34 @@ function Configuration(props: ConfigurationProps) {
|
|||
noValidate
|
||||
autoComplete="off"
|
||||
>
|
||||
<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 0-100%
|
||||
</span>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '5px', marginTop: '10px' }}>
|
||||
<FormControl fullWidth sx={{ marginLeft: 1, width: 390 }}>
|
||||
<InputLabel
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
backgroundColor: 'white'
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="forced_calibration_charge"
|
||||
defaultMessage="Calibration Charge State"
|
||||
/>
|
||||
</InputLabel>
|
||||
<Select
|
||||
value={
|
||||
CalibrationChargeOptions[
|
||||
formValues.CalibrationChargeState
|
||||
]
|
||||
}
|
||||
onChange={handleSelectedCalibrationChargeChange}
|
||||
>
|
||||
{CalibrationChargeOptions.map((option) => (
|
||||
<MenuItem key={option} value={option}>
|
||||
{option}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
{formValues.CalibrationChargeState == 1 && (
|
||||
<div>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||
<DateTimePicker
|
||||
label="Select Next Calibration Charge Date"
|
||||
value={dayjs(formValues.calibrationChargeDate)}
|
||||
onChange={handleConfirm}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
sx={{
|
||||
marginTop: 2, // Apply styles here
|
||||
width: '100%' // Optional: You can adjust the width or other styling here
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/*<DateTimePicker*/}
|
||||
{/* format="DD/MM/YYYY HH:mm"*/}
|
||||
{/* ampm={false}*/}
|
||||
{/* label="Select Next Calibration Charge Date"*/}
|
||||
{/* value={dayjs(formValues.calibrationChargeDate)}*/}
|
||||
{/* onChange={handleConfirm}*/}
|
||||
{/* sx={{*/}
|
||||
{/* marginTop: 2*/}
|
||||
{/* }} // This should work with the correct imports*/}
|
||||
{/*/>*/}
|
||||
</LocalizationProvider>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formValues.CalibrationChargeState == 0 && (
|
||||
{activeTab === 'charge' && (
|
||||
<>
|
||||
<div style={{ marginBottom: '5px' }}>
|
||||
<FormControl
|
||||
<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 0-100%
|
||||
</span>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
fullWidth
|
||||
sx={{ marginLeft: 1, width: 390, marginTop: 2 }}
|
||||
>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '5px', marginTop: '10px' }}>
|
||||
<FormControl fullWidth sx={{ marginLeft: 1, width: 390 }}>
|
||||
<InputLabel
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
|
|
@ -393,106 +477,283 @@ function Configuration(props: ConfigurationProps) {
|
|||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="calibration_charge_day"
|
||||
defaultMessage="Calibration Charge Day"
|
||||
id="forced_calibration_charge"
|
||||
defaultMessage="Calibration Charge State"
|
||||
/>
|
||||
</InputLabel>
|
||||
<Select
|
||||
value={
|
||||
daysInWeek[formValues.calibrationChargeDate.getDay()]
|
||||
CalibrationChargeOptions[
|
||||
formValues.calibrationChargeState
|
||||
]
|
||||
}
|
||||
onChange={handleSelectedCalibrationChargeDay}
|
||||
onChange={handleSelectedCalibrationChargeChange}
|
||||
>
|
||||
{daysInWeek.map((day) => (
|
||||
<MenuItem key={day} value={day}>
|
||||
{day}
|
||||
{CalibrationChargeOptions.map((option) => (
|
||||
<MenuItem key={option} value={option}>
|
||||
{option}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '5px', marginTop: '10px' }}>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||
<TimePicker
|
||||
ampm={false}
|
||||
label="Calibration Charge Hour"
|
||||
value={dayjs(formValues.calibrationChargeDate)}
|
||||
onChange={(newTime) => handleConfirm(dayjs(newTime))}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
{formValues.calibrationChargeState == 1 && (
|
||||
<div>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||
<DateTimePicker
|
||||
label="Select Next Calibration Charge Date"
|
||||
value={dayjs(formValues.calibrationChargeDate)}
|
||||
onChange={handleConfirm}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
sx={{
|
||||
marginTop: 2, // Apply styles here
|
||||
width: '100%' // Optional: You can adjust the width or other styling here
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formValues.calibrationChargeState == 0 && (
|
||||
<>
|
||||
<div style={{ marginBottom: '5px' }}>
|
||||
<FormControl
|
||||
fullWidth
|
||||
sx={{ marginLeft: 1, width: 390, marginTop: 2 }}
|
||||
>
|
||||
<InputLabel
|
||||
sx={{
|
||||
marginTop: 2, // Apply styles here
|
||||
width: '100%' // Optional: You can adjust the width or other styling here
|
||||
fontSize: 14,
|
||||
backgroundColor: 'white'
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="calibration_charge_day"
|
||||
defaultMessage="Calibration Charge Day"
|
||||
/>
|
||||
</InputLabel>
|
||||
<Select
|
||||
value={
|
||||
daysInWeek[
|
||||
formValues.calibrationChargeDate.getDay()
|
||||
]
|
||||
}
|
||||
onChange={handleSelectedCalibrationChargeDay}
|
||||
>
|
||||
{daysInWeek.map((day) => (
|
||||
<MenuItem key={day} value={day}>
|
||||
{day}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '5px', marginTop: '10px' }}>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||
<TimePicker
|
||||
ampm={false}
|
||||
label="Calibration Charge Hour"
|
||||
value={dayjs(formValues.calibrationChargeDate)}
|
||||
onChange={(newTime) =>
|
||||
handleConfirm(dayjs(newTime))
|
||||
}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
sx={{
|
||||
marginTop: 2, // Apply styles here
|
||||
width: '100%' // Optional: You can adjust the width or other styling here
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
</LocalizationProvider>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div style={{ marginBottom: '5px' }}>
|
||||
<TextField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="grid_set_point"
|
||||
defaultMessage="Grid Set Point (kW)"
|
||||
/>
|
||||
}
|
||||
name="gridSetPoint"
|
||||
value={formValues.gridSetPoint}
|
||||
onChange={handleChange}
|
||||
helperText={
|
||||
errors.gridSetPoint ? (
|
||||
<span style={{ color: 'red' }}>
|
||||
Please provide a valid number
|
||||
</span>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: '5px' }}>
|
||||
<TextField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="Installed_Power_DC1010"
|
||||
defaultMessage="Installed Power DC1010 (kW)"
|
||||
/>
|
||||
}
|
||||
value={
|
||||
(props.values.DcDc.SystemControl
|
||||
.NumberOfConnectedSlaves as number) * 10
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
|
||||
{props.values.Config.MaxBatteryDischargingCurrent && (
|
||||
<div style={{ marginBottom: '5px' }}>
|
||||
<TextField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="Maximum_Discharge_Power"
|
||||
defaultMessage="Maximum Discharge Power (W)"
|
||||
/>
|
||||
}
|
||||
value={
|
||||
(props.values.Config
|
||||
.MaxBatteryDischargingCurrent as number) *
|
||||
48 *
|
||||
(props.values.DcDc.SystemControl
|
||||
.NumberOfConnectedSlaves as number)
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div style={{ marginBottom: '5px' }}>
|
||||
<TextField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="grid_set_point"
|
||||
defaultMessage="Grid Set Point (kW)"
|
||||
/>
|
||||
}
|
||||
name="gridSetPoint"
|
||||
value={formValues.gridSetPoint}
|
||||
onChange={handleChange}
|
||||
helperText={
|
||||
errors.gridSetPoint ? (
|
||||
<span style={{ color: 'red' }}>
|
||||
Please provide a valid number
|
||||
</span>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: '5px' }}>
|
||||
<TextField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="Installed_Power_DC1010"
|
||||
defaultMessage="Installed Power DC1010 (kW)"
|
||||
/>
|
||||
}
|
||||
value={
|
||||
(props.values.DcDc.SystemControl
|
||||
.NumberOfConnectedSlaves as number) * 10
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
{product == 3 && activeTab === 'discharge' && (
|
||||
<>
|
||||
<div style={{ marginBottom: '5px', marginTop: '10px' }}>
|
||||
<FormControl fullWidth sx={{ marginLeft: 1, width: 390 }}>
|
||||
<InputLabel
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
backgroundColor: 'white'
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="calibration_discharge_state"
|
||||
defaultMessage="Calibration Discharge State"
|
||||
/>
|
||||
</InputLabel>
|
||||
<Select
|
||||
value={
|
||||
CalibrationChargeOptions[
|
||||
formValues.calibrationDischargeState
|
||||
]
|
||||
}
|
||||
onChange={handleSelectedCalibrationDisChargeChange}
|
||||
>
|
||||
{CalibrationChargeOptions.map((option) => (
|
||||
<MenuItem key={option} value={option}>
|
||||
{option}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
{props.values.Config.MaxBatteryDischargingCurrent && (
|
||||
<div style={{ marginBottom: '5px' }}>
|
||||
<TextField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="Maximum_Discharge_Power"
|
||||
defaultMessage="Maximum Discharge Power (W)"
|
||||
/>
|
||||
}
|
||||
value={
|
||||
(props.values.Config
|
||||
.MaxBatteryDischargingCurrent as number) *
|
||||
48 *
|
||||
(props.values.DcDc.SystemControl
|
||||
.NumberOfConnectedSlaves as number)
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
{formValues.calibrationDischargeState == 1 && (
|
||||
<div>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||
<DateTimePicker
|
||||
label="Select Next Calibration Discharge Date"
|
||||
value={dayjs(formValues.calibrationDischargeDate)}
|
||||
onChange={handleConfirmDischarge}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
sx={{
|
||||
marginTop: 2, // Apply styles here
|
||||
width: '100%' // Optional: You can adjust the width or other styling here
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formValues.calibrationDischargeState == 0 && (
|
||||
<>
|
||||
<div style={{ marginBottom: '5px' }}>
|
||||
<FormControl
|
||||
fullWidth
|
||||
sx={{ marginLeft: 1, width: 390, marginTop: 2 }}
|
||||
>
|
||||
<InputLabel
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
backgroundColor: 'white'
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="calibration_charge_day"
|
||||
defaultMessage="Calibration Discharge Day"
|
||||
/>
|
||||
</InputLabel>
|
||||
<Select
|
||||
value={
|
||||
daysInWeek[
|
||||
formValues.calibrationDischargeDate.getDay()
|
||||
]
|
||||
}
|
||||
onChange={handleSelectedCalibrationDisChargeDay}
|
||||
>
|
||||
{daysInWeek.map((day) => (
|
||||
<MenuItem key={day} value={day}>
|
||||
{day}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '5px', marginTop: '10px' }}>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||
<TimePicker
|
||||
ampm={false}
|
||||
label="Calibration Discharge Hour"
|
||||
value={dayjs(formValues.calibrationDischargeDate)}
|
||||
onChange={(newTime) =>
|
||||
handleConfirmDischarge(dayjs(newTime))
|
||||
}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
sx={{
|
||||
marginTop: 2, // Apply styles here
|
||||
width: '100%' // Optional: You can adjust the width or other styling here
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/*<div>*/}
|
||||
{/* <TextField*/}
|
||||
{/* label={*/}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { I_Installation } from '../../../interfaces/InstallationTypes';
|
|||
import { InstallationsContext } from '../../../contexts/InstallationsContextProvider';
|
||||
import { UserContext } from '../../../contexts/userContext';
|
||||
import { UserType } from '../../../interfaces/UserTypes';
|
||||
import { ProductIdContext } from '../../../contexts/ProductIdContextProvider';
|
||||
|
||||
interface InformationProps {
|
||||
values: I_Installation;
|
||||
|
|
@ -38,6 +39,7 @@ function Information(props: InformationProps) {
|
|||
const [formValues, setFormValues] = useState(props.values);
|
||||
const requiredFields = ['name', 'region', 'location', 'country'];
|
||||
const installationContext = useContext(InstallationsContext);
|
||||
const { product, setProduct } = useContext(ProductIdContext);
|
||||
const {
|
||||
updateInstallation,
|
||||
loading,
|
||||
|
|
@ -265,6 +267,30 @@ function Information(props: InformationProps) {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="information"
|
||||
defaultMessage="Information"
|
||||
/>
|
||||
}
|
||||
name="information"
|
||||
value={formValues.information}
|
||||
onChange={handleChange}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
multiline
|
||||
minRows={6} // 👈 Makes it visually bigger
|
||||
maxRows={12} // 👈 Optional max height before scroll
|
||||
inputProps={{
|
||||
style: {
|
||||
fontFamily: 'monospace' // optional: makes tabs/formatting more clear
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{currentUser.userType == UserType.admin && (
|
||||
<>
|
||||
<div>
|
||||
|
|
@ -282,8 +308,11 @@ function Information(props: InformationProps) {
|
|||
label="S3 Bucket Name"
|
||||
name="s3bucketname"
|
||||
value={
|
||||
formValues.s3BucketId +
|
||||
'-3e5b3069-214a-43ee-8d85-57d72000c19d'
|
||||
product === 0 || product == 3
|
||||
? formValues.s3BucketId +
|
||||
'-3e5b3069-214a-43ee-8d85-57d72000c19d'
|
||||
: formValues.s3BucketId +
|
||||
'-e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa'
|
||||
}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
|
|
|
|||
|
|
@ -1,393 +0,0 @@
|
|||
import {
|
||||
Alert,
|
||||
Box,
|
||||
CardContent,
|
||||
CircularProgress,
|
||||
Container,
|
||||
Grid,
|
||||
IconButton,
|
||||
Modal,
|
||||
TextField,
|
||||
Typography,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import Button from '@mui/material/Button';
|
||||
import { Close as CloseIcon } from '@mui/icons-material';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { I_Installation } from '../../../interfaces/InstallationTypes';
|
||||
import { InstallationsContext } from '../../../contexts/InstallationsContextProvider';
|
||||
import { UserContext } from '../../../contexts/userContext';
|
||||
import routes from '../../../Resources/routes.json';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { UserType } from '../../../interfaces/UserTypes';
|
||||
|
||||
interface InformationSodioHomeProps {
|
||||
values: I_Installation;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
function InformationSodioHome(props: InformationSodioHomeProps) {
|
||||
if (props.values === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const context = useContext(UserContext);
|
||||
const { currentUser } = context;
|
||||
const theme = useTheme();
|
||||
const [formValues, setFormValues] = useState(props.values);
|
||||
const requiredFields = ['name', 'region', 'location', 'country'];
|
||||
const [openModalDeleteInstallation, setOpenModalDeleteInstallation] =
|
||||
useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const DeviceTypes = ['Cerbo', 'Venus'];
|
||||
|
||||
const installationContext = useContext(InstallationsContext);
|
||||
const {
|
||||
updateInstallation,
|
||||
deleteInstallation,
|
||||
loading,
|
||||
setLoading,
|
||||
error,
|
||||
setError,
|
||||
updated,
|
||||
setUpdated
|
||||
} = installationContext;
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormValues({
|
||||
...formValues,
|
||||
[name]: value
|
||||
});
|
||||
};
|
||||
const handleSubmit = () => {
|
||||
setLoading(true);
|
||||
setError(false);
|
||||
updateInstallation(formValues, props.type);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
setLoading(true);
|
||||
setError(false);
|
||||
setOpenModalDeleteInstallation(true);
|
||||
};
|
||||
|
||||
const deleteInstallationModalHandle = () => {
|
||||
setOpenModalDeleteInstallation(false);
|
||||
deleteInstallation(formValues, props.type);
|
||||
setLoading(false);
|
||||
|
||||
navigate(routes.salidomo_installations + routes.list, {
|
||||
replace: true
|
||||
});
|
||||
};
|
||||
|
||||
const deleteInstallationModalHandleCancel = () => {
|
||||
setOpenModalDeleteInstallation(false);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const areRequiredFieldsFilled = () => {
|
||||
for (const field of requiredFields) {
|
||||
if (!formValues[field]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{openModalDeleteInstallation && (
|
||||
<Modal
|
||||
open={openModalDeleteInstallation}
|
||||
onClose={deleteInstallationModalHandleCancel}
|
||||
aria-labelledby="error-modal"
|
||||
aria-describedby="error-modal-description"
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 350,
|
||||
bgcolor: 'background.paper',
|
||||
borderRadius: 4,
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
gutterBottom
|
||||
sx={{ fontWeight: 'bold' }}
|
||||
>
|
||||
Do you want to delete this installation?
|
||||
</Typography>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginTop: 10
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
sx={{
|
||||
marginTop: 2,
|
||||
textTransform: 'none',
|
||||
bgcolor: '#ffc04d',
|
||||
color: '#111111',
|
||||
'&:hover': { bgcolor: '#f7b34d' }
|
||||
}}
|
||||
onClick={deleteInstallationModalHandle}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
<Button
|
||||
sx={{
|
||||
marginTop: 2,
|
||||
marginLeft: 2,
|
||||
textTransform: 'none',
|
||||
bgcolor: '#ffc04d',
|
||||
color: '#111111',
|
||||
'&:hover': { bgcolor: '#f7b34d' }
|
||||
}}
|
||||
onClick={deleteInstallationModalHandleCancel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
<Container maxWidth="xl">
|
||||
<Grid
|
||||
container
|
||||
direction="row"
|
||||
justifyContent="center"
|
||||
alignItems="stretch"
|
||||
spacing={3}
|
||||
>
|
||||
<Grid item xs={12} md={12}>
|
||||
<CardContent>
|
||||
<Box
|
||||
component="form"
|
||||
sx={{
|
||||
'& .MuiTextField-root': { m: 1, width: '50ch' }
|
||||
}}
|
||||
noValidate
|
||||
autoComplete="off"
|
||||
>
|
||||
<div>
|
||||
<TextField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="installation_name"
|
||||
defaultMessage="Installation Name"
|
||||
/>
|
||||
}
|
||||
name="name"
|
||||
value={formValues.name}
|
||||
onChange={handleChange}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<TextField
|
||||
label={
|
||||
<FormattedMessage id="region" defaultMessage="Region" />
|
||||
}
|
||||
name="region"
|
||||
value={formValues.region}
|
||||
onChange={handleChange}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
required
|
||||
error={formValues.region === ''}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<TextField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="location"
|
||||
defaultMessage="Location"
|
||||
/>
|
||||
}
|
||||
name="location"
|
||||
value={formValues.location}
|
||||
onChange={handleChange}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
required
|
||||
error={formValues.location === ''}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<TextField
|
||||
label={
|
||||
<FormattedMessage id="country" defaultMessage="Country" />
|
||||
}
|
||||
name="country"
|
||||
value={formValues.country}
|
||||
onChange={handleChange}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
required
|
||||
error={formValues.country === ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="information"
|
||||
defaultMessage="Information"
|
||||
/>
|
||||
}
|
||||
name="information"
|
||||
value={formValues.information}
|
||||
onChange={handleChange}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
|
||||
{currentUser.userType == UserType.admin && (
|
||||
<>
|
||||
<div>
|
||||
<TextField
|
||||
label="BitWatt Cloud Access Key"
|
||||
name="s3WriteKey"
|
||||
value={formValues.s3WriteKey}
|
||||
onChange={handleChange}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextField
|
||||
label="BitWatt Cloud Secret Key"
|
||||
name="s3WriteSecret"
|
||||
onChange={handleChange}
|
||||
value={formValues.s3WriteSecret}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginTop: 10
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleSubmit}
|
||||
sx={{
|
||||
marginLeft: '10px'
|
||||
}}
|
||||
disabled={!areRequiredFieldsFilled()}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="applyChanges"
|
||||
defaultMessage="Apply Changes"
|
||||
/>
|
||||
</Button>
|
||||
|
||||
{currentUser.userType == UserType.admin && (
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleDelete}
|
||||
sx={{
|
||||
marginLeft: '10px'
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="deleteInstallation"
|
||||
defaultMessage="Delete Installation"
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{loading && (
|
||||
<CircularProgress
|
||||
sx={{
|
||||
color: theme.palette.primary.main,
|
||||
marginLeft: '20px'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{error && (
|
||||
<Alert
|
||||
severity="error"
|
||||
sx={{
|
||||
ml: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="errorOccured"
|
||||
defaultMessage="An error has occurred"
|
||||
/>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
onClick={() => setError(false)}
|
||||
sx={{ marginLeft: '4px' }}
|
||||
>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Alert>
|
||||
)}
|
||||
{updated && (
|
||||
<Alert
|
||||
severity="success"
|
||||
sx={{
|
||||
ml: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="successfullyUpdated"
|
||||
defaultMessage="Successfully updated"
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
onClick={() => setUpdated(false)} // Set error state to false on click
|
||||
sx={{ marginLeft: '4px' }}
|
||||
>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default InformationSodioHome;
|
||||
|
|
@ -135,7 +135,6 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
|
|||
}, []);
|
||||
|
||||
const handleSelectOneInstallation = (installationID: number): void => {
|
||||
console.log('when selecting installation', product);
|
||||
if (selectedInstallation != installationID) {
|
||||
setSelectedInstallation(installationID);
|
||||
setSelectedInstallation(-1);
|
||||
|
|
|
|||
|
|
@ -19,11 +19,14 @@ interface installationFormProps {
|
|||
cancel: () => void;
|
||||
submit: () => void;
|
||||
parentid: number;
|
||||
productToInsert: number;
|
||||
}
|
||||
|
||||
function installationForm(props: installationFormProps) {
|
||||
const theme = useTheme();
|
||||
const [open, setOpen] = useState(true);
|
||||
|
||||
console.log('productToInsert IS ', props.productToInsert);
|
||||
const [formValues, setFormValues] = useState<Partial<I_Installation>>({
|
||||
installationName: '',
|
||||
name: '',
|
||||
|
|
@ -59,7 +62,7 @@ function installationForm(props: installationFormProps) {
|
|||
const handleSubmit = async (e) => {
|
||||
setLoading(true);
|
||||
formValues.parentId = props.parentid;
|
||||
formValues.product = 0;
|
||||
formValues.product = props.productToInsert;
|
||||
const responseData = await createInstallation(formValues);
|
||||
props.submit();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -216,6 +216,20 @@ export interface Line {
|
|||
Power: Power;
|
||||
}
|
||||
|
||||
export interface SodioHomeBattery {
|
||||
AccumulatedChargeEnergy: number;
|
||||
AccumulatedDischargeEnergy: number;
|
||||
Current: number;
|
||||
DailyChargeEnergy: number;
|
||||
DailyDischargeEnergy: number;
|
||||
MaxAllowableChargePower: number;
|
||||
MaxAllowableDischargePower: number;
|
||||
Power: number;
|
||||
Soc: number;
|
||||
Soh: number;
|
||||
Voltage: number;
|
||||
}
|
||||
|
||||
// The interface for the Battery structure, with dynamic keys (Device IDs)
|
||||
export interface JSONRecordData {
|
||||
Battery: {
|
||||
|
|
@ -282,9 +296,12 @@ export interface JSONRecordData {
|
|||
CurtailP: number;
|
||||
DayAndTimeForAdditionalCalibration: string;
|
||||
DayAndTimeForRepetitiveCalibration: string;
|
||||
DownDayAndTimeForAdditionalCalibration: string;
|
||||
DownDayAndTimeForRepetitiveCalibration: string;
|
||||
DisplayIndividualBatteries: string;
|
||||
MaxBatteryDischargingCurrent: number;
|
||||
ForceCalibrationChargeState: string;
|
||||
ForceCalibrationDischargeState: string;
|
||||
GridSetPoint: number;
|
||||
HoldSocZone: number;
|
||||
MinSoc: number;
|
||||
|
|
@ -392,17 +409,101 @@ export interface JSONRecordData {
|
|||
LoadOnDc: { Power: number };
|
||||
|
||||
PvOnDc: {
|
||||
DcWh: number;
|
||||
NbrOfStrings: number;
|
||||
Dc: {
|
||||
Voltage: number;
|
||||
Current: number;
|
||||
Power: number;
|
||||
};
|
||||
Strings: {
|
||||
[PvId: string]: PvString;
|
||||
[deviceId: string]: {
|
||||
DcWh: number;
|
||||
Dc: {
|
||||
Voltage: number;
|
||||
Current: number;
|
||||
Power: number;
|
||||
NbrOfStrings: number;
|
||||
};
|
||||
Strings: {
|
||||
[PvId: string]: PvString;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
AcDcGrowatt: {
|
||||
AcChargeEnable: number;
|
||||
ActivePowerPercentDerating: number;
|
||||
ActualChargeDischargePowerControlValue: number;
|
||||
AlarmMainCode: number;
|
||||
AlarmSubCode: number;
|
||||
BatteriesRecords: {
|
||||
AverageSoc: number;
|
||||
AverageSoh: number;
|
||||
Batteries: { [deviceId: string]: SodioHomeBattery };
|
||||
LowestSoc: number;
|
||||
Power: number;
|
||||
TotalChargeEnergy: number;
|
||||
TotalDischargeEnergy: number;
|
||||
TotalMaxCharge: number;
|
||||
TotalMaxDischarge: number;
|
||||
};
|
||||
|
||||
BatteryChargeCutoffVoltage: number;
|
||||
BatteryClusterIndex: number;
|
||||
BatteryDischargeCutoffVoltage: number;
|
||||
BatteryMaxChargeCurrent: number;
|
||||
BatteryMaxChargePower: number;
|
||||
BatteryMaxDischargePower: number;
|
||||
BatteryMaxdischargeCurrent: number;
|
||||
BatteryOperatingMode: string;
|
||||
BatteryType: number;
|
||||
ChargeCutoffSoc: number;
|
||||
ControlPermession: number;
|
||||
DischargeCutoffSoc: number;
|
||||
EmsCommunicationFailureTime: number;
|
||||
EnableCommand: number;
|
||||
EnableEmsCommunicationFailureTime: number;
|
||||
EnableSyn: string;
|
||||
EnergyToGrid: number;
|
||||
EnergyToUser: number;
|
||||
FaultMainCode: number;
|
||||
FaultSubCode: number;
|
||||
Frequency: number;
|
||||
GridAbLineVoltage: number;
|
||||
GridBcLineVoltage: number;
|
||||
GridCaLineVoltage: number;
|
||||
InverterActivePower: number;
|
||||
InverterReactivePower: number;
|
||||
InverterTemperature: number;
|
||||
LoadPriorityDischargeCutoffSoc: number;
|
||||
MaxActivePower: number;
|
||||
MeterPower: number;
|
||||
OffGridDischargeCutoffSoc: number;
|
||||
OperatingPriority: string;
|
||||
PhaseACurrent: number;
|
||||
PhaseBCurrent: number;
|
||||
PhaseCCurrent: number;
|
||||
PowerFactor: number;
|
||||
Pv1Current: number;
|
||||
Pv1Voltage: number;
|
||||
Pv2Current: number;
|
||||
Pv2Voltage: number;
|
||||
PvInputMaxPower: number;
|
||||
RatedPower: number;
|
||||
RemoteChargDischargePower: number;
|
||||
RemotePowerControl: number;
|
||||
SystemDateTime: string;
|
||||
SystemOperatingMode: string;
|
||||
TotalEnergyToGrid: number;
|
||||
TotalEnergyToUser: number;
|
||||
VppProtocolVerNumber: number;
|
||||
};
|
||||
|
||||
// DcWh: number;
|
||||
// // NbrOfStrings: number;
|
||||
// Dc: { [deviceId: string]: Dc };
|
||||
// // Dc: {
|
||||
// // Voltage: number;
|
||||
// // Current: number;
|
||||
// // Power: number;
|
||||
// // };
|
||||
// Strings: {
|
||||
// [PvId: string]: PvString;
|
||||
// };
|
||||
// };
|
||||
}
|
||||
|
||||
export const parseChunkJson = (
|
||||
|
|
@ -497,8 +598,10 @@ export interface I_BoxDataValue {
|
|||
export type ConfigurationValues = {
|
||||
minimumSoC: string | number;
|
||||
gridSetPoint: number;
|
||||
CalibrationChargeState: number;
|
||||
calibrationChargeState: number;
|
||||
calibrationChargeDate: Date | null;
|
||||
calibrationDischargeState: number;
|
||||
calibrationDischargeDate: Date | null;
|
||||
};
|
||||
//
|
||||
// export interface Pv {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,499 @@
|
|||
import React, {
|
||||
Fragment,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState
|
||||
} from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Container,
|
||||
Divider,
|
||||
FormControl,
|
||||
Grid,
|
||||
IconButton,
|
||||
InputLabel,
|
||||
ListItem,
|
||||
MenuItem,
|
||||
Modal,
|
||||
Select,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import { TokenContext } from 'src/contexts/tokenContext';
|
||||
import { UserContext } from 'src/contexts/userContext';
|
||||
import ListItemAvatar from '@mui/material/ListItemAvatar';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import PersonIcon from '@mui/icons-material/Person';
|
||||
import Button from '@mui/material/Button';
|
||||
import { Close as CloseIcon } from '@mui/icons-material';
|
||||
import { AccessContext } from 'src/contexts/AccessContextProvider';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { InnovEnergyUser, UserType } from '../../../interfaces/UserTypes';
|
||||
import PersonRemoveIcon from '@mui/icons-material/PersonRemove';
|
||||
import {
|
||||
I_Folder,
|
||||
I_Installation
|
||||
} from '../../../interfaces/InstallationTypes';
|
||||
import axiosConfig from '../../../Resources/axiosConfig';
|
||||
|
||||
interface UserAccessProps {
|
||||
current_user: InnovEnergyUser;
|
||||
}
|
||||
|
||||
function UserAccess(props: UserAccessProps) {
|
||||
if (props.current_user == undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const theme = useTheme();
|
||||
const tokencontext = useContext(TokenContext);
|
||||
const { removeToken } = tokencontext;
|
||||
const context = useContext(UserContext);
|
||||
const { currentUser, setUser } = context;
|
||||
const [openFolder, setOpenFolder] = useState(false);
|
||||
const [openInstallation, setOpenInstallation] = useState(false);
|
||||
const [openModal, setOpenModal] = useState(false);
|
||||
|
||||
const [selectedFolderNames, setSelectedFolderNames] = useState<string[]>([]);
|
||||
const [selectedInstallationNames, setSelectedInstallationNames] = useState<
|
||||
string[]
|
||||
>([]);
|
||||
|
||||
const [folders, setFolders] = useState<I_Folder[]>([]);
|
||||
const [installations, setInstallations] = useState<I_Installation[]>([]);
|
||||
const accessContext = useContext(AccessContext);
|
||||
const {
|
||||
fetchInstallationsForUser,
|
||||
accessibleInstallationsForUser,
|
||||
error,
|
||||
setError,
|
||||
updated,
|
||||
setUpdated,
|
||||
updatedmessage,
|
||||
errormessage,
|
||||
setErrorMessage,
|
||||
setUpdatedMessage,
|
||||
RevokeAccessFromResource
|
||||
} = accessContext;
|
||||
|
||||
const fetchFolders = useCallback(async () => {
|
||||
return axiosConfig
|
||||
.get('/GetAllFolders')
|
||||
.then((res) => {
|
||||
setFolders(res.data);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.response && err.response.status == 401) {
|
||||
removeToken();
|
||||
}
|
||||
});
|
||||
}, [setFolders]);
|
||||
|
||||
const fetchInstallations = useCallback(async () => {
|
||||
try {
|
||||
// fetch product 0
|
||||
const res0 = await axiosConfig.get(
|
||||
`/GetAllInstallationsFromProduct?product=0`
|
||||
);
|
||||
const installations0 = res0.data;
|
||||
|
||||
// fetch product 1
|
||||
const res1 = await axiosConfig.get(
|
||||
`/GetAllInstallationsFromProduct?product=3`
|
||||
);
|
||||
const installations1 = res1.data;
|
||||
|
||||
// aggregate
|
||||
const combined = [...installations0, ...installations1];
|
||||
|
||||
// update
|
||||
setInstallations(combined);
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
removeToken();
|
||||
}
|
||||
} finally {
|
||||
}
|
||||
}, [setInstallations]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchInstallationsForUser(props.current_user.id);
|
||||
}, [props.current_user]);
|
||||
|
||||
const handleGrantAccess = () => {
|
||||
fetchFolders();
|
||||
fetchInstallations();
|
||||
setOpenModal(true);
|
||||
setSelectedFolderNames([]);
|
||||
setSelectedInstallationNames([]);
|
||||
};
|
||||
|
||||
const handleFolderChange = (event) => {
|
||||
setSelectedFolderNames(event.target.value);
|
||||
};
|
||||
|
||||
const handleInstallationChange = (event) => {
|
||||
setSelectedInstallationNames(event.target.value);
|
||||
};
|
||||
const handleOpenFolder = () => {
|
||||
setOpenFolder(true);
|
||||
};
|
||||
|
||||
const handleCloseFolder = () => {
|
||||
setOpenFolder(false);
|
||||
};
|
||||
const handleCancel = () => {
|
||||
setOpenModal(false);
|
||||
};
|
||||
const handleOpenInstallation = () => {
|
||||
setOpenInstallation(true);
|
||||
};
|
||||
|
||||
const handleCloseInstallation = () => {
|
||||
setOpenInstallation(false);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
for (const folderName of selectedFolderNames) {
|
||||
const folder = folders.find((folder) => folder.name === folderName);
|
||||
|
||||
await axiosConfig
|
||||
.post(
|
||||
`/GrantUserAccessToFolder?UserId=${props.current_user.id}&FolderId=${folder.id}`
|
||||
)
|
||||
.then((response) => {
|
||||
if (response) {
|
||||
setUpdatedMessage(
|
||||
'Granted access to user ' + props.current_user.name
|
||||
);
|
||||
setUpdated(true);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
setErrorMessage('An error has occured');
|
||||
setError(true);
|
||||
});
|
||||
}
|
||||
|
||||
for (const installationName of selectedInstallationNames) {
|
||||
const installation = installations.find(
|
||||
(installation) => installation.name === installationName
|
||||
);
|
||||
|
||||
await axiosConfig
|
||||
.post(
|
||||
`/GrantUserAccessToInstallation?UserId=${props.current_user.id}&InstallationId=${installation.id}`
|
||||
)
|
||||
.then((response) => {
|
||||
if (response) {
|
||||
setUpdatedMessage(
|
||||
'Granted access to user ' + props.current_user.name
|
||||
);
|
||||
setUpdated(true);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
setErrorMessage('An error has occured');
|
||||
setError(true);
|
||||
});
|
||||
}
|
||||
|
||||
setOpenModal(false);
|
||||
fetchInstallationsForUser(props.current_user.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<Container maxWidth="xl">
|
||||
<Grid container>
|
||||
<Grid item xs={12} md={12}>
|
||||
{updated && (
|
||||
<Alert
|
||||
severity="success"
|
||||
sx={{
|
||||
mt: 1
|
||||
}}
|
||||
>
|
||||
{updatedmessage}
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
onClick={() => setUpdated(false)}
|
||||
>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Alert
|
||||
severity="error"
|
||||
sx={{
|
||||
marginTop: '20px',
|
||||
marginBottom: '20px'
|
||||
}}
|
||||
>
|
||||
{errormessage}
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
onClick={() => setError(false)}
|
||||
sx={{
|
||||
marginLeft: '10px'
|
||||
}}
|
||||
>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
open={openModal}
|
||||
onClose={() => {}}
|
||||
aria-labelledby="error-modal"
|
||||
aria-describedby="error-modal-description"
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '30%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 500,
|
||||
bgcolor: 'background.paper',
|
||||
borderRadius: 4,
|
||||
boxShadow: 24,
|
||||
p: 4
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="form"
|
||||
sx={{
|
||||
textAlign: 'center'
|
||||
}}
|
||||
noValidate
|
||||
autoComplete="off"
|
||||
>
|
||||
<div>
|
||||
<FormControl fullWidth sx={{ marginTop: 1, width: 390 }}>
|
||||
<InputLabel
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
backgroundColor: 'white'
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="grantAccessToFolders"
|
||||
defaultMessage="Grant access to folders"
|
||||
/>
|
||||
</InputLabel>
|
||||
<Select
|
||||
multiple
|
||||
value={selectedFolderNames}
|
||||
onChange={handleFolderChange}
|
||||
open={openFolder}
|
||||
onClose={handleCloseFolder}
|
||||
onOpen={handleOpenFolder}
|
||||
renderValue={(selected) => (
|
||||
<div>
|
||||
{selected.map((folder) => (
|
||||
<span key={folder}>{folder}, </span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
{folders.map((folder) => (
|
||||
<MenuItem key={folder.id} value={folder.name}>
|
||||
{folder.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
<Button
|
||||
sx={{
|
||||
marginLeft: '150px',
|
||||
marginTop: '10px',
|
||||
backgroundColor: theme.colors.primary.main,
|
||||
color: 'white',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.colors.primary.dark
|
||||
},
|
||||
padding: '6px 8px'
|
||||
}}
|
||||
onClick={handleCloseFolder}
|
||||
>
|
||||
<FormattedMessage id="submit" defaultMessage="Submit" />
|
||||
</Button>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<FormControl fullWidth sx={{ marginTop: 2, width: 390 }}>
|
||||
<InputLabel
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
backgroundColor: 'white'
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="grantAccessToInstallations"
|
||||
defaultMessage="Grant access to installations"
|
||||
/>
|
||||
</InputLabel>
|
||||
<Select
|
||||
multiple
|
||||
value={selectedInstallationNames}
|
||||
onChange={handleInstallationChange}
|
||||
open={openInstallation}
|
||||
onClose={handleCloseInstallation}
|
||||
onOpen={handleOpenInstallation}
|
||||
renderValue={(selected) => (
|
||||
<div>
|
||||
{selected.map((installation) => (
|
||||
<span key={installation}>{installation}, </span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
{installations.map((installation) => (
|
||||
<MenuItem
|
||||
key={installation.id}
|
||||
value={installation.name}
|
||||
>
|
||||
{installation.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
<Button
|
||||
sx={{
|
||||
marginLeft: '150px',
|
||||
marginTop: '10px',
|
||||
backgroundColor: theme.colors.primary.main,
|
||||
color: 'white',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.colors.primary.dark
|
||||
},
|
||||
padding: '6px 8px'
|
||||
}}
|
||||
onClick={handleCloseInstallation}
|
||||
>
|
||||
<FormattedMessage id="submit" defaultMessage="Submit" />
|
||||
</Button>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
sx={{
|
||||
marginTop: '20px',
|
||||
backgroundColor: theme.colors.primary.main,
|
||||
color: 'white',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.colors.primary.dark
|
||||
},
|
||||
padding: '6px 8px'
|
||||
}}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
<FormattedMessage id="submit" defaultMessage="Submit" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleCancel}
|
||||
sx={{
|
||||
marginTop: '20px',
|
||||
marginLeft: '10px',
|
||||
backgroundColor: theme.colors.primary.main,
|
||||
color: 'white',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.colors.primary.dark
|
||||
},
|
||||
padding: '6px 8px'
|
||||
}}
|
||||
>
|
||||
<FormattedMessage id="cancel" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleGrantAccess}
|
||||
sx={{
|
||||
marginTop: '20px',
|
||||
marginBottom: '20px',
|
||||
backgroundColor: '#ffc04d',
|
||||
color: '#000000',
|
||||
'&:hover': { bgcolor: '#f7b34d' }
|
||||
}}
|
||||
>
|
||||
<FormattedMessage id="grantAccess" defaultMessage="Grant Access" />
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={12}>
|
||||
{accessibleInstallationsForUser.map((installation, index) => {
|
||||
const isLast = index === accessibleInstallationsForUser.length - 1;
|
||||
|
||||
return (
|
||||
<Fragment key={installation.name}>
|
||||
<ListItem
|
||||
sx={{
|
||||
mb: isLast ? 4 : 0 // Apply margin-bottom to the last item only
|
||||
}}
|
||||
secondaryAction={
|
||||
currentUser.userType === UserType.admin && (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
RevokeAccessFromResource(
|
||||
'ToInstallation',
|
||||
props.current_user.id,
|
||||
'InstallationId',
|
||||
installation.id,
|
||||
props.current_user.name
|
||||
);
|
||||
|
||||
fetchInstallationsForUser(props.current_user.id);
|
||||
}}
|
||||
edge="end"
|
||||
>
|
||||
<PersonRemoveIcon />
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<PersonIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={installation.name} />
|
||||
</ListItem>
|
||||
<Divider />
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
|
||||
{accessibleInstallationsForUser.length == 0 && (
|
||||
<Alert
|
||||
severity="error"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginTop: '20px',
|
||||
marginBottom: '20px'
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="theUserDoesNOtHaveAccessToAnyInstallation"
|
||||
defaultMessage="The user does not have access to any installation "
|
||||
/>
|
||||
<IconButton color="inherit" size="small"></IconButton>
|
||||
</Alert>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserAccess;
|
||||
|
|
@ -24,34 +24,37 @@ function PvView(props: PvViewProps) {
|
|||
if (props.values === null && props.connected == true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentLocation = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const sortedPvView =
|
||||
props.values != null && props.values.PvOnDc
|
||||
? Object.entries(props.values.PvOnDc.Strings)
|
||||
.map(([pvId, pv]) => {
|
||||
return { pvId, pv }; // Here we return an object with the id and device
|
||||
})
|
||||
.sort((a, b) => parseInt(b.pvId) - parseInt(a.pvId))
|
||||
: [];
|
||||
// ✅ Flatten, sort, and assign unique displayId from 1-N
|
||||
const sortedPvView = props.values?.PvOnDc
|
||||
? Object.entries(props.values.PvOnDc)
|
||||
.flatMap(([deviceId, device]) =>
|
||||
Object.entries(device.Strings).map(([pvId, pv], index) => ({
|
||||
pvId,
|
||||
pv,
|
||||
deviceId,
|
||||
displayId: `CU${deviceId} -> AMPT ${index + 1}`
|
||||
}))
|
||||
)
|
||||
.sort((a, b) => {
|
||||
if (a.deviceId === b.deviceId) {
|
||||
return parseInt(a.pvId) - parseInt(b.pvId);
|
||||
}
|
||||
return a.deviceId.localeCompare(b.deviceId);
|
||||
})
|
||||
: [];
|
||||
|
||||
const [loading, setLoading] = useState(sortedPvView.length == 0);
|
||||
const [loading, setLoading] = useState(sortedPvView.length === 0);
|
||||
|
||||
const handleMainStatsButton = () => {
|
||||
navigate(routes.mainstats);
|
||||
};
|
||||
|
||||
// const findBatteryData = (batteryId: number) => {
|
||||
// for (let i = 0; i < props.values.batteryView.length; i++) {
|
||||
// if (props.values.batteryView[i].BatteryId == batteryId) {
|
||||
// return props.values.batteryView[i];
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
|
||||
useEffect(() => {
|
||||
if (sortedPvView.length == 0) {
|
||||
if (sortedPvView.length === 0) {
|
||||
setLoading(true);
|
||||
} else {
|
||||
setLoading(false);
|
||||
|
|
@ -84,6 +87,7 @@ function PvView(props: PvViewProps) {
|
|||
</Typography>
|
||||
</Container>
|
||||
)}
|
||||
|
||||
{loading && props.connected && (
|
||||
<Container
|
||||
maxWidth="xl"
|
||||
|
|
@ -111,80 +115,72 @@ function PvView(props: PvViewProps) {
|
|||
|
||||
{!loading && props.connected && (
|
||||
<Container maxWidth="xl">
|
||||
<TableContainer
|
||||
component={Paper}
|
||||
sx={{
|
||||
marginTop: '20px',
|
||||
marginBottom: '20px'
|
||||
}}
|
||||
>
|
||||
<Table sx={{ minWidth: 250 }} aria-label="simple table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell align="center">Pv</TableCell>
|
||||
<TableCell align="center">Power</TableCell>
|
||||
<TableCell align="center">Voltage</TableCell>
|
||||
<TableCell align="center">Current</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{sortedPvView.map(({ pvId, pv }) => (
|
||||
<TableRow
|
||||
key={pvId}
|
||||
style={{
|
||||
height: '10px'
|
||||
}}
|
||||
>
|
||||
<TableCell
|
||||
component="th"
|
||||
scope="row"
|
||||
align="center"
|
||||
sx={{ width: '10%', fontWeight: 'bold', color: 'black' }}
|
||||
>
|
||||
{'AMPT ' + pvId}
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
sx={{
|
||||
width: '10%',
|
||||
textAlign: 'center',
|
||||
fontWeight: 'bold',
|
||||
backgroundColor:
|
||||
pv.Current == 0 ? '#FF033E' : '#32CD32',
|
||||
color: 'inherit'
|
||||
}}
|
||||
>
|
||||
{pv.Power + ' W'}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{
|
||||
width: '10%',
|
||||
textAlign: 'center',
|
||||
fontWeight: 'bold',
|
||||
backgroundColor:
|
||||
pv.Current == 0 ? '#FF033E' : '#32CD32',
|
||||
color: 'inherit'
|
||||
}}
|
||||
>
|
||||
{pv.Voltage + ' V'}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{
|
||||
width: '10%',
|
||||
textAlign: 'center',
|
||||
fontWeight: 'bold',
|
||||
backgroundColor:
|
||||
pv.Current == 0 ? '#FF033E' : '#32CD32',
|
||||
color: 'inherit'
|
||||
}}
|
||||
>
|
||||
{pv.Current + ' A'}
|
||||
</TableCell>
|
||||
{Object.entries(
|
||||
sortedPvView.reduce((acc, entry) => {
|
||||
if (!acc[entry.deviceId]) acc[entry.deviceId] = [];
|
||||
acc[entry.deviceId].push(entry);
|
||||
return acc;
|
||||
}, {} as Record<string, typeof sortedPvView>)
|
||||
).map(([deviceId, entries]) => (
|
||||
<TableContainer
|
||||
key={deviceId}
|
||||
component={Paper}
|
||||
sx={{ marginTop: '30px', marginBottom: '40px', boxShadow: 3 }}
|
||||
>
|
||||
<Table sx={{ minWidth: 250 }} aria-label={`CU${deviceId} table`}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell align="center">Pv</TableCell>
|
||||
<TableCell align="center">Power</TableCell>
|
||||
<TableCell align="center">Voltage</TableCell>
|
||||
<TableCell align="center">Current</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{entries.map(({ displayId, pv }, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell
|
||||
align="center"
|
||||
sx={{ fontWeight: 'bold', color: 'black' }}
|
||||
>
|
||||
{displayId}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="center"
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
backgroundColor:
|
||||
pv.Current === 0 ? '#FF033E' : '#32CD32'
|
||||
}}
|
||||
>
|
||||
{pv.Power} W
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="center"
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
backgroundColor:
|
||||
pv.Current === 0 ? '#FF033E' : '#32CD32'
|
||||
}}
|
||||
>
|
||||
{pv.Voltage} V
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="center"
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
backgroundColor:
|
||||
pv.Current === 0 ? '#FF033E' : '#32CD32'
|
||||
}}
|
||||
>
|
||||
{pv.Current} A
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
))}
|
||||
</Container>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
|
|||
routes.installation +
|
||||
`${installationID}` +
|
||||
'/' +
|
||||
routes.live,
|
||||
routes.batteryview,
|
||||
{
|
||||
replace: true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,8 +19,11 @@ import HistoryOfActions from '../History/History';
|
|||
import BuildIcon from '@mui/icons-material/Build';
|
||||
import AccessContextProvider from '../../../contexts/AccessContextProvider';
|
||||
import Access from '../ManageAccess/Access';
|
||||
import InformationSodioHome from '../Information/InformationSodioHome';
|
||||
import CryptoJS from 'crypto-js';
|
||||
import Information from '../Information/Information';
|
||||
import { TimeSpan, UnixTime } from '../../../dataCache/time';
|
||||
import { fetchDataJson } from '../Installations/fetchData';
|
||||
import { FetchResult } from '../../../dataCache/dataCache';
|
||||
import BatteryViewSodioHome from '../BatteryView/BatteryViewSodioHome';
|
||||
|
||||
interface singleInstallationProps {
|
||||
current_installation?: I_Installation;
|
||||
|
|
@ -31,6 +34,19 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
|||
if (props.current_installation == undefined) {
|
||||
return null;
|
||||
}
|
||||
const S3data = {
|
||||
s3Region: props.current_installation.s3Region,
|
||||
s3Provider: props.current_installation.s3Provider,
|
||||
s3Key: props.current_installation.s3Key,
|
||||
s3Secret: props.current_installation.s3Secret,
|
||||
s3BucketId: props.current_installation.s3BucketId
|
||||
};
|
||||
|
||||
const s3Bucket =
|
||||
props.current_installation.s3BucketId.toString() +
|
||||
'-' +
|
||||
'e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa';
|
||||
|
||||
const context = useContext(UserContext);
|
||||
const { currentUser } = context;
|
||||
const location = useLocation().pathname;
|
||||
|
|
@ -45,6 +61,7 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
|||
const [connected, setConnected] = useState(true);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [fetchFunctionCalled, setFetchFunctionCalled] = useState(false);
|
||||
const s3Credentials = { s3Bucket, ...S3data };
|
||||
|
||||
//In React, useRef creates a mutable object that persists across renders without triggering re-renders when its value changes.
|
||||
//While fetching, we check the value of continueFetching, if its false, we break. This means that either the user changed tab or the object has been finished its execution (return)
|
||||
|
|
@ -91,48 +108,75 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
|||
}
|
||||
|
||||
const fetchDataPeriodically = async () => {
|
||||
while (continueFetching.current) {
|
||||
//Fetch data from Bitwatt cloud
|
||||
console.log('Fetching from Bitwatt cloud');
|
||||
var timeperiodToSearch = 200;
|
||||
let res;
|
||||
let timestampToFetch;
|
||||
|
||||
console.log(props.current_installation.serialNumber);
|
||||
console.log(props.current_installation.s3WriteKey);
|
||||
console.log(props.current_installation.s3WriteSecret);
|
||||
|
||||
const timeStamp = Date.now().toString();
|
||||
|
||||
// Encrypt timestamp using AES-ECB with PKCS7 padding
|
||||
const key = CryptoJS.enc.Utf8.parse(
|
||||
props.current_installation.s3WriteSecret
|
||||
);
|
||||
const encrypted = CryptoJS.AES.encrypt(timeStamp, key, {
|
||||
mode: CryptoJS.mode.ECB,
|
||||
padding: CryptoJS.pad.Pkcs7
|
||||
}).toString();
|
||||
|
||||
// Set headers
|
||||
const headers = {
|
||||
'X-Signature': encrypted,
|
||||
'X-AccessKey': props.current_installation.s3WriteKey,
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
// API URL
|
||||
const url = `https://www.biwattpower.com/gateway/admin/open/device/currentEnergyFlowData/${props.current_installation.serialNumber}`;
|
||||
for (var i = 0; i < timeperiodToSearch; i += 2) {
|
||||
if (!continueFetching.current) {
|
||||
return false;
|
||||
}
|
||||
timestampToFetch = UnixTime.now().earlier(TimeSpan.fromSeconds(i));
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { method: 'GET', headers });
|
||||
const result = await response.json();
|
||||
console.log('API Response:', result);
|
||||
} catch (error) {
|
||||
console.error('Request failed:', error);
|
||||
res = await fetchDataJson(timestampToFetch, s3Credentials, false);
|
||||
if (res !== FetchResult.notAvailable && res !== FetchResult.tryLater) {
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching data:', err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (i >= timeperiodToSearch) {
|
||||
setConnected(false);
|
||||
setLoading(false);
|
||||
return false;
|
||||
}
|
||||
setConnected(true);
|
||||
setLoading(false);
|
||||
|
||||
while (continueFetching.current) {
|
||||
for (const timestamp of Object.keys(res)) {
|
||||
if (!continueFetching.current) {
|
||||
setFetchFunctionCalled(false);
|
||||
return false;
|
||||
}
|
||||
console.log(`Timestamp: ${timestamp}`);
|
||||
console.log(res[timestamp]);
|
||||
|
||||
setValues(res[timestamp]);
|
||||
await timeout(2000);
|
||||
}
|
||||
|
||||
// Wait for 2 seconds before fetching again
|
||||
await timeout(200000);
|
||||
console.log('ssssssssssssssssssssssssssssssssssssss');
|
||||
timestampToFetch = timestampToFetch.later(TimeSpan.fromSeconds(60));
|
||||
console.log('NEW TIMESTAMP TO FETCH IS ' + timestampToFetch);
|
||||
|
||||
for (i = 0; i < 30; i++) {
|
||||
if (!continueFetching.current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Trying to fetch timestamp ' + timestampToFetch);
|
||||
res = await fetchDataJson(timestampToFetch, s3Credentials, false);
|
||||
if (
|
||||
res !== FetchResult.notAvailable &&
|
||||
res !== FetchResult.tryLater
|
||||
) {
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching data:', err);
|
||||
return false;
|
||||
}
|
||||
timestampToFetch = timestampToFetch.later(TimeSpan.fromSeconds(1));
|
||||
}
|
||||
if (i == 30) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
setFetchFunctionCalled(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -147,11 +191,16 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
|||
}, [status]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(currentTab);
|
||||
if (currentTab == 'live' || location.includes('batteryview')) {
|
||||
//Fetch periodically if the tab is live or batteryview
|
||||
if (
|
||||
currentTab == 'live' ||
|
||||
currentTab == 'pvview' ||
|
||||
currentTab == 'configuration' ||
|
||||
location.includes('batteryview')
|
||||
) {
|
||||
//Fetch periodically if the tab is live, pvview or batteryview
|
||||
if (
|
||||
currentTab == 'live' ||
|
||||
currentTab == 'pvview' ||
|
||||
(location.includes('batteryview') && !location.includes('mainstats'))
|
||||
) {
|
||||
if (!continueFetching.current) {
|
||||
|
|
@ -163,6 +212,10 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
|||
}
|
||||
}
|
||||
}
|
||||
//Fetch only one time in configuration tab
|
||||
// if (currentTab == 'configuration') {
|
||||
// fetchDataForOneTime();
|
||||
// }
|
||||
|
||||
return () => {
|
||||
continueFetching.current = false;
|
||||
|
|
@ -322,10 +375,11 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
|||
<Route
|
||||
path={routes.information}
|
||||
element={
|
||||
<InformationSodioHome
|
||||
<Information
|
||||
values={props.current_installation}
|
||||
s3Credentials={s3Credentials}
|
||||
type={props.type}
|
||||
></InformationSodioHome>
|
||||
></Information>
|
||||
}
|
||||
/>
|
||||
|
||||
|
|
@ -351,28 +405,17 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
|||
}
|
||||
/>
|
||||
|
||||
{/*<Route*/}
|
||||
{/* path={routes.overview}*/}
|
||||
{/* element={*/}
|
||||
{/* <SalidomoOverview*/}
|
||||
{/* s3Credentials={s3Credentials}*/}
|
||||
{/* id={props.current_installation.id}*/}
|
||||
{/* ></SalidomoOverview>*/}
|
||||
{/* }*/}
|
||||
{/*/>*/}
|
||||
|
||||
{/*<Route*/}
|
||||
{/* path={routes.batteryview + '*'}*/}
|
||||
{/* element={*/}
|
||||
{/* <BatteryViewSalidomo*/}
|
||||
{/* values={values}*/}
|
||||
{/* s3Credentials={s3Credentials}*/}
|
||||
{/* installationId={props.current_installation.id}*/}
|
||||
{/* productNum={props.current_installation.product}*/}
|
||||
{/* connected={connected}*/}
|
||||
{/* ></BatteryViewSalidomo>*/}
|
||||
{/* }*/}
|
||||
{/*></Route>*/}
|
||||
<Route
|
||||
path={routes.batteryview + '*'}
|
||||
element={
|
||||
<BatteryViewSodioHome
|
||||
values={values}
|
||||
s3Credentials={s3Credentials}
|
||||
installationId={props.current_installation.id}
|
||||
connected={connected}
|
||||
></BatteryViewSodioHome>
|
||||
}
|
||||
></Route>
|
||||
|
||||
{currentUser.userType == UserType.admin && (
|
||||
<Route
|
||||
|
|
|
|||
|
|
@ -1,286 +0,0 @@
|
|||
import React, { useContext, useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
CircularProgress,
|
||||
IconButton,
|
||||
Modal,
|
||||
TextField,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import Button from '@mui/material/Button';
|
||||
import { Close as CloseIcon } from '@mui/icons-material';
|
||||
import { I_Installation } from 'src/interfaces/InstallationTypes';
|
||||
import { InstallationsContext } from 'src/contexts/InstallationsContextProvider';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
interface SodiohomeInstallationFormProps {
|
||||
cancel: () => void;
|
||||
submit: () => void;
|
||||
parentid: number;
|
||||
}
|
||||
|
||||
function SodiohomeInstallationForm(props: SodiohomeInstallationFormProps) {
|
||||
const theme = useTheme();
|
||||
const [open, setOpen] = useState(true);
|
||||
const [formValues, setFormValues] = useState<Partial<I_Installation>>({
|
||||
name: '',
|
||||
region: '',
|
||||
location: '',
|
||||
country: '',
|
||||
serialNumber: '',
|
||||
s3WriteSecret: '',
|
||||
s3WriteKey: ''
|
||||
});
|
||||
const requiredFields = ['name', 'location', 'country', 'serialNumber'];
|
||||
|
||||
const installationContext = useContext(InstallationsContext);
|
||||
const { createInstallation, loading, setLoading, error, setError } =
|
||||
installationContext;
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
|
||||
setFormValues({
|
||||
...formValues,
|
||||
[name]: value
|
||||
});
|
||||
};
|
||||
const handleSubmit = async (e) => {
|
||||
setLoading(true);
|
||||
formValues.parentId = props.parentid;
|
||||
formValues.product = 2;
|
||||
const responseData = await createInstallation(formValues);
|
||||
props.submit();
|
||||
};
|
||||
const handleCancelSubmit = (e) => {
|
||||
props.cancel();
|
||||
};
|
||||
|
||||
const areRequiredFieldsFilled = () => {
|
||||
for (const field of requiredFields) {
|
||||
if (!formValues[field]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const isMobile = window.innerWidth <= 1490;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={() => {}}
|
||||
aria-labelledby="error-modal"
|
||||
aria-describedby="error-modal-description"
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: isMobile ? '50%' : '40%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 500,
|
||||
bgcolor: 'background.paper',
|
||||
borderRadius: 4,
|
||||
boxShadow: 24,
|
||||
p: 4
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="form"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center', // Center items horizontally
|
||||
'& .MuiTextField-root': {
|
||||
m: 1,
|
||||
width: 390
|
||||
}
|
||||
}}
|
||||
noValidate
|
||||
autoComplete="off"
|
||||
>
|
||||
<div>
|
||||
<TextField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="installationName"
|
||||
defaultMessage="Installation Name"
|
||||
/>
|
||||
}
|
||||
name="name"
|
||||
value={formValues.name}
|
||||
onChange={handleChange}
|
||||
required
|
||||
error={formValues.name === ''}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<TextField
|
||||
label={<FormattedMessage id="region" defaultMessage="Region" />}
|
||||
name="region"
|
||||
value={formValues.region}
|
||||
onChange={handleChange}
|
||||
required
|
||||
error={formValues.region === ''}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<TextField
|
||||
label={
|
||||
<FormattedMessage id="location" defaultMessage="Location" />
|
||||
}
|
||||
name="location"
|
||||
value={formValues.location}
|
||||
onChange={handleChange}
|
||||
required
|
||||
error={formValues.location === ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextField
|
||||
label={
|
||||
<FormattedMessage id="country" defaultMessage="Country" />
|
||||
}
|
||||
name="country"
|
||||
value={formValues.country}
|
||||
onChange={handleChange}
|
||||
required
|
||||
error={formValues.country === ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="serialNumber"
|
||||
defaultMessage="Serial Number"
|
||||
/>
|
||||
}
|
||||
name="serialNumber"
|
||||
value={formValues.serialNumber}
|
||||
onChange={handleChange}
|
||||
required
|
||||
error={formValues.serialNumber === ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="s3WriteKey"
|
||||
defaultMessage="BitWatt Cloud Access Key"
|
||||
/>
|
||||
}
|
||||
name="s3WriteKey"
|
||||
value={formValues.s3WriteKey}
|
||||
onChange={handleChange}
|
||||
required
|
||||
error={formValues.s3WriteKey === ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="s3WriteSecret"
|
||||
defaultMessage="BitWatt Cloud Secret Key"
|
||||
/>
|
||||
}
|
||||
name="s3WriteSecret"
|
||||
value={formValues.s3WriteSecret}
|
||||
onChange={handleChange}
|
||||
required
|
||||
error={formValues.s3WriteSecret === ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="Information"
|
||||
defaultMessage="Information"
|
||||
/>
|
||||
}
|
||||
name="information"
|
||||
value={formValues.information}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
</Box>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginTop: 10
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleSubmit}
|
||||
sx={{
|
||||
marginLeft: '20px'
|
||||
}}
|
||||
disabled={!areRequiredFieldsFilled()}
|
||||
>
|
||||
<FormattedMessage id="submit" defaultMessage="Submit" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleCancelSubmit}
|
||||
sx={{
|
||||
marginLeft: '10px'
|
||||
}}
|
||||
>
|
||||
<FormattedMessage id="cancel" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
|
||||
{loading && (
|
||||
<CircularProgress
|
||||
sx={{
|
||||
color: theme.palette.primary.main,
|
||||
marginLeft: '20px'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Alert
|
||||
severity="error"
|
||||
sx={{
|
||||
ml: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="errorOccured"
|
||||
defaultMessage="An error has occured"
|
||||
/>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
onClick={() => setError(false)}
|
||||
sx={{ marginLeft: '4px' }}
|
||||
>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
</Box>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SodiohomeInstallationForm;
|
||||
|
|
@ -14,12 +14,17 @@ import { ProductIdContext } from '../../../contexts/ProductIdContextProvider';
|
|||
import { UserType } from '../../../interfaces/UserTypes';
|
||||
import SodioHomeInstallation from './Installation';
|
||||
|
||||
function SodioHomeInstallationTabs() {
|
||||
interface SodioHomeInstallationTabsProps {
|
||||
product: number;
|
||||
}
|
||||
|
||||
function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
|
||||
const location = useLocation();
|
||||
const context = useContext(UserContext);
|
||||
const { currentUser } = context;
|
||||
const tabList = [
|
||||
'live',
|
||||
'batteryview',
|
||||
'information',
|
||||
'manage',
|
||||
'overview',
|
||||
|
|
@ -53,26 +58,14 @@ function SodioHomeInstallationTabs() {
|
|||
}, [location]);
|
||||
|
||||
useEffect(() => {
|
||||
if (sodiohomeInstallations.length === 0 && fetchedInstallations === false) {
|
||||
setProduct(props.product);
|
||||
}, [props.product]);
|
||||
|
||||
useEffect(() => {
|
||||
if (product == props.product) {
|
||||
fetchAllSodiohomeInstallations();
|
||||
setFetchedInstallations(true);
|
||||
}
|
||||
}, [sodiohomeInstallations]);
|
||||
|
||||
useEffect(() => {
|
||||
if (sodiohomeInstallations && sodiohomeInstallations.length > 0) {
|
||||
if (!socket) {
|
||||
openSocket(2);
|
||||
} else if (product != 2) {
|
||||
closeSocket();
|
||||
openSocket(2);
|
||||
}
|
||||
}
|
||||
}, [sodiohomeInstallations]);
|
||||
|
||||
useEffect(() => {
|
||||
setProduct(2);
|
||||
}, []);
|
||||
}, [product]);
|
||||
|
||||
const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => {
|
||||
setCurrentTab(value);
|
||||
|
|
@ -101,14 +94,23 @@ function SodioHomeInstallationTabs() {
|
|||
const singleInstallationTabs =
|
||||
currentUser.userType == UserType.admin
|
||||
? [
|
||||
// {
|
||||
// value: 'live',
|
||||
// label: <FormattedMessage id="live" defaultMessage="Live" />
|
||||
// },
|
||||
{
|
||||
value: 'live',
|
||||
label: <FormattedMessage id="live" defaultMessage="Live" />
|
||||
},
|
||||
{
|
||||
value: 'overview',
|
||||
label: <FormattedMessage id="overview" defaultMessage="Overview" />
|
||||
value: 'batteryview',
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id="batteryview"
|
||||
defaultMessage="Battery View"
|
||||
/>
|
||||
)
|
||||
},
|
||||
// {
|
||||
// value: 'overview',
|
||||
// label: <FormattedMessage id="overview" defaultMessage="Overview" />
|
||||
// },
|
||||
{
|
||||
value: 'log',
|
||||
label: <FormattedMessage id="log" defaultMessage="Log" />
|
||||
|
|
@ -141,14 +143,14 @@ function SodioHomeInstallationTabs() {
|
|||
}
|
||||
]
|
||||
: [
|
||||
{
|
||||
value: 'live',
|
||||
label: <FormattedMessage id="live" defaultMessage="Live" />
|
||||
},
|
||||
{
|
||||
value: 'overview',
|
||||
label: <FormattedMessage id="overview" defaultMessage="Overview" />
|
||||
},
|
||||
// {
|
||||
// value: 'live',
|
||||
// label: <FormattedMessage id="live" defaultMessage="Live" />
|
||||
// },
|
||||
// {
|
||||
// value: 'overview',
|
||||
// label: <FormattedMessage id="overview" defaultMessage="Overview" />
|
||||
// },
|
||||
|
||||
{
|
||||
value: 'information',
|
||||
|
|
@ -172,14 +174,23 @@ function SodioHomeInstallationTabs() {
|
|||
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" />
|
||||
value: 'batteryview',
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id="batteryview"
|
||||
defaultMessage="Battery View"
|
||||
/>
|
||||
)
|
||||
},
|
||||
// {
|
||||
// value: 'overview',
|
||||
// label: <FormattedMessage id="overview" defaultMessage="Overview" />
|
||||
// },
|
||||
{
|
||||
value: 'log',
|
||||
label: <FormattedMessage id="log" defaultMessage="Log" />
|
||||
|
|
@ -224,10 +235,10 @@ function SodioHomeInstallationTabs() {
|
|||
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" />
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ function Topology(props: TopologyProps) {
|
|||
|
||||
const { product, setProduct } = useContext(ProductIdContext);
|
||||
|
||||
//console.log('product VALUE IS ', product);
|
||||
const [showValues, setShowValues] = useState(false);
|
||||
|
||||
const handleSwitch = () => () => {
|
||||
|
|
@ -38,6 +37,13 @@ function Topology(props: TopologyProps) {
|
|||
|
||||
const isMobile = window.innerWidth <= 1490;
|
||||
|
||||
const totalPvPower = props.values?.PvOnDc
|
||||
? Object.values(props.values.PvOnDc).reduce(
|
||||
(sum, device) => sum + (device?.Dc?.Power || 0),
|
||||
0
|
||||
)
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<Container maxWidth="xl" style={{ backgroundColor: 'white' }}>
|
||||
<Grid container>
|
||||
|
|
@ -408,7 +414,7 @@ function Topology(props: TopologyProps) {
|
|||
data: props.values?.PvOnDc
|
||||
? [
|
||||
{
|
||||
value: props.values.PvOnDc.Dc.Power,
|
||||
value: totalPvPower,
|
||||
unit: 'W'
|
||||
}
|
||||
]
|
||||
|
|
@ -421,15 +427,12 @@ function Topology(props: TopologyProps) {
|
|||
position: 'top',
|
||||
data: props.values?.PvOnDc
|
||||
? {
|
||||
value: props.values.PvOnDc.Dc.Power,
|
||||
value: totalPvPower,
|
||||
unit: 'W'
|
||||
}
|
||||
: undefined,
|
||||
amount: props.values?.PvOnDc
|
||||
? getAmount(
|
||||
highestConnectionValue,
|
||||
props.values.PvOnDc.Dc.Power
|
||||
)
|
||||
? getAmount(highestConnectionValue, totalPvPower)
|
||||
: 0,
|
||||
showValues: showValues
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ import { InstallationsContext } from '../../../contexts/InstallationsContextProv
|
|||
import { UserType } from '../../../interfaces/UserTypes';
|
||||
import InstallationForm from '../Installations/installationForm';
|
||||
import SalidomoInstallationForm from '../SalidomoInstallations/SalidomoInstallationForm';
|
||||
import SodiohomeInstallationForm from '../SodiohomeInstallations/SodiohomeInstallationForm';
|
||||
|
||||
interface TreeInformationProps {
|
||||
folder: I_Folder;
|
||||
|
|
@ -65,7 +64,7 @@ function TreeInformation(props: TreeInformationProps) {
|
|||
// console.log('Selected Product:', e.target.value);
|
||||
};
|
||||
|
||||
const ProductTypes = ['Salimax', 'Salidomo', 'Sodiohome'];
|
||||
const ProductTypes = ['Salimax', 'Salidomo', 'Sodiohome', 'SodistoreMax'];
|
||||
|
||||
const isMobile = window.innerWidth <= 1490;
|
||||
|
||||
|
|
@ -322,13 +321,17 @@ function TreeInformation(props: TreeInformationProps) {
|
|||
</Box>
|
||||
</Modal>
|
||||
)}
|
||||
{openModalInstallation && product == 'Salimax' && (
|
||||
<InstallationForm
|
||||
cancel={handleFormCancel}
|
||||
submit={handleInstallationFormSubmit}
|
||||
parentid={props.folder.id}
|
||||
/>
|
||||
)}
|
||||
{openModalInstallation &&
|
||||
(product == 'Salimax' ||
|
||||
product == 'Sodiohome' ||
|
||||
product == 'SodistoreMax') && (
|
||||
<InstallationForm
|
||||
cancel={handleFormCancel}
|
||||
submit={handleInstallationFormSubmit}
|
||||
parentid={props.folder.id}
|
||||
productToInsert={ProductTypes.indexOf(product)}
|
||||
/>
|
||||
)}
|
||||
{openModalInstallation && product == 'Salidomo' && (
|
||||
<SalidomoInstallationForm
|
||||
cancel={handleFormCancel}
|
||||
|
|
@ -336,13 +339,13 @@ function TreeInformation(props: TreeInformationProps) {
|
|||
parentid={props.folder.id}
|
||||
/>
|
||||
)}
|
||||
{openModalInstallation && product == 'Sodiohome' && (
|
||||
<SodiohomeInstallationForm
|
||||
cancel={handleFormCancel}
|
||||
submit={handleInstallationFormSubmit}
|
||||
parentid={props.folder.id}
|
||||
/>
|
||||
)}
|
||||
{/*{openModalInstallation && product == 'Sodiohome' && (*/}
|
||||
{/* <SodiohomeInstallationForm*/}
|
||||
{/* cancel={handleFormCancel}*/}
|
||||
{/* submit={handleInstallationFormSubmit}*/}
|
||||
{/* parentid={props.folder.id}*/}
|
||||
{/* />*/}
|
||||
{/*)}*/}
|
||||
|
||||
<Container maxWidth="xl">
|
||||
<Grid
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ const FlatUsersView = (props: FlatUsersViewProps) => {
|
|||
|
||||
return (
|
||||
<Grid container spacing={1} sx={{ marginTop: '1px' }}>
|
||||
<Grid item xs={12} md={isMobile ? 5 : 4}>
|
||||
<Grid item xs={6} md={5}>
|
||||
<Card>
|
||||
<Divider />
|
||||
<TableContainer sx={{ maxHeight: '520px', overflowY: 'auto' }}>
|
||||
|
|
@ -113,13 +113,14 @@ const FlatUsersView = (props: FlatUsersViewProps) => {
|
|||
</TableContainer>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
{selectedUser && (
|
||||
<User
|
||||
current_user={findUser(selectedUser)}
|
||||
fetchDataAgain={props.fetchDataAgain}
|
||||
></User>
|
||||
)}
|
||||
<Grid item xs={6} md={7}>
|
||||
{selectedUser && (
|
||||
<User
|
||||
current_user={findUser(selectedUser)}
|
||||
fetchDataAgain={props.fetchDataAgain}
|
||||
></User>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { InnovEnergyUser } from 'src/interfaces/UserTypes';
|
|||
import { TokenContext } from 'src/contexts/tokenContext';
|
||||
import { TabsContainerWrapper } from 'src/layouts/TabsContainerWrapper';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import UserAccess from '../ManageAccess/UserAccess';
|
||||
|
||||
interface singleUserProps {
|
||||
current_user: InnovEnergyUser;
|
||||
|
|
@ -41,7 +42,10 @@ function User(props: singleUserProps) {
|
|||
const [formValues, setFormValues] = useState(props.current_user);
|
||||
const tokencontext = useContext(TokenContext);
|
||||
const { removeToken } = tokencontext;
|
||||
const tabs = [{ value: 'user', label: 'User' }];
|
||||
const tabs = [
|
||||
{ value: 'user', label: 'User' },
|
||||
{ value: 'manage', label: 'Access Management' }
|
||||
];
|
||||
const [openModalDeleteUser, setOpenModalDeleteUser] = useState(false);
|
||||
|
||||
const UserTypes = ['Client', 'Partner', 'Admin'];
|
||||
|
|
@ -226,7 +230,7 @@ function User(props: singleUserProps) {
|
|||
</Modal>
|
||||
)}
|
||||
|
||||
<Grid item xs={12} md={isMobile ? 7 : 8}>
|
||||
<Grid item xs={12} md={12}>
|
||||
<TabsContainerWrapper>
|
||||
<Tabs
|
||||
onChange={handleTabsChange}
|
||||
|
|
@ -301,7 +305,7 @@ function User(props: singleUserProps) {
|
|||
<div>
|
||||
<FormControl
|
||||
fullWidth
|
||||
sx={{ marginLeft: 1, marginTop: 1, width: 390 }}
|
||||
sx={{ marginLeft: 1, marginTop: 1, width: 445 }}
|
||||
>
|
||||
<InputLabel
|
||||
sx={{
|
||||
|
|
@ -413,6 +417,9 @@ function User(props: singleUserProps) {
|
|||
</Grid>
|
||||
</Container>
|
||||
)}
|
||||
{currentTab === 'manage' && (
|
||||
<UserAccess current_user={props.current_user}></UserAccess>
|
||||
)}
|
||||
</Grid>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
|
|
|||
|
|
@ -69,25 +69,38 @@ function userForm(props: userFormProps) {
|
|||
|
||||
const fetchInstallations = useCallback(async () => {
|
||||
setLoading(true);
|
||||
return axiosConfig
|
||||
.get('/GetAllInstallations')
|
||||
.then((res) => {
|
||||
setInstallations(res.data);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
if (err.response && err.response.status == 401) {
|
||||
removeToken();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
// fetch product 0
|
||||
const res0 = await axiosConfig.get(
|
||||
`/GetAllInstallationsFromProduct?product=0`
|
||||
);
|
||||
const installations0 = res0.data;
|
||||
|
||||
// fetch product 1
|
||||
const res1 = await axiosConfig.get(
|
||||
`/GetAllInstallationsFromProduct?product=3`
|
||||
);
|
||||
const installations1 = res1.data;
|
||||
|
||||
// aggregate
|
||||
const combined = [...installations0, ...installations1];
|
||||
|
||||
// update
|
||||
setInstallations(combined);
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
removeToken();
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [setInstallations]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchFolders();
|
||||
fetchInstallations();
|
||||
}, []);
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormValues({
|
||||
|
|
|
|||
|
|
@ -12,8 +12,11 @@ import {
|
|||
InnovEnergyUser
|
||||
} from '../interfaces/UserTypes';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { I_Installation } from '../interfaces/InstallationTypes';
|
||||
|
||||
interface AccessContextProviderProps {
|
||||
fetchInstallationsForUser: (userId: number) => void;
|
||||
accessibleInstallationsForUser: I_Installation[];
|
||||
availableUsers: InnovEnergyUser[];
|
||||
fetchAvailableUsers: () => Promise<void>;
|
||||
usersWithDirectAccess: InnovEnergyUser[];
|
||||
|
|
@ -44,6 +47,8 @@ interface AccessContextProviderProps {
|
|||
}
|
||||
|
||||
export const AccessContext = createContext<AccessContextProviderProps>({
|
||||
fetchInstallationsForUser: () => Promise.resolve(),
|
||||
accessibleInstallationsForUser: [],
|
||||
availableUsers: [],
|
||||
fetchAvailableUsers: () => {
|
||||
return Promise.resolve();
|
||||
|
|
@ -74,6 +79,8 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
|
|||
InnovEnergyUser[]
|
||||
>([]);
|
||||
const [availableUsers, setAvailableUsers] = useState<InnovEnergyUser[]>([]);
|
||||
const [accessibleInstallationsForUser, setAccessibleInstallationsForUser] =
|
||||
useState<I_Installation[]>([]);
|
||||
|
||||
const [usersWithInheritedAccess, setUsersWithInheritedAccess] = useState<
|
||||
I_UserWithInheritedAccess[]
|
||||
|
|
@ -104,6 +111,26 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
|
|||
[]
|
||||
);
|
||||
|
||||
const fetchInstallationsForUser = useCallback(async (userId: number) => {
|
||||
axiosConfig
|
||||
.get(`/GetInstallationsTheUserHasAccess?userId=${userId}`)
|
||||
.then((response) => {
|
||||
if (response) {
|
||||
setAccessibleInstallationsForUser(response.data);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
setError(true);
|
||||
const message = (
|
||||
<FormattedMessage
|
||||
id="unableToLoadData"
|
||||
defaultMessage="Unable to load data"
|
||||
/>
|
||||
).props.defaultMessage;
|
||||
setErrorMessage(message);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const fetchUsersWithInheritedAccessForResource = useCallback(
|
||||
async (tempresourceType: string, id: number) => {
|
||||
axiosConfig
|
||||
|
|
@ -192,6 +219,8 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
|
|||
return (
|
||||
<AccessContext.Provider
|
||||
value={{
|
||||
fetchInstallationsForUser,
|
||||
accessibleInstallationsForUser,
|
||||
availableUsers,
|
||||
fetchAvailableUsers,
|
||||
usersWithDirectAccess,
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ const InstallationsContextProvider = ({
|
|||
}
|
||||
const tokenString = localStorage.getItem('token');
|
||||
const token = tokenString !== null ? tokenString : '';
|
||||
const urlWithToken = `wss://monitor.innov.energy/api/CreateWebSocket?authToken=${token}`;
|
||||
const urlWithToken = `wss://monitor.inesco.energy/api/CreateWebSocket?authToken=${token}`;
|
||||
|
||||
const new_socket = new WebSocket(urlWithToken);
|
||||
|
||||
|
|
@ -200,9 +200,10 @@ const InstallationsContextProvider = ({
|
|||
const fetchAllSodiohomeInstallations = useCallback(async () => {
|
||||
axiosConfig
|
||||
.get('/GetAllSodioHomeInstallations')
|
||||
.then((res: AxiosResponse<I_Installation[]>) =>
|
||||
setSodiohomeInstallations(res.data)
|
||||
)
|
||||
.then((res: AxiosResponse<I_Installation[]>) => {
|
||||
setSodiohomeInstallations(res.data);
|
||||
openSocket(res.data);
|
||||
})
|
||||
.catch((err: AxiosError) => {
|
||||
if (err.response?.status === 401) {
|
||||
removeToken();
|
||||
|
|
|
|||
|
|
@ -55,112 +55,6 @@ const WebSocketContextProvider = ({ children }: { children: ReactNode }) => {
|
|||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// if (sortedInstallations) {
|
||||
// const tokenString = localStorage.getItem('token');
|
||||
// const token = tokenString !== null ? tokenString : '';
|
||||
// const urlWithToken = `wss://monitor.innov.energy/api/CreateWebSocket?authToken=${token}`;
|
||||
//
|
||||
// const socket = new WebSocket(urlWithToken);
|
||||
// // Connection opened
|
||||
// socket.addEventListener('open', (event) => {
|
||||
// socket.send(
|
||||
// JSON.stringify(
|
||||
// sortedInstallations.map((installation) => installation.id)
|
||||
// )
|
||||
// );
|
||||
// });
|
||||
//
|
||||
// // Periodically send ping messages to keep the connection alive
|
||||
// const pingInterval = setInterval(() => {
|
||||
// if (socket.readyState === WebSocket.OPEN) {
|
||||
// socket.send(JSON.stringify([-1]));
|
||||
// }
|
||||
// }, 10000); // Send a ping every 10 seconds
|
||||
//
|
||||
// let messageBuffer = [];
|
||||
// let isProcessing = false;
|
||||
//
|
||||
// socket.addEventListener('message', (event) => {
|
||||
// const message = JSON.parse(event.data); // Parse the JSON data
|
||||
//
|
||||
// if (Array.isArray(message)) {
|
||||
// message.forEach((item) => {
|
||||
// console.log('status is ' + item.status);
|
||||
// // Update status and testingMode for each installation received
|
||||
// // updateInstallationStatus(item.id, item.status, item.testingMode);
|
||||
// });
|
||||
// }
|
||||
// // } else if (message.id !== -1) {
|
||||
// // // Handle individual messages for installations
|
||||
// // updateInstallationStatus(
|
||||
// // message.id,
|
||||
// // message.status,
|
||||
// // message.testingMode
|
||||
// // );
|
||||
// // }
|
||||
//
|
||||
// // if (Array.isArray(message)) {
|
||||
// // // Existing code for handling arrays, if necessary
|
||||
// // setInstallationMode((prevMode) => {
|
||||
// // const newMode = new Map(prevMode);
|
||||
// // message.forEach((item) => {
|
||||
// // newMode.set(item.id, item.testingMode);
|
||||
// // });
|
||||
// // return newMode;
|
||||
// // });
|
||||
// //
|
||||
// // setInstallationStatus((prevStatus) => {
|
||||
// // const newStatus = new Map(prevStatus);
|
||||
// // message.forEach((item) => {
|
||||
// // newStatus.set(item.id, item.status);
|
||||
// // });
|
||||
// // return newStatus;
|
||||
// // });
|
||||
// // } else if (message.id != -1) {
|
||||
// // // Accumulate messages in the buffer
|
||||
// // messageBuffer.push(message);
|
||||
// //
|
||||
// // // Process the buffer if not already processing
|
||||
// // if (!isProcessing) {
|
||||
// // isProcessing = true;
|
||||
// //
|
||||
// // // Use setTimeout to process the buffer periodically
|
||||
// // setTimeout(() => {
|
||||
// // const newInstallationMode = new Map();
|
||||
// // const newInstallationStatus = new Map();
|
||||
// //
|
||||
// // // Process all accumulated messages
|
||||
// // messageBuffer.forEach((msg) => {
|
||||
// // newInstallationMode.set(msg.id, msg.testingMode);
|
||||
// // newInstallationStatus.set(msg.id, msg.status);
|
||||
// // });
|
||||
// //
|
||||
// // // Update the state with the accumulated messages
|
||||
// // setInstallationMode(
|
||||
// // (prevMode) => new Map([...prevMode, ...newInstallationMode])
|
||||
// // );
|
||||
// // setInstallationStatus(
|
||||
// // (prevStatus) =>
|
||||
// // new Map([...prevStatus, ...newInstallationStatus])
|
||||
// // );
|
||||
// //
|
||||
// // // Clear the buffer after processing
|
||||
// // messageBuffer = [];
|
||||
// // isProcessing = false; // Reset processing flag
|
||||
// // }, 100); // Adjust the delay as needed to control processing frequency
|
||||
// // }
|
||||
// // }
|
||||
// });
|
||||
//
|
||||
// setSocket(socket);
|
||||
// }
|
||||
}, [sortedInstallations]);
|
||||
|
||||
// const openSocket = (installations: I_Installation[]) => {
|
||||
// setInstallations(installations);
|
||||
// };
|
||||
|
||||
const openSocket = (installations) => {
|
||||
// setSortedInstallations(installations.sort((a, b) => b.status - a.status)); // Sort installations by status
|
||||
};
|
||||
|
|
@ -168,19 +62,7 @@ const WebSocketContextProvider = ({ children }: { children: ReactNode }) => {
|
|||
const closeSocket = () => {
|
||||
// socket.close();
|
||||
};
|
||||
|
||||
// const getStatus = (installationId: number) => {
|
||||
// return installationStatus.get(installationId);
|
||||
// // if (installationStatus.has(installationId)) {
|
||||
// // installationStatus.get(installationId);
|
||||
// // } else {
|
||||
// // return -2;
|
||||
// // }
|
||||
// };
|
||||
|
||||
// const getTestingMode = (installationId: number) => {
|
||||
// return installationMode.get(installationId);
|
||||
// };
|
||||
|
||||
|
||||
return (
|
||||
<WebSocketContext.Provider
|
||||
|
|
|
|||
|
|
@ -320,7 +320,7 @@ export const transformInputToDailyDataJson = async (
|
|||
//'Battery.Dc.Power' for salimax,
|
||||
// 'Battery.Power',
|
||||
'GridMeter.Ac.Power.Active',
|
||||
'PvOnDc.Dc.Power',
|
||||
'PvOnDc',
|
||||
'DcDc.Dc.Link.Voltage',
|
||||
'LoadOnAcGrid.Power.Active',
|
||||
'LoadOnDc.Power'
|
||||
|
|
@ -420,23 +420,35 @@ export const transformInputToDailyDataJson = async (
|
|||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
pathsToSearch.forEach((path) => {
|
||||
if (get(result, path) !== undefined) {
|
||||
const value = path
|
||||
.split('.')
|
||||
.reduce((o, key) => (o ? o[key] : undefined), result);
|
||||
let value: number | undefined = undefined;
|
||||
|
||||
if (value < chartOverview[categories[category_index]].min) {
|
||||
chartOverview[categories[category_index]].min = value;
|
||||
if (category_index === 4) {
|
||||
// Custom logic for 'PvOnDc.Dc.Power'
|
||||
value = Object.values(
|
||||
result.PvOnDc as Record<string, { Dc?: { Power?: number } }>
|
||||
).reduce((sum, device) => sum + (device.Dc?.Power || 0), 0);
|
||||
} else if (get(result, path) !== undefined) {
|
||||
// Default path-based extraction
|
||||
value = path
|
||||
.split('.')
|
||||
.reduce((o, key) => (o ? o[key] : undefined), result);
|
||||
}
|
||||
|
||||
if (value > chartOverview[categories[category_index]].max) {
|
||||
chartOverview[categories[category_index]].max = value;
|
||||
// Only push value if defined
|
||||
if (value !== undefined) {
|
||||
if (value < chartOverview[categories[category_index]].min) {
|
||||
chartOverview[categories[category_index]].min = value;
|
||||
}
|
||||
|
||||
if (value > chartOverview[categories[category_index]].max) {
|
||||
chartOverview[categories[category_index]].max = value;
|
||||
}
|
||||
|
||||
chartData[categories[category_index]].data.push([
|
||||
adjustedTimestampArray[i],
|
||||
value
|
||||
]);
|
||||
}
|
||||
chartData[categories[category_index]].data.push([
|
||||
adjustedTimestampArray[i],
|
||||
value
|
||||
]);
|
||||
} else {
|
||||
//data[path].push([adjustedTimestamp, null]);
|
||||
}
|
||||
category_index++;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ function SidebarMenu() {
|
|||
<Box sx={{ marginTop: '3px' }}>
|
||||
<FormattedMessage
|
||||
id="sodistore"
|
||||
defaultMessage="Sodistore"
|
||||
defaultMessage="SodistoreMax"
|
||||
/>
|
||||
</Box>
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { useContext } from 'react';
|
||||
import Scrollbar from 'src/components/Scrollbar';
|
||||
import { SidebarContext } from 'src/contexts/SidebarContext';
|
||||
import innovenergyLogo from 'src/Resources/images/innovenergy-Logo_Speichern-mit-Salz_R_color.svg';
|
||||
import inescoLogo from 'src/Resources/images/inesco_logo.png';
|
||||
|
||||
import {
|
||||
alpha,
|
||||
Box,
|
||||
|
|
@ -17,8 +18,8 @@ import SidebarMenu from './SidebarMenu';
|
|||
|
||||
const SidebarWrapper = styled(Box)(
|
||||
({ theme }) => `
|
||||
width: ${theme.sidebar.width};
|
||||
min-width: ${theme.sidebar.width};
|
||||
width: 280px; /* previously theme.sidebar.width */
|
||||
min-width: 280px;
|
||||
color: ${theme.colors.alpha.trueWhite[70]};
|
||||
position: relative;
|
||||
z-index: 7;
|
||||
|
|
@ -54,16 +55,24 @@ function Sidebar() {
|
|||
<Scrollbar>
|
||||
<Box mt={3}>
|
||||
<Box
|
||||
mx={2}
|
||||
sx={{
|
||||
width: 52
|
||||
px: 2, // Padding left & right
|
||||
py: 1.5, // Optional: padding top & bottom
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
border: '2px solid white', // Optional: border around logo
|
||||
borderRadius: '8px',
|
||||
backgroundColor: '#fff', // Optional: white background
|
||||
mx: 2 // Horizontal margin to avoid sticking to edge
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={innovenergyLogo}
|
||||
alt="innovenergy logo"
|
||||
src={inescoLogo}
|
||||
alt="inesco logo"
|
||||
style={{
|
||||
width: '150px' // Width of the image
|
||||
width: '160px',
|
||||
objectFit: 'contain'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
|
@ -105,8 +114,8 @@ function Sidebar() {
|
|||
}}
|
||||
>
|
||||
<img
|
||||
src={innovenergyLogo}
|
||||
alt="innovenergy logo"
|
||||
src={inescoLogo}
|
||||
alt="inesco logo"
|
||||
style={{
|
||||
width: '150px' // Width of the image
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ const SidebarLayout = (props: SidebarLayoutProps) => {
|
|||
flex: 1,
|
||||
pt: `${theme.header.height}`,
|
||||
[theme.breakpoints.up('lg')]: {
|
||||
ml: `${theme.sidebar.width}`
|
||||
ml: '260px'
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { alpha, createTheme, lighten, darken } from '@mui/material';
|
||||
import { alpha, createTheme, darken, lighten } from '@mui/material';
|
||||
import '@mui/lab/themeAugmentation';
|
||||
|
||||
const themeColors = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue