add sodistore pro as a new product

This commit is contained in:
Yinyin Liu 2026-03-26 08:05:49 +01:00
parent d59027a277
commit 3521da7a1d
34 changed files with 262 additions and 56 deletions

View File

@ -202,6 +202,8 @@ public class Controller : ControllerBase
bucketPath = "s3://" + installation.S3BucketId + "-e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa/" + startTimestamp;
else if (installation.Product == (int)ProductType.SodistoreGrid)
bucketPath = "s3://" + installation.S3BucketId + "-5109c126-e141-43ab-8658-f3c44c838ae8/" + startTimestamp;
else if (installation.Product == (int)ProductType.SodistorePro)
bucketPath = "s3://" + installation.S3BucketId + "-325c9373-9025-4a8d-bf5a-f9eedf1f155c/" + startTimestamp;
else
bucketPath = "s3://" + installation.S3BucketId + "-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e/" + startTimestamp;
Console.WriteLine("Fetching data for "+startTimestamp);
@ -815,9 +817,10 @@ public class Controller : ControllerBase
if (installation is null || !user.HasAccessTo(installation))
return Unauthorized();
// AI diagnostics are scoped to SodistoreHome and SodiStoreMax only
// AI diagnostics are scoped to SodistoreHome, SodiStoreMax, and SodistorePro only
if (installation.Product != (int)ProductType.SodioHome &&
installation.Product != (int)ProductType.SodiStoreMax)
installation.Product != (int)ProductType.SodiStoreMax &&
installation.Product != (int)ProductType.SodistorePro)
return BadRequest("AI diagnostics not available for this product.");
var result = await DiagnosticService.DiagnoseAsync(installationId, errorDescription, user.Language ?? "en");

View File

@ -8,7 +8,8 @@ public enum ProductType
Salidomo = 1,
SodioHome =2,
SodiStoreMax=3,
SodistoreGrid=4
SodistoreGrid=4,
SodistorePro=5
}
public enum StatusType

View File

