Integrate AI on Alarm

This commit is contained in:
Yinyin Liu 2026-02-06 12:57:12 +01:00
parent ed87a4b371
commit e7f8aacc34
7 changed files with 2148 additions and 4 deletions

View File

@ -5,6 +5,7 @@ using InnovEnergy.App.Backend.Database;
using InnovEnergy.App.Backend.DataTypes; using InnovEnergy.App.Backend.DataTypes;
using InnovEnergy.App.Backend.DataTypes.Methods; using InnovEnergy.App.Backend.DataTypes.Methods;
using InnovEnergy.App.Backend.Relations; using InnovEnergy.App.Backend.Relations;
using InnovEnergy.App.Backend.Services;
using InnovEnergy.App.Backend.Websockets; using InnovEnergy.App.Backend.Websockets;
using InnovEnergy.Lib.Utils; using InnovEnergy.Lib.Utils;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -736,7 +737,88 @@ public class Controller : ControllerBase
? Ok() ? Ok()
: Unauthorized(); : 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 OpenAI 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.");
if (!DiagnosticService.IsEnabled)
return StatusCode(503, "AI diagnostics not configured.");
var result = await DiagnosticService.DiagnoseAsync(installationId, errorDescription);
if (result is null)
return StatusCode(500, "Diagnosis failed please try again later.");
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
"Fan fault",
"Abnormal grid voltage",
"Battery 1not connected",
"Inverter power tube fault",
"Island protection",
// Growatt alarms
"Warning 300",
"Warning 500",
"Error 408",
"AFCI Fault",
// Unknown alarm (should return null - would call OpenAI)
"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 OpenAI 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),
WouldCallOpenAI = results.Count(r => !((dynamic)r).FoundInKnowledgeBase),
Results = results
});
}
[HttpPut(nameof(UpdateFolder))] [HttpPut(nameof(UpdateFolder))]
public ActionResult<Folder> UpdateFolder([FromBody] Folder folder, Token authToken) public ActionResult<Folder> UpdateFolder([FromBody] Folder folder, Token authToken)
{ {

View File

@ -2,6 +2,7 @@ using System.Diagnostics;
using Flurl.Http; using Flurl.Http;
using Hellang.Middleware.ProblemDetails; using Hellang.Middleware.ProblemDetails;
using InnovEnergy.App.Backend.Database; using InnovEnergy.App.Backend.Database;
using InnovEnergy.App.Backend.Services;
using InnovEnergy.App.Backend.Websockets; using InnovEnergy.App.Backend.Websockets;
using InnovEnergy.App.Backend.DeleteOldData; using InnovEnergy.App.Backend.DeleteOldData;
using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.HttpOverrides;
@ -24,6 +25,7 @@ public static class Program
Watchdog.NotifyReady(); Watchdog.NotifyReady();
Db.Init(); Db.Init();
DiagnosticService.Initialize();
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
RabbitMqManager.InitializeEnvironment(); RabbitMqManager.InitializeEnvironment();

View File

@ -0,0 +1,3 @@
{
"ApiKey": "sk-your-openai-api-key-here"
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,188 @@
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 OpenAI 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 configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "openAiConfig.json");
if (!File.Exists(configPath))
{
// Fallback: look relative to the working directory (useful in dev)
configPath = Path.Combine("Resources", "openAiConfig.json");
}
if (!File.Exists(configPath))
{
Console.Error.WriteLine("[DiagnosticService] openAiConfig.json not found AI diagnostics disabled.");
return;
}
var json = File.ReadAllText(configPath);
var config = JsonConvert.DeserializeObject<OpenAiConfig>(json);
if (config is null || string.IsNullOrWhiteSpace(config.ApiKey))
{
Console.Error.WriteLine("[DiagnosticService] ApiKey is empty AI diagnostics disabled.");
return;
}
_apiKey = config.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 OpenAI 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 + " " + e.Time) // Date/Time stored as strings in DB
.Select(e => e.Description)
.Distinct() // deduplicate — same error repeated adds no signal
.Take(5)
.ToList();
// 5. Build prompt and call OpenAI API (only for unknown alarms)
Console.WriteLine($"[DiagnosticService] Calling OpenAI for unknown alarm: {errorDescription}");
var prompt = BuildPrompt(errorDescription, productName, recentDescriptions);
var response = await CallOpenAiAsync(prompt);
if (response is null) return null;
// 6. Store in cache for future requests
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 in plain English for a homeowner. List likely causes and next steps.
Reply with ONLY valid JSON, no markdown:
{{""explanation"":""2-3 sentences"",""causes"":[""...""],""nextSteps"":[""...""]}}
";
}
// ── OpenAI HTTP call ────────────────────────────────────────────
private static readonly string OpenAiUrl = "https://api.openai.com/v1/chat/completions";
private static async Task<DiagnosticResponse?> CallOpenAiAsync(string userPrompt)
{
try
{
var requestBody = new
{
model = "gpt-4o-mini", // cost-efficient, fast; swap to "gpt-4" 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 OpenAiUrl
.SetHeader("Authorization", $"Bearer {_apiKey}")
.SetHeader("Content-Type", "application/json")
.PostJsonAsync(requestBody)
.ReceiveString();
// parse OpenAI envelope
var envelope = JsonConvert.DeserializeObject<dynamic>(responseText);
var content = (string?) envelope?.choices?[0]?.message?.content;
if (string.IsNullOrWhiteSpace(content))
{
Console.Error.WriteLine("[DiagnosticService] OpenAI returned empty content.");
return null;
}
// parse the JSON the model produced
var diagnostic = JsonConvert.DeserializeObject<DiagnosticResponse>(content);
return diagnostic;
}
catch (FlurlHttpException httpEx)
{
Console.Error.WriteLine($"[DiagnosticService] HTTP error {httpEx.Response?.StatusCode}: {await httpEx.Response?.GetStringAsync()}");
return null;
}
catch (Exception ex)
{
Console.Error.WriteLine($"[DiagnosticService] {ex.Message}");
return null;
}
}
}
// ── config / response models ────────────────────────────────────────────────
public class OpenAiConfig
{
public string ApiKey { get; set; } = "";
}
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

