Merge remote-tracking branch 'origin/main'

This commit is contained in:
atef 2025-09-01 14:52:57 +02:00
commit 73880f0737
57 changed files with 2330 additions and 1477 deletions

View File

@ -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 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. it easy to identify and analyze the extracted data.
4)Advanced Data Processing Capabilities: 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. 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: 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. 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

View File

@ -18,14 +18,15 @@ def extract_timestamp(filename):
except ValueError: except ValueError:
return 0 return 0
import subprocess
def list_files_in_range(start_timestamp, end_timestamp, sampling_stepsize, product_type, bucket_number): def list_files_in_range(start_timestamp, end_timestamp, sampling_stepsize, product_type, bucket_number):
if product_type == "Salimax" or product_type=="SodistoreMax": if product_type in ["Salimax", "SodistoreMax"]:
hash = "3e5b3069-214a-43ee-8d85-57d72000c19d" hash = "3e5b3069-214a-43ee-8d85-57d72000c19d"
elif product_type == "Salidomo": elif product_type == "Salidomo":
hash = "c0436b6a-d276-4cd8-9c44-1eae86cf5d0e" hash = "c0436b6a-d276-4cd8-9c44-1eae86cf5d0e"
else: else:
raise ValueError("Invalid product type option. Use Salimax or Salidomo or SodistoreMax") raise ValueError("Invalid product type option.")
# Find common prefix # Find common prefix
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) output = subprocess.check_output(s3cmd_command, shell=True, text=True)
files = [line.split()[-1] for line in output.strip().split("\n") if line.strip()] files = [line.split()[-1] for line in output.strip().split("\n") if line.strip()]
filenames = [] filenames = []
count=0
for f in files: for f in files:
name = f.split("/")[-1] # e.g., 1748802020.json name = f.split("/")[-1]
timestamp_str = name.split(".")[0] # extract '1748802020' timestamp_str = name.split(".")[0]
if timestamp_str.isdigit() and int(timestamp_str) <= int(end_timestamp):
if timestamp_str.isdigit():
timestamp = int(timestamp_str)
if start_timestamp <= timestamp <= end_timestamp :
if count % sampling_stepsize == 0:
filenames.append(name) filenames.append(name)
else: count += 1
break
print(filenames) print(filenames)
return filenames return filenames
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
print(f"No files found for prefix {common_prefix}") print(f"No files found for prefix {common_prefix}")
return [] return []
def get_nested_value(data, key_path): def get_nested_value(data, key_path):
try: try:
for key in key_path: 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.") print(f"Files with prefix '{filename}' downloaded successfully.")
decompress_file(os.path.join(output_directory, filename), output_directory) decompress_file(os.path.join(output_directory, filename), output_directory)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
# print(f"Error downloading files: {e}") print(f"Error downloading files: {e}")
continue continue
else: else:
print(f"File '{filename}.json' already exists locally. Skipping download.") print(f"File '{filename}.json' already exists locally. Skipping download.")
@ -187,7 +199,7 @@ def get_last_component(path):
path_without_slashes = path.replace('/', '') path_without_slashes = path.replace('/', '')
return path_without_slashes 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}" output_directory = f"S3cmdData_{bucket_number}"
#if os.path.exists(output_directory): #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) 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"))] 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) files_to_download = set(filenames_to_check) - set(existing_files)
print(files_to_download) #print(files_to_download)
#if os.listdir(output_directory): #if os.listdir(output_directory):
# print("Files already exist in the local folder. Skipping download.") # 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('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('--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('--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('--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') parser.add_argument('--product_name', required=True, help='Use Salimax, Salidomo or SodistoreMax')
args = parser.parse_args() args = parser.parse_args()
@ -243,14 +254,13 @@ def main():
bucket_number = args.bucket_number bucket_number = args.bucket_number
sampling_stepsize = args.sampling_stepsize sampling_stepsize = args.sampling_stepsize
booleans_as_numbers = args.booleans_as_numbers booleans_as_numbers = args.booleans_as_numbers
exact_match = args.exact_match
# new arg for product type # new arg for product type
product_type = args.product_name product_type = args.product_name
if start_timestamp >= end_timestamp: if start_timestamp >= end_timestamp:
print("Error: start_timestamp must be smaller than end_timestamp.") print("Error: start_timestamp must be smaller than end_timestamp.")
return 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__": if __name__ == "__main__":
main() main()

View File

@ -345,6 +345,18 @@ public class Controller : ControllerBase
.ToList(); .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))] [HttpGet(nameof(GetUsersWithInheritedAccessToInstallation))]
public ActionResult<IEnumerable<Object>> GetUsersWithInheritedAccessToInstallation(Int64 id, Token authToken) public ActionResult<IEnumerable<Object>> GetUsersWithInheritedAccessToInstallation(Int64 id, Token authToken)
{ {
@ -927,6 +939,8 @@ public class Controller : ControllerBase
{ {
var session = Db.GetSession(authToken); var session = Db.GetSession(authToken);
Console.WriteLine("CONFIG IS " + config.GetConfigurationString());
// Send configuration changes // Send configuration changes
var success = await session.SendInstallationConfig(installationId, config); var success = await session.SendInstallationConfig(installationId, config);
@ -940,6 +954,7 @@ public class Controller : ControllerBase
Timestamp = DateTime.Now, Timestamp = DateTime.Now,
Description = config.GetConfigurationString() Description = config.GetConfigurationString()
}; };
Console.WriteLine(action.Description);
var actionSuccess = await session.InsertUserAction(action); var actionSuccess = await session.InsertUserAction(action);
return actionSuccess?Ok():Unauthorized(); return actionSuccess?Ok():Unauthorized();
@ -1020,7 +1035,7 @@ public class Controller : ControllerBase
Db.DeleteUserPassword(user); 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
} }
} }

View File

@ -413,6 +413,8 @@ public static class ExoCmd
byte[] data = Encoding.UTF8.GetBytes(JsonSerializer.Serialize<Configuration>(config)); byte[] data = Encoding.UTF8.GetBytes(JsonSerializer.Serialize<Configuration>(config));
udpClient.Send(data, data.Length, installation.VpnIp, port); 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}: {config}");
Console.WriteLine($"Sent UDP message to {installation.VpnIp}:{port}"+" GridSetPoint is "+config.GridSetPoint +" and MinimumSoC is "+config.MinimumSoC); 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; 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);
} }
} }

View File

@ -7,12 +7,9 @@ namespace InnovEnergy.App.Backend.DataTypes.Methods;
public static class InstallationMethods public static class InstallationMethods
{ {
private static readonly String BucketNameSalt = private static readonly String BucketNameSalt = "3e5b3069-214a-43ee-8d85-57d72000c19d";
// Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == ""
// ? "stage" :"3e5b3069-214a-43ee-8d85-57d72000c19d";
"3e5b3069-214a-43ee-8d85-57d72000c19d";
private static readonly String SalidomoBucketNameSalt = "c0436b6a-d276-4cd8-9c44-1eae86cf5d0e"; 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) public static String BucketName(this Installation installation)
{ {
@ -21,6 +18,11 @@ public static class InstallationMethods
return $"{installation.S3BucketId}-{BucketNameSalt}"; return $"{installation.S3BucketId}-{BucketNameSalt}";
} }
if (installation.Product == (int)ProductType.SodioHome)
{
return $"{installation.S3BucketId}-{SodioHomeBucketNameSalt}";
}
return $"{installation.S3BucketId}-{SalidomoBucketNameSalt}"; return $"{installation.S3BucketId}-{SalidomoBucketNameSalt}";
} }

View File

@ -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 return user is not null
&& user.UserType != 0 && user.UserType != 0
@ -244,15 +244,15 @@ public static class SessionMethods
&& await installation.CreateBucket() && await installation.CreateBucket()
&& await installation.RenewS3Credentials(); && await installation.RenewS3Credentials();
} }
//
if (installation.Product == (int)ProductType.SodioHome) // if (installation.Product == (int)ProductType.SodioHome)
{ // {
return user is not null // return user is not null
&& user.UserType != 0 // && user.UserType != 0
&& user.HasAccessToParentOf(installation) // && user.HasAccessToParentOf(installation)
&& Db.Create(installation); // && Db.Create(installation);
} // }
//
return false; return false;

View File

@ -229,6 +229,9 @@ public static class UserMethods
public static Task SendEmail(this User user, String subject, String body) 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); return Mailer.Send(user.Name, user.Email, subject, body);
} }

View File

@ -284,6 +284,7 @@ public static partial class Db
} }
catch catch
{ {
Console.WriteLine("return false");
return false; return false;
} }
} }

View File

@ -1,8 +1,8 @@
{ {
"SmtpServerUrl" : "mail.agenturserver.de", "SmtpServerUrl" : "smtp.gmail.com",
"SmtpUsername" : "p518526p69", "SmtpUsername" : "angelis@inesco.energy",
"SmtpPassword" : "i;b*xqm4iB5uhl", "SmtpPassword" : "huvu pkqd kakz hqtm ",
"SmtpPort" : 587, "SmtpPort" : 587,
"SenderName" : "InnovEnergy", "SenderName" : "Inesco Energy",
"SenderAddress" : "noreply@innov.energy" "SenderAddress" : "noreply@inesco.energy"
} }

View File

@ -28,9 +28,8 @@ public static class Program
RabbitMqManager.InitializeEnvironment(); RabbitMqManager.InitializeEnvironment();
RabbitMqManager.StartRabbitMqConsumer().SupressAwaitWarning(); RabbitMqManager.StartRabbitMqConsumer().SupressAwaitWarning();
WebsocketManager.MonitorSalimaxInstallationTable().SupressAwaitWarning();
WebsocketManager.MonitorSalidomoInstallationTable().SupressAwaitWarning(); WebsocketManager.MonitorInstallationTable().SupressAwaitWarning();
WebsocketManager.MonitorSodistoreInstallationTable().SupressAwaitWarning();
// Task.Run(() => DeleteOldDataFromS3.DeleteOldData()); // Task.Run(() => DeleteOldDataFromS3.DeleteOldData());

