Compare commits

..

7 Commits

Author SHA1 Message Date
Yinyin Liu c076d55407 AI diagnosis UX improvements: status-aware, time-filtered, simpler explanations
- Only show AI diagnosis when installation status is red/orange (not green/offline)
- Filter alarms to last 24 hours to avoid showing outdated issues
- Show alarm name first with "Last seen" timestamp instead of "AI Diagnosis" label
- Update Mistral prompt for shorter, non-technical bullet-point explanations
- Fix Mistral JSON parsing when response wrapped in markdown code fences
- Add TestDiagnoseError endpoint for testing full AI flow without auth

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:16:35 +01:00
Yinyin Liu 9a723c0a6f Merge branch 'main' into integrated_AI 2026-02-16 15:18:54 +01:00
Yinyin Liu 6460328eb0 Safe mode for AI diagnostics, align alarm keys with device enums, and multi-diagnosis frontend
- Remove API key gate so knowledge base works without Mistral key
- Return 204 No Content instead of 500 when no diagnosis available
- Rewrite AlarmKnowledgeBase keys to match Sinexcel property names and Growatt enum names
- Fix SQLite OrderBy crash in DiagnosticService
- Frontend: show latest 3 unique alarms with independent expand/collapse and handle 204

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 11:32:49 +01:00
Yinyin Liu 0c918e86ae Fix build errors: FrozenDictionary (.NET 8) and Flurl 3.x API compatibility
- Replace FrozenDictionary with IReadOnlyDictionary for .NET 6 compat
- Use WithHeader instead of SetHeader for Flurl.Http 3.2.4
- Fix FlurlHttpException error logging for Flurl 3.x

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 07:55:43 +01:00
Yinyin Liu 2da2ded84c Merge branch 'main' into integrated_AI 2026-02-12 07:45:22 +01:00
Yinyin Liu 68f4006f55 Switch AI diagnostics from OpenAI to Mistral and use .env for API key
- Changed API endpoint to api.mistral.ai, model to mistral-small-latest
- Replaced openAiConfig.json with .env file for secure API key storage
- Added .env loader in Program.cs, added .env to .gitignore

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 07:45:16 +01:00
Yinyin Liu e7f8aacc34 Integrate AI on Alarm 2026-02-06 12:57:12 +01:00
10 changed files with 2268 additions and 4 deletions

View File

@ -43,6 +43,9 @@
<None Update="Resources/s3cmd.py">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update=".env">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Remove="DbBackups\db-1698326303.sqlite" />
<None Remove="DbBackups\db-1698327045.sqlite" />
<None Remove="DbBackups\db-1699453468.sqlite" />

View File

