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
it easy to identify and analyze the extracted data.
4)Advanced Data Processing Capabilities:
Booleans as Numbers: The --booleans_as_numbers flag allows users to convert boolean values (True/False) into numeric representations (1/0). This feature
i) Booleans as Numbers: The --booleans_as_numbers flag allows users to convert boolean values (True/False) into numeric representations (1/0). This feature
is particularly useful for analytical tasks that require numerical data processing.
ii) Sampling Stepsize: The --sampling_stepsize parameter enables users to define the granularity of the time range for data extraction. By specifying the number
of 1 minute intervals, users can adjust the sampling interval, allowing for flexible data retrieval based on time.
Example Command:
python3 extractS3data.py 1749062721 1749106001 --keys AcDc/SystemControl/ResetAlarmsAndWarnings,AcDc/Devices/1/Status/Ac/L1/Voltage --bucket-number 12 --product_name=SodistoreMax
python3 extractS3data.py 1749062721 1749106001 --keys AcDc/SystemControl/ResetAlarmsAndWarnings,AcDc/Devices/1/Status/Ac/L1/Voltage --bucket-number 12 --product_name=SodistoreMax --sampling_stepsize 2 --booleans_as_numbers
This command extracts data for AcDc/SystemControl/ResetAlarmsAndWarnings and AcDc/Devices/1/Status/Ac/L1/Voltage keys from bucket number 12, between the specified timestamps, with boolean values converted to numbers.
The script will fetch data in 2 minutes intervals

View File

@ -18,14 +18,15 @@ def extract_timestamp(filename):
except ValueError:
return 0
import subprocess
def list_files_in_range(start_timestamp, end_timestamp, sampling_stepsize, product_type, bucket_number):
if product_type == "Salimax" or product_type=="SodistoreMax":
if product_type in ["Salimax", "SodistoreMax"]:
hash = "3e5b3069-214a-43ee-8d85-57d72000c19d"
elif product_type == "Salidomo":
hash = "c0436b6a-d276-4cd8-9c44-1eae86cf5d0e"
else:
raise ValueError("Invalid product type option. Use Salimax or Salidomo or SodistoreMax")
raise ValueError("Invalid product type option.")
# Find common prefix
common_prefix = ""
@ -43,20 +44,31 @@ def list_files_in_range(start_timestamp, end_timestamp, sampling_stepsize,produc
output = subprocess.check_output(s3cmd_command, shell=True, text=True)
files = [line.split()[-1] for line in output.strip().split("\n") if line.strip()]
filenames = []
count=0
for f in files:
name = f.split("/")[-1] # e.g., 1748802020.json
timestamp_str = name.split(".")[0] # extract '1748802020'
if timestamp_str.isdigit() and int(timestamp_str) <= int(end_timestamp):
name = f.split("/")[-1]
timestamp_str = name.split(".")[0]
if timestamp_str.isdigit():
timestamp = int(timestamp_str)
if start_timestamp <= timestamp <= end_timestamp :
if count % sampling_stepsize == 0:
filenames.append(name)
else:
break
count += 1
print(filenames)
return filenames
except subprocess.CalledProcessError:
print(f"No files found for prefix {common_prefix}")
return []
def get_nested_value(data, key_path):
try:
for key in key_path:
@ -151,7 +163,7 @@ def download_files(bucket_number, filenames_to_download, product_type):
print(f"Files with prefix '{filename}' downloaded successfully.")
decompress_file(os.path.join(output_directory, filename), output_directory)
except subprocess.CalledProcessError as e:
# print(f"Error downloading files: {e}")
print(f"Error downloading files: {e}")
continue
else:
print(f"File '{filename}.json' already exists locally. Skipping download.")
@ -187,7 +199,7 @@ def get_last_component(path):
path_without_slashes = path.replace('/', '')
return path_without_slashes
def download_and_process_files(bucket_number, start_timestamp, end_timestamp, sampling_stepsize, keys, booleans_as_numbers, exact_match, product_type):
def download_and_process_files(bucket_number, start_timestamp, end_timestamp, sampling_stepsize, keys, booleans_as_numbers, product_type):
output_directory = f"S3cmdData_{bucket_number}"
#if os.path.exists(output_directory):
@ -200,7 +212,7 @@ def download_and_process_files(bucket_number, start_timestamp, end_timestamp, sa
filenames_to_check = list_files_in_range(start_timestamp, end_timestamp, sampling_stepsize,product_type,bucket_number)
existing_files = [filename for filename in filenames_to_check if os.path.exists(os.path.join(output_directory, f"{filename}.json"))]
files_to_download = set(filenames_to_check) - set(existing_files)
print(files_to_download)
#print(files_to_download)
#if os.listdir(output_directory):
# print("Files already exist in the local folder. Skipping download.")
@ -231,9 +243,8 @@ def main():
parser.add_argument('end_timestamp', type=int, help='The end timestamp for the range (even number)')
parser.add_argument('--keys', type=parse_keys, required=True, help='The part to match from each CSV file, can be a single key or a comma-separated list of keys')
parser.add_argument('--bucket-number', type=int, required=True, help='The number of the bucket to download from')
parser.add_argument('--sampling_stepsize', type=int, required=False, default=1, help='The number of 2sec intervals, which define the length of the sampling interval in S3 file retrieval')
parser.add_argument('--sampling_stepsize', type=int, required=False, default=1, help='The number of 1 minute intervals, which define the length of the sampling interval in S3 file retrieval')
parser.add_argument('--booleans_as_numbers', action="store_true", required=False, help='If key used, then booleans are converted to numbers [0/1], if key not used, then booleans maintained as text [False/True]')
parser.add_argument('--exact_match', action="store_true", required=False, help='If key used, then key has to match exactly "=", else it is enough that key is found "in" text')
parser.add_argument('--product_name', required=True, help='Use Salimax, Salidomo or SodistoreMax')
args = parser.parse_args()
@ -243,14 +254,13 @@ def main():
bucket_number = args.bucket_number
sampling_stepsize = args.sampling_stepsize
booleans_as_numbers = args.booleans_as_numbers
exact_match = args.exact_match
# new arg for product type
product_type = args.product_name
if start_timestamp >= end_timestamp:
print("Error: start_timestamp must be smaller than end_timestamp.")
return
download_and_process_files(bucket_number, start_timestamp, end_timestamp, sampling_stepsize, keys, booleans_as_numbers, exact_match, product_type)
download_and_process_files(bucket_number, start_timestamp, end_timestamp, sampling_stepsize, keys, booleans_as_numbers, product_type)
if __name__ == "__main__":
main()

View File

@ -345,6 +345,18 @@ public class Controller : ControllerBase
.ToList();
}
[HttpGet(nameof(GetInstallationsTheUserHasAccess))]
public ActionResult<IEnumerable<Object>> GetInstallationsTheUserHasAccess(Int64 userId, Token authToken)
{
var user = Db.GetUserById(userId);
if (user == null)
return Unauthorized();
return user.AccessibleInstallations().ToList();
}
[HttpGet(nameof(GetUsersWithInheritedAccessToInstallation))]
public ActionResult<IEnumerable<Object>> GetUsersWithInheritedAccessToInstallation(Int64 id, Token authToken)
{
@ -927,6 +939,8 @@ public class Controller : ControllerBase
{
var session = Db.GetSession(authToken);
Console.WriteLine("CONFIG IS " + config.GetConfigurationString());
// Send configuration changes
var success = await session.SendInstallationConfig(installationId, config);
@ -940,6 +954,7 @@ public class Controller : ControllerBase
Timestamp = DateTime.Now,
Description = config.GetConfigurationString()
};
Console.WriteLine(action.Description);
var actionSuccess = await session.InsertUserAction(action);
return actionSuccess?Ok():Unauthorized();
@ -1020,7 +1035,7 @@ public class Controller : ControllerBase
Db.DeleteUserPassword(user);
return Redirect($"https://monitor.innov.energy/?username={user.Email}&reset=true"); // TODO: move to settings file
return Redirect($"https://monitor.inesco.energy/?username={user.Email}&reset=true"); // TODO: move to settings file
}
}

