restructure sodistore home Information tab

This commit is contained in:
Yinyin Liu 2026-03-23 13:19:14 +01:00
parent 897f3137f5
commit 730f337502
10 changed files with 752 additions and 372 deletions

View File

@ -49,7 +49,10 @@ public class Installation : TreeNode
public int BatteryClusterNumber { get; set; } = 0;
public int BatteryNumber { get; set; } = 0;
public string BatterySerialNumbers { get; set; } = "";
public string PvStringsPerInverter { get; set; } = "";
public string InstallationModel { get; set; } = "";
public string ExternalEms { get; set; } = "No";
[Ignore]
public String OrderNumbers { get; set; }
public String VrmLink { get; set; } = "";

View File

@ -130,6 +130,11 @@ public static partial class Db
fileConnection.CreateTable<TicketAiDiagnosis>();
fileConnection.CreateTable<TicketTimelineEvent>();
// Migrate new columns: set defaults for existing rows where NULL or empty
fileConnection.Execute("UPDATE Installation SET ExternalEms = 'No' WHERE ExternalEms IS NULL OR ExternalEms = ''");
fileConnection.Execute("UPDATE Installation SET InstallationModel = '' WHERE InstallationModel IS NULL");
fileConnection.Execute("UPDATE Installation SET PvStringsPerInverter = '' WHERE PvStringsPerInverter IS NULL");
return fileConnection;
//return CopyDbToMemory(fileConnection);
}

View File

@ -0,0 +1,112 @@
// [inverter][cluster] = batteryCount
export type PresetConfig = number[][];
// 3D array: [inverter][cluster][batteryIndex] = serialNumber
export type BatterySnTree = string[][][];
export const INSTALLATION_PRESETS: Record<string, PresetConfig> = {
'sodistore home 9': [[1, 1]],
'sodistore home 18': [[2, 2]],
'sodistore home 27': [[2, 2], [1, 1]],
'sodistore home 36': [[2, 2], [2, 2]],
};
export const buildEmptyTree = (preset: PresetConfig): BatterySnTree => {
return preset.map((inv) =>
inv.map((batteryCount) => Array.from({ length: batteryCount }, () => ''))
);
};
export const parseBatterySnTree = (
raw: string,
preset: PresetConfig
): BatterySnTree => {
if (!raw || raw.trim() === '') {
return buildEmptyTree(preset);
}
const isStructured = raw.includes('/') || raw.includes('|');
if (isStructured) {
const inverters = raw.split('/');
return preset.map((invPreset, invIdx) => {
const clusterStr = inverters[invIdx] || '';
const clusters = clusterStr ? clusterStr.split('|') : [];
return invPreset.map((batteryCount, clIdx) => {
const batteries = clusters[clIdx]
? clusters[clIdx].split(',').map((s) => s.trim())
: [];
return Array.from({ length: batteryCount }, (_, i) => batteries[i] || '');
});
});
}
// Legacy flat format: distribute by preset layout
const allSns = raw
.split(',')
.map((s) => s.trim())
.filter((s) => s !== '');
let idx = 0;
return preset.map((inv) =>
inv.map((batteryCount) =>
Array.from({ length: batteryCount }, () => allSns[idx++] || '')
)
);
};
export const serializeBatterySnTree = (tree: BatterySnTree): string => {
return tree
.map((inv) => inv.map((cluster) => cluster.join(',')).join('|'))
.join('/');
};
export const remapTree = (
oldTree: BatterySnTree,
newPreset: PresetConfig
): BatterySnTree => {
return newPreset.map((inv, invIdx) =>
inv.map((batteryCount, clIdx) =>
Array.from(
{ length: batteryCount },
(_, batIdx) => oldTree[invIdx]?.[clIdx]?.[batIdx] || ''
)
)
);
};
export const computeFlatValues = (
preset: PresetConfig,
tree: BatterySnTree
) => {
const totalBatteries = preset.flat().reduce((a, b) => a + b, 0);
const totalClusters = preset.reduce((sum, inv) => sum + inv.length, 0);
return {
batteryNumber: totalBatteries,
batteryClusterNumber: totalClusters,
batterySerialNumbers: serializeBatterySnTree(tree),
};
};
export const wouldLoseData = (
oldTree: BatterySnTree,
newPreset: PresetConfig
): boolean => {
for (let invIdx = 0; invIdx < oldTree.length; invIdx++) {
for (let clIdx = 0; clIdx < (oldTree[invIdx] || []).length; clIdx++) {
for (
let batIdx = 0;
batIdx < (oldTree[invIdx][clIdx] || []).length;
batIdx++
) {
const sn = oldTree[invIdx][clIdx][batIdx];
if (sn && sn.trim() !== '') {
if (invIdx >= newPreset.length) return true;
if (clIdx >= (newPreset[invIdx] || []).length) return true;
const newBatCount = newPreset[invIdx]?.[clIdx] ?? 0;
if (batIdx >= newBatCount) return true;
}
}
}
}
return false;
};