@ -5,6 +5,7 @@ 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;
@ -740,7 +741,127 @@ public class Controller : ControllerBase
? 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);
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")
{
// 1. Try knowledge base first
var kbResult = AlarmKnowledgeBase.TryGetDiagnosis(errorDescription);
if (kbResult is not null)
{
return Ok(new
{
Source = "KnowledgeBase",
Alarm = errorDescription,
MistralEnabled = DiagnosticService.IsEnabled,
kbResult.Explanation,
kbResult.Causes,
kbResult.NextSteps
});
}
// 2. If not in KB, try Mistral directly with a test prompt
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);
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
});
}
[HttpPut(nameof(UpdateFolder))]
public ActionResult<Folder> UpdateFolder([FromBody] Folder folder, Token authToken)
{

View File

@ -2,6 +2,7 @@ using System.Diagnostics;
using Flurl.Http;
using Hellang.Middleware.ProblemDetails;
using InnovEnergy.App.Backend.Database;
using InnovEnergy.App.Backend.Services;
using InnovEnergy.App.Backend.Websockets;
using InnovEnergy.App.Backend.DeleteOldData;
using Microsoft.AspNetCore.HttpOverrides;
@ -24,6 +25,8 @@ public static class Program
Watchdog.NotifyReady();
Db.Init();
LoadEnvFile();
DiagnosticService.Initialize();
var builder = WebApplication.CreateBuilder(args);
RabbitMqManager.InitializeEnvironment();
@ -87,6 +90,33 @@ public static class Program
app.Run();
}
private static void LoadEnvFile()
{
var envPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ".env");
if (!File.Exists(envPath))
envPath = ".env"; // fallback for dev
if (!File.Exists(envPath))
return;
foreach (var line in File.ReadAllLines(envPath))
{
var trimmed = line.Trim();
if (trimmed.Length == 0 || trimmed.StartsWith('#'))
continue;
var idx = trimmed.IndexOf('=');
if (idx <= 0)
continue;
var key = trimmed[..idx].Trim();
var value = trimmed[(idx + 1)..].Trim();
Environment.SetEnvironmentVariable(key, value);
}
}
private static OpenApiInfo OpenApiInfo { get; } = new OpenApiInfo
{
Title = "Inesco Backend API",

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,204 @@
using System.Collections.Concurrent;
using Flurl.Http;
using InnovEnergy.App.Backend.Database;
using InnovEnergy.App.Backend.DataTypes;
using Newtonsoft.Json;
namespace InnovEnergy.App.Backend.Services;
/// <summary>
/// Calls Mistral AI to generate plain-English diagnostics for errors/warnings.
/// Caches responses in-memory keyed by error description so the same
/// error code is only sent to the API once.
/// </summary>
public static class DiagnosticService
{
private static string _apiKey = "";
/// <summary>In-memory cache: errorDescription → parsed response.</summary>
private static readonly ConcurrentDictionary<string, DiagnosticResponse> Cache = new();
// ── initialisation ──────────────────────────────────────────────
public static void Initialize()
{
var apiKey = Environment.GetEnvironmentVariable("MISTRAL_API_KEY");
if (string.IsNullOrWhiteSpace(apiKey))
{
Console.Error.WriteLine("[DiagnosticService] MISTRAL_API_KEY not set AI diagnostics disabled.");
return;
}
_apiKey = apiKey;
Console.WriteLine("[DiagnosticService] initialised.");
}
public static bool IsEnabled => !string.IsNullOrEmpty(_apiKey);
// ── public entry-point ──────────────────────────────────────────
/// <summary>
/// Returns a diagnosis for <paramref name="errorDescription"/>.
/// First checks the static AlarmKnowledgeBase for known Sinexcel/Growatt alarms.
/// Falls back to in-memory cache, then calls Mistral AI only for unknown alarms.
/// </summary>
public static async Task<DiagnosticResponse?> DiagnoseAsync(Int64 installationId, string errorDescription)
{
// 1. Check the static knowledge base first (no API call needed)
var knownDiagnosis = AlarmKnowledgeBase.TryGetDiagnosis(errorDescription);
if (knownDiagnosis is not null)
{
Console.WriteLine($"[DiagnosticService] Found diagnosis in knowledge base for: {errorDescription}");
return knownDiagnosis;
}
// 2. If AI is not enabled, we can't proceed further
if (!IsEnabled) return null;
// 3. Check in-memory cache for previously fetched AI diagnoses
if (Cache.TryGetValue(errorDescription, out var cached))
return cached;
// 4. Gather context from the DB for AI prompt
var installation = Db.GetInstallationById(installationId);
if (installation is null) return null;
var productName = ((ProductType)installation.Product).ToString();
var recentDescriptions = Db.Errors
.Where(e => e.InstallationId == installationId)
.OrderByDescending(e => e.Date)
.ThenByDescending(e => e.Time)
.ToList() // materialize before LINQ-to-objects ops
.Select(e => e.Description)
.Distinct() // deduplicate — same error repeated adds no signal
.Take(5)
.ToList();
// 5. Build prompt and call Mistral API (only for unknown alarms)
Console.WriteLine($"[DiagnosticService] Calling Mistral for unknown alarm: {errorDescription}");
var prompt = BuildPrompt(errorDescription, productName, recentDescriptions);
var response = await CallMistralAsync(prompt);
if (response is null) return null;
// 6. Store in cache for future requests
Cache.TryAdd(errorDescription, response);
return response;
}
// ── test helper (no DB dependency) ─────────────────────────────
/// <summary>
/// Calls Mistral directly with a generic prompt. For testing only - no DB lookup.
/// </summary>
public static async Task<DiagnosticResponse?> TestCallMistralAsync(string errorDescription)
{
if (!IsEnabled) return null;
// Check cache first
if (Cache.TryGetValue(errorDescription, out var cached))
return cached;
var prompt = BuildPrompt(errorDescription, "SodioHome", new List<string>());
var response = await CallMistralAsync(prompt);
if (response is not null)
Cache.TryAdd(errorDescription, response);
return response;
}
// ── prompt ──────────────────────────────────────────────────────
private static string BuildPrompt(string errorDescription, string productName, List<string> recentErrors)
{
var recentList = recentErrors.Count > 0
? string.Join(", ", recentErrors)
: "none";
return $@"You are a technician for Innovenergy {productName} battery energy storage systems.
These are lithium-ion BESS units with a BMS, PV inverter, and grid inverter.
Error: {errorDescription}
Other recent errors: {recentList}
Explain for a non-technical homeowner. Keep it very short and simple:
- explanation: 1 short sentence, no jargon
- causes: 2-3 bullet points, plain language
- nextSteps: 2-3 simple action items a homeowner can understand
Reply with ONLY valid JSON, no markdown:
{{""explanation"":""1 short sentence"",""causes"":[""...""],""nextSteps"":[""...""]}}
";
}
// ── Mistral HTTP call ────────────────────────────────────────────
private static readonly string MistralUrl = "https://api.mistral.ai/v1/chat/completions";
private static async Task<DiagnosticResponse?> CallMistralAsync(string userPrompt)
{
try
{
var requestBody = new
{
model = "mistral-small-latest", // cost-efficient, fast; swap to "mistral-large-latest" if quality needs tuning
messages = new[]
{
new { role = "user", content = userPrompt }
},
max_tokens = 400,
temperature = 0.2 // low temperature for factual consistency
};
var responseText = await MistralUrl
.WithHeader("Authorization", $"Bearer {_apiKey}")
.PostJsonAsync(requestBody)
.ReceiveString();
// parse Mistral envelope (same structure as OpenAI)
var envelope = JsonConvert.DeserializeObject<dynamic>(responseText);
var content = (string?) envelope?.choices?[0]?.message?.content;
if (string.IsNullOrWhiteSpace(content))
{
Console.Error.WriteLine("[DiagnosticService] Mistral returned empty content.");
return null;
}
// strip markdown code fences if Mistral wraps the JSON in ```json ... ```
var json = content.Trim();
if (json.StartsWith("```"))
{
var firstNewline = json.IndexOf('\n');
if (firstNewline >= 0) json = json[(firstNewline + 1)..];
if (json.EndsWith("```")) json = json[..^3];
json = json.Trim();
}
// parse the JSON the model produced
var diagnostic = JsonConvert.DeserializeObject<DiagnosticResponse>(json);
return diagnostic;
}
catch (FlurlHttpException httpEx)
{
Console.Error.WriteLine($"[DiagnosticService] HTTP error {httpEx.StatusCode}: {httpEx.Message}");
return null;
}
catch (Exception ex)
{
Console.Error.WriteLine($"[DiagnosticService] {ex.Message}");
return null;
}
}
}
// ── config / response models ────────────────────────────────────────────────
public class DiagnosticResponse
{
public string Explanation { get; set; } = "";
public IReadOnlyList<string> Causes { get; set; } = Array.Empty<string>();
public IReadOnlyList<string> NextSteps { get; set; } = Array.Empty<string>();
}

View File

@ -461,6 +461,7 @@ function Installation(props: singleInstallationProps) {
<Log
errorLoadingS3Data={errorLoadingS3Data}
id={props.current_installation.id}
status={props.current_installation.status}
></Log>
}
/>

View File

@ -1,7 +1,9 @@
import React, { useContext, useEffect, useState } from 'react';
import {
Alert,
Box,
Card,
CircularProgress,
Container,
Divider,
Grid,
@ -17,7 +19,7 @@ import { AxiosError, AxiosResponse } from 'axios/index';
import routes from '../../../Resources/routes.json';
import { useNavigate } from 'react-router-dom';
import { TokenContext } from '../../../contexts/tokenContext';
import { ErrorMessage } from '../../../interfaces/S3Types';
import { ErrorMessage, DiagnosticResponse } from '../../../interfaces/S3Types';
import Button from '@mui/material/Button';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';
@ -25,6 +27,7 @@ import Checkbox from '@mui/material/Checkbox';
interface LogProps {
errorLoadingS3Data: boolean;
id: number;
status?: number;
}
function Log(props: LogProps) {
@ -45,6 +48,10 @@ function Log(props: LogProps) {
const tokencontext = useContext(TokenContext);
const { removeToken } = tokencontext;
const [diagnoses, setDiagnoses] = useState<{ description: string; lastSeen: string; response: DiagnosticResponse }[]>([]);
const [diagnosisLoading, setDiagnosisLoading] = useState(false);
const [expandedDiagnoses, setExpandedDiagnoses] = useState<Set<number>>(new Set());
useEffect(() => {
axiosConfig
.get(`/GetAllErrorsForInstallation?id=${props.id}`)
@ -71,6 +78,66 @@ function Log(props: LogProps) {
});
}, [updateCount]);
// fetch AI diagnosis for the latest 3 unique errors/warnings
// only when installation status is red (2) or orange (1)
useEffect(() => {
// skip diagnosis if status is not alarm (2) or warning (1)
if (props.status !== 1 && props.status !== 2) {
setDiagnoses([]);
return;
}
// filter to last 24 hours only
const now = new Date();
const cutoff = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const ignore = new Set(['NoAlarm', '0', '']);
const all = [...errors, ...warnings]
.filter(item => {
if (ignore.has(item.description.trim())) return false;
const itemDate = new Date(item.date + 'T' + item.time);
return itemDate >= cutoff;
})
.sort((a, b) => (b.date + ' ' + b.time).localeCompare(a.date + ' ' + a.time));
const seen = new Set<string>();
const targets: ErrorMessage[] = [];
for (const item of all) {
if (!seen.has(item.description)) {
seen.add(item.description);
targets.push(item);
if (targets.length >= 3) break;
}
}
if (targets.length === 0) {
setDiagnoses([]);
return;
}
// check if the targets changed compared to what we already have
const currentDescs = diagnoses.map(d => d.description).join('|');
const newDescs = targets.map(t => t.description).join('|');
if (currentDescs === newDescs) return;
setDiagnosisLoading(true);
Promise.all(
targets.map(target =>
axiosConfig
.get(`/DiagnoseError?installationId=${props.id}&errorDescription=${encodeURIComponent(target.description)}`)
.then((res: AxiosResponse<DiagnosticResponse>) => {
if (res.status === 204 || !res.data || !res.data.explanation) return null;
return { description: target.description, lastSeen: target.date + ' ' + target.time, response: res.data };
})
.catch(() => null)
)
).then(results => {
setDiagnoses(results.filter(r => r !== null) as { description: string; lastSeen: string; response: DiagnosticResponse }[]);
}).finally(() => {
setDiagnosisLoading(false);
});
}, [errors, warnings, props.status]);
const handleErrorButtonPressed = () => {
setErrorButtonPressed(!errorButtonPressed);
};
@ -108,6 +175,7 @@ function Log(props: LogProps) {
};
const warningDescriptionMap: { [key: string]: string } = {
// BMS warnings
"TaM1": "TaM1: BMS temperature high",
"TbM1": "TbM1: Battery temperature high",
"VBm1": "VBm1: Bus voltage low",
@ -124,10 +192,131 @@ function Log(props: LogProps) {
"MPMM": "MPMM: Midpoint wiring problem",
"TCdi": "TCdi: Temperature difference between strings high",
"LMPW": "LMPW: String voltages unbalance warning",
"TOCW": "TOCW: Top of Charge requested"
"TOCW": "TOCW: Top of Charge requested",
// Sinexcel warnings (WARNING/INFO severity)
"Inverted sequenceof grid voltage": "Grid phase sequence reversed",
"Excessivelyhigh ambient temperature": "Ambient temperature too high",
"Excessive radiator temperature": "Radiator/heatsink temperature high",
"Island protection": "Island protection active (auto-recovers)",
"Battery 1over voltage": "Battery 1 voltage too high",
"Battery 1under voltage": "Battery 1 voltage too low",
"Battery 1discharge end": "Battery 1 discharge complete (auto-recovers)",
"Battery 1inverted": "Battery 1 polarity reversed!",
"Battery 2over voltage": "Battery 2 voltage too high",
"Battery 2under voltage": "Battery 2 voltage too low",
"Battery 2discharge end": "Battery 2 discharge complete (auto-recovers)",
"Battery 2inverted": "Battery 2 polarity reversed!",
"PV 1notaccessed": "PV string 1 not accessible",
"PV 1over voltage": "PV string 1 voltage too high",
"PV 2notaccessed": "PV string 2 not accessible",
"PV 2over voltage": "PV string 2 voltage too high",
"DC busover voltage": "DC bus voltage too high",
"DC busunder voltage": "DC bus voltage too low",
"Inverter soft start failure": "Inverter soft-start failed",
"Battery 1soft start failure": "Battery 1 soft-start failed",
"Battery 2soft start failure": "Battery 2 soft-start failed",
"Output voltageDC overlimit": "DC component in output voltage high",
"Output currentDC overlimit": "DC component in output current high",
"Poorgrounding": "Poor ground connection detected",
"PV 1soft startfailure": "PV 1 soft-start failed",
"PV 2soft startfailure": "PV 2 soft-start failed",
"PCBover temperature": "PCB temperature too high",
"DC converter over temperature": "DC converter temperature high",
"Busslow over voltage": "Slow bus over-voltage",
"DC converter over voltage": "DC converter voltage high",
"DC converter over current": "DC converter current high",
"DC converter resonator over current": "DC converter resonator overcurrent",
"PV 1insufficient power": "PV 1 power insufficient (auto-recovers)",
"PV 2insufficient power": "PV 2 power insufficient (auto-recovers)",
"Battery 1insufficient power": "Battery 1 power insufficient (auto-recovers)",
"Battery 2insufficiency power": "Battery 2 power insufficient",
"Lithium battery 1 chargeforbidden": "Lithium battery 1 charging forbidden",
"Lithium battery 1 dischargeforbidden": "Lithium battery 1 discharging forbidden",
"Lithium battery 2 chargeforbidden": "Lithium battery 2 charging forbidden",
"Lithium battery 2 dischargeforbidden": "Lithium battery 2 discharging forbidden",
"Lithium battery 1full": "Lithium battery 1 fully charged",
"Lithium battery 1 dischargeend": "Lithium battery 1 discharge end",
"Lithium battery 2full": "Lithium battery 2 fully charged",
"Lithium battery 2 dischargeend": "Lithium battery 2 discharge end",
"Inverter over temperaturealarm": "Inverter over-temperature alarm",
"Inverter over temperature": "Inverter temperature high",
"DC converter over temperaturealarm": "DC converter over-temperature alarm",
"Systemderating": "System power derating active",
"PVaccessmethod erroralarm": "PV access method error",
"Parallelmodule missing": "Parallel module missing",
"Duplicatemachine numbersforparallel modules": "Duplicate parallel module IDs",
"Para meterconflictin parallelmodule": "Parameter conflict in parallel modules",
"Reservedalarms 4": "Reserved alarm 4",
"InverterSealPulse": "Inverter seal pulse active",
"PV 3over voltage": "PV 3 voltage too high",
"PV 3average current anomaly": "PV 3 current anomaly",
"PV 4over voltage": "PV 4 voltage too high",
"PV 4average current anomaly": "PV 4 current anomaly",
"PV 3soft startfailure": "PV 3 soft-start failed",
"PV 4soft startfailure": "PV 4 soft-start failed",
"Batteryaccessmethod error": "Battery access method error",
"Reservedalarms 5": "Reserved alarm 5",
"Battery 1backup prohibited": "Battery 1 backup prohibited",
"Battery 2backup prohibited": "Battery 2 backup prohibited",
"Bus soft startfailure": "Bus soft-start failed",
"Insufficient photovoltaic power": "Insufficient PV power",
"Photovoltaic 1 over current": "PV 1 overcurrent",
"Photovoltaic 2 over current": "PV 2 overcurrent",
"Photovoltaic 3 over current": "PV 3 overcurrent",
"Photovoltaic 4 over current": "PV 4 overcurrent",
"Battery 1over current": "Battery 1 overcurrent",
"Battery 2over current": "Battery 2 overcurrent",
"Battery 1charging sealingwave": "Battery 1 charge limiting",
"Battery 2charging sealingwave": "Battery 2 charge limiting",
// Growatt warnings
"Warning 200": "String fault",
"Warning 201": "PV string/PID terminals abnormal",
"Warning 203": "PV1 or PV2 short circuited",
"Warning 208": "DC fuse blown",
"Warning 209": "DC input voltage too high",
"Warning 219": "PID function abnormal",
"Warning 220": "PV string disconnected",
"Warning 221": "PV string current unbalanced",
"Warning 300": "No grid connection / grid power failure",
"Warning 301": "Grid voltage out of range",
"Warning 302": "Grid frequency out of range",
"Warning 303": "System overload",
"Warning 308": "Meter disconnected",
"Warning 309": "Meter L/N reversed",
"Warning 310": "N-PE voltage abnormal",
"Warning 311": "Phase sequence error (auto-adjusts)",
"Warning 400": "Fan failure",
"Warning 401": "Meter abnormal",
"Warning 402": "Optimizer communication abnormal",
"Warning 407": "Over-temperature",
"Warning 408": "NTC temperature sensor broken",
"Warning 411": "Sync signal abnormal",
"Warning 412": "Grid connection requirements not met",
"Warning 500": "Inverter-battery communication failed",
"Warning 501": "Battery disconnected",
"Warning 502": "Battery voltage too high",
"Warning 503": "Battery voltage too low",
"Warning 504": "Battery terminals reversed",
"Warning 505": "Lead-acid battery temp sensor disconnected",
"Warning 506": "Battery temperature out of range",
"Warning 507": "BMS fault: charging/discharging failed",
"Warning 508": "Lithium battery overload protection",
"Warning 509": "BMS communication abnormal",
"Warning 510": "BAT SPD function abnormal",
"Warning 600": "Output DC component bias abnormal",
"Warning 601": "High DC in output voltage",
"Warning 602": "Off-grid output voltage too low",
"Warning 603": "Off-grid output voltage too high",
"Warning 604": "Off-grid output overcurrent",
"Warning 605": "Off-grid bus voltage too low",
"Warning 606": "Off-grid output overload",
"Warning 609": "Balanced circuit abnormal"
};
const errorDescriptionMap: { [key: string]: string } = {
// BMS errors
"Tam": "Tam: Recoverable, BMS temperature too low",
"TaM2": "TaM2: Recoverable, BMS temperature too high",
"Tbm": "Tbm: Recoverable, Battery temperature too low",
@ -153,12 +342,208 @@ function Log(props: LogProps) {
"HTFS": "HTFS: Recoverable, Unrecoverable: Heater Fuse Blown",
"DATA": "DATA: Recoverable, Unrecoverable: Parameters out of range",
"LMPA": "LMPA: Unrecoverable, String voltages unbalance alarm",
"HEBT": "HEBT: Recoverable, oss of heartbeat"
"HEBT": "HEBT: Recoverable, oss of heartbeat",
// Sinexcel errors (ERROR severity - require manual intervention)
"Abnormal grid voltage": "Grid voltage abnormal",
"Abnormal grid frequency": "Grid frequency abnormal",
"Grid voltage phase loss": "Grid phase loss detected",
"Abnormal output voltage": "Output voltage abnormal",
"Abnormal output frequency": "Output frequency abnormal",
"Abnormalnullline": "Null/neutral line abnormal",
"Insulation fault": "Insulation fault detected",
"Leakage protection fault": "Leakage/ground fault protection tripped",
"Auxiliary power fault": "Auxiliary power supply fault",
"Fan fault": "Cooling fan fault",
"Model capacity fault": "Model/capacity configuration fault",
"Abnormal lightning arrester": "Surge protection device abnormal",
"Battery 1not connected": "Battery 1 not connected",
"Battery 2not connected": "Battery 2 not connected",
"AbnormalPV 1current sharing": "PV 1 current sharing abnormal",
"AbnormalPV 2current sharing": "PV 2 current sharing abnormal",
"DC bus voltage unbalance": "DC bus voltage unbalance",
"System output overload": "System output overloaded",
"Inverter overload": "Inverter overloaded",
"Inverter overload timeout": "Inverter overload timeout",
"Battery 1overload timeout": "Battery 1 overload timeout",
"Battery 2overload timeout": "Battery 2 overload timeout",
"DSP 1para meter setting fault": "DSP 1 parameter setting fault",
"DSP 2para meter setting fault": "DSP 2 parameter setting fault",
"DSPversion compatibility fault": "DSP version compatibility fault",
"CPLDversion compatibility fault": "CPLD version compatibility fault",
"CPLD communication fault": "CPLD communication fault",
"DSP communication fault": "DSP communication fault",
"Relayself-checkfails": "Relay self-check failed",
"Abnormal inverter": "Abnormal inverter condition",
"Balancedcircuit overload timeout": "Balance circuit overload timeout",
"PV 1overload timeout": "PV 1 overload timeout",
"PV 2overload timeout": "PV 2 overload timeout",
"Abnormaloff-grid output voltage": "Off-grid output voltage abnormal",
"Parallel communicationalarm": "Parallel communication alarm",
"Inverter relayopen": "Inverter relay open",
"PV 3not connected": "PV 3 not connected",
"PV 4not connected": "PV 4 not connected",
"PV 3overload timeout": "PV 3 overload timeout",
"PV 4overload timeout": "PV 4 overload timeout",
"Abnormal diesel generator voltage": "Diesel generator voltage abnormal",
"Abnormal diesel generator frequency": "Diesel generator frequency abnormal",
"Diesel generator voltageoutof phase": "Diesel generator out of phase",
"Lead battery temperature abnormality": "Lead battery temperature abnormal",
"Abnormal grid current": "Grid current abnormal",
"Generator overload": "Generator overloaded",
"Opencircuitof power grid relay": "Grid relay open circuit",
"Shortcircuitof power grid relay": "Grid relay short circuit",
"generator Relayopencircuit": "Generator relay open circuit",
"generator Relayshortcircuit": "Generator relay short circuit",
"Load power overload": "Load power overload",
"Abnormal leakage self-check": "Leakage self-check abnormal",
// Sinexcel PROTECTION errors (require service - do not restart)
"PV 1power tube fault": "PV 1 power tube fault - Contact Service",
"PV 2power tube fault": "PV 2 power tube fault - Contact Service",
"Battery 1power tube fault": "Battery 1 power tube fault - Contact Service",
"Battery 2power tube fault": "Battery 2 power tube fault - Contact Service",
"Inverter power tube fault": "Inverter power tube fault - Contact Service",
"Hardware bus over voltage": "Hardware bus overvoltage - Contact Service",
"Hardware over current": "Hardware overcurrent - Contact Service",
"DC converter hardware over voltage": "DC converter hardware overvoltage - Contact Service",
"DC converter hardware over current": "DC converter hardware overcurrent - Contact Service",
"Inverter relayshort circuit": "Inverter relay short circuit - Contact Service",
"Reverse meter connection": "Meter connected in reverse - Contact Service",
"PV 3power tube failure": "PV 3 power tube failure - Contact Service",
"PV 4power tube Failure": "PV 4 power tube failure - Contact Service",
"PV 3reverse connection": "PV 3 reverse connection - Contact Service",
"PV 4reverse connection": "PV 4 reverse connection - Contact Service",
"Diesel generator voltage reverse sequence": "Generator phase reversed - Contact Service",
// Growatt errors (PROTECTION severity)
"Error 309": "Grid ROCOF abnormal",
"Error 311": "Export limitation fail-safe",
"Error 400": "DCI bias abnormal",
"Error 402": "High DC in output current",
"Error 404": "Bus voltage sampling abnormal",
"Error 405": "Relay fault",
"Error 408": "Over-temperature protection",
"Error 409": "Bus voltage abnormal",
"Error 411": "Internal communication failure",
"Error 412": "Temperature sensor disconnected",
"Error 413": "IGBT drive fault",
"Error 414": "EEPROM error",
"Error 415": "Auxiliary power supply abnormal",
"Error 416": "DC/AC overcurrent protection",
"Error 417": "Communication protocol mismatch",
"Error 418": "DSP/COM firmware mismatch",
"Error 419": "DSP software/hardware mismatch",
"Error 421": "CPLD abnormal",
"Error 422": "Redundancy sampling inconsistent",
"Error 423": "PWM pass-through signal failure",
"Error 425": "AFCI self-test failure",
"Error 426": "PV current sampling abnormal",
"Error 427": "AC current sampling abnormal",
"Error 429": "BUS soft-boot failure",
"Error 430": "EPO fault",
"Error 431": "Monitoring chip BOOT verification failed",
"Error 500": "BMS-inverter communication failed",
"Error 501": "BMS: battery charge/discharge failed",
"Error 503": "Battery voltage exceeds threshold",
"Error 504": "Battery temperature out of range",
"Error 506": "Battery open-circuited",
"Error 507": "Battery overload protection",
"Error 508": "BUS2 voltage abnormal",
"Error 509": "BAT charge overcurrent protection",
"Error 510": "BAT discharge overcurrent protection",
"Error 511": "BAT soft start failed",
"Error 601": "Off-grid bus voltage low",
"Error 602": "Abnormal voltage at off-grid terminal",
"Error 603": "Off-grid soft start failed",
"Error 604": "Off-grid output voltage abnormal",
"Error 605": "Balanced circuit self-test failed",
"Error 606": "High DC in output voltage",
"Error 608": "Off-grid parallel signal abnormal",
"AFCI Fault": "Arc fault detected - Check PV connections",
"GFCI High": "High leakage current detected",
"PV Voltage High": "DC input voltage exceeds limit"
};
return (
<Container maxWidth="xl">
<Grid container>
{/* AI Diagnosis banner — shown when loading or diagnoses are available */}
{diagnosisLoading && (
<Grid item xs={12} md={12}>
<Card sx={{ marginTop: '20px', borderLeft: '4px solid #1976d2' }}>
<Box sx={{ padding: '16px', display: 'flex', alignItems: 'center', gap: '12px' }}>
<CircularProgress size={22} />
<Typography variant="body2" color="text.secondary">
<FormattedMessage id="ai_analyzing" defaultMessage="AI is analyzing..." />
</Typography>
</Box>
</Card>
</Grid>
)}
{!diagnosisLoading && diagnoses.map((diag, idx) => {
const isExpanded = expandedDiagnoses.has(idx);
return (
<Grid item xs={12} md={12} key={idx}>
<Card sx={{ marginTop: idx === 0 ? '20px' : '10px', borderLeft: '4px solid #1976d2' }}>
<Box sx={{ padding: '16px' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: '8px', mb: 1 }}>
<Typography variant="subtitle2" fontWeight="bold" color="primary">
{diag.description}
</Typography>
<Typography variant="caption" color="text.secondary">
Last seen: {diag.lastSeen}
</Typography>
</Box>
<Typography variant="body2">
{diag.response.explanation}
</Typography>
<Button
size="small"
onClick={() => {
const next = new Set(expandedDiagnoses);
if (isExpanded) next.delete(idx); else next.add(idx);
setExpandedDiagnoses(next);
}}
sx={{ textTransform: 'none', p: 0, mt: 1 }}
>
<FormattedMessage
id={isExpanded ? 'ai_show_less' : 'ai_show_details'}
defaultMessage={isExpanded ? 'Show less' : 'Show details'}
/>
</Button>
{isExpanded && (
<Box sx={{ mt: 1 }}>
<Typography variant="caption" fontWeight="bold">
<FormattedMessage id="ai_likely_causes" defaultMessage="Likely causes:" />
</Typography>
<ul style={{ margin: '4px 0', paddingLeft: '20px' }}>
{diag.response.causes.map((cause, i) => (
<li key={i}><Typography variant="caption">{cause}</Typography></li>
))}
</ul>
<Typography variant="caption" fontWeight="bold">
<FormattedMessage id="ai_next_steps" defaultMessage="Suggested next steps:" />
</Typography>
<ol style={{ margin: '4px 0', paddingLeft: '20px' }}>
{diag.response.nextSteps.map((step, i) => (
<li key={i}><Typography variant="caption">{step}</Typography></li>
))}
</ol>
</Box>
)}
</Box>
</Card>
</Grid>
);
})}
<Grid item xs={12} md={12}>
<Button
variant="contained"

View File

@ -346,6 +346,7 @@ function SalidomoInstallation(props: singleInstallationProps) {
<Log
errorLoadingS3Data={errorLoadingS3Data}
id={props.current_installation.id}
status={props.current_installation.status}
></Log>
}
/>

View File

@ -478,6 +478,7 @@ function SodioHomeInstallation(props: singleInstallationProps) {
<Log
errorLoadingS3Data={errorLoadingS3Data}
id={props.current_installation.id}
status={props.current_installation.status}
></Log>
}
/>

View File

@ -31,3 +31,9 @@ export interface Action {
description: string;
testingMode: boolean;
}
export interface DiagnosticResponse {
explanation: string;
causes: string[];
nextSteps: string[];
}