@ -1,7 +1,9 @@
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { import {
Alert, Alert,
Box,
Card, Card,
CircularProgress,
Container, Container,
Divider, Divider,
Grid, Grid,
@ -17,7 +19,7 @@ import { AxiosError, AxiosResponse } from 'axios/index';
import routes from '../../../Resources/routes.json'; import routes from '../../../Resources/routes.json';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { TokenContext } from '../../../contexts/tokenContext'; import { TokenContext } from '../../../contexts/tokenContext';
import { ErrorMessage } from '../../../interfaces/S3Types'; import { ErrorMessage, DiagnosticResponse } from '../../../interfaces/S3Types';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import FormControlLabel from '@mui/material/FormControlLabel'; import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox'; import Checkbox from '@mui/material/Checkbox';
@ -45,6 +47,11 @@ function Log(props: LogProps) {
const tokencontext = useContext(TokenContext); const tokencontext = useContext(TokenContext);
const { removeToken } = tokencontext; const { removeToken } = tokencontext;
const [diagnosis, setDiagnosis] = useState<DiagnosticResponse | null>(null);
const [diagnosisLoading, setDiagnosisLoading] = useState(false);
const [diagnosisExpanded, setDiagnosisExpanded] = useState(false);
const [diagnosedError, setDiagnosedError] = useState<string>('');
useEffect(() => { useEffect(() => {
axiosConfig axiosConfig
.get(`/GetAllErrorsForInstallation?id=${props.id}`) .get(`/GetAllErrorsForInstallation?id=${props.id}`)
@ -71,6 +78,34 @@ function Log(props: LogProps) {
}); });
}, [updateCount]); }, [updateCount]);
// fetch AI diagnosis for the first unseen error (or warning if no unseen errors)
useEffect(() => {
const target = errors.find(e => !e.seen) || warnings.find(w => !w.seen);
if (!target) {
setDiagnosis(null);
setDiagnosedError('');
return;
}
// already have a diagnosis for this exact description — skip
if (target.description === diagnosedError && diagnosis) return;
setDiagnosisLoading(true);
axiosConfig
.get(`/DiagnoseError?installationId=${props.id}&errorDescription=${encodeURIComponent(target.description)}`)
.then((res: AxiosResponse<DiagnosticResponse>) => {
setDiagnosis(res.data);
setDiagnosedError(target.description);
})
.catch(() => {
setDiagnosis(null);
})
.finally(() => {
setDiagnosisLoading(false);
});
}, [errors, warnings]);
const handleErrorButtonPressed = () => { const handleErrorButtonPressed = () => {
setErrorButtonPressed(!errorButtonPressed); setErrorButtonPressed(!errorButtonPressed);
}; };
@ -108,6 +143,7 @@ function Log(props: LogProps) {
}; };
const warningDescriptionMap: { [key: string]: string } = { const warningDescriptionMap: { [key: string]: string } = {
// BMS warnings
"TaM1": "TaM1: BMS temperature high", "TaM1": "TaM1: BMS temperature high",
"TbM1": "TbM1: Battery temperature high", "TbM1": "TbM1: Battery temperature high",
"VBm1": "VBm1: Bus voltage low", "VBm1": "VBm1: Bus voltage low",
@ -124,10 +160,131 @@ function Log(props: LogProps) {
"MPMM": "MPMM: Midpoint wiring problem", "MPMM": "MPMM: Midpoint wiring problem",
"TCdi": "TCdi: Temperature difference between strings high", "TCdi": "TCdi: Temperature difference between strings high",
"LMPW": "LMPW: String voltages unbalance warning", "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 } = { const errorDescriptionMap: { [key: string]: string } = {
// BMS errors
"Tam": "Tam: Recoverable, BMS temperature too low", "Tam": "Tam: Recoverable, BMS temperature too low",
"TaM2": "TaM2: Recoverable, BMS temperature too high", "TaM2": "TaM2: Recoverable, BMS temperature too high",
"Tbm": "Tbm: Recoverable, Battery temperature too low", "Tbm": "Tbm: Recoverable, Battery temperature too low",
@ -153,12 +310,204 @@ function Log(props: LogProps) {
"HTFS": "HTFS: Recoverable, Unrecoverable: Heater Fuse Blown", "HTFS": "HTFS: Recoverable, Unrecoverable: Heater Fuse Blown",
"DATA": "DATA: Recoverable, Unrecoverable: Parameters out of range", "DATA": "DATA: Recoverable, Unrecoverable: Parameters out of range",
"LMPA": "LMPA: Unrecoverable, String voltages unbalance alarm", "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 ( return (
<Container maxWidth="xl"> <Container maxWidth="xl">
<Grid container> <Grid container>
{/* AI Diagnosis banner — shown when loading or a diagnosis is available */}
{(diagnosisLoading || diagnosis) && (
<Grid item xs={12} md={12}>
<Card sx={{ marginTop: '20px', borderLeft: '4px solid #1976d2' }}>
<Box sx={{ padding: '16px' }}>
{/* loading state */}
{diagnosisLoading && (
<Box sx={{ 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>
)}
{/* diagnosis result */}
{diagnosis && !diagnosisLoading && (
<>
<Box sx={{ display: 'flex', alignItems: 'center', gap: '8px', mb: 1 }}>
<Typography variant="subtitle2" fontWeight="bold" color="primary">
<FormattedMessage id="ai_diagnosis" defaultMessage="AI Diagnosis" />
</Typography>
<Typography variant="caption" color="text.secondary">
{diagnosedError}
</Typography>
</Box>
<Typography variant="body2">
{diagnosis.explanation}
</Typography>
<Button
size="small"
onClick={() => setDiagnosisExpanded(!diagnosisExpanded)}
sx={{ textTransform: 'none', p: 0, mt: 1 }}
>
<FormattedMessage
id={diagnosisExpanded ? 'ai_show_less' : 'ai_show_details'}
defaultMessage={diagnosisExpanded ? 'Show less' : 'Show details'}
/>
</Button>
{diagnosisExpanded && (
<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' }}>
{diagnosis.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' }}>
{diagnosis.nextSteps.map((step, i) => (
<li key={i}><Typography variant="caption">{step}</Typography></li>
))}
</ol>
</Box>
)}
</>
)}
</Box>
</Card>
</Grid>
)}
<Grid item xs={12} md={12}> <Grid item xs={12} md={12}>
<Button <Button
variant="contained" variant="contained"

View File

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