update sodistore home information tab based on weekly meeting's feedback

This commit is contained in:
Yinyin Liu 2026-03-24 13:42:36 +01:00
parent baaabbecd0
commit 0657a5fb82
13 changed files with 142 additions and 83 deletions

View File

@ -27,6 +27,13 @@ public class Installation : TreeNode
public String Location { get; set; } = ""; public String Location { get; set; } = "";
public String Region { get; set; } = ""; public String Region { get; set; } = "";
public String Country { get; set; } = ""; public String Country { get; set; } = "";
public String Street { get; set; } = "";
public String PostCode { get; set; } = "";
public String City { get; set; } = "";
public String Canton { get; set; } = "";
public String DistributionPartner { get; set; } = "";
public String InverterFirmwareVersion { get; set; } = "";
public String BatteryFirmwareVersion { get; set; } = "";
public String VpnIp { get; set; } = ""; public String VpnIp { get; set; } = "";
public String InstallationName { get; set; } = ""; public String InstallationName { get; set; } = "";

View File

@ -364,12 +364,15 @@ public static class ReportAggregationService
var installationName = installation?.Name ?? $"Installation {installationId}"; var installationName = installation?.Name ?? $"Installation {installationId}";
var monthName = new DateTime(year, month, 1).ToString("MMMM yyyy"); var monthName = new DateTime(year, month, 1).ToString("MMMM yyyy");
var weatherCity = !string.IsNullOrWhiteSpace(installation?.City) ? installation.City : installation?.Location;
var weatherRegion = !string.IsNullOrWhiteSpace(installation?.Canton) ? installation.Canton : installation?.Region;
var aiInsight = await GenerateMonthlyAiInsightAsync( var aiInsight = await GenerateMonthlyAiInsightAsync(
installationName, monthName, days.Count, installationName, monthName, days.Count,
totalPv, totalConsump, totalGridIn, totalGridOut, totalPv, totalConsump, totalGridIn, totalGridOut,
totalBattChg, totalBattDis, energySaved, savingsCHF, totalBattChg, totalBattDis, energySaved, savingsCHF,
selfSufficiency, batteryEff, language, selfSufficiency, batteryEff, language,
installation?.Location, installation?.Country, installation?.Region); weatherCity, installation?.Country, weatherRegion);
var monthlySummary = new MonthlyReportSummary var monthlySummary = new MonthlyReportSummary
{ {
@ -591,6 +594,8 @@ public static class ReportAggregationService
var installationName = installation?.Name var installationName = installation?.Name
?? $"Installation {report.InstallationId}"; ?? $"Installation {report.InstallationId}";
var monthName = new DateTime(report.Year, report.Month, 1).ToString("MMMM yyyy"); var monthName = new DateTime(report.Year, report.Month, 1).ToString("MMMM yyyy");
var weatherCity = !string.IsNullOrWhiteSpace(installation?.City) ? installation.City : installation?.Location;
var weatherRegion = !string.IsNullOrWhiteSpace(installation?.Canton) ? installation.Canton : installation?.Region;
return GetOrGenerateInsightAsync("monthly", report.Id, language, return GetOrGenerateInsightAsync("monthly", report.Id, language,
() => GenerateMonthlyAiInsightAsync( () => GenerateMonthlyAiInsightAsync(
installationName, monthName, report.WeekCount, installationName, monthName, report.WeekCount,
@ -599,7 +604,7 @@ public static class ReportAggregationService
report.TotalBatteryCharged, report.TotalBatteryDischarged, report.TotalBatteryCharged, report.TotalBatteryDischarged,
report.TotalEnergySaved, report.TotalSavingsCHF, report.TotalEnergySaved, report.TotalSavingsCHF,
report.SelfSufficiencyPercent, report.BatteryEfficiencyPercent, language, report.SelfSufficiencyPercent, report.BatteryEfficiencyPercent, language,
installation?.Location, installation?.Country, installation?.Region)); weatherCity, installation?.Country, weatherRegion));
} }
/// <summary>Cached-or-generated AI insight for a stored YearlyReportSummary.</summary> /// <summary>Cached-or-generated AI insight for a stored YearlyReportSummary.</summary>

View File