View File

@ -49,6 +49,10 @@ public class Session : Relation<String, Int64>
AccessToSalidomo = user.AccessibleInstallations(product: (int)ProductType.Salidomo).ToList().Count > 0; AccessToSalidomo = user.AccessibleInstallations(product: (int)ProductType.Salidomo).ToList().Count > 0;
AccessToSodistoreMax = user.AccessibleInstallations(product: (int)ProductType.SodiStoreMax).ToList().Count > 0; AccessToSodistoreMax = user.AccessibleInstallations(product: (int)ProductType.SodiStoreMax).ToList().Count > 0;
AccessToSodioHome = user.AccessibleInstallations(product: (int)ProductType.SodioHome).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() private static String CreateToken()

View File

@ -13,101 +13,41 @@ public static class WebsocketManager
{ {
public static Dictionary<Int64, InstallationInfo> InstallationConnections = new Dictionary<Int64, InstallationInfo>(); public static Dictionary<Int64, InstallationInfo> InstallationConnections = new Dictionary<Int64, InstallationInfo>();
//Every 1 minute, check the timestamp of the latest received message for every installation. public static async Task MonitorInstallationTable()
//If the difference between the two timestamps is more than two minutes, we consider this Salimax installation unavailable.
public static async Task MonitorSalimaxInstallationTable()
{ {
while (true){ while (true)
lock (InstallationConnections){ {
// Console.WriteLine("MONITOR SALIMAX INSTALLATIONS\n"); lock (InstallationConnections)
foreach (var installationConnection in 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)){ 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("Installation ID is " + installationConnection.Key);
// Console.WriteLine("installationConnection.Value.Timestamp is "+installationConnection.Value.Timestamp); Console.WriteLine("installationConnection.Value.Timestamp is " + installationConnection.Value.Timestamp);
// Console.WriteLine("diff is "+(DateTime.Now-installationConnection.Value.Timestamp)); // Console.WriteLine("diff is "+(DateTime.Now-installationConnection.Value.Timestamp));
installationConnection.Value.Status = (int)StatusType.Offline; installationConnection.Value.Status = (int)StatusType.Offline;
Installation installation = Db.Installations.FirstOrDefault(f => f.Product == (int)ProductType.Salimax && f.Id == installationConnection.Key); Installation installation = Db.Installations.FirstOrDefault(f => f.Product == installationConnection.Value.Product && f.Id == installationConnection.Key);
installation.Status = (int)StatusType.Offline; installation.Status = (int)StatusType.Offline;
installation.Apply(Db.Update); installation.Apply(Db.Update);
if (installationConnection.Value.Connections.Count > 0){InformWebsocketsForInstallation(installationConnection.Key);} if (installationConnection.Value.Connections.Count > 0)
}
}
// 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){ InformWebsocketsForInstallation(installationConnection.Key);
//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));
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));
} }
} }
//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)); await Task.Delay(TimeSpan.FromMinutes(1));
} }
} }
@ -140,6 +80,7 @@ public static class WebsocketManager
} }
} }
public static async Task HandleWebSocketConnection(WebSocket currentWebSocket) public static async Task HandleWebSocketConnection(WebSocket currentWebSocket)
{ {
var buffer = new byte[4096]; var buffer = new byte[4096];
@ -155,6 +96,7 @@ public static class WebsocketManager
var message = Encoding.UTF8.GetString(buffer, 0, result.Count); var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
var installationIds = JsonSerializer.Deserialize<int[]>(message); 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 //This is a ping message to keep the connection alive, reply with a pong
if (installationIds[0] == -1) if (installationIds[0] == -1)

View File

@ -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'>&times;</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;
}
}

View File

@ -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>

View File

@ -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();
}
}

View File

@ -103,6 +103,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GrowattCommunication", "App
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WITGrowatt4-15K", "Lib\Devices\WITGrowatt4-15K\WITGrowatt4-15K.csproj", "{44DD9E5E-2AD3-4579-A47D-7A40FD28D369}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WITGrowatt4-15K", "Lib\Devices\WITGrowatt4-15K\WITGrowatt4-15K.csproj", "{44DD9E5E-2AD3-4579-A47D-7A40FD28D369}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataCollectorWebApp", "DataCollectorWebApp\DataCollectorWebApp.csproj", "{6069D487-DBAB-4253-BFA1-CF994B84BE49}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution 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}.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.ActiveCfg = Release|Any CPU
{44DD9E5E-2AD3-4579-A47D-7A40FD28D369}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
{CF4834CB-91B7-4172-AC13-ECDA8613CD17} = {145597B4-3E30-45E6-9F72-4DD43194539A} {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} {39B83793-49DB-4940-9C25-A7F944607407} = {145597B4-3E30-45E6-9F72-4DD43194539A}
{DC0BE34A-368F-46DC-A081-70C9A1EFE9C0} = {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} {44DD9E5E-2AD3-4579-A47D-7A40FD28D369} = {4931A385-24DC-4E78-BFF4-356F8D6D5183}
{6069D487-DBAB-4253-BFA1-CF994B84BE49} = {145597B4-3E30-45E6-9F72-4DD43194539A}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View File

@ -5191,7 +5191,7 @@
"order": 5, "order": 5,
"width": 0, "width": 0,
"height": 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, "storeOutMessages": true,
"fwdInMessages": true, "fwdInMessages": true,
"resendOnRefresh": true, "resendOnRefresh": true,

View File

@ -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'

View File

@ -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

View File

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<link href="%PUBLIC_URL%/favicon.png" rel="shortcut icon"/> <link href="%PUBLIC_URL%/Logo.svg" rel="shortcut icon"/>
<meta <meta
content="width=device-width, initial-scale=1, shrink-to-fit=no" content="width=device-width, initial-scale=1, shrink-to-fit=no"
name="viewport" name="viewport"
@ -13,7 +13,7 @@
href="https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,400&display=swap" href="https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,400&display=swap"
rel="stylesheet" rel="stylesheet"
/> />
<title>InnovEnergy</title> <title>Inesco Energy</title>
</head> </head>
<body> <body>

View File

@ -186,7 +186,7 @@ function App() {
path={routes.sodiohome_installations + '*'} path={routes.sodiohome_installations + '*'}
element={ element={
<AccessContextProvider> <AccessContextProvider>
<SodioHomeInstallationTabs /> <SodioHomeInstallationTabs product={2} />
</AccessContextProvider> </AccessContextProvider>
} }
/> />

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -1,12 +1,12 @@
import axios from 'axios'; import axios from 'axios';
export const axiosConfigWithoutToken = axios.create({ 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' //baseURL: 'http://127.0.0.1:7087/api'
}); });
const axiosConfig = axios.create({ const axiosConfig = axios.create({
baseURL: 'https://monitor.innov.energy/api' baseURL: 'https://monitor.inesco.energy/api'
//baseURL: 'http://127.0.0.1:7087/api' //baseURL: 'http://127.0.0.1:7087/api'
}); });

View File