View File

@ -17,6 +17,7 @@ import { Close as CloseIcon } from '@mui/icons-material';
import { I_Installation } from 'src/interfaces/InstallationTypes';
import { InstallationsContext } from 'src/contexts/InstallationsContextProvider';
import { FormattedMessage } from 'react-intl';
import { INSTALLATION_PRESETS } from '../Information/installationSetupUtils';
interface SodistorehomeInstallationFormPros {
cancel: () => void;
@ -33,8 +34,10 @@ function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros)
location: '',
country: '',
vpnIp: '',
installationModel: '',
externalEms: 'No',
});
const requiredFields = ['name', 'location', 'country', 'vpnIp'];
const requiredFields = ['name', 'location', 'country', 'vpnIp', 'installationModel'];
const DeviceTypes = [
{ id: 3, name: 'Growatt' },
@ -171,6 +174,42 @@ function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros)
/>
</div>
<div>
<FormControl
fullWidth
required
error={formValues.installationModel === ''}
sx={{
marginTop: 1,
marginBottom: 1,
width: 390
}}
>
<InputLabel
sx={{
fontSize: 14,
backgroundColor: 'white'
}}
>
<FormattedMessage
id="installationModel"
defaultMessage="Installation Model"
/>
</InputLabel>
<Select
name="installationModel"
value={formValues.installationModel || ''}
onChange={handleChange}
>
{Object.keys(INSTALLATION_PRESETS).map((name) => (
<MenuItem key={name} value={name}>
{name}
</MenuItem>
))}
</Select>
</FormControl>
</div>
<div>
<FormControl
fullWidth

View File