@ -179,9 +179,9 @@ public static class WeeklyReportService
// 4. Get installation location for weather forecast // 4. Get installation location for weather forecast
var installation = Db.GetInstallationById(installationId); var installation = Db.GetInstallationById(installationId);
var location = installation?.Location; var location = !string.IsNullOrWhiteSpace(installation?.City) ? installation.City : installation?.Location;
var country = installation?.Country; var country = installation?.Country;
var region = installation?.Region; var region = !string.IsNullOrWhiteSpace(installation?.Canton) ? installation.Canton : installation?.Region;
Console.WriteLine($"[WeeklyReportService] Installation {installationId}: Location='{location}', Region='{region}', Country='{country}', HourlyRecords={currentHourlyData.Count}"); Console.WriteLine($"[WeeklyReportService] Installation {installationId}: Location='{location}', Region='{region}', Country='{country}', HourlyRecords={currentHourlyData.Count}");
return await GenerateReportFromDataAsync( return await GenerateReportFromDataAsync(

View File

@ -60,7 +60,7 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
const theme = useTheme(); const theme = useTheme();
const intl = useIntl(); const intl = useIntl();
const [formValues, setFormValues] = useState(props.values); const [formValues, setFormValues] = useState(props.values);
const requiredFields = ['name', 'region', 'location', 'country']; const requiredFields = ['name'];
const [openModalDeleteInstallation, setOpenModalDeleteInstallation] = const [openModalDeleteInstallation, setOpenModalDeleteInstallation] =
useState(false); useState(false);
const [pendingPreset, setPendingPreset] = useState<string | null>(null); const [pendingPreset, setPendingPreset] = useState<string | null>(null);
@ -95,7 +95,7 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
const DeviceTypes = [ const DeviceTypes = [
{ id: 3, name: 'Growatt' }, { id: 3, name: 'Growatt' },
{ id: 4, name: 'Sinexcel' } { id: 4, name: 'inesco 12K - WR Hybrid' }
]; ];
// Preset state — initializes from persisted installationModel, empty for legacy // Preset state — initializes from persisted installationModel, empty for legacy
@ -533,27 +533,45 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
</div> </div>
<div> <div>
<TextField <TextField
label={<FormattedMessage id="region" defaultMessage="Region" />} label={<FormattedMessage id="street" defaultMessage="Street" />}
name="region" name="street"
value={formValues.region} value={formValues.street || ''}
onChange={handleChange} onChange={handleChange}
variant="outlined" variant="outlined"
fullWidth fullWidth
required={canEdit}
error={canEdit && formValues.region === ''}
inputProps={{ readOnly: !canEdit }} inputProps={{ readOnly: !canEdit }}
/> />
</div> </div>
<div> <div>
<TextField <TextField
label={<FormattedMessage id="location" defaultMessage="Location" />} label={<FormattedMessage id="postCode" defaultMessage="Postcode" />}
name="location" name="postCode"
value={formValues.location} value={formValues.postCode || ''}
onChange={handleChange}
variant="outlined"
fullWidth
inputProps={{ readOnly: !canEdit }}
/>
</div>
<div>
<TextField
label={<FormattedMessage id="city" defaultMessage="City" />}
name="city"
value={formValues.city || ''}
onChange={handleChange}
variant="outlined"
fullWidth
inputProps={{ readOnly: !canEdit }}
/>
</div>
<div>
<TextField
label={<FormattedMessage id="canton" defaultMessage="Canton" />}
name="canton"
value={formValues.canton || ''}
onChange={handleChange} onChange={handleChange}
variant="outlined" variant="outlined"
fullWidth fullWidth
required={canEdit}
error={canEdit && formValues.location === ''}
inputProps={{ readOnly: !canEdit }} inputProps={{ readOnly: !canEdit }}
/> />
</div> </div>
@ -561,12 +579,21 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
<TextField <TextField
label={<FormattedMessage id="country" defaultMessage="Country" />} label={<FormattedMessage id="country" defaultMessage="Country" />}
name="country" name="country"
value={formValues.country} value={formValues.country || ''}
onChange={handleChange}
variant="outlined"
fullWidth
inputProps={{ readOnly: !canEdit }}
/>
</div>
<div>
<TextField
label={<FormattedMessage id="distributionPartner" defaultMessage="Distribution Partner" />}
name="distributionPartner"
value={formValues.distributionPartner || ''}
onChange={handleChange} onChange={handleChange}
variant="outlined" variant="outlined"
fullWidth fullWidth
required={canEdit}
error={canEdit && formValues.country === ''}
inputProps={{ readOnly: !canEdit }} inputProps={{ readOnly: !canEdit }}
/> />
</div> </div>
@ -696,6 +723,18 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
<FormattedMessage id="installationSetup" defaultMessage="Installation Setup" /> <FormattedMessage id="installationSetup" defaultMessage="Installation Setup" />
</Typography> </Typography>
<div>
<TextField
label={<FormattedMessage id="installationSerialNumber" defaultMessage="Installation Serial Number" />}
name="serialNumber"
value={formValues.serialNumber || ''}
onChange={handleChange}
variant="outlined"
fullWidth
inputProps={{ readOnly: !canEdit }}
/>
</div>
<div> <div>
<FormControl sx={{ m: 1, width: '50ch' }}> <FormControl sx={{ m: 1, width: '50ch' }}>
<InputLabel <InputLabel
@ -725,9 +764,21 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
<div> <div>
<TextField <TextField
label={<FormattedMessage id="installationSerialNumber" defaultMessage="Installation Serial Number" />} label={<FormattedMessage id="inverterFirmwareVersion" defaultMessage="Inverter Firmware Version" />}
name="serialNumber" name="inverterFirmwareVersion"
value={formValues.serialNumber} value={formValues.inverterFirmwareVersion || ''}
onChange={handleChange}
variant="outlined"
fullWidth
inputProps={{ readOnly: !canEdit }}
/>
</div>
<div>
<TextField
label={<FormattedMessage id="batteryFirmwareVersion" defaultMessage="Battery Firmware Version" />}
name="batteryFirmwareVersion"
value={formValues.batteryFirmwareVersion || ''}
onChange={handleChange} onChange={handleChange}
variant="outlined" variant="outlined"
fullWidth fullWidth

View File

@ -288,7 +288,7 @@ function Log(props: LogProps) {
onChange={e => { setDemoAlarm(e.target.value); setDemoResult(null); }} onChange={e => { setDemoAlarm(e.target.value); setDemoResult(null); }}
sx={{ minWidth: 260 }} sx={{ minWidth: 260 }}
> >
<ListSubheader>Sinexcel</ListSubheader> <ListSubheader>inesco 12K - WR Hybrid</ListSubheader>
{DEMO_ALARMS.sinexcel.map(a => ( {DEMO_ALARMS.sinexcel.map(a => (
<MenuItem key={a} value={a}>{a}</MenuItem> <MenuItem key={a} value={a}>{a}</MenuItem>
))} ))}

View File

@ -96,14 +96,14 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
<TableCell> <TableCell>
<FormattedMessage id="name" defaultMessage="Name" /> <FormattedMessage id="name" defaultMessage="Name" />
</TableCell> </TableCell>
<TableCell>
<FormattedMessage id="location" defaultMessage="Location" />
</TableCell>
<TableCell> <TableCell>
<FormattedMessage id="installationSN" defaultMessage="Installation SN" /> <FormattedMessage id="installationSN" defaultMessage="Installation SN" />
</TableCell> </TableCell>
<TableCell> <TableCell>
<FormattedMessage id="region" defaultMessage="Region" /> <FormattedMessage id="DeviceType" defaultMessage="Device Type" />
</TableCell>
<TableCell>
<FormattedMessage id="canton" defaultMessage="Canton" />
</TableCell> </TableCell>
<TableCell> <TableCell>
<FormattedMessage id="country" defaultMessage="Country" /> <FormattedMessage id="country" defaultMessage="Country" />
@ -146,19 +146,6 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
</Typography> </Typography>
</TableCell> </TableCell>
<TableCell>
<Typography
variant="body2"
fontWeight="bold"
color="text.primary"
gutterBottom
noWrap
sx={{ marginTop: '10px', fontSize: 'small' }}
>
{installation.location}
</Typography>
</TableCell>
<TableCell> <TableCell>
<Typography <Typography
variant="body2" variant="body2"
@ -181,7 +168,20 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
noWrap noWrap
sx={{ marginTop: '10px', fontSize: 'small' }} sx={{ marginTop: '10px', fontSize: 'small' }}
> >
{installation.region} {installation.device === 3 ? 'Growatt' : installation.device === 4 ? 'inesco 12K - WR Hybrid' : ''}
</Typography>
</TableCell>
<TableCell>
<Typography
variant="body2"
fontWeight="bold"
color="text.primary"
gutterBottom
noWrap
sx={{ marginTop: '10px', fontSize: 'small' }}
>
{installation.canton || ''}
</Typography> </Typography>
</TableCell> </TableCell>

View File

@ -30,18 +30,15 @@ function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros)
const [open, setOpen] = useState(true); const [open, setOpen] = useState(true);
const [formValues, setFormValues] = useState<Partial<I_Installation>>({ const [formValues, setFormValues] = useState<Partial<I_Installation>>({
name: '', name: '',
region: '',
location: '',
country: '',
vpnIp: '', vpnIp: '',
installationModel: '', installationModel: '',
externalEms: 'No', externalEms: 'No',
}); });
const requiredFields = ['name', 'location', 'country', 'vpnIp', 'installationModel']; const requiredFields = ['name', 'vpnIp', 'installationModel'];
const DeviceTypes = [ const DeviceTypes = [
{ id: 3, name: 'Growatt' }, { id: 3, name: 'Growatt' },
{ id: 4, name: 'Sinexcel' } { id: 4, name: 'inesco 12K - WR Hybrid' }
]; ];
const installationContext = useContext(InstallationsContext); const installationContext = useContext(InstallationsContext);
const { createInstallation, loading, setLoading, error, setError } = const { createInstallation, loading, setLoading, error, setError } =
@ -127,42 +124,6 @@ function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros)
error={formValues.name === ''} error={formValues.name === ''}
/> />
</div> </div>
<div>
<TextField
label={<FormattedMessage id="region" defaultMessage="Region" />}
name="region"
value={formValues.region}
onChange={handleChange}
required
error={formValues.region === ''}
/>
</div>
<div>
<TextField
label={
<FormattedMessage id="location" defaultMessage="Location" />
}
name="location"
value={formValues.location}
onChange={handleChange}
required
error={formValues.location === ''}
/>
</div>
<div>
<TextField
label={
<FormattedMessage id="country" defaultMessage="Country" />
}
name="country"
value={formValues.country}
onChange={handleChange}
required
error={formValues.country === ''}
/>
</div>
<div> <div>
<TextField <TextField
label={<FormattedMessage id="VpnIp" defaultMessage="VpnIp" />} label={<FormattedMessage id="VpnIp" defaultMessage="VpnIp" />}

View File

@ -47,7 +47,7 @@ const deviceOptionsByProduct: Record<number, { value: number; label: string }[]>
], ],
2: [ 2: [
{ value: 3, label: 'Growatt' }, { value: 3, label: 'Growatt' },
{ value: 4, label: 'Sinexcel' } { value: 4, label: 'inesco 12K - WR Hybrid' }
] ]
}; };