@ -18,7 +18,7 @@ function Footer() {
> >
<Box> <Box>
<Typography variant="subtitle1"> <Typography variant="subtitle1">
&copy; 2024 - InnovEnergy AG &copy; 2025 - Inesco Energy AG
</Typography> </Typography>
</Box> </Box>
<Typography <Typography
@ -29,11 +29,11 @@ function Footer() {
> >
Crafted by{' '} Crafted by{' '}
<Link <Link
href="https://www.innov.energy/" href="https://www.inesco.energy/"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
InnovEnergy AG Inesco Energy AG
</Link> </Link>
</Typography> </Typography>
</Box> </Box>

View File

@ -10,7 +10,8 @@ import {
Typography, Typography,
useTheme useTheme
} from '@mui/material'; } 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 { UserContext } from 'src/contexts/userContext';
import { TokenContext } from 'src/contexts/tokenContext'; import { TokenContext } from 'src/contexts/tokenContext';
import Avatar from '@mui/material/Avatar'; import Avatar from '@mui/material/Avatar';
@ -76,8 +77,8 @@ function ForgotPassword() {
<Container maxWidth="xl" sx={{ pt: 2 }}> <Container maxWidth="xl" sx={{ pt: 2 }}>
<Grid container> <Grid container>
<Grid item xs={3} container justifyContent="flex-start" mb={2}> <Grid item xs={3} container justifyContent="flex-start" mb={2}>
<a href="https://monitor.innov.energy/"> <a href="https://monitor.inesco.energy/">
<img src={innovenergyLogo} alt="innovenergy logo" height="100" /> <img src={inescologo} alt="inesco logo" height="100" />
</a> </a>
</Grid> </Grid>
</Grid> </Grid>
@ -100,7 +101,7 @@ function ForgotPassword() {
transform: 'translate(-50%, -50%)' transform: 'translate(-50%, -50%)'
}} }}
> >
<Avatar sx={{ m: 1, bgcolor: '#ffc04d' }}> <Avatar sx={{ m: 1, bgcolor: '#00b33c' }}>
<LockOutlinedIcon /> <LockOutlinedIcon />
</Avatar> </Avatar>
<Typography component="h1" variant="h5"> <Typography component="h1" variant="h5">
@ -134,15 +135,15 @@ function ForgotPassword() {
}} }}
/> />
{loading && <CircularProgress sx={{ color: '#ffc04d' }} />} {loading && <CircularProgress sx={{ color: '#00b33c' }} />}
<Button <Button
sx={{ sx={{
mt: 3, mt: 3,
mb: 2, mb: 2,
textTransform: 'none', textTransform: 'none',
bgcolor: '#ffc04d', bgcolor: '#00b33c',
'&:hover': { bgcolor: '#f7b34d' } '&:hover': { bgcolor: '#009933' }
}} }}
variant="contained" variant="contained"
fullWidth={true} fullWidth={true}
@ -181,9 +182,9 @@ function ForgotPassword() {
sx={{ sx={{
marginTop: 2, marginTop: 2,
textTransform: 'none', textTransform: 'none',
bgcolor: '#ffc04d', bgcolor: '#00b33c',
color: '#111111', color: '#111111',
'&:hover': { bgcolor: '#f7b34d' } '&:hover': { bgcolor: '#009933' }
}} }}
onClick={() => setErrorModalOpen(false)} onClick={() => setErrorModalOpen(false)}
> >
@ -221,9 +222,9 @@ function ForgotPassword() {
sx={{ sx={{
marginTop: 2, marginTop: 2,
textTransform: 'none', textTransform: 'none',
bgcolor: '#ffc04d', bgcolor: '#00b33c',
color: '#111111', color: '#111111',
'&:hover': { bgcolor: '#f7b34d' } '&:hover': { bgcolor: '#009933' }
}} }}
onClick={handleReturn} onClick={handleReturn}
> >

View File

@ -10,7 +10,8 @@ import {
Typography, Typography,
useTheme useTheme
} from '@mui/material'; } 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 axiosConfig from 'src/Resources/axiosConfig';
import { UserContext } from 'src/contexts/userContext'; import { UserContext } from 'src/contexts/userContext';
import { TokenContext } from 'src/contexts/tokenContext'; import { TokenContext } from 'src/contexts/tokenContext';
@ -73,8 +74,8 @@ function ResetPassword() {
<Container maxWidth="xl" sx={{ pt: 2 }}> <Container maxWidth="xl" sx={{ pt: 2 }}>
<Grid container> <Grid container>
<Grid item xs={3} container justifyContent="flex-start" mb={2}> <Grid item xs={3} container justifyContent="flex-start" mb={2}>
<a href="https://monitor.innov.energy/"> <a href="https://monitor.inesco.energy/">
<img src={innovenergyLogo} alt="innovenergy logo" height="100" /> <img src={inescologo} alt="inesco logo" height="100" />
</a> </a>
</Grid> </Grid>
</Grid> </Grid>
@ -97,7 +98,7 @@ function ResetPassword() {
transform: 'translate(-50%, -50%)' transform: 'translate(-50%, -50%)'
}} }}
> >
<Avatar sx={{ m: 1, bgcolor: '#ffc04d' }}> <Avatar sx={{ m: 1, bgcolor: '#00b33c' }}>
<LockOutlinedIcon /> <LockOutlinedIcon />
</Avatar> </Avatar>
<Typography component="h1" variant="h5"> <Typography component="h1" variant="h5">
@ -137,7 +138,7 @@ function ResetPassword() {
/> />
{loading && ( {loading && (
<CircularProgress sx={{ color: '#ffc04d', marginLeft: '170px' }} /> <CircularProgress sx={{ color: '#00b33c', marginLeft: '170px' }} />
)} )}
{password != verifypassword && ( {password != verifypassword && (
@ -155,8 +156,8 @@ function ResetPassword() {
mt: 3, mt: 3,
mb: 2, mb: 2,
textTransform: 'none', textTransform: 'none',
bgcolor: '#ffc04d', bgcolor: '#00b33c',
'&:hover': { bgcolor: '#f7b34d' } '&:hover': { bgcolor: '#009933' }
}} }}
variant="contained" variant="contained"
fullWidth={true} fullWidth={true}
@ -195,9 +196,9 @@ function ResetPassword() {
sx={{ sx={{
marginTop: 2, marginTop: 2,
textTransform: 'none', textTransform: 'none',
bgcolor: '#ffc04d', bgcolor: '#00b33c',
color: '#111111', color: '#111111',
'&:hover': { bgcolor: '#f7b34d' } '&:hover': { bgcolor: '#009933' }
}} }}
onClick={() => setOpen(false)} onClick={() => setOpen(false)}
> >

View File

@ -10,7 +10,8 @@ import {
Typography, Typography,
useTheme useTheme
} from '@mui/material'; } 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 axiosConfig from 'src/Resources/axiosConfig';
import { UserContext } from 'src/contexts/userContext'; import { UserContext } from 'src/contexts/userContext';
import { TokenContext } from 'src/contexts/tokenContext'; import { TokenContext } from 'src/contexts/tokenContext';
@ -74,8 +75,8 @@ function SetNewPassword() {
<Container maxWidth="xl" sx={{ pt: 2 }}> <Container maxWidth="xl" sx={{ pt: 2 }}>
<Grid container> <Grid container>
<Grid item xs={3} container justifyContent="flex-start" mb={2}> <Grid item xs={3} container justifyContent="flex-start" mb={2}>
<a href="https://monitor.innov.energy/"> <a href="https://monitor.inesco.energy/">
<img src={innovenergyLogo} alt="innovenergy logo" height="100" /> <img src={inescologo} alt="inesco logo" height="100" />
</a> </a>
</Grid> </Grid>
</Grid> </Grid>
@ -98,7 +99,7 @@ function SetNewPassword() {
transform: 'translate(-50%, -50%)' transform: 'translate(-50%, -50%)'
}} }}
> >
<Avatar sx={{ m: 1, bgcolor: '#ffc04d' }}> <Avatar sx={{ m: 1, bgcolor: '#00b33c' }}>
<LockOutlinedIcon /> <LockOutlinedIcon />
</Avatar> </Avatar>
<Typography component="h1" variant="h5"> <Typography component="h1" variant="h5">
@ -138,7 +139,7 @@ function SetNewPassword() {
/> />
{loading && ( {loading && (
<CircularProgress sx={{ color: '#ffc04d', marginLeft: '0px' }} /> <CircularProgress sx={{ color: '#00b33c', marginLeft: '0px' }} />
)} )}
{password != verifypassword && ( {password != verifypassword && (
@ -156,8 +157,8 @@ function SetNewPassword() {
mt: 3, mt: 3,
mb: 2, mb: 2,
textTransform: 'none', textTransform: 'none',
bgcolor: '#ffc04d', bgcolor: '#00b33c',
'&:hover': { bgcolor: '#f7b34d' } '&:hover': { bgcolor: '#009933' }
}} }}
variant="contained" variant="contained"
fullWidth={true} fullWidth={true}
@ -196,9 +197,9 @@ function SetNewPassword() {
sx={{ sx={{
marginTop: 2, marginTop: 2,
textTransform: 'none', textTransform: 'none',
bgcolor: '#ffc04d', bgcolor: '#00b33c',
color: '#111111', color: '#111111',
'&:hover': { bgcolor: '#f7b34d' } '&:hover': { bgcolor: '#009933' }
}} }}
onClick={() => setOpen(false)} onClick={() => setOpen(false)}
> >

View File

@ -15,7 +15,7 @@ import {
import Avatar from '@mui/material/Avatar'; import Avatar from '@mui/material/Avatar';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import Link from '@mui/material/Link'; 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 { axiosConfigWithoutToken } from 'src/Resources/axiosConfig';
import Cookies from 'universal-cookie'; import Cookies from 'universal-cookie';
import { UserContext } from 'src/contexts/userContext'; import { UserContext } from 'src/contexts/userContext';
@ -93,6 +93,8 @@ function Login() {
navigate(routes.installations); navigate(routes.installations);
} else if (response.data.accessToSalidomo) { } else if (response.data.accessToSalidomo) {
navigate(routes.salidomo_installations); navigate(routes.salidomo_installations);
} else if (response.data.accessToSodistoreMax) {
navigate(routes.sodistore_installations);
} else { } else {
navigate(routes.sodiohome_installations); navigate(routes.sodiohome_installations);
} }
@ -113,8 +115,8 @@ function Login() {
<Container maxWidth="xl" sx={{ pt: 2 }} className="login"> <Container maxWidth="xl" sx={{ pt: 2 }} className="login">
<Grid container> <Grid container>
<Grid item xs={3} container justifyContent="flex-start" mb={2}> <Grid item xs={3} container justifyContent="flex-start" mb={2}>
<a href="https://monitor.innov.energy/"> <a href="https://monitor.inesco.energy/">
<img src={innovenergyLogo} alt="innovenergy logo" height="100" /> <img src={inescologo} alt="inescologo" height="100" />
</a> </a>
</Grid> </Grid>
</Grid> </Grid>
@ -137,7 +139,7 @@ function Login() {
transform: 'translate(-50%, -50%)' transform: 'translate(-50%, -50%)'
}} }}
> >
<Avatar sx={{ m: 1, bgcolor: '#ffc04d' }}> <Avatar sx={{ m: 1, bgcolor: '#00b33c' }}>
<LockOutlinedIcon /> <LockOutlinedIcon />
</Avatar> </Avatar>
<Typography component="h1" variant="h5"> <Typography component="h1" variant="h5">
@ -193,7 +195,7 @@ function Login() {
checked={rememberMe} checked={rememberMe}
onChange={handleRememberMeChange} onChange={handleRememberMeChange}
icon={<CheckBoxOutlineBlankIcon style={{ color: 'grey' }} />} icon={<CheckBoxOutlineBlankIcon style={{ color: 'grey' }} />}
checkedIcon={<CheckBoxIcon style={{ color: '#ffc04d' }} />} checkedIcon={<CheckBoxIcon style={{ color: '#00b33c' }} />}
style={{ marginLeft: -175 }} style={{ marginLeft: -175 }}
/> />
} }
@ -204,8 +206,8 @@ function Login() {
sx={{ sx={{
mb: 2, mb: 2,
textTransform: 'none', textTransform: 'none',
bgcolor: '#ffc04d', bgcolor: '#00b33c',
'&:hover': { bgcolor: '#f7b34d' } '&:hover': { bgcolor: '#009933' }
}} }}
variant="contained" variant="contained"
fullWidth={true} fullWidth={true}
@ -218,7 +220,7 @@ function Login() {
{loading && ( {loading && (
<CircularProgress <CircularProgress
sx={{ sx={{
color: '#ffc04d', color: '#009933',
marginLeft: '20px' marginLeft: '20px'
}} }}
/> />
@ -253,9 +255,9 @@ function Login() {
sx={{ sx={{
marginTop: 2, marginTop: 2,
textTransform: 'none', textTransform: 'none',
bgcolor: '#ffc04d', bgcolor: '#00b33c',
color: '#111111', color: '#111111',
'&:hover': { bgcolor: '#f7b34d' } '&:hover': { bgcolor: '#009933' }
}} }}
onClick={() => setOpen(false)} onClick={() => setOpen(false)}
> >

View File

@ -43,6 +43,8 @@ function BatteryView(props: BatteryViewProps) {
const currentLocation = useLocation(); const currentLocation = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const { product, setProduct } = useContext(ProductIdContext);
const sortedBatteryView = const sortedBatteryView =
props.values != null && props.values?.Battery?.Devices props.values != null && props.values?.Battery?.Devices
? Object.entries(props.values.Battery.Devices) ? Object.entries(props.values.Battery.Devices)
@ -58,8 +60,6 @@ function BatteryView(props: BatteryViewProps) {
navigate(routes.mainstats); navigate(routes.mainstats);
}; };
const { product, setProduct } = useContext(ProductIdContext);
useEffect(() => { useEffect(() => {
if (sortedBatteryView.length == 0) { if (sortedBatteryView.length == 0) {
setLoading(true); setLoading(true);
@ -232,7 +232,7 @@ function BatteryView(props: BatteryViewProps) {
<TableCell align="center">Battery</TableCell> <TableCell align="center">Battery</TableCell>
<TableCell align="center">Firmware</TableCell> <TableCell align="center">Firmware</TableCell>
<TableCell align="center">Power</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">SoC</TableCell>
<TableCell align="center">Temperature</TableCell> <TableCell align="center">Temperature</TableCell>
{product === 0 ? ( {product === 0 ? (
@ -293,7 +293,7 @@ function BatteryView(props: BatteryViewProps) {
</TableCell> </TableCell>
<TableCell <TableCell
sx={{ sx={{
width: '10%', width: '14%',
textAlign: 'center', textAlign: 'center',
backgroundColor: backgroundColor:

View File

@ -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;

View File

@ -169,7 +169,7 @@ function DetailedBatteryViewSodistore(
align="left" align="left"
sx={{ fontWeight: 'bold' }} sx={{ fontWeight: 'bold' }}
> >
Total Battery Voltage Bus Voltage
</TableCell> </TableCell>
<TableCell <TableCell
align="right" align="right"
@ -183,6 +183,29 @@ function DetailedBatteryViewSodistore(
' V'} ' V'}
</TableCell> </TableCell>
</TableRow> </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> <TableRow>
<TableCell <TableCell
component="th" component="th"
@ -349,25 +372,6 @@ function DetailedBatteryViewSodistore(
{props.batteryData.BatteryDeligreenDataRecord.Soh + ' %'} {props.batteryData.BatteryDeligreenDataRecord.Soh + ' %'}
</TableCell> </TableCell>
</TableRow> </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> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>

View File

@ -11,10 +11,13 @@ import {
InputLabel, InputLabel,
Modal, Modal,
Select, Select,
Tab,
Tabs,
TextField, TextField,
Typography, Typography,
useTheme useTheme
} from '@mui/material'; } from '@mui/material';
import React, { useContext, useState } from 'react'; import React, { useContext, useState } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
@ -29,6 +32,7 @@ import { UserContext } from '../../../contexts/userContext';
import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
import { TimePicker } from '@mui/lab'; import { TimePicker } from '@mui/lab';
import { ProductIdContext } from '../../../contexts/ProductIdContextProvider';
interface ConfigurationProps { interface ConfigurationProps {
values: JSONRecordData; values: JSONRecordData;
@ -40,6 +44,9 @@ function Configuration(props: ConfigurationProps) {
return null; return null;
} }
//console.log(props.values.Config);
const [activeTab, setActiveTab] = useState<'charge' | 'discharge'>('charge');
const CalibrationChargeOptions = [ const CalibrationChargeOptions = [
'Repetitive Calibration', 'Repetitive Calibration',
'Additional Calibration', 'Additional Calibration',
@ -84,11 +91,12 @@ function Configuration(props: ConfigurationProps) {
const [isErrorDateModalOpen, setErrorDateModalOpen] = useState(false); const [isErrorDateModalOpen, setErrorDateModalOpen] = useState(false);
const context = useContext(UserContext); const context = useContext(UserContext);
const { currentUser, setUser } = context; const { currentUser, setUser } = context;
const { product, setProduct } = useContext(ProductIdContext);
const [formValues, setFormValues] = useState<ConfigurationValues>({ const [formValues, setFormValues] = useState<ConfigurationValues>({
minimumSoC: props.values.Config.MinSoc, minimumSoC: props.values.Config.MinSoc,
gridSetPoint: (props.values.Config.GridSetPoint as number) / 1000, gridSetPoint: (props.values.Config.GridSetPoint as number) / 1000,
CalibrationChargeState: CalibrationChargeOptionsController.indexOf( calibrationChargeState: CalibrationChargeOptionsController.indexOf(
props.values.Config.ForceCalibrationChargeState.toString() props.values.Config.ForceCalibrationChargeState.toString()
), ),
calibrationChargeDate: calibrationChargeDate:
@ -100,9 +108,27 @@ function Configuration(props: ConfigurationProps) {
.toDate() .toDate()
: dayjs(props.values.Config.DayAndTimeForAdditionalCalibration) : dayjs(props.values.Config.DayAndTimeForAdditionalCalibration)
// .add(localOffset, 'minute') // .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) => { const handleSubmit = async (e) => {
if ( if (
CalibrationChargeOptionsController.indexOf( CalibrationChargeOptionsController.indexOf(
@ -116,7 +142,7 @@ function Configuration(props: ConfigurationProps) {
setErrorDateModalOpen(true); setErrorDateModalOpen(true);
return; return;
} else if ( } else if (
formValues.CalibrationChargeState === 1 && formValues.calibrationChargeState === 1 &&
dayjs(formValues.calibrationChargeDate).isBefore(dayjs()) dayjs(formValues.calibrationChargeDate).isBefore(dayjs())
) { ) {
//console.log('asked for', dayjs(formValues.calibrationChargeDate)); //console.log('asked for', dayjs(formValues.calibrationChargeDate));
@ -128,13 +154,17 @@ function Configuration(props: ConfigurationProps) {
const configurationToSend: ConfigurationValues = { const configurationToSend: ConfigurationValues = {
minimumSoC: formValues.minimumSoC, minimumSoC: formValues.minimumSoC,
gridSetPoint: formValues.gridSetPoint, gridSetPoint: formValues.gridSetPoint,
CalibrationChargeState: formValues.CalibrationChargeState, calibrationChargeState: formValues.calibrationChargeState,
calibrationChargeDate: dayjs calibrationChargeDate: dayjs
.utc(formValues.calibrationChargeDate) .utc(formValues.calibrationChargeDate)
.add(localOffset, 'minute') .add(localOffset, 'minute')
.toDate(),
calibrationDischargeState: formValues.calibrationDischargeState,
calibrationDischargeDate: dayjs
.utc(formValues.calibrationDischargeDate)
.add(localOffset, 'minute')
.toDate() .toDate()
}; };
// console.log('will send ', dayjs(formValues.calibrationChargeDate));
setLoading(true); setLoading(true);
const res = await axiosConfig 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 handleSelectedCalibrationChargeDay = (event) => {
const selectedDay = daysInWeek.indexOf(event.target.value); const selectedDay = daysInWeek.indexOf(event.target.value);
const currentDate = dayjs(); 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) => { const handleSelectedCalibrationChargeChange = (event) => {
setFormValues({ setFormValues({
...formValues, ...formValues,
['CalibrationChargeState']: CalibrationChargeOptions.indexOf( ['calibrationChargeState']: CalibrationChargeOptions.indexOf(
event.target.value event.target.value
), ),
['calibrationChargeDate']: ['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 handleChange = (e) => {
const { name, value } = e.target; const { name, value } = e.target;
@ -284,7 +355,84 @@ function Configuration(props: ConfigurationProps) {
</Box> </Box>
</Modal> </Modal>
)} )}
<Grid item xs={12} md={12}> <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> <CardContent>
<Box <Box
component="form" component="form"
@ -294,6 +442,8 @@ function Configuration(props: ConfigurationProps) {
noValidate noValidate
autoComplete="off" autoComplete="off"
> >
{activeTab === 'charge' && (
<>
<div style={{ marginBottom: '5px' }}> <div style={{ marginBottom: '5px' }}>
<TextField <TextField
label={ label={
@ -334,7 +484,7 @@ function Configuration(props: ConfigurationProps) {
<Select <Select
value={ value={
CalibrationChargeOptions[ CalibrationChargeOptions[
formValues.CalibrationChargeState formValues.calibrationChargeState
] ]
} }
onChange={handleSelectedCalibrationChargeChange} onChange={handleSelectedCalibrationChargeChange}
@ -347,7 +497,8 @@ function Configuration(props: ConfigurationProps) {
</Select> </Select>
</FormControl> </FormControl>
</div> </div>
{formValues.CalibrationChargeState == 1 && (
{formValues.calibrationChargeState == 1 && (
<div> <div>
<LocalizationProvider dateAdapter={AdapterDayjs}> <LocalizationProvider dateAdapter={AdapterDayjs}>
<DateTimePicker <DateTimePicker
@ -364,22 +515,11 @@ function Configuration(props: ConfigurationProps) {
/> />
)} )}
/> />
{/*<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> </LocalizationProvider>
</div> </div>
)} )}
{formValues.CalibrationChargeState == 0 && ( {formValues.calibrationChargeState == 0 && (
<> <>
<div style={{ marginBottom: '5px' }}> <div style={{ marginBottom: '5px' }}>
<FormControl <FormControl
@ -399,7 +539,9 @@ function Configuration(props: ConfigurationProps) {
</InputLabel> </InputLabel>
<Select <Select
value={ value={
daysInWeek[formValues.calibrationChargeDate.getDay()] daysInWeek[
formValues.calibrationChargeDate.getDay()
]
} }
onChange={handleSelectedCalibrationChargeDay} onChange={handleSelectedCalibrationChargeDay}
> >
@ -418,7 +560,9 @@ function Configuration(props: ConfigurationProps) {
ampm={false} ampm={false}
label="Calibration Charge Hour" label="Calibration Charge Hour"
value={dayjs(formValues.calibrationChargeDate)} value={dayjs(formValues.calibrationChargeDate)}
onChange={(newTime) => handleConfirm(dayjs(newTime))} onChange={(newTime) =>
handleConfirm(dayjs(newTime))
}
renderInput={(params) => ( renderInput={(params) => (
<TextField <TextField
{...params} {...params}
@ -493,6 +637,123 @@ function Configuration(props: ConfigurationProps) {
/> />
</div> </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>
{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>*/} {/*<div>*/}
{/* <TextField*/} {/* <TextField*/}
{/* label={*/} {/* label={*/}

View File

@ -20,6 +20,7 @@ import { I_Installation } from '../../../interfaces/InstallationTypes';
import { InstallationsContext } from '../../../contexts/InstallationsContextProvider'; import { InstallationsContext } from '../../../contexts/InstallationsContextProvider';
import { UserContext } from '../../../contexts/userContext'; import { UserContext } from '../../../contexts/userContext';
import { UserType } from '../../../interfaces/UserTypes'; import { UserType } from '../../../interfaces/UserTypes';
import { ProductIdContext } from '../../../contexts/ProductIdContextProvider';
interface InformationProps { interface InformationProps {
values: I_Installation; values: I_Installation;
@ -38,6 +39,7 @@ function Information(props: InformationProps) {
const [formValues, setFormValues] = useState(props.values); const [formValues, setFormValues] = useState(props.values);
const requiredFields = ['name', 'region', 'location', 'country']; const requiredFields = ['name', 'region', 'location', 'country'];
const installationContext = useContext(InstallationsContext); const installationContext = useContext(InstallationsContext);
const { product, setProduct } = useContext(ProductIdContext);
const { const {
updateInstallation, updateInstallation,
loading, loading,
@ -265,6 +267,30 @@ function Information(props: InformationProps) {
/> />
</div> </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 && ( {currentUser.userType == UserType.admin && (
<> <>
<div> <div>
@ -282,8 +308,11 @@ function Information(props: InformationProps) {
label="S3 Bucket Name" label="S3 Bucket Name"
name="s3bucketname" name="s3bucketname"
value={ value={
formValues.s3BucketId + product === 0 || product == 3
? formValues.s3BucketId +
'-3e5b3069-214a-43ee-8d85-57d72000c19d' '-3e5b3069-214a-43ee-8d85-57d72000c19d'
: formValues.s3BucketId +
'-e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa'
} }
variant="outlined" variant="outlined"
fullWidth fullWidth

View File

@ -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;

View File

@ -135,7 +135,6 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
}, []); }, []);
const handleSelectOneInstallation = (installationID: number): void => { const handleSelectOneInstallation = (installationID: number): void => {
console.log('when selecting installation', product);
if (selectedInstallation != installationID) { if (selectedInstallation != installationID) {
setSelectedInstallation(installationID); setSelectedInstallation(installationID);
setSelectedInstallation(-1); setSelectedInstallation(-1);

View File

@ -19,11 +19,14 @@ interface installationFormProps {
cancel: () => void; cancel: () => void;
submit: () => void; submit: () => void;
parentid: number; parentid: number;
productToInsert: number;
} }
function installationForm(props: installationFormProps) { function installationForm(props: installationFormProps) {
const theme = useTheme(); const theme = useTheme();
const [open, setOpen] = useState(true); const [open, setOpen] = useState(true);
console.log('productToInsert IS ', props.productToInsert);
const [formValues, setFormValues] = useState<Partial<I_Installation>>({ const [formValues, setFormValues] = useState<Partial<I_Installation>>({
installationName: '', installationName: '',
name: '', name: '',
@ -59,7 +62,7 @@ function installationForm(props: installationFormProps) {
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
setLoading(true); setLoading(true);
formValues.parentId = props.parentid; formValues.parentId = props.parentid;
formValues.product = 0; formValues.product = props.productToInsert;
const responseData = await createInstallation(formValues); const responseData = await createInstallation(formValues);
props.submit(); props.submit();
}; };

View File

@ -216,6 +216,20 @@ export interface Line {
Power: Power; 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) // The interface for the Battery structure, with dynamic keys (Device IDs)
export interface JSONRecordData { export interface JSONRecordData {
Battery: { Battery: {
@ -282,9 +296,12 @@ export interface JSONRecordData {
CurtailP: number; CurtailP: number;
DayAndTimeForAdditionalCalibration: string; DayAndTimeForAdditionalCalibration: string;
DayAndTimeForRepetitiveCalibration: string; DayAndTimeForRepetitiveCalibration: string;
DownDayAndTimeForAdditionalCalibration: string;
DownDayAndTimeForRepetitiveCalibration: string;
DisplayIndividualBatteries: string; DisplayIndividualBatteries: string;
MaxBatteryDischargingCurrent: number; MaxBatteryDischargingCurrent: number;
ForceCalibrationChargeState: string; ForceCalibrationChargeState: string;
ForceCalibrationDischargeState: string;
GridSetPoint: number; GridSetPoint: number;
HoldSocZone: number; HoldSocZone: number;
MinSoc: number; MinSoc: number;
@ -392,17 +409,101 @@ export interface JSONRecordData {
LoadOnDc: { Power: number }; LoadOnDc: { Power: number };
PvOnDc: { PvOnDc: {
[deviceId: string]: {
DcWh: number; DcWh: number;
NbrOfStrings: number;
Dc: { Dc: {
Voltage: number; Voltage: number;
Current: number; Current: number;
Power: number; Power: number;
NbrOfStrings: number;
}; };
Strings: { Strings: {
[PvId: string]: PvString; [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 = ( export const parseChunkJson = (
@ -497,8 +598,10 @@ export interface I_BoxDataValue {
export type ConfigurationValues = { export type ConfigurationValues = {
minimumSoC: string | number; minimumSoC: string | number;
gridSetPoint: number; gridSetPoint: number;
CalibrationChargeState: number; calibrationChargeState: number;
calibrationChargeDate: Date | null; calibrationChargeDate: Date | null;
calibrationDischargeState: number;
calibrationDischargeDate: Date | null;
}; };
// //
// export interface Pv { // export interface Pv {

View File

@ -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;

View File

@ -24,34 +24,37 @@ function PvView(props: PvViewProps) {
if (props.values === null && props.connected == true) { if (props.values === null && props.connected == true) {
return null; return null;
} }
const currentLocation = useLocation(); const currentLocation = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const sortedPvView = // ✅ Flatten, sort, and assign unique displayId from 1-N
props.values != null && props.values.PvOnDc const sortedPvView = props.values?.PvOnDc
? Object.entries(props.values.PvOnDc.Strings) ? Object.entries(props.values.PvOnDc)
.map(([pvId, pv]) => { .flatMap(([deviceId, device]) =>
return { pvId, pv }; // Here we return an object with the id and 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);
}) })
.sort((a, b) => parseInt(b.pvId) - parseInt(a.pvId))
: []; : [];
const [loading, setLoading] = useState(sortedPvView.length == 0); const [loading, setLoading] = useState(sortedPvView.length === 0);
const handleMainStatsButton = () => { const handleMainStatsButton = () => {
navigate(routes.mainstats); 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(() => { useEffect(() => {
if (sortedPvView.length == 0) { if (sortedPvView.length === 0) {
setLoading(true); setLoading(true);
} else { } else {
setLoading(false); setLoading(false);
@ -84,6 +87,7 @@ function PvView(props: PvViewProps) {
</Typography> </Typography>
</Container> </Container>
)} )}
{loading && props.connected && ( {loading && props.connected && (
<Container <Container
maxWidth="xl" maxWidth="xl"
@ -111,14 +115,19 @@ function PvView(props: PvViewProps) {
{!loading && props.connected && ( {!loading && props.connected && (
<Container maxWidth="xl"> <Container maxWidth="xl">
{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 <TableContainer
key={deviceId}
component={Paper} component={Paper}
sx={{ sx={{ marginTop: '30px', marginBottom: '40px', boxShadow: 3 }}
marginTop: '20px',
marginBottom: '20px'
}}
> >
<Table sx={{ minWidth: 250 }} aria-label="simple table"> <Table sx={{ minWidth: 250 }} aria-label={`CU${deviceId} table`}>
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell align="center">Pv</TableCell> <TableCell align="center">Pv</TableCell>
@ -128,63 +137,50 @@ function PvView(props: PvViewProps) {
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{sortedPvView.map(({ pvId, pv }) => ( {entries.map(({ displayId, pv }, index) => (
<TableRow <TableRow key={index}>
key={pvId}
style={{
height: '10px'
}}
>
<TableCell <TableCell
component="th"
scope="row"
align="center" align="center"
sx={{ width: '10%', fontWeight: 'bold', color: 'black' }} sx={{ fontWeight: 'bold', color: 'black' }}
> >
{'AMPT ' + pvId} {displayId}
</TableCell>
<TableCell
sx={{
width: '10%',
textAlign: 'center',
fontWeight: 'bold',
backgroundColor:
pv.Current == 0 ? '#FF033E' : '#32CD32',
color: 'inherit'
}}
>
{pv.Power + ' W'}
</TableCell> </TableCell>
<TableCell <TableCell
align="center"
sx={{ sx={{
width: '10%',
textAlign: 'center',
fontWeight: 'bold', fontWeight: 'bold',
backgroundColor: backgroundColor:
pv.Current == 0 ? '#FF033E' : '#32CD32', pv.Current === 0 ? '#FF033E' : '#32CD32'
color: 'inherit'
}} }}
> >
{pv.Voltage + ' V'} {pv.Power} W
</TableCell> </TableCell>
<TableCell <TableCell
align="center"
sx={{ sx={{
width: '10%',
textAlign: 'center',
fontWeight: 'bold', fontWeight: 'bold',
backgroundColor: backgroundColor:
pv.Current == 0 ? '#FF033E' : '#32CD32', pv.Current === 0 ? '#FF033E' : '#32CD32'
color: 'inherit'
}} }}
> >
{pv.Current + ' A'} {pv.Voltage} V
</TableCell>
<TableCell
align="center"
sx={{
fontWeight: 'bold',
backgroundColor:
pv.Current === 0 ? '#FF033E' : '#32CD32'
}}
>
{pv.Current} A
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>
))}
</Container> </Container>
)} )}
</> </>

View File

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

View File

@ -19,8 +19,11 @@ import HistoryOfActions from '../History/History';
import BuildIcon from '@mui/icons-material/Build'; import BuildIcon from '@mui/icons-material/Build';
import AccessContextProvider from '../../../contexts/AccessContextProvider'; import AccessContextProvider from '../../../contexts/AccessContextProvider';
import Access from '../ManageAccess/Access'; import Access from '../ManageAccess/Access';
import InformationSodioHome from '../Information/InformationSodioHome'; import Information from '../Information/Information';
import CryptoJS from 'crypto-js'; import { TimeSpan, UnixTime } from '../../../dataCache/time';
import { fetchDataJson } from '../Installations/fetchData';
import { FetchResult } from '../../../dataCache/dataCache';
import BatteryViewSodioHome from '../BatteryView/BatteryViewSodioHome';
interface singleInstallationProps { interface singleInstallationProps {
current_installation?: I_Installation; current_installation?: I_Installation;
@ -31,6 +34,19 @@ function SodioHomeInstallation(props: singleInstallationProps) {
if (props.current_installation == undefined) { if (props.current_installation == undefined) {
return null; 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 context = useContext(UserContext);
const { currentUser } = context; const { currentUser } = context;
const location = useLocation().pathname; const location = useLocation().pathname;
@ -45,6 +61,7 @@ function SodioHomeInstallation(props: singleInstallationProps) {
const [connected, setConnected] = useState(true); const [connected, setConnected] = useState(true);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [fetchFunctionCalled, setFetchFunctionCalled] = useState(false); 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. //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) //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 () => { const fetchDataPeriodically = async () => {
while (continueFetching.current) { var timeperiodToSearch = 200;
//Fetch data from Bitwatt cloud let res;
console.log('Fetching from Bitwatt cloud'); let timestampToFetch;
console.log(props.current_installation.serialNumber); for (var i = 0; i < timeperiodToSearch; i += 2) {
console.log(props.current_installation.s3WriteKey); if (!continueFetching.current) {
console.log(props.current_installation.s3WriteSecret); return false;
}
const timeStamp = Date.now().toString(); timestampToFetch = UnixTime.now().earlier(TimeSpan.fromSeconds(i));
// 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}`;
try { try {
const response = await fetch(url, { method: 'GET', headers }); res = await fetchDataJson(timestampToFetch, s3Credentials, false);
const result = await response.json(); if (res !== FetchResult.notAvailable && res !== FetchResult.tryLater) {
console.log('API Response:', result); break;
} catch (error) { }
console.error('Request failed:', error); } catch (err) {
console.error('Error fetching data:', err);
return false;
}
} }
// Wait for 2 seconds before fetching again if (i >= timeperiodToSearch) {
await timeout(200000); setConnected(false);
console.log('ssssssssssssssssssssssssssssssssssssss'); setLoading(false);
return false;
} }
setConnected(true);
setLoading(false);
while (continueFetching.current) {
for (const timestamp of Object.keys(res)) {
if (!continueFetching.current) {
setFetchFunctionCalled(false); setFetchFunctionCalled(false);
return false;
}
console.log(`Timestamp: ${timestamp}`);
console.log(res[timestamp]);
setValues(res[timestamp]);
await timeout(2000);
}
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;
}
}
}; };
useEffect(() => { useEffect(() => {
@ -147,11 +191,16 @@ function SodioHomeInstallation(props: singleInstallationProps) {
}, [status]); }, [status]);
useEffect(() => { useEffect(() => {
console.log(currentTab);
if (currentTab == 'live' || location.includes('batteryview')) {
//Fetch periodically if the tab is live or batteryview
if ( if (
currentTab == 'live' || 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')) (location.includes('batteryview') && !location.includes('mainstats'))
) { ) {
if (!continueFetching.current) { if (!continueFetching.current) {
@ -163,6 +212,10 @@ function SodioHomeInstallation(props: singleInstallationProps) {
} }
} }
} }
//Fetch only one time in configuration tab
// if (currentTab == 'configuration') {
// fetchDataForOneTime();
// }
return () => { return () => {
continueFetching.current = false; continueFetching.current = false;
@ -322,10 +375,11 @@ function SodioHomeInstallation(props: singleInstallationProps) {
<Route <Route
path={routes.information} path={routes.information}
element={ element={
<InformationSodioHome <Information
values={props.current_installation} values={props.current_installation}
s3Credentials={s3Credentials}
type={props.type} type={props.type}
></InformationSodioHome> ></Information>
} }
/> />
@ -351,28 +405,17 @@ function SodioHomeInstallation(props: singleInstallationProps) {
} }
/> />
{/*<Route*/} <Route
{/* path={routes.overview}*/} path={routes.batteryview + '*'}
{/* element={*/} element={
{/* <SalidomoOverview*/} <BatteryViewSodioHome
{/* s3Credentials={s3Credentials}*/} values={values}
{/* id={props.current_installation.id}*/} s3Credentials={s3Credentials}
{/* ></SalidomoOverview>*/} installationId={props.current_installation.id}
{/* }*/} connected={connected}
{/*/>*/} ></BatteryViewSodioHome>
}
{/*<Route*/} ></Route>
{/* path={routes.batteryview + '*'}*/}
{/* element={*/}
{/* <BatteryViewSalidomo*/}
{/* values={values}*/}
{/* s3Credentials={s3Credentials}*/}
{/* installationId={props.current_installation.id}*/}
{/* productNum={props.current_installation.product}*/}
{/* connected={connected}*/}
{/* ></BatteryViewSalidomo>*/}
{/* }*/}
{/*></Route>*/}
{currentUser.userType == UserType.admin && ( {currentUser.userType == UserType.admin && (
<Route <Route

View File

@ -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;

View File

@ -14,12 +14,17 @@ import { ProductIdContext } from '../../../contexts/ProductIdContextProvider';
import { UserType } from '../../../interfaces/UserTypes'; import { UserType } from '../../../interfaces/UserTypes';
import SodioHomeInstallation from './Installation'; import SodioHomeInstallation from './Installation';
function SodioHomeInstallationTabs() { interface SodioHomeInstallationTabsProps {
product: number;
}
function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
const location = useLocation(); const location = useLocation();
const context = useContext(UserContext); const context = useContext(UserContext);
const { currentUser } = context; const { currentUser } = context;
const tabList = [ const tabList = [
'live', 'live',
'batteryview',
'information', 'information',
'manage', 'manage',
'overview', 'overview',
@ -53,26 +58,14 @@ function SodioHomeInstallationTabs() {
}, [location]); }, [location]);
useEffect(() => { useEffect(() => {
if (sodiohomeInstallations.length === 0 && fetchedInstallations === false) { setProduct(props.product);
}, [props.product]);
useEffect(() => {
if (product == props.product) {
fetchAllSodiohomeInstallations(); fetchAllSodiohomeInstallations();
setFetchedInstallations(true);
} }
}, [sodiohomeInstallations]); }, [product]);
useEffect(() => {
if (sodiohomeInstallations && sodiohomeInstallations.length > 0) {
if (!socket) {
openSocket(2);
} else if (product != 2) {
closeSocket();
openSocket(2);
}
}
}, [sodiohomeInstallations]);
useEffect(() => {
setProduct(2);
}, []);
const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => { const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => {
setCurrentTab(value); setCurrentTab(value);
@ -101,14 +94,23 @@ function SodioHomeInstallationTabs() {
const singleInstallationTabs = const singleInstallationTabs =
currentUser.userType == UserType.admin currentUser.userType == UserType.admin
? [ ? [
// {
// value: 'live',
// label: <FormattedMessage id="live" defaultMessage="Live" />
// },
{ {
value: 'live', value: 'batteryview',
label: <FormattedMessage id="live" defaultMessage="Live" /> label: (
}, <FormattedMessage
{ id="batteryview"
value: 'overview', defaultMessage="Battery View"
label: <FormattedMessage id="overview" defaultMessage="Overview" /> />
)
}, },
// {
// value: 'overview',
// label: <FormattedMessage id="overview" defaultMessage="Overview" />
// },
{ {
value: 'log', value: 'log',
label: <FormattedMessage id="log" defaultMessage="Log" /> label: <FormattedMessage id="log" defaultMessage="Log" />
@ -141,14 +143,14 @@ function SodioHomeInstallationTabs() {
} }
] ]
: [ : [
{ // {
value: 'live', // value: 'live',
label: <FormattedMessage id="live" defaultMessage="Live" /> // label: <FormattedMessage id="live" defaultMessage="Live" />
}, // },
{ // {
value: 'overview', // value: 'overview',
label: <FormattedMessage id="overview" defaultMessage="Overview" /> // label: <FormattedMessage id="overview" defaultMessage="Overview" />
}, // },
{ {
value: 'information', value: 'information',
@ -172,14 +174,23 @@ function SodioHomeInstallationTabs() {
value: 'tree', value: 'tree',
icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" /> icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" />
}, },
// {
// value: 'live',
// label: <FormattedMessage id="live" defaultMessage="Live" />
// },
{ {
value: 'live', value: 'batteryview',
label: <FormattedMessage id="live" defaultMessage="Live" /> label: (
}, <FormattedMessage
{ id="batteryview"
value: 'overview', defaultMessage="Battery View"
label: <FormattedMessage id="overview" defaultMessage="Overview" /> />
)
}, },
// {
// value: 'overview',
// label: <FormattedMessage id="overview" defaultMessage="Overview" />
// },
{ {
value: 'log', value: 'log',
label: <FormattedMessage id="log" defaultMessage="Log" /> label: <FormattedMessage id="log" defaultMessage="Log" />
@ -224,10 +235,10 @@ function SodioHomeInstallationTabs() {
value: 'tree', value: 'tree',
icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" /> icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" />
}, },
{ // {
value: 'live', // value: 'live',
label: <FormattedMessage id="live" defaultMessage="Live" /> // label: <FormattedMessage id="live" defaultMessage="Live" />
}, // },
{ {
value: 'overview', value: 'overview',
label: <FormattedMessage id="overview" defaultMessage="Overview" /> label: <FormattedMessage id="overview" defaultMessage="Overview" />

View File

@ -29,7 +29,6 @@ function Topology(props: TopologyProps) {
const { product, setProduct } = useContext(ProductIdContext); const { product, setProduct } = useContext(ProductIdContext);
//console.log('product VALUE IS ', product);
const [showValues, setShowValues] = useState(false); const [showValues, setShowValues] = useState(false);
const handleSwitch = () => () => { const handleSwitch = () => () => {
@ -38,6 +37,13 @@ function Topology(props: TopologyProps) {
const isMobile = window.innerWidth <= 1490; 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 ( return (
<Container maxWidth="xl" style={{ backgroundColor: 'white' }}> <Container maxWidth="xl" style={{ backgroundColor: 'white' }}>
<Grid container> <Grid container>
@ -408,7 +414,7 @@ function Topology(props: TopologyProps) {
data: props.values?.PvOnDc data: props.values?.PvOnDc
? [ ? [
{ {
value: props.values.PvOnDc.Dc.Power, value: totalPvPower,
unit: 'W' unit: 'W'
} }
] ]
@ -421,15 +427,12 @@ function Topology(props: TopologyProps) {
position: 'top', position: 'top',
data: props.values?.PvOnDc data: props.values?.PvOnDc
? { ? {
value: props.values.PvOnDc.Dc.Power, value: totalPvPower,
unit: 'W' unit: 'W'
} }
: undefined, : undefined,
amount: props.values?.PvOnDc amount: props.values?.PvOnDc
? getAmount( ? getAmount(highestConnectionValue, totalPvPower)
highestConnectionValue,
props.values.PvOnDc.Dc.Power
)
: 0, : 0,
showValues: showValues showValues: showValues
}} }}

View File

@ -26,7 +26,6 @@ import { InstallationsContext } from '../../../contexts/InstallationsContextProv
import { UserType } from '../../../interfaces/UserTypes'; import { UserType } from '../../../interfaces/UserTypes';
import InstallationForm from '../Installations/installationForm'; import InstallationForm from '../Installations/installationForm';
import SalidomoInstallationForm from '../SalidomoInstallations/SalidomoInstallationForm'; import SalidomoInstallationForm from '../SalidomoInstallations/SalidomoInstallationForm';
import SodiohomeInstallationForm from '../SodiohomeInstallations/SodiohomeInstallationForm';
interface TreeInformationProps { interface TreeInformationProps {
folder: I_Folder; folder: I_Folder;
@ -65,7 +64,7 @@ function TreeInformation(props: TreeInformationProps) {
// console.log('Selected Product:', e.target.value); // console.log('Selected Product:', e.target.value);
}; };
const ProductTypes = ['Salimax', 'Salidomo', 'Sodiohome']; const ProductTypes = ['Salimax', 'Salidomo', 'Sodiohome', 'SodistoreMax'];
const isMobile = window.innerWidth <= 1490; const isMobile = window.innerWidth <= 1490;
@ -322,11 +321,15 @@ function TreeInformation(props: TreeInformationProps) {
</Box> </Box>
</Modal> </Modal>
)} )}
{openModalInstallation && product == 'Salimax' && ( {openModalInstallation &&
(product == 'Salimax' ||
product == 'Sodiohome' ||
product == 'SodistoreMax') && (
<InstallationForm <InstallationForm
cancel={handleFormCancel} cancel={handleFormCancel}
submit={handleInstallationFormSubmit} submit={handleInstallationFormSubmit}
parentid={props.folder.id} parentid={props.folder.id}
productToInsert={ProductTypes.indexOf(product)}
/> />
)} )}
{openModalInstallation && product == 'Salidomo' && ( {openModalInstallation && product == 'Salidomo' && (
@ -336,13 +339,13 @@ function TreeInformation(props: TreeInformationProps) {
parentid={props.folder.id} parentid={props.folder.id}
/> />
)} )}
{openModalInstallation && product == 'Sodiohome' && ( {/*{openModalInstallation && product == 'Sodiohome' && (*/}
<SodiohomeInstallationForm {/* <SodiohomeInstallationForm*/}
cancel={handleFormCancel} {/* cancel={handleFormCancel}*/}
submit={handleInstallationFormSubmit} {/* submit={handleInstallationFormSubmit}*/}
parentid={props.folder.id} {/* parentid={props.folder.id}*/}
/> {/* />*/}
)} {/*)}*/}
<Container maxWidth="xl"> <Container maxWidth="xl">
<Grid <Grid

View File

@ -49,7 +49,7 @@ const FlatUsersView = (props: FlatUsersViewProps) => {
return ( return (
<Grid container spacing={1} sx={{ marginTop: '1px' }}> <Grid container spacing={1} sx={{ marginTop: '1px' }}>
<Grid item xs={12} md={isMobile ? 5 : 4}> <Grid item xs={6} md={5}>
<Card> <Card>
<Divider /> <Divider />
<TableContainer sx={{ maxHeight: '520px', overflowY: 'auto' }}> <TableContainer sx={{ maxHeight: '520px', overflowY: 'auto' }}>
@ -113,7 +113,7 @@ const FlatUsersView = (props: FlatUsersViewProps) => {
</TableContainer> </TableContainer>
</Card> </Card>
</Grid> </Grid>
<Grid item xs={6} md={7}>
{selectedUser && ( {selectedUser && (
<User <User
current_user={findUser(selectedUser)} current_user={findUser(selectedUser)}
@ -121,6 +121,7 @@ const FlatUsersView = (props: FlatUsersViewProps) => {
></User> ></User>
)} )}
</Grid> </Grid>
</Grid>
); );
}; };

View File

@ -26,6 +26,7 @@ import { InnovEnergyUser } from 'src/interfaces/UserTypes';
import { TokenContext } from 'src/contexts/tokenContext'; import { TokenContext } from 'src/contexts/tokenContext';
import { TabsContainerWrapper } from 'src/layouts/TabsContainerWrapper'; import { TabsContainerWrapper } from 'src/layouts/TabsContainerWrapper';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import UserAccess from '../ManageAccess/UserAccess';
interface singleUserProps { interface singleUserProps {
current_user: InnovEnergyUser; current_user: InnovEnergyUser;
@ -41,7 +42,10 @@ function User(props: singleUserProps) {
const [formValues, setFormValues] = useState(props.current_user); const [formValues, setFormValues] = useState(props.current_user);
const tokencontext = useContext(TokenContext); const tokencontext = useContext(TokenContext);
const { removeToken } = 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 [openModalDeleteUser, setOpenModalDeleteUser] = useState(false);
const UserTypes = ['Client', 'Partner', 'Admin']; const UserTypes = ['Client', 'Partner', 'Admin'];
@ -226,7 +230,7 @@ function User(props: singleUserProps) {
</Modal> </Modal>
)} )}
<Grid item xs={12} md={isMobile ? 7 : 8}> <Grid item xs={12} md={12}>
<TabsContainerWrapper> <TabsContainerWrapper>
<Tabs <Tabs
onChange={handleTabsChange} onChange={handleTabsChange}
@ -301,7 +305,7 @@ function User(props: singleUserProps) {
<div> <div>
<FormControl <FormControl
fullWidth fullWidth
sx={{ marginLeft: 1, marginTop: 1, width: 390 }} sx={{ marginLeft: 1, marginTop: 1, width: 445 }}
> >
<InputLabel <InputLabel
sx={{ sx={{
@ -413,6 +417,9 @@ function User(props: singleUserProps) {
</Grid> </Grid>
</Container> </Container>
)} )}
{currentTab === 'manage' && (
<UserAccess current_user={props.current_user}></UserAccess>
)}
</Grid> </Grid>
</Card> </Card>
</Grid> </Grid>

View File

@ -69,25 +69,38 @@ function userForm(props: userFormProps) {
const fetchInstallations = useCallback(async () => { const fetchInstallations = useCallback(async () => {
setLoading(true); setLoading(true);
return axiosConfig
.get('/GetAllInstallations') try {
.then((res) => { // fetch product 0
setInstallations(res.data); const res0 = await axiosConfig.get(
setLoading(false); `/GetAllInstallationsFromProduct?product=0`
}) );
.catch((err) => { const installations0 = res0.data;
setLoading(false);
if (err.response && err.response.status == 401) { // 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(); removeToken();
} }
}); } finally {
setLoading(false);
}
}, [setInstallations]); }, [setInstallations]);
useEffect(() => { useEffect(() => {
fetchFolders(); fetchFolders();
fetchInstallations(); fetchInstallations();
}, []); }, []);
const handleChange = (e) => { const handleChange = (e) => {
const { name, value } = e.target; const { name, value } = e.target;
setFormValues({ setFormValues({

View File

@ -12,8 +12,11 @@ import {
InnovEnergyUser InnovEnergyUser
} from '../interfaces/UserTypes'; } from '../interfaces/UserTypes';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { I_Installation } from '../interfaces/InstallationTypes';
interface AccessContextProviderProps { interface AccessContextProviderProps {
fetchInstallationsForUser: (userId: number) => void;
accessibleInstallationsForUser: I_Installation[];
availableUsers: InnovEnergyUser[]; availableUsers: InnovEnergyUser[];
fetchAvailableUsers: () => Promise<void>; fetchAvailableUsers: () => Promise<void>;
usersWithDirectAccess: InnovEnergyUser[]; usersWithDirectAccess: InnovEnergyUser[];
@ -44,6 +47,8 @@ interface AccessContextProviderProps {
} }
export const AccessContext = createContext<AccessContextProviderProps>({ export const AccessContext = createContext<AccessContextProviderProps>({
fetchInstallationsForUser: () => Promise.resolve(),
accessibleInstallationsForUser: [],
availableUsers: [], availableUsers: [],
fetchAvailableUsers: () => { fetchAvailableUsers: () => {
return Promise.resolve(); return Promise.resolve();
@ -74,6 +79,8 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
InnovEnergyUser[] InnovEnergyUser[]
>([]); >([]);
const [availableUsers, setAvailableUsers] = useState<InnovEnergyUser[]>([]); const [availableUsers, setAvailableUsers] = useState<InnovEnergyUser[]>([]);
const [accessibleInstallationsForUser, setAccessibleInstallationsForUser] =
useState<I_Installation[]>([]);
const [usersWithInheritedAccess, setUsersWithInheritedAccess] = useState< const [usersWithInheritedAccess, setUsersWithInheritedAccess] = useState<
I_UserWithInheritedAccess[] 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( const fetchUsersWithInheritedAccessForResource = useCallback(
async (tempresourceType: string, id: number) => { async (tempresourceType: string, id: number) => {
axiosConfig axiosConfig
@ -192,6 +219,8 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
return ( return (
<AccessContext.Provider <AccessContext.Provider
value={{ value={{
fetchInstallationsForUser,
accessibleInstallationsForUser,
availableUsers, availableUsers,
fetchAvailableUsers, fetchAvailableUsers,
usersWithDirectAccess, usersWithDirectAccess,

View File

@ -121,7 +121,7 @@ const InstallationsContextProvider = ({
} }
const tokenString = localStorage.getItem('token'); const tokenString = localStorage.getItem('token');
const token = tokenString !== null ? tokenString : ''; 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); const new_socket = new WebSocket(urlWithToken);
@ -200,9 +200,10 @@ const InstallationsContextProvider = ({
const fetchAllSodiohomeInstallations = useCallback(async () => { const fetchAllSodiohomeInstallations = useCallback(async () => {
axiosConfig axiosConfig
.get('/GetAllSodioHomeInstallations') .get('/GetAllSodioHomeInstallations')
.then((res: AxiosResponse<I_Installation[]>) => .then((res: AxiosResponse<I_Installation[]>) => {
setSodiohomeInstallations(res.data) setSodiohomeInstallations(res.data);
) openSocket(res.data);
})
.catch((err: AxiosError) => { .catch((err: AxiosError) => {
if (err.response?.status === 401) { if (err.response?.status === 401) {
removeToken(); removeToken();

View File

@ -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) => { const openSocket = (installations) => {
// setSortedInstallations(installations.sort((a, b) => b.status - a.status)); // Sort installations by status // setSortedInstallations(installations.sort((a, b) => b.status - a.status)); // Sort installations by status
}; };
@ -169,18 +63,6 @@ const WebSocketContextProvider = ({ children }: { children: ReactNode }) => {
// socket.close(); // 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 ( return (
<WebSocketContext.Provider <WebSocketContext.Provider

View File

@ -320,7 +320,7 @@ export const transformInputToDailyDataJson = async (
//'Battery.Dc.Power' for salimax, //'Battery.Dc.Power' for salimax,
// 'Battery.Power', // 'Battery.Power',
'GridMeter.Ac.Power.Active', 'GridMeter.Ac.Power.Active',
'PvOnDc.Dc.Power', 'PvOnDc',
'DcDc.Dc.Link.Voltage', 'DcDc.Dc.Link.Voltage',
'LoadOnAcGrid.Power.Active', 'LoadOnAcGrid.Power.Active',
'LoadOnDc.Power' 'LoadOnDc.Power'
@ -420,10 +420,22 @@ export const transformInputToDailyDataJson = async (
// eslint-disable-next-line @typescript-eslint/no-loop-func // eslint-disable-next-line @typescript-eslint/no-loop-func
pathsToSearch.forEach((path) => { pathsToSearch.forEach((path) => {
if (get(result, path) !== undefined) { if (get(result, path) !== undefined) {
const value = path let value: number | undefined = undefined;
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('.') .split('.')
.reduce((o, key) => (o ? o[key] : undefined), result); .reduce((o, key) => (o ? o[key] : undefined), result);
}
// Only push value if defined
if (value !== undefined) {
if (value < chartOverview[categories[category_index]].min) { if (value < chartOverview[categories[category_index]].min) {
chartOverview[categories[category_index]].min = value; chartOverview[categories[category_index]].min = value;
} }
@ -431,12 +443,12 @@ export const transformInputToDailyDataJson = async (
if (value > chartOverview[categories[category_index]].max) { if (value > chartOverview[categories[category_index]].max) {
chartOverview[categories[category_index]].max = value; chartOverview[categories[category_index]].max = value;
} }
chartData[categories[category_index]].data.push([ chartData[categories[category_index]].data.push([
adjustedTimestampArray[i], adjustedTimestampArray[i],
value value
]); ]);
} else { }
//data[path].push([adjustedTimestamp, null]);
} }
category_index++; category_index++;
}); });

View File

@ -213,7 +213,7 @@ function SidebarMenu() {
<Box sx={{ marginTop: '3px' }}> <Box sx={{ marginTop: '3px' }}>
<FormattedMessage <FormattedMessage
id="sodistore" id="sodistore"
defaultMessage="Sodistore" defaultMessage="SodistoreMax"
/> />
</Box> </Box>
</Button> </Button>

View File

@ -1,7 +1,8 @@
import { useContext } from 'react'; import { useContext } from 'react';
import Scrollbar from 'src/components/Scrollbar'; import Scrollbar from 'src/components/Scrollbar';
import { SidebarContext } from 'src/contexts/SidebarContext'; 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 { import {
alpha, alpha,
Box, Box,
@ -17,8 +18,8 @@ import SidebarMenu from './SidebarMenu';
const SidebarWrapper = styled(Box)( const SidebarWrapper = styled(Box)(
({ theme }) => ` ({ theme }) => `
width: ${theme.sidebar.width}; width: 280px; /* previously theme.sidebar.width */
min-width: ${theme.sidebar.width}; min-width: 280px;
color: ${theme.colors.alpha.trueWhite[70]}; color: ${theme.colors.alpha.trueWhite[70]};
position: relative; position: relative;
z-index: 7; z-index: 7;
@ -54,16 +55,24 @@ function Sidebar() {
<Scrollbar> <Scrollbar>
<Box mt={3}> <Box mt={3}>
<Box <Box
mx={2}
sx={{ 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 <img
src={innovenergyLogo} src={inescoLogo}
alt="innovenergy logo" alt="inesco logo"
style={{ style={{
width: '150px' // Width of the image width: '160px',
objectFit: 'contain'
}} }}
/> />
</Box> </Box>
@ -105,8 +114,8 @@ function Sidebar() {
}} }}
> >
<img <img
src={innovenergyLogo} src={inescoLogo}
alt="innovenergy logo" alt="inesco logo"
style={{ style={{
width: '150px' // Width of the image width: '150px' // Width of the image
}} }}

View File

@ -58,7 +58,7 @@ const SidebarLayout = (props: SidebarLayoutProps) => {
flex: 1, flex: 1,
pt: `${theme.header.height}`, pt: `${theme.header.height}`,
[theme.breakpoints.up('lg')]: { [theme.breakpoints.up('lg')]: {
ml: `${theme.sidebar.width}` ml: '260px'
} }
}} }}
> >

View File

@ -1,4 +1,4 @@
import { alpha, createTheme, lighten, darken } from '@mui/material'; import { alpha, createTheme, darken, lighten } from '@mui/material';
import '@mui/lab/themeAugmentation'; import '@mui/lab/themeAugmentation';
const themeColors = { const themeColors = {