@ -146,6 +146,7 @@ public static class ExoCmd
String rolename = installation.Product==(int)ProductType.Salimax?Db.Installations.Count(f => f.Product == (int)ProductType.Salimax) + installation.Name:
installation.Product==(int)ProductType.SodiStoreMax?Db.Installations.Count(f => f.Product == (int)ProductType.SodiStoreMax) + installation.Name:
installation.Product==(int)ProductType.SodistoreGrid?Db.Installations.Count(f => f.Product == (int)ProductType.SodistoreGrid) + installation.Name:
installation.Product==(int)ProductType.SodistorePro?Db.Installations.Count(f => f.Product == (int)ProductType.SodistorePro) + installation.Name:
Db.Installations.Count(f => f.Product == (int)ProductType.Salidomo) + installation.Name;
@ -350,6 +351,7 @@ public static class ExoCmd
String rolename = installation.Product==(int)ProductType.Salimax?Db.Installations.Count(f => f.Product == (int)ProductType.Salimax) + installation.Name:
installation.Product==(int)ProductType.SodiStoreMax?Db.Installations.Count(f => f.Product == (int)ProductType.SodiStoreMax) + installation.Name:
installation.Product==(int)ProductType.SodistoreGrid?Db.Installations.Count(f => f.Product == (int)ProductType.SodistoreGrid) + installation.Name:
installation.Product==(int)ProductType.SodistorePro?Db.Installations.Count(f => f.Product == (int)ProductType.SodistorePro) + installation.Name:
Db.Installations.Count(f => f.Product == (int)ProductType.Salidomo) + installation.Name;
var contentString = $$"""

View File

@ -11,6 +11,7 @@ public static class InstallationMethods
private static readonly String SalidomoBucketNameSalt = "c0436b6a-d276-4cd8-9c44-1eae86cf5d0e";
private static readonly String SodioHomeBucketNameSalt = "e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa";
private static readonly String SodistoreGridBucketNameSalt = "5109c126-e141-43ab-8658-f3c44c838ae8";
private static readonly String SodistoreProBucketNameSalt = "325c9373-9025-4a8d-bf5a-f9eedf1f155c";
public static String BucketName(this Installation installation)
{
@ -29,6 +30,11 @@ public static class InstallationMethods
return $"{installation.S3BucketId}-{SodistoreGridBucketNameSalt}";
}
if (installation.Product == (int)ProductType.SodistorePro)
{
return $"{installation.S3BucketId}-{SodistoreProBucketNameSalt}";
}
return $"{installation.S3BucketId}-{SalidomoBucketNameSalt}";
}

View File

@ -239,7 +239,7 @@ public static class SessionMethods
}
if (installation.Product == (int)ProductType.SodiStoreMax || installation.Product == (int)ProductType.SodioHome || installation.Product == (int)ProductType.SodistoreGrid)
if (installation.Product == (int)ProductType.SodiStoreMax || installation.Product == (int)ProductType.SodioHome || installation.Product == (int)ProductType.SodistoreGrid || installation.Product == (int)ProductType.SodistorePro)
{
return user is not null
&& user.UserType != 0
@ -295,7 +295,7 @@ public static class SessionMethods
.Apply(Db.Update);
}
if (installation.Product == (int)ProductType.SodiStoreMax || installation.Product == (int)ProductType.SodistoreGrid)
if (installation.Product == (int)ProductType.SodiStoreMax || installation.Product == (int)ProductType.SodistoreGrid || installation.Product == (int)ProductType.SodistorePro)
{
return user is not null

View File

@ -17,6 +17,7 @@ public class Session : Relation<String, Int64>
public Boolean AccessToSodistoreMax { get; set; } = false;
public Boolean AccessToSodioHome { get; set; } = false;
public Boolean AccessToSodistoreGrid { get; set; } = false;
public Boolean AccessToSodistorePro { get; set; } = false;
[Ignore] public Boolean Valid => DateTime.Now - LastSeen <=MaxAge ;
// Private backing field
@ -51,6 +52,7 @@ public class Session : Relation<String, Int64>
AccessToSodistoreMax = user.AccessibleInstallations(product: (int)ProductType.SodiStoreMax).ToList().Count > 0;
AccessToSodioHome = user.AccessibleInstallations(product: (int)ProductType.SodioHome).ToList().Count > 0;
AccessToSodistoreGrid = user.AccessibleInstallations(product: (int)ProductType.SodistoreGrid).ToList().Count > 0;
AccessToSodistorePro = user.AccessibleInstallations(product: (int)ProductType.SodistorePro).ToList().Count > 0;
Console.WriteLine("salimax" +user.AccessibleInstallations(product: (int)ProductType.Salimax).ToList().Count);
Console.WriteLine("AccessToSodistoreMax" +user.AccessibleInstallations(product: (int)ProductType.SodiStoreMax).ToList().Count);

View File

@ -50,7 +50,7 @@ public static class DailyIngestionService
Console.WriteLine($"[DailyIngestion] Starting ingestion for all SodioHome installations...");
var installations = Db.Installations
.Where(i => i.Product == (Int32)ProductType.SodioHome && i.Device != 3) // Skip Growatt (device=3)
.Where(i => (i.Product == (Int32)ProductType.SodioHome || i.Product == (Int32)ProductType.SodistorePro) && i.Device != 3) // Skip Growatt (device=3)
.ToList();
foreach (var installation in installations)

View File

@ -106,7 +106,7 @@ public static class ReportAggregationService
Console.WriteLine("[ReportAggregation] Running Monday weekly report generation...");
var installations = Db.Installations
.Where(i => i.Product == (Int32)ProductType.SodioHome && i.Device != 3) // Skip Growatt (device=3)
.Where(i => (i.Product == (Int32)ProductType.SodioHome || i.Product == (Int32)ProductType.SodistorePro) && i.Device != 3) // Skip Growatt (device=3)
.ToList();
var generated = 0;

View File

@ -105,6 +105,11 @@ public static class RabbitMqManager
monitorLink =
$"https://monitor.inesco.energy/sodistoregrid_installations/list/installation/{installation.S3BucketId}/batteryview";
}
else if (installation.Product == (int)ProductType.SodistorePro)
{
monitorLink =
$"https://monitor.inesco.energy/sodistorepro_installations/list/installation/{installation.S3BucketId}/batteryview";
}
else
{
monitorLink =

View File

@ -31,7 +31,8 @@ public static class WebsocketManager
(installationConnection.Value.Product == (int)ProductType.Salidomo && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(60)) ||
(installationConnection.Value.Product == (int)ProductType.SodioHome && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(4)) ||
(installationConnection.Value.Product == (int)ProductType.SodiStoreMax && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2)) ||
(installationConnection.Value.Product == (int)ProductType.SodistoreGrid && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2))
(installationConnection.Value.Product == (int)ProductType.SodistoreGrid && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2)) ||
(installationConnection.Value.Product == (int)ProductType.SodistorePro && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(4))
)
{
Console.WriteLine("Installation ID is " + installationConnection.Key);

View File

@ -38,7 +38,8 @@ function App() {
setAccessToSalidomo,
setAccessToSodiohome,
setAccessToSodistore,
setAccessToSodistoreGrid
setAccessToSodistoreGrid,
setAccessToSodistorePro
} = useContext(ProductIdContext);
const [language, setLanguage] = useState<string>(
@ -106,6 +107,7 @@ function App() {
setAccessToSodiohome(response.data.accessToSodioHome);
setAccessToSodistore(response.data.accessToSodistoreMax);
setAccessToSodistoreGrid(response.data.accessToSodistoreGrid);
setAccessToSodistorePro(response.data.accessToSodistorePro);
if (response.data.accessToSalimax) {
navigate(routes.installations);
} else if (response.data.accessToSalidomo) {
@ -114,6 +116,8 @@ function App() {
navigate(routes.sodistore_installations);
} else if (response.data.accessToSodistoreGrid) {
navigate(routes.sodistoregrid_installations);
} else if (response.data.accessToSodistorePro) {
navigate(routes.sodistorepro_installations);
} else {
navigate(routes.sodiohome_installations);
}
@ -228,6 +232,15 @@ function App() {
}
/>
<Route
path={routes.sodistorepro_installations + '*'}
element={
<AccessContextProvider>
<SodioHomeInstallationTabs product={5} />
</AccessContextProvider>
}
/>
<Route
path={routes.sodistoregrid_installations + '*'}
element={

View File

@ -5,6 +5,7 @@
"sodistore_installations": "/sodistore_installations/",
"sodiohome_installations": "/sodiohome_installations/",
"sodistoregrid_installations": "/sodistoregrid_installations/",
"sodistorepro_installations": "/sodistorepro_installations/",
"installation": "installation/",
"login": "/login/",
"forgotPassword": "/forgotPassword/",

View File

@ -42,7 +42,8 @@ function Login() {
setAccessToSalidomo,
setAccessToSodiohome,
setAccessToSodistore,
setAccessToSodistoreGrid
setAccessToSodistoreGrid,
setAccessToSodistorePro
} = useContext(ProductIdContext);
const navigate = useNavigate();
@ -86,6 +87,7 @@ function Login() {
setAccessToSodiohome(response.data.accessToSodioHome);
setAccessToSodistore(response.data.accessToSodistoreMax);
setAccessToSodistoreGrid(response.data.accessToSodistoreGrid);
setAccessToSodistorePro(response.data.accessToSodistorePro);
if (response.data.accessToSalimax) {
navigate(routes.installations);
} else if (response.data.accessToSalidomo) {
@ -94,6 +96,8 @@ function Login() {
navigate(routes.sodistore_installations);
} else if (response.data.accessToSodistoreGrid) {
navigate(routes.sodistoregrid_installations);
} else if (response.data.accessToSodistorePro) {
navigate(routes.sodistorepro_installations);
} else {
navigate(routes.sodiohome_installations);
}

View File

@ -43,6 +43,7 @@ import {
computeFlatValues,
wouldLoseData,
SODIOHOME_DEVICE_TYPES,
buildSodistoreProPreset,
} from './installationSetupUtils';
interface InformationSodistorehomeProps {
@ -94,13 +95,25 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
return [value.trim()];
};
const DeviceTypes = SODIOHOME_DEVICE_TYPES;
const isSodistorePro = props.values.product === 5;
const DeviceTypes = isSodistorePro
? [{ id: 4, name: 'inesco 12K - WR Hybrid' } as const]
: SODIOHOME_DEVICE_TYPES;
// Preset state — initializes from persisted installationModel, empty for legacy
const [selectedPreset, setSelectedPreset] = useState<string>(
props.values.installationModel || ''
);
const presetConfig: PresetConfig | null = INSTALLATION_PRESETS[selectedPreset] || null;
const [inverterCount, setInverterCount] = useState<string>(
isSodistorePro && props.values.installationModel
? props.values.installationModel
: ''
);
const presetConfig: PresetConfig | null = isSodistorePro
? (inverterCount && parseInt(inverterCount, 10) > 0
? buildSodistoreProPreset(parseInt(inverterCount, 10))
: null)
: (INSTALLATION_PRESETS[selectedPreset] || null);
const [batterySnTree, setBatterySnTree] = useState<BatterySnTree>(() => {
if (presetConfig) {
@ -200,6 +213,38 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
});
};
const handleInverterCountChange = (value: string) => {
if (value !== '' && !/^\d+$/.test(value)) return;
if (value !== '' && parseInt(value, 10) > 20) return;
setInverterCount(value);
const count = parseInt(value, 10);
if (isNaN(count) || count < 1) {
setBatterySnTree([]);
setFormValues({ ...formValues, installationModel: value });
return;
}
const newConfig = buildSodistoreProPreset(count);
const newTree = batterySnTree.length > 0
? remapTree(batterySnTree, newConfig)
: buildEmptyTree(newConfig);
setBatterySnTree(newTree);
const newInvSNs = Array.from({ length: count }, (_, i) => inverterSerialNumbers[i] || '');
const newDlSNs = Array.from({ length: count }, (_, i) => dataloggerSerialNumbers[i] || '');
const newPvStrings = Array.from({ length: count }, (_, i) => pvStringsPerInverter[i] || '1');
setInverterSerialNumbers(newInvSNs);
setDataloggerSerialNumbers(newDlSNs);
setPvStringsPerInverter(newPvStrings);
const flat = computeFlatValues(newConfig, newTree);
setFormValues({
...formValues,
...flat,
installationModel: value,
inverterSN: newInvSNs.join('/'),
dataloggerSN: newDlSNs.join('/'),
pvStringsPerInverter: newPvStrings.join(','),
});
};
const handleInverterSnChange = (invIdx: number, value: string) => {
const updated = [...inverterSerialNumbers];
updated[invIdx] = value;
@ -608,6 +653,7 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
</div>
)}
{!isSodistorePro && (
<div>
<FormControl fullWidth sx={{ marginLeft: 1, marginTop: 1, marginBottom: 1, width: 440 }}>
<InputLabel sx={{ fontSize: 14, backgroundColor: 'white' }}>
@ -627,6 +673,7 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
</Select>
</FormControl>
</div>
)}
<div>
<Autocomplete
@ -733,6 +780,19 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
/>
</div>
{isSodistorePro ? (
<div>
<TextField
label={<FormattedMessage id="numberOfInverters" defaultMessage="Number of Inverters" />}
type="text"
value={inverterCount}
onChange={(e) => handleInverterCountChange(e.target.value)}
variant="outlined"
fullWidth
inputProps={{ readOnly: !canEdit }}
/>
</div>
) : (
<div>
<FormControl sx={{ m: 1, width: '50ch' }}>
<InputLabel
@ -759,6 +819,7 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
</Select>
</FormControl>
</div>
)}
<div>
<TextField
@ -919,7 +980,7 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
<div>
<TextField
label="S3 Bucket Name"
value={formValues.s3BucketId + '-e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa'}
value={formValues.s3BucketId + '-' + (isSodistorePro ? '325c9373-9025-4a8d-bf5a-f9eedf1f155c' : 'e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa')}
variant="outlined"
fullWidth
/>

View File

@ -19,6 +19,14 @@ export const INSTALLATION_PRESETS: Record<string, PresetConfig> = {
'sodistore home 36': [[2, 2], [2, 2]],
};
export const buildSodistoreProPreset = (inverterCount: number): PresetConfig =>
Array.from({ length: inverterCount }, () => [2, 2]);
export const parseSodistoreProInverterCount = (model: string): number => {
const n = parseInt(model, 10);
return isNaN(n) || n < 1 ? 1 : n;
};
export const buildEmptyTree = (preset: PresetConfig): BatterySnTree => {
return preset.map((inv) =>
inv.map((batteryCount) => Array.from({ length: batteryCount }, () => ''))

View File

@ -131,7 +131,7 @@ export const fetchAggregatedDataJson = (
} else if (r.status === 200) {
const jsontext = await r.text();
if (product === 2) {
if (product === 2 || product === 5) {
return parseSinexcelAggregatedData(jsontext);
}

View File

@ -107,13 +107,15 @@ function UserAccess(props: UserAccessProps) {
const fetchAvailableInstallations = useCallback(async () => {
try {
const [res0, res1, res2, res3] = await Promise.all([
const [res0, res1, res2, res3, res4, res5] = await Promise.all([
axiosConfig.get(`/GetAllInstallationsFromProduct?product=0`),
axiosConfig.get(`/GetAllInstallationsFromProduct?product=1`),
axiosConfig.get(`/GetAllInstallationsFromProduct?product=2`),
axiosConfig.get(`/GetAllInstallationsFromProduct?product=3`)
axiosConfig.get(`/GetAllInstallationsFromProduct?product=3`),
axiosConfig.get(`/GetAllInstallationsFromProduct?product=4`),
axiosConfig.get(`/GetAllInstallationsFromProduct?product=5`)
]);
setAvailableInstallations([...res0.data, ...res1.data, ...res2.data, ...res3.data]);
setAvailableInstallations([...res0.data, ...res1.data, ...res2.data, ...res3.data, ...res4.data, ...res5.data]);
} catch (err) {
if (err.response && err.response.status === 401) removeToken();
}

View File

@ -817,7 +817,7 @@ function Overview(props: OverviewProps) {
type: 'bar',
color: '#ff9900'
},
...(product !== 2 ? [{
...((product !== 2 && product !== 5) ? [{
name: 'Net Energy',
color: '#e65100',
type: 'line',
@ -840,7 +840,7 @@ function Overview(props: OverviewProps) {
alignItems="stretch"
spacing={3}
>
{!(aggregatedData && product === 2) && (
{!(aggregatedData && (product === 2 || product === 5)) && (
<Grid item md={6} xs={12}>
<Card
sx={{
@ -933,7 +933,7 @@ function Overview(props: OverviewProps) {
</Card>
</Grid>
)}
<Grid item md={(aggregatedData && product === 2) ? 12 : 6} xs={12}>
<Grid item md={(aggregatedData && (product === 2 || product === 5)) ? 12 : 6} xs={12}>
<Card
sx={{
overflow: 'visible',
@ -1001,14 +1001,14 @@ function Overview(props: OverviewProps) {
<ReactApexChart
options={{
...getChartOptions(
product === 2
(product === 2 || product === 5)
? aggregatedDataArray[aggregatedChartState]
.chartOverview.dcPowerWithoutHeating
: aggregatedDataArray[aggregatedChartState]
.chartOverview.dcPower,
'weekly',
aggregatedDataArray[aggregatedChartState].datelist,
product === 2
(product === 2 || product === 5)
)
}}
series={[
@ -1017,7 +1017,7 @@ function Overview(props: OverviewProps) {
.chartData.dcChargingPower,
color: '#008FFB'
},
...(product !== 2 ? [{
...((product !== 2 && product !== 5) ? [{
...aggregatedDataArray[aggregatedChartState]
.chartData.heatingPower,
color: '#ff9900'
@ -1073,7 +1073,7 @@ function Overview(props: OverviewProps) {
alignItems="stretch"
spacing={3}
>
{product !== 2 && (
{(product !== 2 && product !== 5) && (
<Grid item md={6} xs={12}>
<Card
sx={{
@ -1136,7 +1136,7 @@ function Overview(props: OverviewProps) {
</Card>
</Grid>
)}
{product !== 2 && (
{(product !== 2 && product !== 5) && (
<Grid item md={6} xs={12}>
<Card
sx={{
@ -1392,7 +1392,7 @@ function Overview(props: OverviewProps) {
</Grid>
</Grid>
{aggregatedData && product === 2 && (
{aggregatedData && (product === 2 || product === 5) && (
<Grid
container
direction="row"
@ -1457,7 +1457,7 @@ function Overview(props: OverviewProps) {
alignItems="stretch"
spacing={3}
>
<Grid item md={product === 2 ? 12 : 6} xs={12}>
<Grid item md={(product === 2 || product === 5) ? 12 : 6} xs={12}>
<Card
sx={{
overflow: 'visible',
@ -1518,7 +1518,7 @@ function Overview(props: OverviewProps) {
/>
</Card>
</Grid>
{product !== 2 && (
{(product !== 2 && product !== 5) && (
<Grid item md={6} xs={12}>
<Card
sx={{

View File

@ -23,12 +23,14 @@ import { getDeviceTypeName } from '../Information/installationSetupUtils';
interface FlatInstallationViewProps {
installations: I_Installation[];
product?: number;
}
const FlatInstallationView = (props: FlatInstallationViewProps) => {
const navigate = useNavigate();
const [selectedInstallation, setSelectedInstallation] = useState<number>(-1);
const currentLocation = useLocation();
const baseRoute = props.product === 5 ? routes.sodistorepro_installations : routes.sodiohome_installations;
//
const sortedInstallations = [...props.installations].sort((a, b) => {
// Compare the status field of each installation and sort them based on the status.
@ -51,7 +53,7 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
setSelectedInstallation(-1);
navigate(
routes.sodiohome_installations +
baseRoute +
routes.list +
routes.installation +
`${installationID}` +
@ -82,9 +84,9 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
sx={{
display:
currentLocation.pathname ===
routes.sodiohome_installations + 'list' ||
baseRoute + 'list' ||
currentLocation.pathname ===
routes.sodiohome_installations + routes.list
baseRoute + routes.list
? 'block'
: 'none'
}}

View File

@ -50,7 +50,9 @@ function SodioHomeInstallation(props: singleInstallationProps) {
const s3Bucket =
props.current_installation.s3BucketId.toString() +
'-' +
'e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa';
(props.current_installation.product === 5
? '325c9373-9025-4a8d-bf5a-f9eedf1f155c'
: 'e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa');
const context = useContext(UserContext);
const { currentUser } = context;

View File

@ -10,12 +10,14 @@ import SodioHomeInstallation from './Installation';
interface installationSearchProps {
installations: I_Installation[];
product?: number;
}
function InstallationSearch(props: installationSearchProps) {
const intl = useIntl();
const [searchTerm, setSearchTerm] = useState('');
const currentLocation = useLocation();
const baseRoute = props.product === 5 ? routes.sodistorepro_installations : routes.sodiohome_installations;
// const [filteredData, setFilteredData] = useState(props.installations);
const indexedData = useMemo(() => {
@ -46,9 +48,9 @@ function InstallationSearch(props: installationSearchProps) {
sx={{
display:
currentLocation.pathname ===
routes.sodiohome_installations + 'list' ||
baseRoute + 'list' ||
currentLocation.pathname ===
routes.sodiohome_installations + routes.list
baseRoute + routes.list
? 'block'
: 'none'
}}
@ -79,7 +81,7 @@ function InstallationSearch(props: installationSearchProps) {
</Grid>
</Grid>
<FlatInstallationView installations={filteredData} />
<FlatInstallationView installations={filteredData} product={props.product} />
<Routes>
{filteredData.map((installation) => {
return (

View File

@ -23,20 +23,26 @@ interface SodistorehomeInstallationFormPros {
cancel: () => void;
submit: () => void;
parentid: number;
product?: number;
}
function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros) {
const theme = useTheme();
const [open, setOpen] = useState(true);
const isSodistorePro = props.product === 5;
const [formValues, setFormValues] = useState<Partial<I_Installation>>({
name: '',
vpnIp: '',
installationModel: '',
externalEms: 'No',
...(isSodistorePro ? { device: 4 } : {}),
});
const requiredFields = ['name', 'vpnIp', 'installationModel'];
const [inverterCount, setInverterCount] = useState('');
const requiredFields = ['name', 'vpnIp', ...(isSodistorePro ? [] : ['installationModel'])];
const DeviceTypes = SODIOHOME_DEVICE_TYPES;
const DeviceTypes = isSodistorePro
? [{ id: 4, name: 'inesco 12K - WR Hybrid' }]
: SODIOHOME_DEVICE_TYPES;
const installationContext = useContext(InstallationsContext);
const { createInstallation, loading, setLoading, error, setError } =
installationContext;
@ -52,7 +58,10 @@ function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros)
const handleSubmit = async (e) => {
setLoading(true);
formValues.parentId = props.parentid;
formValues.product = 2;
formValues.product = props.product ?? 2;
if (isSodistorePro) {
formValues.installationModel = inverterCount;
}
const responseData = await createInstallation(formValues);
props.submit();
};
@ -66,6 +75,9 @@ function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros)
return false;
}
}
if (isSodistorePro && (!inverterCount || parseInt(inverterCount, 10) < 1)) {
return false;
}
return true;
};
@ -132,6 +144,29 @@ function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros)
/>
</div>
{isSodistorePro ? (
<div>
<TextField
label={
<FormattedMessage
id="numberOfInverters"
defaultMessage="Number of Inverters"
/>
}
name="inverterCount"
type="text"
value={inverterCount}
onChange={(e) => {
const val = e.target.value;
if (val === '' || (/^\d+$/.test(val) && parseInt(val, 10) <= 20)) {
setInverterCount(val);
}
}}
required
error={!inverterCount || parseInt(inverterCount, 10) < 1}
/>
</div>
) : (
<div>
<FormControl
fullWidth
@ -167,7 +202,9 @@ function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros)
</Select>
</FormControl>
</div>
)}
{!isSodistorePro && (
<div>
<FormControl
fullWidth
@ -201,6 +238,7 @@ function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros)
</Select>
</FormControl>
</div>
)}
</Box>
<div

View File

@ -66,6 +66,7 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
closeSocket
} = useContext(InstallationsContext);
const { product, setProduct } = useContext(ProductIdContext);
const baseRoute = props.product === 5 ? routes.sodistorepro_installations : routes.sodiohome_installations;
useEffect(() => {
let path = location.pathname.split('/');
@ -484,6 +485,7 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
<Box p={4}>
<InstallationSearch
installations={sodiohomeInstallations}
product={props.product}
/>
</Box>
</Grid>
@ -496,7 +498,7 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
path={'*'}
element={
<Navigate
to={routes.sodiohome_installations + routes.list}
to={baseRoute + routes.list}
></Navigate>
}
></Route>

View File

@ -742,7 +742,8 @@ function TicketDetailPage() {
1: routes.salidomo_installations,
2: routes.sodiohome_installations,
3: routes.sodistore_installations,
4: routes.sodistoregrid_installations
4: routes.sodistoregrid_installations,
5: routes.sodistorepro_installations
};
const prefix = productRoutes[detail.installationProduct] ?? routes.installations;
navigate(

View File

@ -60,6 +60,8 @@ function CustomTreeItem(props: CustomTreeItemProps) {
? routes.salidomo_installations
: installation.product == 2
? routes.sodiohome_installations
: installation.product == 5
? routes.sodistorepro_installations
: routes.sodistore_installations;
let folder_path =
@ -69,6 +71,8 @@ function CustomTreeItem(props: CustomTreeItemProps) {
? routes.salidomo_installations
: product == 2
? routes.sodiohome_installations
: product == 5
? routes.sodistorepro_installations
: routes.sodistore_installations;
if (installation.type != 'Folder') {

View File

@ -65,11 +65,12 @@ function TreeInformation(props: TreeInformationProps) {
setProduct(e.target.value); // Directly update the product state
};
const ProductTypes = ['Salimax', 'Salidomo', 'SodistoreHome', 'SodistoreMax', 'SodistoreGrid'];
const ProductTypes = ['Salimax', 'Salidomo', 'SodistoreHome', 'SodistoreMax', 'SodistoreGrid', 'SodistorePro'];
const ProductDisplayNames: Record<string, string> = {
'SodistoreHome': 'Sodistore Home',
'SodistoreMax': 'Sodistore Max',
'SodistoreGrid': 'Sodistore Grid'
'SodistoreGrid': 'Sodistore Grid',
'SodistorePro': 'Sodistore Pro'
};
const isMobile = window.innerWidth <= 1490;
@ -345,11 +346,12 @@ function TreeInformation(props: TreeInformationProps) {
/>
)}
{openModalInstallation && product == 'SodistoreHome' && (
{openModalInstallation && (product == 'SodistoreHome' || product == 'SodistorePro') && (
<SodiostorehomeInstallationForm
cancel={handleFormCancel}
submit={handleInstallationFormSubmit}
parentid={props.folder.id}
product={product == 'SodistorePro' ? 5 : undefined}
/>
)}

View File

@ -191,7 +191,7 @@ const InstallationsContextProvider = ({
`/GetAllInstallationsFromProduct?product=${product}`
);
if (product === 2) {
if (product === 2 || product === 5) {
setSodiohomeInstallations(res.data);
} else if (product === 1) {
setSalidomoInstallations(res.data);

View File

@ -10,11 +10,13 @@ interface ProductIdContextType {
accessToSodiohome: boolean;
accessToSodistore: boolean;
accessToSodistoreGrid: boolean;
accessToSodistorePro: boolean;
setAccessToSalimax: (access: boolean) => void;
setAccessToSalidomo: (access: boolean) => void;
setAccessToSodiohome: (access: boolean) => void;
setAccessToSodistore: (access: boolean) => void;
setAccessToSodistoreGrid: (access: boolean) => void;
setAccessToSodistorePro: (access: boolean) => void;
}
// Create the context.
@ -49,6 +51,10 @@ export const ProductIdContextProvider = ({
const storedValue = localStorage.getItem('accessToSodistoreGrid');
return storedValue === 'true';
});
const [accessToSodistorePro, setAccessToSodistorePro] = useState(() => {
const storedValue = localStorage.getItem('accessToSodistorePro');
return storedValue === 'true';
});
// const [product, setProduct] = useState(location.includes('salidomo') ? 1 : 0);
// const [product, setProduct] = useState<number>(
// productMapping[Object.keys(productMapping).find((key) => location.includes(key)) || ''] || -1
@ -56,6 +62,8 @@ export const ProductIdContextProvider = ({
const [product, setProduct] = useState<number>(() => {
if (location.includes('salidomo')) {
return 1;
} else if (location.includes('sodistorepro')) {
return 5;
} else if (location.includes('sodiohome')) {
return 2;
} else if (location.includes('sodistoregrid')) {
@ -92,6 +100,10 @@ export const ProductIdContextProvider = ({
setAccessToSodistoreGrid(access);
localStorage.setItem('accessToSodistoreGrid', JSON.stringify(access));
};
const changeAccessSodistorePro = (access: boolean) => {
setAccessToSodistorePro(access);
localStorage.setItem('accessToSodistorePro', JSON.stringify(access));
};
return (
<ProductIdContext.Provider
@ -103,11 +115,13 @@ export const ProductIdContextProvider = ({
accessToSodiohome,
accessToSodistore,
accessToSodistoreGrid,
accessToSodistorePro,
setAccessToSalimax: changeAccessSalimax,
setAccessToSalidomo: changeAccessSalidomo,
setAccessToSodiohome: changeAccessSodiohome,
setAccessToSodistore: changeAccessSodistore,
setAccessToSodistoreGrid: changeAccessSodistoreGrid
setAccessToSodistoreGrid: changeAccessSodistoreGrid,
setAccessToSodistorePro: changeAccessSodistorePro
}}
>
{children}

View File

@ -86,7 +86,7 @@ export const transformInputToBatteryViewDataJson = async (
}> => {
const prefixes = ['', 'k', 'M', 'G', 'T'];
const MAX_NUMBER = 9999999;
const isSodioHome = product === 2;
const isSodioHome = product === 2 || product === 5;
const categories = isSodioHome
? ['Soc', 'Power', 'Voltage', 'Current', 'Soh']
: ['Soc', 'Temperature', 'Power', 'Voltage', 'Current'];
@ -169,7 +169,7 @@ export const transformInputToBatteryViewDataJson = async (
);
const adjustedTimestamp =
product == 0 || product == 2 || product == 3 || product == 4
product == 0 || product == 2 || product == 3 || product == 4 || product == 5
? new Date(timestampArray[i] * 1000)
: new Date(timestampArray[i] * 100000);
//Timezone offset is negative, so we convert the timestamp to the current zone by subtracting the corresponding offset
@ -393,7 +393,7 @@ export const transformInputToDailyDataJson = async (
// custom fallback logic to handle differences between Growatt and Sinexcel.
// Growatt has: Battery1AmbientTemperature, GridPower, PvPower
// Sinexcel has: Battery1Temperature, TotalGridPower (meter may be offline), PvPower1-4
const pathsToSearch = product == 2
const pathsToSearch = (product == 2 || product == 5)
? [
'SODIOHOME_SOC',
'SODIOHOME_TEMPERATURE',
@ -516,8 +516,8 @@ export const transformInputToDailyDataJson = async (
let value: number | undefined = undefined;
if (product === 2) {
// SodioHome: use top-level aggregated values (Sinexcel multi-inverter)
if (product === 2 || product === 5) {
// SodioHome/SodistorePro: use top-level aggregated values (Sinexcel multi-inverter)
const inv = result?.InverterRecord;
if (inv) {
switch (category_index) {
@ -735,7 +735,7 @@ export const transformInputToAggregatedDataJson = async (
const timestampPromises = [];
while (currentDay.isBefore(end_date)) {
const dateFormat = product === 2
const dateFormat = (product === 2 || product === 5)
? currentDay.format('DDMMYYYY')
: currentDay.format('YYYY-MM-DD');
timestampPromises.push(

View File

@ -648,5 +648,7 @@
"terms_cookies_body": "Browser-Speicher wird für Anmeldesitzungen und Benutzereinstellungen verwendet. Dies ist für die korrekte Funktion der Plattform erforderlich.",
"terms_usage_heading": "Nutzungsbedingungen",
"terms_usage_body": "Durch die Nutzung dieser Plattform erkennen Sie die allgemeinen Nutzungsbedingungen von inesco Energy an. Bei Fragen wenden Sie sich bitte an Ihren Systemadministrator.",
"terms_acknowledge_button": "Ich verstehe"
"terms_acknowledge_button": "Ich verstehe",
"sodistorepro": "Sodistore Pro",
"numberOfInverters": "Anzahl der Wechselrichter"
}

View File

@ -396,5 +396,7 @@
"terms_cookies_body": "Browser storage is used for login sessions and user preferences. This is required for the platform to function correctly.",
"terms_usage_heading": "Terms of Use",
"terms_usage_body": "By using this platform, you acknowledge the general terms of use of inesco Energy. For questions, please contact your system administrator.",
"terms_acknowledge_button": "I understand"
"terms_acknowledge_button": "I understand",
"sodistorepro": "Sodistore Pro",
"numberOfInverters": "Number of Inverters"
}

View File

@ -648,5 +648,7 @@
"terms_cookies_body": "Le stockage du navigateur est utilisé pour les sessions de connexion et les préférences utilisateur. Ceci est nécessaire au bon fonctionnement de la plateforme.",
"terms_usage_heading": "Conditions d'utilisation",
"terms_usage_body": "En utilisant cette plateforme, vous reconnaissez les conditions générales d'utilisation d'inesco Energy. Pour toute question, veuillez contacter votre administrateur système.",
"terms_acknowledge_button": "Je comprends"
"terms_acknowledge_button": "Je comprends",
"sodistorepro": "Sodistore Pro",
"numberOfInverters": "Nombre d'onduleurs"
}

View File

@ -648,5 +648,7 @@
"terms_cookies_body": "L'archiviazione del browser viene utilizzata per le sessioni di accesso e le preferenze utente. Questo è necessario per il corretto funzionamento della piattaforma.",
"terms_usage_heading": "Condizioni d'uso",
"terms_usage_body": "Utilizzando questa piattaforma, si riconoscono le condizioni generali d'uso di inesco Energy. Per domande, contattare l'amministratore di sistema.",
"terms_acknowledge_button": "Ho capito"
"terms_acknowledge_button": "Ho capito",
"sodistorepro": "Sodistore Pro",
"numberOfInverters": "Numero di inverter"
}

View File

@ -170,7 +170,8 @@ function SidebarMenu() {
accessToSodistore,
accessToSalidomo,
accessToSodiohome,
accessToSodistoreGrid
accessToSodistoreGrid,
accessToSodistorePro
} = useContext(ProductIdContext);
return (
@ -285,6 +286,27 @@ function SidebarMenu() {
</ListItem>
</List>
)}
{accessToSodistorePro && (
<List component="div">
<ListItem component="div">
<Button
disableRipple
component={RouterLink}
onClick={closeSidebar}
to="/sodistorepro_installations"
startIcon={<BrightnessLowTwoToneIcon />}
>
<Box sx={{ marginTop: '3px' }}>
<FormattedMessage
id="sodistorepro"
defaultMessage="Sodistore Pro"
/>
</Box>
</Button>
</ListItem>
</List>
)}
</SubMenuWrapper>
</List>