View File

@ -413,6 +413,8 @@ public static class ExoCmd
byte[] data = Encoding.UTF8.GetBytes(JsonSerializer.Serialize<Configuration>(config));
udpClient.Send(data, data.Length, installation.VpnIp, port);
Console.WriteLine(config.GetConfigurationString());
//Console.WriteLine($"Sent UDP message to {installation.VpnIp}:{port}: {config}");
Console.WriteLine($"Sent UDP message to {installation.VpnIp}:{port}"+" GridSetPoint is "+config.GridSetPoint +" and MinimumSoC is "+config.MinimumSoC);
@ -435,15 +437,6 @@ public static class ExoCmd
}
}
}
return false;
//var s3Region = new S3Region($"https://{installation.S3Region}.{installation.S3Provider}", S3Credentials!);
//var url = s3Region.Bucket(installation.BucketName()).Path("config.json");
//return await url.PutObject(config);
}
}

View File

@ -7,12 +7,9 @@ namespace InnovEnergy.App.Backend.DataTypes.Methods;
public static class InstallationMethods
{
private static readonly String BucketNameSalt =
// Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == ""
// ? "stage" :"3e5b3069-214a-43ee-8d85-57d72000c19d";
"3e5b3069-214a-43ee-8d85-57d72000c19d";
private static readonly String BucketNameSalt = "3e5b3069-214a-43ee-8d85-57d72000c19d";
private static readonly String SalidomoBucketNameSalt = "c0436b6a-d276-4cd8-9c44-1eae86cf5d0e";
private static readonly String SodioHomeBucketNameSalt = "e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa";
public static String BucketName(this Installation installation)
{
@ -21,6 +18,11 @@ public static class InstallationMethods
return $"{installation.S3BucketId}-{BucketNameSalt}";
}
if (installation.Product == (int)ProductType.SodioHome)
{
return $"{installation.S3BucketId}-{SodioHomeBucketNameSalt}";
}
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
&& user.UserType != 0
@ -244,15 +244,15 @@ public static class SessionMethods
&& await installation.CreateBucket()
&& await installation.RenewS3Credentials();
}
if (installation.Product == (int)ProductType.SodioHome)
{
return user is not null
&& user.UserType != 0
&& user.HasAccessToParentOf(installation)
&& Db.Create(installation);
}
//
// if (installation.Product == (int)ProductType.SodioHome)
// {
// return user is not null
// && user.UserType != 0
// && user.HasAccessToParentOf(installation)
// && Db.Create(installation);
// }
//
return false;

View File

@ -229,6 +229,9 @@ public static class UserMethods
public static Task SendEmail(this User user, String subject, String body)
{
Console.WriteLine(user.Name);
Console.WriteLine(subject);
return Mailer.Send(user.Name, user.Email, subject, body);
}

View File

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

View File

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

View File

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

View File

@ -49,6 +49,10 @@ public class Session : Relation<String, Int64>
AccessToSalidomo = user.AccessibleInstallations(product: (int)ProductType.Salidomo).ToList().Count > 0;
AccessToSodistoreMax = user.AccessibleInstallations(product: (int)ProductType.SodiStoreMax).ToList().Count > 0;
AccessToSodioHome = user.AccessibleInstallations(product: (int)ProductType.SodioHome).ToList().Count > 0;
Console.WriteLine("salimax" +user.AccessibleInstallations(product: (int)ProductType.Salimax).ToList().Count);
Console.WriteLine("AccessToSodistoreMax" +user.AccessibleInstallations(product: (int)ProductType.SodiStoreMax).ToList().Count);
Console.WriteLine("sodio" + user.AccessibleInstallations(product: (int)ProductType.SodioHome).ToList().Count);
}
private static String CreateToken()

View File

@ -13,101 +13,41 @@ public static class WebsocketManager
{
public static Dictionary<Int64, InstallationInfo> InstallationConnections = new Dictionary<Int64, InstallationInfo>();
//Every 1 minute, check the timestamp of the latest received message for every installation.
//If the difference between the two timestamps is more than two minutes, we consider this Salimax installation unavailable.
public static async Task MonitorSalimaxInstallationTable()
public static async Task MonitorInstallationTable()
{
while (true){
lock (InstallationConnections){
// Console.WriteLine("MONITOR SALIMAX INSTALLATIONS\n");
foreach (var installationConnection in InstallationConnections){
while (true)
{
lock (InstallationConnections)
{
Console.WriteLine("Monitoring installation table...");
foreach (var installationConnection in InstallationConnections)
{
Console.WriteLine("installationConnection ID is " + installationConnection.Key + "latest timestamp is" +installationConnection.Value.Timestamp + "product is "+ installationConnection.Value.Product
+ "and time diff is "+ (DateTime.Now - installationConnection.Value.Timestamp));
if (installationConnection.Value.Product==(int)ProductType.Salimax && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2)){
if ((installationConnection.Value.Product == (int)ProductType.Salimax && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2)) ||
(installationConnection.Value.Product == (int)ProductType.Salidomo && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(60)) ||
(installationConnection.Value.Product == (int)ProductType.SodioHome && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2)) ||
(installationConnection.Value.Product == (int)ProductType.SodiStoreMax && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2))
)
{
// Console.WriteLine("Installation ID is "+installationConnection.Key);
// Console.WriteLine("installationConnection.Value.Timestamp is "+installationConnection.Value.Timestamp);
Console.WriteLine("Installation ID is " + installationConnection.Key);
Console.WriteLine("installationConnection.Value.Timestamp is " + installationConnection.Value.Timestamp);
// Console.WriteLine("diff is "+(DateTime.Now-installationConnection.Value.Timestamp));
installationConnection.Value.Status = (int)StatusType.Offline;
Installation installation = Db.Installations.FirstOrDefault(f => f.Product == (int)ProductType.Salimax && f.Id == installationConnection.Key);
Installation installation = Db.Installations.FirstOrDefault(f => f.Product == installationConnection.Value.Product && f.Id == installationConnection.Key);
installation.Status = (int)StatusType.Offline;
installation.Apply(Db.Update);
if (installationConnection.Value.Connections.Count > 0){InformWebsocketsForInstallation(installationConnection.Key);}
}
}
// Console.WriteLine("FINISHED MONITORING SALIMAX INSTALLATIONS\n");
}
await Task.Delay(TimeSpan.FromMinutes(1));
}
}
//Every 1 minute, check the timestamp of the latest received message for every installation.
//If the difference between the two timestamps is more than 1 hour, we consider this Salidomo installation unavailable.
public static async Task MonitorSalidomoInstallationTable()
if (installationConnection.Value.Connections.Count > 0)
{
while (true){
//Console.WriteLine("TRY TO LOCK FOR MONITOR SALIDOMO INSTALLATIONS\n");
lock (InstallationConnections){
//Console.WriteLine("MONITOR SALIDOMO INSTALLATIONS\n");
foreach (var installationConnection in InstallationConnections)
{
//Console.WriteLine("Installation ID is "+installationConnection.Key);
// if (installationConnection.Value.Product == (int)ProductType.Salidomo && (DateTime.Now - installationConnection.Value.Timestamp) < TimeSpan.FromMinutes(60)){
// Console.WriteLine("Installation ID is "+installationConnection.Key + " diff is "+(DateTime.Now-installationConnection.Value.Timestamp));
// }
if (installationConnection.Value.Product==(int)ProductType.Salidomo && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(60))
{
//Console.WriteLine("installationConnection.Value.Timestamp is "+installationConnection.Value.Timestamp);
//Console.WriteLine("timestamp now is is "+(DateTime.Now));
Installation installation = Db.Installations.FirstOrDefault(f => f.Product == (int)ProductType.Salidomo && f.Id == installationConnection.Key);
//Console.WriteLine("Installation ID is "+installation.Name + " diff is "+(DateTime.Now-installationConnection.Value.Timestamp));
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-------------------------------------------------------------");}
InformWebsocketsForInstallation(installationConnection.Key);
}
}
//Console.WriteLine("FINISHED WITH UPDATING\n");
}
await Task.Delay(TimeSpan.FromMinutes(1));
}
}
//Every 1 minute, check the timestamp of the latest received message for every installation.
//If the difference between the two timestamps is more than two minutes, we consider this Sodistore installation unavailable.
public static async Task MonitorSodistoreInstallationTable()
{
while (true){
//Console.WriteLine("TRY TO LOCK FOR MONITOR SODISTORE INSTALLATIONS\n");
lock (InstallationConnections){
//Console.WriteLine("MONITOR SODISTORE INSTALLATIONS\n");
foreach (var installationConnection in InstallationConnections)
{
if (installationConnection.Value.Product==(int)ProductType.SodiStoreMax && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2))
{
//Console.WriteLine("installationConnection.Value.Timestamp is "+installationConnection.Value.Timestamp);
//Console.WriteLine("timestamp now is is "+(DateTime.Now));
Installation installation = Db.Installations.FirstOrDefault(f => f.Product == (int)ProductType.SodiStoreMax && f.Id == installationConnection.Key);
//Console.WriteLine("Installation ID is "+installation.Name + " diff is "+(DateTime.Now-installationConnection.Value.Timestamp));
installation.Status = (int)StatusType.Offline;
installation.Apply(Db.Update);
installationConnection.Value.Status = (int)StatusType.Offline;
if (installationConnection.Value.Connections.Count > 0){InformWebsocketsForInstallation(installationConnection.Key);}
//else{Console.WriteLine("NONE IS CONNECTED TO THAT INSTALLATION-------------------------------------------------------------");}
}
}
//Console.WriteLine("FINISHED WITH UPDATING\n");
}
await Task.Delay(TimeSpan.FromMinutes(1));
}
}
@ -140,6 +80,7 @@ public static class WebsocketManager
}
}
public static async Task HandleWebSocketConnection(WebSocket currentWebSocket)
{
var buffer = new byte[4096];
@ -155,6 +96,7 @@ public static class WebsocketManager
var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
var installationIds = JsonSerializer.Deserialize<int[]>(message);
Console.WriteLine("Received Websocket message: " + message);
//This is a ping message to keep the connection alive, reply with a pong
if (installationIds[0] == -1)

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

View File

@ -5191,7 +5191,7 @@
"order": 5,
"width": 0,
"height": 0,
"format": "<a href=\"https://monitor.innov.energy/salidomo_installations/list/\" target=\"_blank\" class=\"button\"> Battery Monitor </a>\n",
"format": "<a href=\"https://monitor.inesco.energy/salidomo_installations/list/\" target=\"_blank\" class=\"button\"> Battery Monitor </a>\n",
"storeOutMessages": true,
"fwdInMessages": true,
"resendOnRefresh": true,

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

View File

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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -11,10 +11,13 @@ import {
InputLabel,
Modal,
Select,
Tab,
Tabs,
TextField,
Typography,
useTheme
} from '@mui/material';
import React, { useContext, useState } from 'react';
import { FormattedMessage } from 'react-intl';
import Button from '@mui/material/Button';
@ -29,6 +32,7 @@ import { UserContext } from '../../../contexts/userContext';
import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
import { TimePicker } from '@mui/lab';
import { ProductIdContext } from '../../../contexts/ProductIdContextProvider';
interface ConfigurationProps {
values: JSONRecordData;
@ -40,6 +44,9 @@ function Configuration(props: ConfigurationProps) {
return null;
}
//console.log(props.values.Config);
const [activeTab, setActiveTab] = useState<'charge' | 'discharge'>('charge');
const CalibrationChargeOptions = [
'Repetitive Calibration',
'Additional Calibration',
@ -84,11 +91,12 @@ function Configuration(props: ConfigurationProps) {
const [isErrorDateModalOpen, setErrorDateModalOpen] = useState(false);
const context = useContext(UserContext);
const { currentUser, setUser } = context;
const { product, setProduct } = useContext(ProductIdContext);
const [formValues, setFormValues] = useState<ConfigurationValues>({
minimumSoC: props.values.Config.MinSoc,
gridSetPoint: (props.values.Config.GridSetPoint as number) / 1000,
CalibrationChargeState: CalibrationChargeOptionsController.indexOf(
calibrationChargeState: CalibrationChargeOptionsController.indexOf(
props.values.Config.ForceCalibrationChargeState.toString()
),
calibrationChargeDate:
@ -100,9 +108,27 @@ function Configuration(props: ConfigurationProps) {
.toDate()
: dayjs(props.values.Config.DayAndTimeForAdditionalCalibration)
// .add(localOffset, 'minute')
.toDate()
.toDate(),
...(product === 3 && {
calibrationDischargeState: CalibrationChargeOptionsController.indexOf(
props.values.Config.ForceCalibrationDischargeState.toString()
),
calibrationDischargeDate:
CalibrationChargeOptionsController.indexOf(
props.values.Config.ForceCalibrationDischargeState.toString()
) == 0
? dayjs(
props.values.Config.DownDayAndTimeForRepetitiveCalibration
).toDate()
: dayjs(
props.values.Config.DownDayAndTimeForAdditionalCalibration
).toDate()
})
});
// console.log(formValues);
const handleSubmit = async (e) => {
if (
CalibrationChargeOptionsController.indexOf(
@ -116,7 +142,7 @@ function Configuration(props: ConfigurationProps) {
setErrorDateModalOpen(true);
return;
} else if (
formValues.CalibrationChargeState === 1 &&
formValues.calibrationChargeState === 1 &&
dayjs(formValues.calibrationChargeDate).isBefore(dayjs())
) {
//console.log('asked for', dayjs(formValues.calibrationChargeDate));
@ -128,13 +154,17 @@ function Configuration(props: ConfigurationProps) {
const configurationToSend: ConfigurationValues = {
minimumSoC: formValues.minimumSoC,
gridSetPoint: formValues.gridSetPoint,
CalibrationChargeState: formValues.CalibrationChargeState,
calibrationChargeState: formValues.calibrationChargeState,
calibrationChargeDate: dayjs
.utc(formValues.calibrationChargeDate)
.add(localOffset, 'minute')
.toDate(),
calibrationDischargeState: formValues.calibrationDischargeState,
calibrationDischargeDate: dayjs
.utc(formValues.calibrationDischargeDate)
.add(localOffset, 'minute')
.toDate()
};
// console.log('will send ', dayjs(formValues.calibrationChargeDate));
setLoading(true);
const res = await axiosConfig
@ -169,6 +199,15 @@ function Configuration(props: ConfigurationProps) {
});
};
const handleConfirmDischarge = (newDate) => {
//console.log('non adapted day is ', newDate);
//console.log('adapted day is ', dayjs.utc(newDate).toDate());
setFormValues({
...formValues,
['calibrationDischargeDate']: dayjs(newDate).toDate()
});
};
const handleSelectedCalibrationChargeDay = (event) => {
const selectedDay = daysInWeek.indexOf(event.target.value);
const currentDate = dayjs();
@ -184,10 +223,25 @@ function Configuration(props: ConfigurationProps) {
});
};
const handleSelectedCalibrationDisChargeDay = (event) => {
const selectedDay = daysInWeek.indexOf(event.target.value);
const currentDate = dayjs();
let difference = selectedDay - currentDate.day();
if (difference < 0) {
difference += 7;
}
const adjustedDate = currentDate.add(difference, 'day');
setFormValues({
...formValues,
['calibrationDischargeDate']: adjustedDate.toDate()
});
};
const handleSelectedCalibrationChargeChange = (event) => {
setFormValues({
...formValues,
['CalibrationChargeState']: CalibrationChargeOptions.indexOf(
['calibrationChargeState']: CalibrationChargeOptions.indexOf(
event.target.value
),
['calibrationChargeDate']:
@ -201,6 +255,23 @@ function Configuration(props: ConfigurationProps) {
});
};
const handleSelectedCalibrationDisChargeChange = (event) => {
setFormValues({
...formValues,
['calibrationDischargeState']: CalibrationChargeOptions.indexOf(
event.target.value
),
['calibrationDischargeDate']:
CalibrationChargeOptions.indexOf(event.target.value) == 0
? dayjs(props.values.Config.DownDayAndTimeForRepetitiveCalibration)
// .add(localOffset, 'minute')
.toDate()
: dayjs(props.values.Config.DownDayAndTimeForAdditionalCalibration)
// .add(localOffset, 'minute')
.toDate()
});
};
const handleChange = (e) => {
const { name, value } = e.target;
@ -284,7 +355,84 @@ function Configuration(props: ConfigurationProps) {
</Box>
</Modal>
)}
<Grid item xs={12} md={12}>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
mt: 2,
mb: 2
}}
>
<Tabs
value={activeTab}
onChange={(e, newValue) => setActiveTab(newValue)}
textColor="inherit"
TabIndicatorProps={{ style: { display: 'none' } }} // hide default underline
sx={{
bgcolor: '#f5f5f5',
borderRadius: 2,
p: 0.5,
boxShadow: 1,
width: 500,
height: 47
}}
>
<Tab
value="charge"
label="Calibration Charge"
sx={(theme) => ({
flex: 2,
fontWeight: 'bold',
borderRadius: 2,
textTransform: 'none',
color:
activeTab === 'charge'
? 'white'
: theme.palette.text.primary,
bgcolor:
activeTab === 'charge'
? theme.palette.primary.main
: 'transparent',
'&:hover': {
bgcolor:
activeTab === 'charge'
? theme.palette.primary.dark
: '#eee'
}
})}
/>
{product === 3 && (
<Tab
value="discharge"
label="Calibration Discharge"
sx={(theme) => ({
flex: 2,
fontWeight: 'bold',
borderRadius: 2,
textTransform: 'none',
color:
activeTab === 'discharge'
? 'white'
: theme.palette.text.primary,
bgcolor:
activeTab === 'discharge'
? theme.palette.primary.main
: 'transparent',
'&:hover': {
bgcolor:
activeTab === 'discharge'
? theme.palette.primary.dark
: '#eee'
}
})}
/>
)}
</Tabs>
</Box>
<CardContent>
<Box
component="form"
@ -294,6 +442,8 @@ function Configuration(props: ConfigurationProps) {
noValidate
autoComplete="off"
>
{activeTab === 'charge' && (
<>
<div style={{ marginBottom: '5px' }}>
<TextField
label={
@ -334,7 +484,7 @@ function Configuration(props: ConfigurationProps) {
<Select
value={
CalibrationChargeOptions[
formValues.CalibrationChargeState
formValues.calibrationChargeState
]
}
onChange={handleSelectedCalibrationChargeChange}
@ -347,7 +497,8 @@ function Configuration(props: ConfigurationProps) {
</Select>
</FormControl>
</div>
{formValues.CalibrationChargeState == 1 && (
{formValues.calibrationChargeState == 1 && (
<div>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<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>
</div>
)}
{formValues.CalibrationChargeState == 0 && (
{formValues.calibrationChargeState == 0 && (
<>
<div style={{ marginBottom: '5px' }}>
<FormControl
@ -399,7 +539,9 @@ function Configuration(props: ConfigurationProps) {
</InputLabel>
<Select
value={
daysInWeek[formValues.calibrationChargeDate.getDay()]
daysInWeek[
formValues.calibrationChargeDate.getDay()
]
}
onChange={handleSelectedCalibrationChargeDay}
>
@ -418,7 +560,9 @@ function Configuration(props: ConfigurationProps) {
ampm={false}
label="Calibration Charge Hour"
value={dayjs(formValues.calibrationChargeDate)}
onChange={(newTime) => handleConfirm(dayjs(newTime))}
onChange={(newTime) =>
handleConfirm(dayjs(newTime))
}
renderInput={(params) => (
<TextField
{...params}
@ -493,6 +637,123 @@ function Configuration(props: ConfigurationProps) {
/>
</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>*/}
{/* <TextField*/}
{/* label={*/}

View File

@ -20,6 +20,7 @@ import { I_Installation } from '../../../interfaces/InstallationTypes';
import { InstallationsContext } from '../../../contexts/InstallationsContextProvider';
import { UserContext } from '../../../contexts/userContext';
import { UserType } from '../../../interfaces/UserTypes';
import { ProductIdContext } from '../../../contexts/ProductIdContextProvider';
interface InformationProps {
values: I_Installation;
@ -38,6 +39,7 @@ function Information(props: InformationProps) {
const [formValues, setFormValues] = useState(props.values);
const requiredFields = ['name', 'region', 'location', 'country'];
const installationContext = useContext(InstallationsContext);
const { product, setProduct } = useContext(ProductIdContext);
const {
updateInstallation,
loading,
@ -265,6 +267,30 @@ function Information(props: InformationProps) {
/>
</div>
<div>
<TextField
label={
<FormattedMessage
id="information"
defaultMessage="Information"
/>
}
name="information"
value={formValues.information}
onChange={handleChange}
variant="outlined"
fullWidth
multiline
minRows={6} // 👈 Makes it visually bigger
maxRows={12} // 👈 Optional max height before scroll
inputProps={{
style: {
fontFamily: 'monospace' // optional: makes tabs/formatting more clear
}
}}
/>
</div>
{currentUser.userType == UserType.admin && (
<>
<div>
@ -282,8 +308,11 @@ function Information(props: InformationProps) {
label="S3 Bucket Name"
name="s3bucketname"
value={
formValues.s3BucketId +
product === 0 || product == 3
? formValues.s3BucketId +
'-3e5b3069-214a-43ee-8d85-57d72000c19d'
: formValues.s3BucketId +
'-e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa'
}
variant="outlined"
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 => {
console.log('when selecting installation', product);
if (selectedInstallation != installationID) {
setSelectedInstallation(installationID);
setSelectedInstallation(-1);

View File

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

View File

@ -216,6 +216,20 @@ export interface Line {
Power: Power;
}
export interface SodioHomeBattery {
AccumulatedChargeEnergy: number;
AccumulatedDischargeEnergy: number;
Current: number;
DailyChargeEnergy: number;
DailyDischargeEnergy: number;
MaxAllowableChargePower: number;
MaxAllowableDischargePower: number;
Power: number;
Soc: number;
Soh: number;
Voltage: number;
}
// The interface for the Battery structure, with dynamic keys (Device IDs)
export interface JSONRecordData {
Battery: {
@ -282,9 +296,12 @@ export interface JSONRecordData {
CurtailP: number;
DayAndTimeForAdditionalCalibration: string;
DayAndTimeForRepetitiveCalibration: string;
DownDayAndTimeForAdditionalCalibration: string;
DownDayAndTimeForRepetitiveCalibration: string;
DisplayIndividualBatteries: string;
MaxBatteryDischargingCurrent: number;
ForceCalibrationChargeState: string;
ForceCalibrationDischargeState: string;
GridSetPoint: number;
HoldSocZone: number;
MinSoc: number;
@ -392,17 +409,101 @@ export interface JSONRecordData {
LoadOnDc: { Power: number };
PvOnDc: {
[deviceId: string]: {
DcWh: number;
NbrOfStrings: number;
Dc: {
Voltage: number;
Current: number;
Power: number;
NbrOfStrings: number;
};
Strings: {
[PvId: string]: PvString;
};
};
};
AcDcGrowatt: {
AcChargeEnable: number;
ActivePowerPercentDerating: number;
ActualChargeDischargePowerControlValue: number;
AlarmMainCode: number;
AlarmSubCode: number;
BatteriesRecords: {
AverageSoc: number;
AverageSoh: number;
Batteries: { [deviceId: string]: SodioHomeBattery };
LowestSoc: number;
Power: number;
TotalChargeEnergy: number;
TotalDischargeEnergy: number;
TotalMaxCharge: number;
TotalMaxDischarge: number;
};
BatteryChargeCutoffVoltage: number;
BatteryClusterIndex: number;
BatteryDischargeCutoffVoltage: number;
BatteryMaxChargeCurrent: number;
BatteryMaxChargePower: number;
BatteryMaxDischargePower: number;
BatteryMaxdischargeCurrent: number;
BatteryOperatingMode: string;
BatteryType: number;
ChargeCutoffSoc: number;
ControlPermession: number;
DischargeCutoffSoc: number;
EmsCommunicationFailureTime: number;
EnableCommand: number;
EnableEmsCommunicationFailureTime: number;
EnableSyn: string;
EnergyToGrid: number;
EnergyToUser: number;
FaultMainCode: number;
FaultSubCode: number;
Frequency: number;
GridAbLineVoltage: number;
GridBcLineVoltage: number;
GridCaLineVoltage: number;
InverterActivePower: number;
InverterReactivePower: number;
InverterTemperature: number;
LoadPriorityDischargeCutoffSoc: number;
MaxActivePower: number;
MeterPower: number;
OffGridDischargeCutoffSoc: number;
OperatingPriority: string;
PhaseACurrent: number;
PhaseBCurrent: number;
PhaseCCurrent: number;
PowerFactor: number;
Pv1Current: number;
Pv1Voltage: number;
Pv2Current: number;
Pv2Voltage: number;
PvInputMaxPower: number;
RatedPower: number;
RemoteChargDischargePower: number;
RemotePowerControl: number;
SystemDateTime: string;
SystemOperatingMode: string;
TotalEnergyToGrid: number;
TotalEnergyToUser: number;
VppProtocolVerNumber: number;
};
// DcWh: number;
// // NbrOfStrings: number;
// Dc: { [deviceId: string]: Dc };
// // Dc: {
// // Voltage: number;
// // Current: number;
// // Power: number;
// // };
// Strings: {
// [PvId: string]: PvString;
// };
// };
}
export const parseChunkJson = (
@ -497,8 +598,10 @@ export interface I_BoxDataValue {
export type ConfigurationValues = {
minimumSoC: string | number;
gridSetPoint: number;
CalibrationChargeState: number;
calibrationChargeState: number;
calibrationChargeDate: Date | null;
calibrationDischargeState: number;
calibrationDischargeDate: Date | null;
};
//
// export interface Pv {

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

View File

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

View File

@ -19,8 +19,11 @@ import HistoryOfActions from '../History/History';
import BuildIcon from '@mui/icons-material/Build';
import AccessContextProvider from '../../../contexts/AccessContextProvider';
import Access from '../ManageAccess/Access';
import InformationSodioHome from '../Information/InformationSodioHome';
import CryptoJS from 'crypto-js';
import Information from '../Information/Information';
import { TimeSpan, UnixTime } from '../../../dataCache/time';
import { fetchDataJson } from '../Installations/fetchData';
import { FetchResult } from '../../../dataCache/dataCache';
import BatteryViewSodioHome from '../BatteryView/BatteryViewSodioHome';
interface singleInstallationProps {
current_installation?: I_Installation;
@ -31,6 +34,19 @@ function SodioHomeInstallation(props: singleInstallationProps) {
if (props.current_installation == undefined) {
return null;
}
const S3data = {
s3Region: props.current_installation.s3Region,
s3Provider: props.current_installation.s3Provider,
s3Key: props.current_installation.s3Key,
s3Secret: props.current_installation.s3Secret,
s3BucketId: props.current_installation.s3BucketId
};
const s3Bucket =
props.current_installation.s3BucketId.toString() +
'-' +
'e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa';
const context = useContext(UserContext);
const { currentUser } = context;
const location = useLocation().pathname;
@ -45,6 +61,7 @@ function SodioHomeInstallation(props: singleInstallationProps) {
const [connected, setConnected] = useState(true);
const [loading, setLoading] = useState(true);
const [fetchFunctionCalled, setFetchFunctionCalled] = useState(false);
const s3Credentials = { s3Bucket, ...S3data };
//In React, useRef creates a mutable object that persists across renders without triggering re-renders when its value changes.
//While fetching, we check the value of continueFetching, if its false, we break. This means that either the user changed tab or the object has been finished its execution (return)
@ -91,48 +108,75 @@ function SodioHomeInstallation(props: singleInstallationProps) {
}
const fetchDataPeriodically = async () => {
while (continueFetching.current) {
//Fetch data from Bitwatt cloud
console.log('Fetching from Bitwatt cloud');
var timeperiodToSearch = 200;
let res;
let timestampToFetch;
console.log(props.current_installation.serialNumber);
console.log(props.current_installation.s3WriteKey);
console.log(props.current_installation.s3WriteSecret);
const timeStamp = Date.now().toString();
// Encrypt timestamp using AES-ECB with PKCS7 padding
const key = CryptoJS.enc.Utf8.parse(
props.current_installation.s3WriteSecret
);
const encrypted = CryptoJS.AES.encrypt(timeStamp, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
}).toString();
// Set headers
const headers = {
'X-Signature': encrypted,
'X-AccessKey': props.current_installation.s3WriteKey,
'Content-Type': 'application/json'
};
// API URL
const url = `https://www.biwattpower.com/gateway/admin/open/device/currentEnergyFlowData/${props.current_installation.serialNumber}`;
for (var i = 0; i < timeperiodToSearch; i += 2) {
if (!continueFetching.current) {
return false;
}
timestampToFetch = UnixTime.now().earlier(TimeSpan.fromSeconds(i));
try {
const response = await fetch(url, { method: 'GET', headers });
const result = await response.json();
console.log('API Response:', result);
} catch (error) {
console.error('Request failed:', error);
res = await fetchDataJson(timestampToFetch, s3Credentials, false);
if (res !== FetchResult.notAvailable && res !== FetchResult.tryLater) {
break;
}
} catch (err) {
console.error('Error fetching data:', err);
return false;
}
}
// Wait for 2 seconds before fetching again
await timeout(200000);
console.log('ssssssssssssssssssssssssssssssssssssss');
if (i >= timeperiodToSearch) {
setConnected(false);
setLoading(false);
return false;
}
setConnected(true);
setLoading(false);
while (continueFetching.current) {
for (const timestamp of Object.keys(res)) {
if (!continueFetching.current) {
setFetchFunctionCalled(false);
return false;
}
console.log(`Timestamp: ${timestamp}`);
console.log(res[timestamp]);
setValues(res[timestamp]);
await timeout(2000);
}
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(() => {
@ -147,11 +191,16 @@ function SodioHomeInstallation(props: singleInstallationProps) {
}, [status]);
useEffect(() => {
console.log(currentTab);
if (currentTab == 'live' || location.includes('batteryview')) {
//Fetch periodically if the tab is live or batteryview
if (
currentTab == 'live' ||
currentTab == 'pvview' ||
currentTab == 'configuration' ||
location.includes('batteryview')
) {
//Fetch periodically if the tab is live, pvview or batteryview
if (
currentTab == 'live' ||
currentTab == 'pvview' ||
(location.includes('batteryview') && !location.includes('mainstats'))
) {
if (!continueFetching.current) {
@ -163,6 +212,10 @@ function SodioHomeInstallation(props: singleInstallationProps) {
}
}
}
//Fetch only one time in configuration tab
// if (currentTab == 'configuration') {
// fetchDataForOneTime();
// }
return () => {
continueFetching.current = false;
@ -322,10 +375,11 @@ function SodioHomeInstallation(props: singleInstallationProps) {
<Route
path={routes.information}
element={
<InformationSodioHome
<Information
values={props.current_installation}
s3Credentials={s3Credentials}
type={props.type}
></InformationSodioHome>
></Information>
}
/>
@ -351,28 +405,17 @@ function SodioHomeInstallation(props: singleInstallationProps) {
}
/>
{/*<Route*/}
{/* path={routes.overview}*/}
{/* element={*/}
{/* <SalidomoOverview*/}
{/* s3Credentials={s3Credentials}*/}
{/* id={props.current_installation.id}*/}
{/* ></SalidomoOverview>*/}
{/* }*/}
{/*/>*/}
{/*<Route*/}
{/* path={routes.batteryview + '*'}*/}
{/* element={*/}
{/* <BatteryViewSalidomo*/}
{/* values={values}*/}
{/* s3Credentials={s3Credentials}*/}
{/* installationId={props.current_installation.id}*/}
{/* productNum={props.current_installation.product}*/}
{/* connected={connected}*/}
{/* ></BatteryViewSalidomo>*/}
{/* }*/}
{/*></Route>*/}
<Route
path={routes.batteryview + '*'}
element={
<BatteryViewSodioHome
values={values}
s3Credentials={s3Credentials}
installationId={props.current_installation.id}
connected={connected}
></BatteryViewSodioHome>
}
></Route>
{currentUser.userType == UserType.admin && (
<Route

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,8 +12,11 @@ import {
InnovEnergyUser
} from '../interfaces/UserTypes';
import { FormattedMessage } from 'react-intl';
import { I_Installation } from '../interfaces/InstallationTypes';
interface AccessContextProviderProps {
fetchInstallationsForUser: (userId: number) => void;
accessibleInstallationsForUser: I_Installation[];
availableUsers: InnovEnergyUser[];
fetchAvailableUsers: () => Promise<void>;
usersWithDirectAccess: InnovEnergyUser[];
@ -44,6 +47,8 @@ interface AccessContextProviderProps {
}
export const AccessContext = createContext<AccessContextProviderProps>({
fetchInstallationsForUser: () => Promise.resolve(),
accessibleInstallationsForUser: [],
availableUsers: [],
fetchAvailableUsers: () => {
return Promise.resolve();
@ -74,6 +79,8 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
InnovEnergyUser[]
>([]);
const [availableUsers, setAvailableUsers] = useState<InnovEnergyUser[]>([]);
const [accessibleInstallationsForUser, setAccessibleInstallationsForUser] =
useState<I_Installation[]>([]);
const [usersWithInheritedAccess, setUsersWithInheritedAccess] = useState<
I_UserWithInheritedAccess[]
@ -104,6 +111,26 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
[]
);
const fetchInstallationsForUser = useCallback(async (userId: number) => {
axiosConfig
.get(`/GetInstallationsTheUserHasAccess?userId=${userId}`)
.then((response) => {
if (response) {
setAccessibleInstallationsForUser(response.data);
}
})
.catch((error) => {
setError(true);
const message = (
<FormattedMessage
id="unableToLoadData"
defaultMessage="Unable to load data"
/>
).props.defaultMessage;
setErrorMessage(message);
});
}, []);
const fetchUsersWithInheritedAccessForResource = useCallback(
async (tempresourceType: string, id: number) => {
axiosConfig
@ -192,6 +219,8 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
return (
<AccessContext.Provider
value={{
fetchInstallationsForUser,
accessibleInstallationsForUser,
availableUsers,
fetchAvailableUsers,
usersWithDirectAccess,

View File

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

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) => {
// setSortedInstallations(installations.sort((a, b) => b.status - a.status)); // Sort installations by status
};
@ -169,18 +63,6 @@ const WebSocketContextProvider = ({ children }: { children: ReactNode }) => {
// socket.close();
};
// const getStatus = (installationId: number) => {
// return installationStatus.get(installationId);
// // if (installationStatus.has(installationId)) {
// // installationStatus.get(installationId);
// // } else {
// // return -2;
// // }
// };
// const getTestingMode = (installationId: number) => {
// return installationMode.get(installationId);
// };
return (
<WebSocketContext.Provider

View File

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

View File

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

View File

@ -1,7 +1,8 @@
import { useContext } from 'react';
import Scrollbar from 'src/components/Scrollbar';
import { SidebarContext } from 'src/contexts/SidebarContext';
import innovenergyLogo from 'src/Resources/images/innovenergy-Logo_Speichern-mit-Salz_R_color.svg';
import inescoLogo from 'src/Resources/images/inesco_logo.png';
import {
alpha,
Box,
@ -17,8 +18,8 @@ import SidebarMenu from './SidebarMenu';
const SidebarWrapper = styled(Box)(
({ theme }) => `
width: ${theme.sidebar.width};
min-width: ${theme.sidebar.width};
width: 280px; /* previously theme.sidebar.width */
min-width: 280px;
color: ${theme.colors.alpha.trueWhite[70]};
position: relative;
z-index: 7;
@ -54,16 +55,24 @@ function Sidebar() {
<Scrollbar>
<Box mt={3}>
<Box
mx={2}
sx={{
width: 52
px: 2, // Padding left & right
py: 1.5, // Optional: padding top & bottom
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
border: '2px solid white', // Optional: border around logo
borderRadius: '8px',
backgroundColor: '#fff', // Optional: white background
mx: 2 // Horizontal margin to avoid sticking to edge
}}
>
<img
src={innovenergyLogo}
alt="innovenergy logo"
src={inescoLogo}
alt="inesco logo"
style={{
width: '150px' // Width of the image
width: '160px',
objectFit: 'contain'
}}
/>
</Box>
@ -105,8 +114,8 @@ function Sidebar() {
}}
>
<img
src={innovenergyLogo}
alt="innovenergy logo"
src={inescoLogo}
alt="inesco logo"
style={{
width: '150px' // Width of the image
}}

View File

@ -58,7 +58,7 @@ const SidebarLayout = (props: SidebarLayoutProps) => {
flex: 1,
pt: `${theme.header.height}`,
[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';
const themeColors = {