View File

@ -7,6 +7,13 @@ export interface I_Installation extends I_S3Credentials {
location: string; location: string;
region: string; region: string;
country: string; country: string;
street?: string;
postCode?: string;
city?: string;
canton?: string;
distributionPartner?: string;
inverterFirmwareVersion?: string;
batteryFirmwareVersion?: string;
installationName: string; installationName: string;
vpnIp: string; vpnIp: string;
orderNumbers: string[] | string; orderNumbers: string[] | string;

View File

@ -6,6 +6,13 @@
"alarms": "Alarme", "alarms": "Alarme",
"applyChanges": "Änderungen speichern", "applyChanges": "Änderungen speichern",
"country": "Land", "country": "Land",
"street": "Strasse",
"postCode": "PLZ",
"city": "Ort",
"canton": "Kanton",
"distributionPartner": "Vertriebspartner",
"inverterFirmwareVersion": "Wechselrichter-Firmware-Version",
"batteryFirmwareVersion": "Batterie-Firmware-Version",
"networkProvider": "Netzbetreiber", "networkProvider": "Netzbetreiber",
"createNewFolder": "Neuer Ordner", "createNewFolder": "Neuer Ordner",
"createNewUser": "Neuer Benutzer", "createNewUser": "Neuer Benutzer",

View File

@ -2,6 +2,13 @@
"allInstallations": "All installations", "allInstallations": "All installations",
"applyChanges": "Apply changes", "applyChanges": "Apply changes",
"country": "Country", "country": "Country",
"street": "Street",
"postCode": "Postcode",
"city": "City",
"canton": "Canton",
"distributionPartner": "Distribution Partner",
"inverterFirmwareVersion": "Inverter Firmware Version",
"batteryFirmwareVersion": "Battery Firmware Version",
"networkProvider": "Network Provider", "networkProvider": "Network Provider",
"customerName": "Customer name", "customerName": "Customer name",
"english": "English", "english": "English",

View File

@ -4,6 +4,13 @@
"alarms": "Alarmes", "alarms": "Alarmes",
"applyChanges": "Appliquer", "applyChanges": "Appliquer",
"country": "Pays", "country": "Pays",
"street": "Rue",
"postCode": "Code postal",
"city": "Ville",
"canton": "Canton",
"distributionPartner": "Partenaire de distribution",
"inverterFirmwareVersion": "Version firmware onduleur",
"batteryFirmwareVersion": "Version firmware batterie",
"networkProvider": "Gestionnaire de réseau", "networkProvider": "Gestionnaire de réseau",
"createNewFolder": "Nouveau dossier", "createNewFolder": "Nouveau dossier",
"createNewUser": "Nouvel utilisateur", "createNewUser": "Nouvel utilisateur",

View File

@ -2,6 +2,13 @@
"allInstallations": "Tutte le installazioni", "allInstallations": "Tutte le installazioni",
"applyChanges": "Applica modifiche", "applyChanges": "Applica modifiche",
"country": "Paese", "country": "Paese",
"street": "Via",
"postCode": "CAP",
"city": "Città",
"canton": "Cantone",
"distributionPartner": "Partner di distribuzione",
"inverterFirmwareVersion": "Versione firmware inverter",
"batteryFirmwareVersion": "Versione firmware batteria",
"networkProvider": "Gestore di rete", "networkProvider": "Gestore di rete",
"customerName": "Nome cliente", "customerName": "Nome cliente",
"english": "Inglese", "english": "Inglese",