adapt sodistore home setup for growatt and sinexcel, add battery voltage to OverView page
This commit is contained in:
parent
cac4b0e5f3
commit
a8db23cadf
|
|
@ -34,7 +34,7 @@ import { useNavigate } from 'react-router-dom';
|
|||
import { UserType } from '../../../interfaces/UserTypes';
|
||||
import axiosConfig from '../../../Resources/axiosConfig';
|
||||
import {
|
||||
INSTALLATION_PRESETS,
|
||||
getPresetsForDevice,
|
||||
PresetConfig,
|
||||
BatterySnTree,
|
||||
parseBatterySnTree,
|
||||
|
|
@ -113,7 +113,7 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
|
|||
? (inverterCount && parseInt(inverterCount, 10) > 0
|
||||
? buildSodistoreProPreset(parseInt(inverterCount, 10))
|
||||
: null)
|
||||
: (INSTALLATION_PRESETS[selectedPreset] || null);
|
||||
: (getPresetsForDevice(formValues.device)[selectedPreset] || null);
|
||||
|
||||
const [batterySnTree, setBatterySnTree] = useState<BatterySnTree>(() => {
|
||||
if (presetConfig) {
|
||||
|
|
@ -141,8 +141,32 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
|
|||
return Array.from({ length: invCount }, () => '1');
|
||||
});
|
||||
|
||||
// When presetConfig is available, ensure flat values (batteryClusterNumber, batteryNumber)
|
||||
// stay in sync with the current preset structure. This handles:
|
||||
// - Legacy installations with device=0 → user sets device type
|
||||
// - Preset structure changed (e.g., Growatt home 9 was [[1,1]] → now [[2]])
|
||||
useEffect(() => {
|
||||
if (!presetConfig) return;
|
||||
|
||||
// Re-parse battery tree if empty but serial numbers exist
|
||||
let tree = batterySnTree;
|
||||
if (tree.length === 0 && props.values.batterySerialNumbers) {
|
||||
tree = parseBatterySnTree(props.values.batterySerialNumbers, presetConfig);
|
||||
setBatterySnTree(tree);
|
||||
}
|
||||
|
||||
// Always recalculate flat values from current preset to keep DB in sync
|
||||
const flat = computeFlatValues(presetConfig, tree);
|
||||
if (
|
||||
flat.batteryClusterNumber !== formValues.batteryClusterNumber ||
|
||||
flat.batteryNumber !== formValues.batteryNumber
|
||||
) {
|
||||
setFormValues((prev) => ({ ...prev, ...flat }));
|
||||
}
|
||||
}, [presetConfig]);
|
||||
|
||||
const handlePresetChange = (newPreset: string) => {
|
||||
const newConfig = INSTALLATION_PRESETS[newPreset];
|
||||
const newConfig = getPresetsForDevice(formValues.device)[newPreset];
|
||||
if (!newConfig) return;
|
||||
|
||||
// Check for data loss — either from existing tree or legacy flat data
|
||||
|
|
@ -171,7 +195,7 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
|
|||
};
|
||||
|
||||
const applyPreset = (newPreset: string) => {
|
||||
const newConfig = INSTALLATION_PRESETS[newPreset];
|
||||
const newConfig = getPresetsForDevice(formValues.device)[newPreset];
|
||||
if (!newConfig) return;
|
||||
|
||||
setSelectedPreset(newPreset);
|
||||
|
|
@ -315,10 +339,28 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
|
|||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormValues({
|
||||
...formValues,
|
||||
[name]: value
|
||||
});
|
||||
const updated = { ...formValues, [name]: value };
|
||||
|
||||
// When device type changes, reset preset if it's not available for the new device
|
||||
if (name === 'device' && !isSodistorePro) {
|
||||
const newDevicePresets = getPresetsForDevice(Number(value));
|
||||
if (selectedPreset && !newDevicePresets[selectedPreset]) {
|
||||
setSelectedPreset('');
|
||||
setBatterySnTree([]);
|
||||
setInverterSerialNumbers([]);
|
||||
setDataloggerSerialNumbers([]);
|
||||
setPvStringsPerInverter([]);
|
||||
updated.installationModel = '';
|
||||
updated.batteryNumber = 0;
|
||||
updated.batteryClusterNumber = 0;
|
||||
updated.batterySerialNumbers = '';
|
||||
updated.inverterSN = '';
|
||||
updated.dataloggerSN = '';
|
||||
updated.pvStringsPerInverter = '';
|
||||
}
|
||||
}
|
||||
|
||||
setFormValues(updated);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
|
|
@ -849,7 +891,7 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
|
|||
<MenuItem value="" disabled>
|
||||
<em><FormattedMessage id="selectModel" defaultMessage="Select model..." /></em>
|
||||
</MenuItem>
|
||||
{Object.keys(INSTALLATION_PRESETS).map((name) => (
|
||||
{Object.keys(getPresetsForDevice(formValues.device)).map((name) => (
|
||||
<MenuItem key={name} value={name}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
|
|
|
|||
|
|
@ -12,13 +12,24 @@ 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]],
|
||||
// Device-aware presets: keyed by device ID, then model name
|
||||
// Device 3 = Growatt, Device 4 = inesco 12K
|
||||
export const INSTALLATION_PRESETS: Record<number, Record<string, PresetConfig>> = {
|
||||
3: {
|
||||
'sodistore home 9': [[2]],
|
||||
'sodistore home 18': [[4]],
|
||||
},
|
||||
4: {
|
||||
'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 getPresetsForDevice = (deviceId: number): Record<string, PresetConfig> =>
|
||||
INSTALLATION_PRESETS[deviceId] ?? {};
|
||||
|
||||
export const buildSodistoreProPreset = (inverterCount: number): PresetConfig =>
|
||||
Array.from({ length: inverterCount }, () => [2, 2]);
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ export const getChartOptions = (
|
|||
chartInfo: chartInfoInterface,
|
||||
type: string,
|
||||
dateList: string[],
|
||||
stacked: Boolean
|
||||
stacked: Boolean,
|
||||
voltageInfo?: chartInfoInterface
|
||||
): ApexOptions => {
|
||||
return type.includes('daily')
|
||||
? {
|
||||
|
|
@ -165,7 +166,28 @@ export const getChartOptions = (
|
|||
return Math.round(value).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
...(voltageInfo ? [{
|
||||
seriesName: 'Battery Voltage',
|
||||
opposite: true,
|
||||
tickAmount: 5,
|
||||
min: voltageInfo.min > 0 ? Math.floor(voltageInfo.min / 5) * 5 : 0,
|
||||
max: Math.ceil(voltageInfo.max / 5) * 5,
|
||||
title: {
|
||||
text: '(V)',
|
||||
style: {
|
||||
fontSize: '12px'
|
||||
},
|
||||
offsetY: -190,
|
||||
offsetX: -45,
|
||||
rotate: 0
|
||||
},
|
||||
labels: {
|
||||
formatter: function (value: number) {
|
||||
return Math.round(value).toString();
|
||||
}
|
||||
}
|
||||
}] : [])
|
||||
]
|
||||
: {
|
||||
tickAmount: chartInfo.unit === '(%)' ? 5 : 6,
|
||||
|
|
|
|||
|
|
@ -712,7 +712,8 @@ function Overview(props: OverviewProps) {
|
|||
dailyDataArray[chartState].chartOverview.overview,
|
||||
'dailyoverview',
|
||||
[],
|
||||
true
|
||||
true,
|
||||
(product === 2 || product === 5) ? dailyDataArray[chartState].chartOverview.batteryVoltage : undefined
|
||||
),
|
||||
chart: {
|
||||
events: {
|
||||
|
|
@ -744,7 +745,12 @@ function Overview(props: OverviewProps) {
|
|||
...dailyDataArray[chartState].chartData.soc,
|
||||
type: 'line',
|
||||
color: '#008FFB'
|
||||
}
|
||||
},
|
||||
...((product === 2 || product === 5) ? [{
|
||||
...dailyDataArray[chartState].chartData.batteryVoltage,
|
||||
type: 'line' as const,
|
||||
color: '#9b59b6'
|
||||
}] : [])
|
||||
]}
|
||||
height={420}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -17,7 +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, SODIOHOME_DEVICE_TYPES } from '../Information/installationSetupUtils';
|
||||
import { getPresetsForDevice, SODIOHOME_DEVICE_TYPES } from '../Information/installationSetupUtils';
|
||||
|
||||
interface SodistorehomeInstallationFormPros {
|
||||
cancel: () => void;
|
||||
|
|
@ -38,7 +38,7 @@ function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros)
|
|||
...(isSodistorePro ? { device: 4 } : {}),
|
||||
});
|
||||
const [inverterCount, setInverterCount] = useState('');
|
||||
const requiredFields = ['name', 'vpnIp', ...(isSodistorePro ? [] : ['installationModel'])];
|
||||
const requiredFields = ['name', 'vpnIp', ...(isSodistorePro ? [] : ['device', 'installationModel'])];
|
||||
|
||||
const DeviceTypes = isSodistorePro
|
||||
? [{ id: 4, name: 'inesco 12K - WR Hybrid' }]
|
||||
|
|
@ -49,11 +49,17 @@ function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros)
|
|||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
const updated = { ...formValues, [name]: value };
|
||||
|
||||
setFormValues({
|
||||
...formValues,
|
||||
[name]: value
|
||||
});
|
||||
// Reset preset when device type changes if current preset is invalid
|
||||
if (name === 'device' && !isSodistorePro) {
|
||||
const newDevicePresets = getPresetsForDevice(Number(value));
|
||||
if (formValues.installationModel && !newDevicePresets[formValues.installationModel]) {
|
||||
updated.installationModel = '';
|
||||
}
|
||||
}
|
||||
|
||||
setFormValues(updated);
|
||||
};
|
||||
const handleSubmit = async (e) => {
|
||||
setLoading(true);
|
||||
|
|
@ -167,10 +173,48 @@ function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros)
|
|||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Device type must be selected before model — it determines available presets */}
|
||||
<div>
|
||||
<FormControl
|
||||
fullWidth
|
||||
required
|
||||
sx={{
|
||||
marginTop: 1,
|
||||
marginBottom: 1,
|
||||
width: 390
|
||||
}}
|
||||
>
|
||||
<InputLabel
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
backgroundColor: 'white'
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="DeviceType"
|
||||
defaultMessage="Device Type"
|
||||
/>
|
||||
</InputLabel>
|
||||
<Select
|
||||
name="device"
|
||||
value={formValues.device ?? ''}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{DeviceTypes.map((device) => (
|
||||
<MenuItem key={device.id} value={device.id}>
|
||||
{device.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<FormControl
|
||||
fullWidth
|
||||
required
|
||||
disabled={!formValues.device}
|
||||
error={formValues.installationModel === ''}
|
||||
sx={{
|
||||
marginTop: 1,
|
||||
|
|
@ -194,7 +238,7 @@ function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros)
|
|||
value={formValues.installationModel || ''}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{Object.keys(INSTALLATION_PRESETS).map((name) => (
|
||||
{Object.keys(getPresetsForDevice(formValues.device as number)).map((name) => (
|
||||
<MenuItem key={name} value={name}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
|
|
@ -202,42 +246,7 @@ function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros)
|
|||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isSodistorePro && (
|
||||
<div>
|
||||
<FormControl
|
||||
fullWidth
|
||||
sx={{
|
||||
marginTop: 1,
|
||||
marginBottom: 1,
|
||||
width: 390
|
||||
}}
|
||||
>
|
||||
<InputLabel
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
backgroundColor: 'white'
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="DeviceType"
|
||||
defaultMessage="Device Type"
|
||||
/>
|
||||
</InputLabel>
|
||||
<Select
|
||||
name="device"
|
||||
value={formValues.device}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{DeviceTypes.map((device) => (
|
||||
<MenuItem key={device.id} value={device.id}>
|
||||
{device.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export interface overviewInterface {
|
|||
overview: chartInfoInterface;
|
||||
ACLoad: chartInfoInterface;
|
||||
DCLoad: chartInfoInterface;
|
||||
batteryVoltage: chartInfoInterface;
|
||||
}
|
||||
|
||||
export interface chartAggregatedDataInterface {
|
||||
|
|
@ -53,6 +54,7 @@ export interface chartDataInterface {
|
|||
dcBusVoltage: { name: string; data: number[] };
|
||||
ACLoad: { name: string; data: number[] };
|
||||
DCLoad: { name: string; data: number[] };
|
||||
batteryVoltage: { name: string; data: number[] };
|
||||
}
|
||||
|
||||
export interface BatteryDataInterface {
|
||||
|
|
@ -428,7 +430,8 @@ export const transformInputToDailyDataJson = async (
|
|||
'SODIOHOME_PV_POWER',
|
||||
null, // dcBusVoltage not available for SodioHome
|
||||
'SODIOHOME_CONSUMPTION',
|
||||
null // DCLoad not available for SodioHome
|
||||
null, // DCLoad not available for SodioHome
|
||||
'SODIOHOME_BATTERY_VOLTAGE'
|
||||
]
|
||||
: [
|
||||
'Battery.Soc',
|
||||
|
|
@ -438,7 +441,8 @@ export const transformInputToDailyDataJson = async (
|
|||
'PvOnDc',
|
||||
'DcDc.Dc.Link.Voltage',
|
||||
'LoadOnAcGrid.Power.Active',
|
||||
'LoadOnDc.Power'
|
||||
'LoadOnDc.Power',
|
||||
null // batteryVoltage not available for Salimax
|
||||
];
|
||||
const categories = [
|
||||
'soc',
|
||||
|
|
@ -448,7 +452,8 @@ export const transformInputToDailyDataJson = async (
|
|||
'pvProduction',
|
||||
'dcBusVoltage',
|
||||
'ACLoad',
|
||||
'DCLoad'
|
||||
'DCLoad',
|
||||
'batteryVoltage'
|
||||
];
|
||||
|
||||
const chartData: chartDataInterface = {
|
||||
|
|
@ -459,7 +464,8 @@ export const transformInputToDailyDataJson = async (
|
|||
pvProduction: { name: 'PV Power', data: [] },
|
||||
dcBusVoltage: { name: 'DC Bus Voltage', data: [] },
|
||||
ACLoad: { name: 'AC Load', data: [] },
|
||||
DCLoad: { name: 'DC Load', data: [] }
|
||||
DCLoad: { name: 'DC Load', data: [] },
|
||||
batteryVoltage: { name: 'Battery Voltage', data: [] }
|
||||
};
|
||||
|
||||
const chartOverview: overviewInterface = {
|
||||
|
|
@ -472,7 +478,8 @@ export const transformInputToDailyDataJson = async (
|
|||
dcBusVoltage: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||
overview: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||
ACLoad: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||
DCLoad: { magnitude: 0, unit: '', min: 0, max: 0 }
|
||||
DCLoad: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||
batteryVoltage: { magnitude: 0, unit: '', min: 0, max: 0 }
|
||||
};
|
||||
|
||||
categories.forEach((category) => {
|
||||
|
|
@ -566,6 +573,9 @@ export const transformInputToDailyDataJson = async (
|
|||
case 6: // consumption
|
||||
value = inv.TotalLoadPower ?? inv.ConsumptionPower;
|
||||
break;
|
||||
case 8: // battery voltage
|
||||
value = inv.AvgBatteryVoltage ?? inv.Battery1Voltage;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (category_index === 4) {
|
||||
|
|
@ -636,6 +646,7 @@ export const transformInputToDailyDataJson = async (
|
|||
'(' + prefixes[chartOverview['ACLoad'].magnitude] + 'W' + ')';
|
||||
chartOverview.DCLoad.unit =
|
||||
'(' + prefixes[chartOverview['DCLoad'].magnitude] + 'W' + ')';
|
||||
chartOverview.batteryVoltage.unit = '(V)';
|
||||
|
||||
chartOverview.overview = {
|
||||
magnitude: Math.max(
|
||||
|
|
@ -750,7 +761,8 @@ export const transformInputToAggregatedDataJson = async (
|
|||
dcBusVoltage: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||
overview: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||
ACLoad: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||
DCLoad: { magnitude: 0, unit: '', min: 0, max: 0 }
|
||||
DCLoad: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||
batteryVoltage: { magnitude: 0, unit: '', min: 0, max: 0 }
|
||||
};
|
||||
|
||||
pathsToSearch.forEach((path) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue