Innovenergy_trunk/csharp/App/Backend/Controller.cs

1392 lines
49 KiB
C#

using System.Diagnostics;
using System.Net;
using System.Text.RegularExpressions;
using InnovEnergy.App.Backend.Database;
using InnovEnergy.App.Backend.DataTypes;
using InnovEnergy.App.Backend.DataTypes.Methods;
using InnovEnergy.App.Backend.Relations;
using InnovEnergy.App.Backend.Services;
using InnovEnergy.App.Backend.Websockets;
using InnovEnergy.Lib.Utils;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
namespace InnovEnergy.App.Backend;
using Token = String;
// create JobStatus class to track download battery log job
public class JobStatus
{
public string JobId { get; set; }
public string Status { get; set; }
public string FileName { get; set; }
public DateTime StartTime { get; set; }
}
[Controller]
[Route("api/")]
//All the http requests from the frontend that contain "/api" will be forwarded to this controller from the nginx reverse proxy.
public class Controller : ControllerBase
{
[HttpPost(nameof(Login))]
public ActionResult<Session> Login(String username, String? password)
{
//Find the user to the database, verify its password and create a new session.
//Store the new session to the database and return it to the frontend.
//If the user log out, the session will be deleted. Each session is valid for 24 hours. The db deletes all invalid/expired sessions every 30 minutes.
var user = Db.GetUserByEmail(username);
if (user is null)
throw new Exceptions(400, "Null User Exception", "Must provide a user to log in as.", Request.Path.Value!);
if (!(user.Password.IsNullOrEmpty() && user.MustResetPassword) && !user.VerifyPassword(password))
{
throw new Exceptions(401, "Wrong Password Exception", "Please try again.", Request.Path.Value!);
}
var session = new Session(user.HidePassword().HideParentIfUserHasNoAccessToParent(user));
return Db.Create(session)
? session
: throw new Exceptions(401,"Session Creation Exception", "Not allowed to log in.", Request.Path.Value!);
}
[HttpPost(nameof(Logout))]
public ActionResult Logout(Token authToken)
{
//Find the session and delete it from the database.
var session = Db.GetSession(authToken);
return session.Logout()
? Ok()
: Unauthorized();
}
[HttpGet(nameof(CreateWebSocket))]
public async Task CreateWebSocket(Token authToken)
{
//Everytime a user logs in, this function is called
var session = Db.GetSession(authToken)?.User;
if (session is null)
{
Console.WriteLine("------------------------------------Unauthorized user----------------------------------------------");
HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
HttpContext.Abort();
return;
}
if (!HttpContext.WebSockets.IsWebSocketRequest)
{
Console.WriteLine("------------------------------------Not a websocket request ----------------------------------------------");
HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
HttpContext.Abort();
return;
}
//Create a websocket and pass its descriptor to the HandleWebSocketConnection method.
//This descriptor is returned to the frontend on the background
var webSocketContext = await HttpContext.WebSockets.AcceptWebSocketAsync();
var webSocket = webSocketContext;
//Handle the WebSocket connection
await WebsocketManager.HandleWebSocketConnection(webSocket);
}
[HttpGet(nameof(GetAllErrorsForInstallation))]
public ActionResult<IEnumerable<Error>> GetAllErrorsForInstallation(Int64 id, Token authToken)
{
var user = Db.GetSession(authToken)?.User;
if (user == null)
return Unauthorized();
var installation = Db.GetInstallationById(id);
if (installation is null || !user.HasAccessTo(installation))
return Unauthorized();
return Db.Errors
.Where(error => error.InstallationId == id)
.OrderByDescending(error => error.Date)
.ThenByDescending(error => error.Time)
.ToList();
}
[HttpGet(nameof(GetHistoryForInstallation))]
public ActionResult<IEnumerable<UserAction>> GetHistoryForInstallation(Int64 id, Token authToken)
{
var user = Db.GetSession(authToken)?.User;
if (user == null)
return Unauthorized();
var installation = Db.GetInstallationById(id);
if (installation is null || !user.HasAccessTo(installation))
return Unauthorized();
return Db.UserActions
.Where(action =>action.InstallationId == id)
.OrderByDescending(action => action.Timestamp)
.ToList();
}
[HttpGet(nameof(GetAllWarningsForInstallation))]
public ActionResult<IEnumerable<Warning>> GetAllWarningsForInstallation(Int64 id, Token authToken)
{
var user = Db.GetSession(authToken)?.User;
if (user == null)
return Unauthorized();
var installation = Db.GetInstallationById(id);
if (installation is null || !user.HasAccessTo(installation))
return Unauthorized();
return Db.Warnings
.Where(error => error.InstallationId == id)
.OrderByDescending(error => error.Date)
.ThenByDescending(error => error.Time)
.ToList();
}
[HttpGet(nameof(GetCsvTimestampsForInstallation))]
public ActionResult<IEnumerable<Int64>> GetCsvTimestampsForInstallation(Int64 id, Int32 start, Int32 end, Token authToken)
{
var user = Db.GetSession(authToken)?.User;
if (user == null)
return Unauthorized();
var installation = Db.GetInstallationById(id);
if (installation is null || !user.HasAccessTo(installation))
return Unauthorized();
var sampleSize = 100;
var allTimestamps = new List<Int64>();
static string FindCommonPrefix(string str1, string str2)
{
int minLength = Math.Min(str1.Length, str2.Length);
int i = 0;
while (i < minLength && str1[i] == str2[i])
{
i++;
}
return str1.Substring(0, i);
}
Int64 startTimestamp = Int64.Parse(start.ToString().Substring(0,5));
Int64 endTimestamp = Int64.Parse(end.ToString().Substring(0,5));
if (installation.Product == (int)ProductType.Salidomo)
{
start = Int32.Parse(start.ToString().Substring(0, start.ToString().Length - 2));
end = Int32.Parse(end.ToString().Substring(0, end.ToString().Length - 2));
}
string configPath = "/home/ubuntu/.s3cfg";
while (startTimestamp <= endTimestamp)
{
string bucketPath;
if (installation.Product == (int)ProductType.Salimax || installation.Product == (int)ProductType.SodiStoreMax)
bucketPath = "s3://" + installation.S3BucketId + "-3e5b3069-214a-43ee-8d85-57d72000c19d/" + startTimestamp;
else if (installation.Product == (int)ProductType.SodioHome)
bucketPath = "s3://" + installation.S3BucketId + "-e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa/" + startTimestamp;
else
bucketPath = "s3://" + installation.S3BucketId + "-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e/" + startTimestamp;
Console.WriteLine("Fetching data for "+startTimestamp);
try
{
// Set up process start info
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "s3cmd",
Arguments = $"--config {configPath} ls {bucketPath}",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
// Start the process
Process process = new Process
{
StartInfo = startInfo
};
process.Start();
// Read the output
string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();
process.WaitForExit();
// Check for errors
if (process.ExitCode != 0)
{
Console.WriteLine("Error executing command:");
Console.WriteLine(error);
}
else
{
// Define a regex pattern to match the filenames without .csv extension
var pattern = @"/([^/]+)\.(csv|json)$";
var regex = new Regex(pattern);
// Process each line of the output
foreach (var line in output.Split('\n'))
{
var match = regex.Match(line);
if (match.Success && long.Parse(match.Groups[1].Value) >= start && long.Parse(match.Groups[1].Value) <= end)
{
allTimestamps.Add(long.Parse(match.Groups[1].Value));
//Console.WriteLine(match.Groups[1].Value);
}
}
}
}
catch (Exception e)
{
Console.WriteLine($"Exception: {e.Message}");
}
startTimestamp++;
}
int totalRecords = allTimestamps.Count;
if (totalRecords <= sampleSize)
{
// If the total records are less than or equal to the sample size, return all records
Console.WriteLine("Start timestamp = "+start +" end timestamp = "+end);
Console.WriteLine("SampledTimestamps = " + allTimestamps.Count);
return allTimestamps;
}
int interval = totalRecords / sampleSize;
var sampledTimestamps = new List<Int64>();
for (int i = 0; i < totalRecords; i += interval)
{
sampledTimestamps.Add(allTimestamps[i]);
}
// If we haven't picked enough records (due to rounding), add the latest record to ensure completeness
if (sampledTimestamps.Count < sampleSize)
{
sampledTimestamps.Add(allTimestamps.Last());
}
Console.WriteLine("Start timestamp = "+start +" end timestamp = "+end);
Console.WriteLine("TotalRecords = "+totalRecords + " interval = "+ interval);
Console.WriteLine("SampledTimestamps = " + sampledTimestamps.Count);
return sampledTimestamps;
}
[HttpGet(nameof(GetUserById))]
public ActionResult<User> GetUserById(Int64 id, Token authToken)
{
var session = Db.GetSession(authToken)?.User;
if (session == null)
return Unauthorized();
var user = Db.GetUserById(id);
if (user is null || !session.HasAccessTo(user))
return Unauthorized();
return user
.HidePassword()
.HideParentIfUserHasNoAccessToParent(session);
}
[HttpGet(nameof(GetInstallationById))]
public ActionResult<Installation> GetInstallationById(Int64 id, Token authToken)
{
var user = Db.GetSession(authToken)?.User;
if (user == null)
return Unauthorized();
var installation = Db.GetInstallationById(id);
if (installation is null || !user.HasAccessTo(installation))
return Unauthorized();
return installation
.FillOrderNumbers()
.HideParentIfUserHasNoAccessToParent(user)
.HideWriteKeyIfUserIsNotAdmin(user.UserType);
}
[HttpGet(nameof(GetUsersWithDirectAccessToInstallation))]
public ActionResult<IEnumerable<Object>> GetUsersWithDirectAccessToInstallation(Int64 id, Token authToken)
{
var user = Db.GetSession(authToken)?.User;
if (user == null)
return Unauthorized();
var installation = Db.GetInstallationById(id);
if (installation is null || !user.HasAccessTo(installation))
return Unauthorized();
return installation
.UsersWithDirectAccess()
.Where(u => u.IsDescendantOf(user))
.Select(u => u.HidePassword())
.ToList();
}
[HttpGet(nameof(GetInstallationsTheUserHasAccess))]
public ActionResult<IEnumerable<Object>> GetInstallationsTheUserHasAccess(Int64 userId, Token authToken)
{
var user = Db.GetUserById(userId);
if (user == null)
return Unauthorized();
return user.AccessibleInstallations().ToList();
}
[HttpGet(nameof(GetDirectInstallationAccessForUser))]
public ActionResult<IEnumerable<Object>> GetDirectInstallationAccessForUser(Int64 userId, Token authToken)
{
var sessionUser = Db.GetSession(authToken)?.User;
if (sessionUser == null)
return Unauthorized();
var user = Db.GetUserById(userId);
if (user == null)
return Unauthorized();
return user.DirectlyAccessibleInstallations()
.Select(i => new { i.Id, i.Name })
.ToList<Object>();
}
[HttpGet(nameof(GetDirectFolderAccessForUser))]
public ActionResult<IEnumerable<Object>> GetDirectFolderAccessForUser(Int64 userId, Token authToken)
{
var sessionUser = Db.GetSession(authToken)?.User;
if (sessionUser == null)
return Unauthorized();
var user = Db.GetUserById(userId);
if (user == null)
return Unauthorized();
return user.DirectlyAccessibleFolders()
.Select(f => new { f.Id, f.Name })
.ToList<Object>();
}
[HttpGet(nameof(GetUsersWithInheritedAccessToInstallation))]
public ActionResult<IEnumerable<Object>> GetUsersWithInheritedAccessToInstallation(Int64 id, Token authToken)
{
var user = Db.GetSession(authToken)?.User;
if (user == null)
return Unauthorized();
var installation = Db.GetInstallationById(id);
if (installation is null || !user.HasAccessTo(installation))
return Unauthorized();
return installation
.Ancestors()
.SelectMany(f => f.UsersWithDirectAccess()
.Where(u => u.IsDescendantOf(user))
.Select(u => new { folderId = f.Id, folderName = f.Name, user = u.HidePassword() }))
.ToList();
}
[HttpGet(nameof(GetUsersWithDirectAccessToFolder))]
public ActionResult<IEnumerable<Object>> GetUsersWithDirectAccessToFolder(Int64 id, Token authToken)
{
var user = Db.GetSession(authToken)?.User;
if (user == null)
return Unauthorized();
var folder = Db.GetFolderById(id);
if (folder is null || !user.HasAccessTo(folder))
return Unauthorized();
return folder
.UsersWithDirectAccess()
.Where(u => u.IsDescendantOf(user))
.Select(u => u.HidePassword())
.ToList();
}
[HttpGet(nameof(GetUsersWithInheritedAccessToFolder))]
public ActionResult<IEnumerable<Object>> GetUsersWithInheritedAccessToFolder(Int64 id, Token authToken)
{
var user = Db.GetSession(authToken)?.User;
if (user == null)
return Unauthorized();
var folder = Db.GetFolderById(id);
if (folder is null || !user.HasAccessTo(folder))
return Unauthorized();
return folder
.Ancestors()
.SelectMany(f => f.UsersWithDirectAccess()
.Where(u => u.IsDescendantOf(user))
.Select(u => new { folderId = f.Id, folderName = f.Name, user = u.HidePassword() }))
.ToList();
}
[HttpGet(nameof(GetFolderById))]
public ActionResult<Folder> GetFolderById(Int64 id, Token authToken)
{
var user = Db.GetSession(authToken)?.User;
if (user == null)
return Unauthorized();
var folder = Db.GetFolderById(id);
if (folder is null || !user.HasAccessTo(folder))
return Unauthorized();
return folder.HideParentIfUserHasNoAccessToParent(user);
}
[HttpGet(nameof(GetAllDirectChildUsers))]
public ActionResult<IEnumerable<User>> GetAllDirectChildUsers(Token authToken)
{
var user = Db.GetSession(authToken)?.User;
if (user == null)
return Unauthorized();
return user.ChildUsers().Select(u => u.HidePassword()).ToList();
}
[HttpGet(nameof(GetAllChildUsers))]
public ActionResult<IEnumerable<User>> GetAllChildUsers(Token authToken)
{
var user = Db.GetSession(authToken)?.User;
if (user == null)
return Unauthorized();
return user
.DescendantUsers()
.Select(u => u.HidePassword())
.ToList();
}
[HttpGet(nameof(GetAllInstallationsFromProduct))]
public ActionResult<IEnumerable<Installation>> GetAllInstallationsFromProduct(int product,Token authToken)
{
var user = Db.GetSession(authToken)?.User;
if (user is null)
return Unauthorized();
return user
.AccessibleInstallations(product)
.Select(i => i.FillOrderNumbers().HideParentIfUserHasNoAccessToParent(user).HideWriteKeyIfUserIsNotAdmin(user.UserType))
.ToList();
}
[HttpGet(nameof(GetAllInstallations))]
public ActionResult<IEnumerable<Installation>> GetAllInstallations(Token authToken)
{
var user = Db.GetSession(authToken)?.User;
if (user is null)
return Unauthorized();
return user
.AccessibleInstallations(product:(int)ProductType.Salimax)
.ToList();
}
[HttpGet(nameof(GetAllSalidomoInstallations))]
public ActionResult<IEnumerable<Installation>> GetAllSalidomoInstallations(Token authToken)
{
var user = Db.GetSession(authToken)?.User;
if (user is null)
return Unauthorized();
return user
.AccessibleInstallations(product:(int)ProductType.Salidomo)
.ToList();
}
[HttpGet(nameof(GetAllSodioHomeInstallations))]
public ActionResult<IEnumerable<Installation>> GetAllSodioHomeInstallations(Token authToken)
{
var user = Db.GetSession(authToken)?.User;
if (user is null)
return Unauthorized();
return user
.AccessibleInstallations(product:(int)ProductType.SodioHome)
.ToList();
}
[HttpGet(nameof(GetAllFolders))]
public ActionResult<IEnumerable<Folder>> GetAllFolders(Token authToken)
{
var user = Db.GetSession(authToken)?.User;
if (user is null)
return Unauthorized();
return new(user.AccessibleFolders().HideParentIfUserHasNoAccessToParent(user));
}
[HttpGet(nameof(GetAllFoldersAndInstallations))]
public ActionResult<IEnumerable<Object>> GetAllFoldersAndInstallations(int productId, Token authToken)
{
var user = Db.GetSession(authToken)?.User;
if (user is null)
return Unauthorized();
var foldersAndInstallations = user
.AccessibleFoldersAndInstallations()
.Do(o => o.FillOrderNumbers())
.Select(o => o.HideParentIfUserHasNoAccessToParent(user))
.OfType<Object>(); // Important! JSON serializer must see Objects otherwise
// it will just serialize the members of TreeNode %&@#!!!
// TODO Filter out write keys
return new (foldersAndInstallations);
}
[HttpPost(nameof(CreateUser))]
public async Task<ActionResult<User>> CreateUser([FromBody] User newUser, Token authToken)
{
var create = Db.GetSession(authToken).Create(newUser);
if (create)
{
var mail_success= await Db.SendNewUserEmail(newUser);
if (!mail_success)
{
Db.GetSession(authToken).Delete(newUser);
return StatusCode(500, "Welcome email failed to send");
}
return mail_success ? newUser.HidePassword():Unauthorized();
}
return Unauthorized() ;
}
[HttpPost(nameof(CreateInstallation))]
public async Task<ActionResult<Installation>> CreateInstallation([FromBody] Installation installation, Token authToken)
{
var session = Db.GetSession(authToken);
if (! await session.Create(installation))
return Unauthorized();
return installation;
}
[HttpPost(nameof(CreateFolder))]
public ActionResult<Folder> CreateFolder([FromBody] Folder folder, Token authToken)
{
var session = Db.GetSession(authToken);
if (!session.Create(folder))
return Unauthorized();
return folder.HideParentIfUserHasNoAccessToParent(session!.User);
}
[HttpPost(nameof(GrantUserAccessToFolder))]
public ActionResult GrantUserAccessToFolder(FolderAccess folderAccess, Token authToken)
{
var session = Db.GetSession(authToken);
// TODO: automatic BadRequest when properties are null during deserialization
var folder = Db.GetFolderById(folderAccess.FolderId);
var user = Db.GetUserById(folderAccess.UserId);
// Check if user already has access - treat as idempotent (success)
if (user is not null && folder is not null && user.HasAccessTo(folder))
{
Console.WriteLine($"GrantUserAccessToFolder: User {user.Id} ({user.Name}) already has access to folder {folder.Id} ({folder.Name}) - returning success");
return Ok();
}
return session.GrantUserAccessTo(user, folder)
? Ok()
: Unauthorized();
}
[HttpPost(nameof(RevokeUserAccessToFolder))]
public ActionResult RevokeUserAccessToFolder(FolderAccess folderAccess, Token authToken)
{
var session = Db.GetSession(authToken);
// TODO: automatic BadRequest when properties are null during deserialization
var folder = Db.GetFolderById(folderAccess.FolderId);
var user = Db.GetUserById(folderAccess.UserId);
return session.RevokeUserAccessTo(user, folder)
? Ok()
: Unauthorized();
}
[HttpPost(nameof(GrantUserAccessToInstallation))]
public ActionResult GrantUserAccessToInstallation(InstallationAccess installationAccess, Token authToken)
{
var session = Db.GetSession(authToken);
// TODO: automatic BadRequest when properties are null during deserialization
var installation = Db.GetInstallationById(installationAccess.InstallationId);
var user = Db.GetUserById(installationAccess.UserId);
// Check if user already has access - treat as idempotent (success)
if (user is not null && installation is not null && user.HasAccessTo(installation))
{
Console.WriteLine($"GrantUserAccessToInstallation: User {user.Id} ({user.Name}) already has access to installation {installation.Id} ({installation.Name}) - returning success");
return Ok();
}
return session.GrantUserAccessTo(user, installation)
? Ok()
: Unauthorized();
}
[HttpPost(nameof(RevokeUserAccessToInstallation))]
public ActionResult RevokeUserAccessToInstallation(InstallationAccess installationAccess, Token authToken)
{
var session = Db.GetSession(authToken);
// TODO: automatic BadRequest when properties are null during deserialization
var installation = Db.GetInstallationById(installationAccess.InstallationId);
var user = Db.GetUserById(installationAccess.UserId);
return session.RevokeUserAccessTo(user, installation)
? Ok()
: Unauthorized();
}
[HttpPut(nameof(UpdateUser))]
public ActionResult<User> UpdateUser([FromBody] User updatedUser, Token authToken)
{
var session = Db.GetSession(authToken);
if (!session.Update(updatedUser))
return Unauthorized();
return updatedUser.HidePassword();
}
[HttpPut(nameof(UpdatePassword))]
public ActionResult<User> UpdatePassword(String newPassword, Token authToken)
{
var session = Db.GetSession(authToken);
return session.UpdatePassword(newPassword)
? Ok()
: Unauthorized();
}
[HttpPut(nameof(UpdateInstallation))]
public ActionResult<Installation> UpdateInstallation([FromBody] Installation installation, Token authToken)
{
var session = Db.GetSession(authToken);
if (!session.Update(installation))
return Unauthorized();
if (installation.Product == (int)ProductType.Salimax)
{
return installation.FillOrderNumbers().HideParentIfUserHasNoAccessToParent(session!.User).HideWriteKeyIfUserIsNotAdmin(session.User.UserType);
}
return installation.HideParentIfUserHasNoAccessToParent(session!.User);
}
[HttpPost(nameof(AcknowledgeError))]
public ActionResult AcknowledgeError(Int64 id, Token authToken)
{
var session = Db.GetSession(authToken);
if (session == null)
return Unauthorized();
var error=Db.Errors
.FirstOrDefault(error => error.Id == id);
error.Seen = true;
return Db.Update(error)
? Ok()
: Unauthorized();
}
[HttpPost(nameof(AcknowledgeWarning))]
public ActionResult AcknowledgeWarning(Int64 id, Token authToken)
{
var session = Db.GetSession(authToken);
if (session == null)
return Unauthorized();
var warning=Db.Warnings
.FirstOrDefault(warning => warning.Id == id);
warning.Seen = true;
return Db.Update(warning)
? Ok()
: Unauthorized();
}
/// <summary>
/// Returns an AI-generated diagnosis for a single error/alarm description.
/// Responses are cached in memory — repeated calls for the same error code
/// do not hit Mistral again.
/// </summary>
[HttpGet(nameof(DiagnoseError))]
public async Task<ActionResult<DiagnosticResponse>> DiagnoseError(Int64 installationId, string errorDescription, Token authToken)
{
var user = Db.GetSession(authToken)?.User;
if (user == null)
return Unauthorized();
var installation = Db.GetInstallationById(installationId);
if (installation is null || !user.HasAccessTo(installation))
return Unauthorized();
// AI diagnostics are scoped to SodistoreHome and SodiStoreMax only
if (installation.Product != (int)ProductType.SodioHome &&
installation.Product != (int)ProductType.SodiStoreMax)
return BadRequest("AI diagnostics not available for this product.");
var result = await DiagnosticService.DiagnoseAsync(installationId, errorDescription, user.Language ?? "en");
if (result is null)
return NoContent(); // no diagnosis available (not in knowledge base, no API key)
return result;
}
/// <summary>
/// Test endpoint for AlarmKnowledgeBase - no authentication required.
/// Tests multiple Sinexcel and Growatt alarms to verify the knowledge base works.
/// Remove this endpoint in production if not needed.
/// </summary>
[HttpGet(nameof(TestAlarmKnowledgeBase))]
public ActionResult TestAlarmKnowledgeBase()
{
var testCases = new[]
{
// Sinexcel alarms (keys match SinexcelRecord.Api.cs property names)
"FanFault",
"AbnormalGridVoltage",
"Battery1NotConnected",
"InverterPowerTubeFault",
"IslandProtection",
// Growatt alarms (keys match GrowattWarningCode/GrowattErrorCode enum names)
"NoUtilityGrid",
"BatteryCommunicationFailure",
"BmsFault",
"OverTemperature",
"AFCI Fault",
// Unknown alarm (should return null - would call Mistral)
"Some unknown alarm XYZ123"
};
var results = new List<object>();
foreach (var alarm in testCases)
{
var diagnosis = AlarmKnowledgeBase.TryGetDiagnosis(alarm);
results.Add(new
{
Alarm = alarm,
FoundInKnowledgeBase = diagnosis != null,
Explanation = diagnosis?.Explanation ?? "NOT FOUND - Would call Mistral API",
CausesCount = diagnosis?.Causes.Count ?? 0,
NextStepsCount = diagnosis?.NextSteps.Count ?? 0
});
}
return Ok(new
{
TestTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
TotalTests = testCases.Length,
FoundInKnowledgeBase = results.Count(r => ((dynamic)r).FoundInKnowledgeBase),
WouldCallMistral = results.Count(r => !((dynamic)r).FoundInKnowledgeBase),
Results = results
});
}
/// <summary>
/// Test endpoint for the full AI diagnostic flow (knowledge base + Mistral API).
/// No auth required. Remove before production.
/// Usage: GET /api/TestDiagnoseError?errorDescription=SomeAlarm
/// </summary>
[HttpGet(nameof(TestDiagnoseError))]
public async Task<ActionResult> TestDiagnoseError(string errorDescription = "AbnormalGridVoltage", string language = "en")
{
// 1. Try static lookup (KB for English, pre-generated translations for others)
var staticResult = DiagnosticService.TryGetTranslation(errorDescription, language);
if (staticResult is not null)
{
return Ok(new
{
Source = "KnowledgeBase",
Alarm = errorDescription,
MistralEnabled = DiagnosticService.IsEnabled,
staticResult.Explanation,
staticResult.Causes,
staticResult.NextSteps
});
}
// 2. If not found, try Mistral with the correct language
if (!DiagnosticService.IsEnabled)
return Ok(new { Source = "None", Alarm = errorDescription, Message = "Not in knowledge base and Mistral API key not configured." });
var aiResult = await DiagnosticService.TestCallMistralAsync(errorDescription, language);
if (aiResult is null)
return Ok(new { Source = "MistralFailed", Alarm = errorDescription, Message = "Mistral API call failed or returned empty." });
return Ok(new
{
Source = "MistralAI",
Alarm = errorDescription,
aiResult.Explanation,
aiResult.Causes,
aiResult.NextSteps
});
}
// ── Weekly Performance Report ──────────────────────────────────────
/// <summary>
/// Generates a weekly performance report from an Excel file in tmp_report/{installationId}.xlsx
/// Returns JSON with daily data, weekly totals, ratios, and AI insight.
/// </summary>
[HttpGet(nameof(GetWeeklyReport))]
public async Task<ActionResult<WeeklyReportResponse>> GetWeeklyReport(Int64 installationId, Token authToken, string? language = null)
{
var user = Db.GetSession(authToken)?.User;
if (user == null)
return Unauthorized();
var installation = Db.GetInstallationById(installationId);
if (installation is null || !user.HasAccessTo(installation))
return Unauthorized();
var filePath = Environment.CurrentDirectory + "/tmp_report/" + installationId + ".xlsx";
if (!System.IO.File.Exists(filePath))
return NotFound($"No report data file found. Please place the Excel export at: tmp_report/{installationId}.xlsx");
try
{
var lang = language ?? user.Language ?? "en";
var report = await WeeklyReportService.GenerateReportAsync(installationId, installation.InstallationName, lang);
return Ok(report);
}
catch (Exception ex)
{
Console.Error.WriteLine($"[GetWeeklyReport] Error: {ex.Message}");
return BadRequest($"Failed to generate report: {ex.Message}");
}
}
/// <summary>
/// Sends the weekly report as a formatted HTML email.
/// </summary>
[HttpPost(nameof(SendWeeklyReportEmail))]
public async Task<ActionResult> SendWeeklyReportEmail(Int64 installationId, string emailAddress, Token authToken)
{
var user = Db.GetSession(authToken)?.User;
if (user == null)
return Unauthorized();
var installation = Db.GetInstallationById(installationId);
if (installation is null || !user.HasAccessTo(installation))
return Unauthorized();
try
{
var lang = user.Language ?? "en";
var report = await WeeklyReportService.GenerateReportAsync(installationId, installation.InstallationName, lang);
await ReportEmailService.SendReportEmailAsync(report, emailAddress, lang);
return Ok(new { message = $"Report sent to {emailAddress}" });
}
catch (Exception ex)
{
Console.Error.WriteLine($"[SendWeeklyReportEmail] Error: {ex.Message}");
return BadRequest($"Failed to send report: {ex.Message}");
}
}
[HttpPut(nameof(UpdateFolder))]
public ActionResult<Folder> UpdateFolder([FromBody] Folder folder, Token authToken)
{
var session = Db.GetSession(authToken);
if (!session.Update(folder))
return Unauthorized();
return folder.HideParentIfUserHasNoAccessToParent(session!.User);
}
[HttpPut(nameof(MoveInstallation))]
public ActionResult MoveInstallation(Int64 installationId,Int64 parentId, Token authToken)
{
var session = Db.GetSession(authToken);
return session.MoveInstallation(installationId, parentId)
? Ok()
: Unauthorized();
}
[HttpPost(nameof(UpdateFirmware))]
public async Task<ActionResult> UpdateFirmware(Int64 batteryNode, Int64 installationId,String version,Token authToken)
{
var session = Db.GetSession(authToken);
var installationToUpdate = Db.GetInstallationById(installationId);
if (installationToUpdate != null)
{
_ = session.RunScriptInBackground(installationToUpdate.VpnIp, batteryNode,version,installationToUpdate.Product);
}
return Ok();
}
private static Dictionary<string, JobStatus> JobStatuses = new Dictionary<string, JobStatus>();
[HttpPost("StartDownloadBatteryLog")]
public async Task<ActionResult<string>> StartDownloadBatteryLog(long batteryNode, long installationId, Token authToken)
{
var session = Db.GetSession(authToken);
var installationToDownload = Db.GetInstallationById(installationId);
if (installationToDownload != null)
{
string jobId = Guid.NewGuid().ToString();
_ = Task.Run(async () =>
{
await session.RunDownloadLogScript(installationToDownload.VpnIp, batteryNode, installationToDownload.Product);
string fileName = $"{installationToDownload.VpnIp}-node{batteryNode}-{DateTime.Now:dd-MM-yyyy}.bin";
string filePath = $"/home/ubuntu/backend/downloadBatteryLog/{fileName}";
if (System.IO.File.Exists(filePath))
{
SaveJobStatus(jobId, "Completed", fileName:fileName);
}
else
{
SaveJobStatus(jobId, "Failed");
}
});
// Store initial job status in in-memory storage
SaveJobStatus(jobId, "Processing");
return Ok(jobId);
}
return NotFound();
}
[HttpGet("DownloadBatteryLog")]
public async Task<ActionResult> DownloadBatteryLog(string jobId)
{
Console.WriteLine("-----------------------------------Start uploading battery log-----------------------------------");
var jobStatus = JobStatuses.TryGetValue(jobId, out var status) ? status : null;
if (jobStatus == null || jobStatus.Status != "Completed" || string.IsNullOrEmpty(jobStatus.FileName))
{
return NotFound();
}
string fileName = jobStatus.FileName;
string filePath = $"/home/ubuntu/backend/downloadBatteryLog/{fileName}";
if (!System.IO.File.Exists(filePath))
{
return NotFound();
}
string contentType = "application/octet-stream";
var memory = new MemoryStream();
await using (var stream = new FileStream(filePath, FileMode.Open))
{
await stream.CopyToAsync(memory);
}
memory.Position = 0;
var fileContentResult = new FileContentResult(memory.ToArray(), contentType)
{
//FileDownloadName = Path.GetFileName(filePath)
FileDownloadName = fileName
};
Console.WriteLine("-----------------------------------Stop uploading battery log-----------------------------------");
return fileContentResult;
}
[HttpDelete("DeleteBatteryLog")]
public IActionResult DeleteBatteryLog(string fileName)
{
Console.WriteLine("-----------------------------------Start deleting downloaded battery log-----------------------------------");
string filePath = $"/home/ubuntu/backend/downloadBatteryLog/{fileName}";
try
{
if (System.IO.File.Exists(filePath))
{
System.IO.File.Delete(filePath);
Console.WriteLine("-----------------------------------Stop deleting downloaded battery log-----------------------------------");
return Ok();
}
else
{
return NotFound("File not found.");
}
}
catch (Exception ex)
{
return StatusCode(500, $"Internal server error: {ex.Message}");
}
}
private void SaveJobStatus(string jobId, string status, string fileName = null)
{
JobStatuses[jobId] = new JobStatus
{
JobId = jobId,
Status = status,
FileName = fileName,
StartTime = DateTime.UtcNow // Initialize StartTime when saving
};
}
[HttpGet("GetJobResult")]
public ActionResult GetJobResult(string jobId)
{
if (string.IsNullOrEmpty(jobId))
{
return BadRequest(new { status = "Error", message = "Job ID is required." });
}
if (!JobStatuses.TryGetValue(jobId, out var jobStatus))
{
return NotFound();
}
if (jobStatus.Status == "Completed")
{
return Ok(new { status = "Completed", fileName = jobStatus.FileName });
}
else if (jobStatus.Status == "Failed")
{
return StatusCode(500, new { status = "Failed", message = "Job processing failed." });
}
else if (jobStatus.Status == "Processing")
{
// Check for timeout
var startTime = jobStatus.StartTime;
var currentTime = DateTime.UtcNow;
if ((currentTime - startTime).TotalMinutes > 60)//60 minutes as timeout => Running multiple tasks in parallel on a crowded backend server will increase the time each task takes to complete
{
return StatusCode(500, new { status = "Failed", message = "Job in back end timeout exceeded." });
}
return Ok(new { status = "Processing" });
}
else
{
return BadRequest(new { status = "Unknown", message = "Unknown job status." });
}
}
[HttpPost(nameof(InsertNewAction))]
public async Task<ActionResult<IEnumerable<Object>>> InsertNewAction([FromBody] UserAction action, Token authToken)
{
var session = Db.GetSession(authToken);
var actionSuccess = await session.InsertUserAction(action);
return actionSuccess ? Ok() : Unauthorized();
}
[HttpPost(nameof(UpdateAction))]
public async Task<ActionResult<IEnumerable<Object>>> UpdateAction([FromBody] UserAction action, Token authToken)
{
var session = Db.GetSession(authToken);
var actionSuccess = await session.UpdateUserAction(action);
return actionSuccess ? Ok() : Unauthorized();
}
[HttpPost(nameof(DeleteAction))]
public async Task<ActionResult<IEnumerable<Object>>> DeleteAction(Int64 actionId, Token authToken)
{
var session = Db.GetSession(authToken);
var actionSuccess = await session.DeleteUserAction(actionId);
return actionSuccess ? Ok() : Unauthorized();
}
[HttpPost(nameof(EditInstallationConfig))]
public async Task<ActionResult<IEnumerable<Object>>> EditInstallationConfig([FromBody] Configuration config, Int64 installationId,int product,Token authToken)
{
var session = Db.GetSession(authToken);
string configString = product switch
{
0 => config.GetConfigurationSalimax(), // Salimax
3 => config.GetConfigurationSodistoreMax(), // SodiStoreMax
2 => config.GetConfigurationSodistoreHome(), // SodiStoreHome
_ => config.GetConfigurationString() // fallback
};
Console.WriteLine("CONFIG IS " + configString);
//Send configuration changes
var success = await session.SendInstallationConfig(installationId, config);
// Record configuration change
if (success)
{
// // Update Configuration colum in Installation table
// var installation = Db.GetInstallationById(installationId);
//
// installation.Configuration = JsonConvert.SerializeObject(config);
//
// if (!installation.Apply(Db.Update))
// return StatusCode(500, "Failed to update installation configuration in database");
var action = new UserAction
{
InstallationId = installationId,
Timestamp = DateTime.UtcNow,
Description = configString
};
var actionSuccess = await session.InsertUserAction(action);
return actionSuccess ? Ok() : StatusCode(500, "Failed to record Configuration changes in History of Action");
}
return Unauthorized();
}
[HttpPut(nameof(MoveFolder))]
public ActionResult MoveFolder(Int64 folderId,Int64 parentId, Token authToken)
{
var session = Db.GetSession(authToken);
return session.MoveFolder(folderId, parentId)
? Ok()
: Unauthorized();
}
[HttpDelete(nameof(DeleteUser))]
public ActionResult DeleteUser(Int64 userId, Token authToken)
{
var session = Db.GetSession(authToken);
var user = Db.GetUserById(userId);
return session.Delete(user)
? Ok()
: Unauthorized();
}
[HttpDelete(nameof(DeleteInstallation))]
public async Task<ActionResult> DeleteInstallation(Int64 installationId, Token authToken)
{
var session = Db.GetSession(authToken);
var installation = Db.GetInstallationById(installationId);
return await session.Delete(installation)
? Ok()
: Unauthorized();
}
[HttpDelete(nameof(DeleteFolder))]
public ActionResult DeleteFolder(Int64 folderId, Token authToken)
{
var session = Db.GetSession(authToken);
var folder = Db.GetFolderById(folderId);
return session.Delete(folder)
? Ok()
: Unauthorized();
}
[HttpPost(nameof(ResetPasswordRequest))]
public async Task<ActionResult<IEnumerable<Object>>> ResetPasswordRequest(String username)
{
var user = Db.GetUserByEmail(username);
if (user is null)
return Unauthorized();
var session = new Session(user.HidePassword().HideParentIfUserHasNoAccessToParent(user));
var success = Db.Create(session);
return success && await Db.SendPasswordResetEmail(user, session.Token)
? Ok()
: Unauthorized();
}
[HttpGet(nameof(ResetPassword))]
public ActionResult<Object> ResetPassword(Token token)
{
var user = Db.GetSession(token)?.User;
if (user is null)
return Unauthorized();
Db.DeleteUserPassword(user);
return Redirect($"https://monitor.inesco.energy/?username={user.Email}&reset=true"); // TODO: move to settings file
}
// ── Alarm Review Campaign ────────────────────────────────────────────────
[HttpPost(nameof(SendTestAlarmReview))]
public async Task<ActionResult> SendTestAlarmReview()
{
await AlarmReviewService.SendTestBatchAsync();
return Ok(new { message = "Test review email sent to liu@inesco.energy. Check your inbox." });
}
[HttpPost(nameof(StartAlarmReviewCampaign))]
public ActionResult StartAlarmReviewCampaign()
{
AlarmReviewService.StartCampaign();
return Ok(new { message = "Alarm review campaign started." });
}
[HttpPost(nameof(StopAlarmReviewCampaign))]
public ActionResult StopAlarmReviewCampaign()
{
AlarmReviewService.StopCampaign();
return Ok(new { message = "Campaign paused — progress preserved. Use ResumeAlarmReviewCampaign to restart timers." });
}
[HttpPost(nameof(ResumeAlarmReviewCampaign))]
public ActionResult ResumeAlarmReviewCampaign()
{
AlarmReviewService.ResumeCampaign();
return Ok(new { message = "Campaign resumed — timers restarted from existing progress." });
}
[HttpPost(nameof(ResetAlarmReviewCampaign))]
public ActionResult ResetAlarmReviewCampaign()
{
AlarmReviewService.ResetCampaign();
return Ok(new { message = "Campaign fully reset — all progress deleted. Use StartAlarmReviewCampaign to begin again." });
}
[HttpGet(nameof(CorrectAlarm))]
public ActionResult CorrectAlarm(int batch, string key)
{
var html = AlarmReviewService.GetCorrectionPage(batch, key);
return Content(html, "text/html");
}
[HttpPost(nameof(ApplyAlarmCorrection))]
public ActionResult ApplyAlarmCorrection([FromBody] AlarmCorrectionRequest req)
{
if (req == null) return BadRequest();
var correction = new DiagnosticResponse
{
Explanation = req.Explanation ?? "",
Causes = req.Causes ?? new List<string>(),
NextSteps = req.NextSteps ?? new List<string>(),
};
var ok = AlarmReviewService.ApplyCorrection(req.BatchNumber, req.AlarmKey ?? "", correction);
return ok ? Ok(new { message = "Korrektur gespeichert." }) : BadRequest("Batch or alarm not found.");
}
[HttpGet(nameof(ReviewAlarms))]
public ActionResult ReviewAlarms(int batch, string reviewer)
{
var html = AlarmReviewService.GetReviewPage(batch, reviewer);
if (html is null) return NotFound("Batch not found or reviewer not recognised.");
return Content(html, "text/html");
}
[HttpPost(nameof(SubmitAlarmReview))]
public async Task<ActionResult> SubmitAlarmReview(int batch, string? reviewer, [FromBody] List<ReviewFeedback>? feedbacks)
{
// Batch 0 = test mode — run dry-run synthesis and return preview HTML (nothing is saved)
if (batch == 0)
{
var previewHtml = await AlarmReviewService.PreviewSynthesisAsync(feedbacks);
return Ok(new { preview = previewHtml });
}
var ok = AlarmReviewService.SubmitFeedback(batch, reviewer, feedbacks);
return ok ? Ok(new { message = "Feedback saved. Thank you!" })
: BadRequest("Batch not found, reviewer not recognised, or already submitted.");
}
[HttpGet(nameof(GetAlarmReviewStatus))]
public ActionResult GetAlarmReviewStatus()
{
return Ok(AlarmReviewService.GetStatus());
}
[HttpGet(nameof(DownloadCheckedKnowledgeBase))]
public ActionResult DownloadCheckedKnowledgeBase()
{
var content = AlarmReviewService.GetCheckedFileContent();
if (content is null) return NotFound("AlarmKnowledgeBaseChecked.cs has not been generated yet.");
return File(System.Text.Encoding.UTF8.GetBytes(content),
"text/plain",
"AlarmKnowledgeBaseChecked.cs");
}
}