@ -18,6 +18,10 @@ export interface I_Installation extends I_S3Credentials {
batteryClusterNumber: number;
batteryNumber: number;
batterySerialNumbers: string;
pvStringsPerInverter: string;
installationModel: string;
externalEms: string;
parentId: number;
s3WriteKey: string;
s3WriteSecret: string;

View File

@ -73,6 +73,24 @@
"live": "Live Daten",
"deleteInstallation": "Installation löschen",
"confirmDeleteInstallation": "Möchten Sie diese Installation löschen?",
"installationModel": "Installationsmodell",
"externalEms": "Externes EMS",
"externalEmsOther": "Externes EMS (angeben)",
"emsNo": "Nein",
"emsOther": "Andere",
"generalInfo": "Allgemeine Informationen",
"installationSetup": "Installationseinrichtung",
"selectModel": "Modell auswählen...",
"inverterN": "Wechselrichter {n}",
"clusterN": "Cluster {n}",
"clustersBatteriesSummary": "{filledClusters}/{totalClusters} Cluster, {filledBat}/{totalBat} Batterien",
"batteriesSummary": "{filled}/{total} Batterien",
"inverterNSerialNumber": "Wechselrichter {n} Seriennummer",
"dataloggerNSerialNumber": "Datenlogger {n} Seriennummer",
"pvStringsOnInverterN": "Anzahl PV-Strings an Wechselrichter {n}",
"batteryNSerialNumber": "Batterie {n} Seriennummer",
"adminSection": "Admin",
"confirmPresetSwitch": "Der Wechsel zu einer kleineren Konfiguration entfernt einige Batterie-Seriennummern. Fortfahren?",
"bucketLabel": "Bucket",
"deleteInstallationWarning": "Bitte notieren Sie den Bucket-Namen oben. Das Löschen der S3-Daten kann mehrere Minuten dauern. Überprüfen Sie nach dem Löschen in Exoscale, ob der Bucket entfernt wurde. Falls nicht, leeren und löschen Sie den Bucket manuell.",
"errorOccured": "Ein Fehler ist aufgetreten",
@ -85,6 +103,7 @@
"noUsersWithDirectAccessToThis": "Keine Benutzer mit direktem Zugriff",
"selectUsers": "Benutzer auswählen",
"cancel": "Abbrechen",
"continue": "Fortfahren",
"addNewFolder": "Neuen Ordner hinzufügen",
"addNewInstallation": "Neue Installation hinzufügen",
"deleteFolder": "Ordner löschen",

View File

@ -55,6 +55,24 @@
"live": "Live View",
"deleteInstallation": "Delete Installation",
"confirmDeleteInstallation": "Do you want to delete this installation?",
"installationModel": "Installation Model",
"externalEms": "External EMS",
"externalEmsOther": "External EMS (specify)",
"emsNo": "No",
"emsOther": "Other",
"generalInfo": "General Info",
"installationSetup": "Installation Setup",
"selectModel": "Select model...",
"inverterN": "Inverter {n}",
"clusterN": "Cluster {n}",
"clustersBatteriesSummary": "{filledClusters}/{totalClusters} clusters, {filledBat}/{totalBat} batteries",
"batteriesSummary": "{filled}/{total} batteries",
"inverterNSerialNumber": "Inverter {n} Serial Number",
"dataloggerNSerialNumber": "Datalogger {n} Serial Number",
"pvStringsOnInverterN": "Number of PV Strings on Inverter {n}",
"batteryNSerialNumber": "Battery {n} Serial Number",
"adminSection": "Admin",
"confirmPresetSwitch": "Switching to a smaller configuration will remove some battery serial number entries. Continue?",
"bucketLabel": "Bucket",
"deleteInstallationWarning": "Please note the bucket name above. Purging S3 data may take several minutes. After deletion, verify in Exoscale that the bucket has been removed. If not, purge and delete the bucket manually.",
"errorOccured": "An error has occurred",
@ -67,6 +85,7 @@
"noUsersWithDirectAccessToThis": "No users with direct access to this ",
"selectUsers": "Select Users",
"cancel": "Cancel",
"continue": "Continue",
"addNewFolder": "Add new Folder",
"addNewInstallation": "Add new Installation",
"deleteFolder": "Delete Folder",

View File

@ -67,6 +67,24 @@
"live": "Diffusion en direct",
"deleteInstallation": "Supprimer l'installation",
"confirmDeleteInstallation": "Voulez-vous supprimer cette installation ?",
"installationModel": "Modèle d'installation",
"externalEms": "EMS externe",
"externalEmsOther": "EMS externe (préciser)",
"emsNo": "Non",
"emsOther": "Autre",
"generalInfo": "Informations générales",
"installationSetup": "Configuration de l'installation",
"selectModel": "Sélectionner le modèle...",
"inverterN": "Onduleur {n}",
"clusterN": "Cluster {n}",
"clustersBatteriesSummary": "{filledClusters}/{totalClusters} clusters, {filledBat}/{totalBat} batteries",
"batteriesSummary": "{filled}/{total} batteries",
"inverterNSerialNumber": "Numéro de série onduleur {n}",
"dataloggerNSerialNumber": "Numéro de série datalogger {n}",
"pvStringsOnInverterN": "Nombre de chaînes PV sur onduleur {n}",
"batteryNSerialNumber": "Numéro de série batterie {n}",
"adminSection": "Admin",
"confirmPresetSwitch": "Le passage à une configuration plus petite supprimera certains numéros de série de batteries. Continuer ?",
"bucketLabel": "Bucket",
"deleteInstallationWarning": "Veuillez noter le nom du bucket ci-dessus. La purge des données S3 peut prendre plusieurs minutes. Après la suppression, vérifiez dans Exoscale que le bucket a bien été supprimé. Sinon, purgez et supprimez le bucket manuellement.",
"errorOccured": "Une erreur s'est produite",
@ -79,6 +97,7 @@
"noUsersWithDirectAccessToThis": "Aucun utilisateur ayant un accès direct",
"selectUsers": "Sélectionnez les utilisateurs",
"cancel": "Annuler",
"continue": "Continuer",
"addNewFolder": "Ajouter un nouveau dossier",
"addNewInstallation": "Ajouter une nouvelle installation",
"deleteFolder": "Supprimer le dossier",

View File

@ -55,6 +55,24 @@
"live": "Vista in diretta",
"deleteInstallation": "Elimina installazione",
"confirmDeleteInstallation": "Vuoi eliminare questa installazione?",
"installationModel": "Modello di installazione",
"externalEms": "EMS esterno",
"externalEmsOther": "EMS esterno (specificare)",
"emsNo": "No",
"emsOther": "Altro",
"generalInfo": "Informazioni generali",
"installationSetup": "Configurazione installazione",
"selectModel": "Seleziona modello...",
"inverterN": "Inverter {n}",
"clusterN": "Cluster {n}",
"clustersBatteriesSummary": "{filledClusters}/{totalClusters} cluster, {filledBat}/{totalBat} batterie",
"batteriesSummary": "{filled}/{total} batterie",
"inverterNSerialNumber": "Numero di serie inverter {n}",
"dataloggerNSerialNumber": "Numero di serie datalogger {n}",
"pvStringsOnInverterN": "Numero di stringhe PV sull'inverter {n}",
"batteryNSerialNumber": "Numero di serie batteria {n}",
"adminSection": "Admin",
"confirmPresetSwitch": "Il passaggio a una configurazione più piccola rimuoverà alcuni numeri di serie delle batterie. Continuare?",
"bucketLabel": "Bucket",
"deleteInstallationWarning": "Prendi nota del nome del bucket qui sopra. L'eliminazione dei dati S3 potrebbe richiedere diversi minuti. Dopo l'eliminazione, verifica in Exoscale che il bucket sia stato rimosso. In caso contrario, svuota ed elimina il bucket manualmente.",
"errorOccured": "Si è verificato un errore",
@ -67,6 +85,7 @@
"noUsersWithDirectAccessToThis": "Nessun utente con accesso diretto a questo",
"selectUsers": "Seleziona utenti",
"cancel": "Annulla",
"continue": "Continua",
"addNewFolder": "Aggiungi nuova cartella",
"addNewInstallation": "Aggiungi nuova installazione",
"deleteFolder": "Elimina cartella",