added yes or no data collection mode

This commit is contained in:
Yinyin Liu 2026-04-14 15:56:22 +02:00
parent 52c9a42e42
commit 5bced9374b
16 changed files with 285 additions and 136 deletions

View File

@ -48,6 +48,7 @@ public class Installation : TreeNode
public String ReadRoleId { get; set; } = "";
public String WriteRoleId { get; set; } = "";
public Boolean TestingMode { get; set; } = false;
public Boolean DataCollectionEnabled { get; set; } = true;
public int Status { get; set; } = -1;
public int Product { get; set; } = (int)ProductType.Salimax;
public int Device { get; set; } = 0;

View File

@ -91,6 +91,11 @@ public static partial class Db
Connection.Execute("UPDATE User SET Language = 'fr' WHERE Language = 'french'");
Connection.Execute("UPDATE User SET Language = 'it' WHERE Language = 'italian'");
// Backfill: SQLite-net adds new bool columns as nullable with NULL for existing rows.
// LINQ `.Where(i => i.DataCollectionEnabled)` translates to `WHERE ... = 1` and excludes
// NULL rows, which would silently disable ingestion for every pre-existing installation.
Connection.Execute("UPDATE Installation SET DataCollectionEnabled = 1 WHERE DataCollectionEnabled IS NULL");
// One-time migration: rebrand to inesco energy
Connection.Execute("UPDATE Folder SET Name = 'inesco energy' WHERE Name = 'InnovEnergy'");
Connection.Execute("UPDATE Folder SET Name = 'inesco energy' WHERE Name = 'inesco Energy'");

View File

@ -39,7 +39,7 @@ public static class DeleteOldDataFromS3
{
var cutoffTimestamp = DateTimeOffset.UtcNow.AddYears(-1).ToUnixTimeSeconds();
var cutoffKey = cutoffTimestamp.ToString();
var installations = Db.Installations.ToList();
var installations = Db.Installations.Where(i => i.DataCollectionEnabled).ToList();
Console.WriteLine($"[S3Cleanup] Starting cleanup for {installations.Count} installations, cutoff: {cutoffKey}");

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.Product == (Int32)ProductType.SodistorePro) && i.Device != 3) // Skip Growatt (device=3)
.Where(i => (i.Product == (Int32)ProductType.SodioHome || i.Product == (Int32)ProductType.SodistorePro) && i.Device != 3 && i.DataCollectionEnabled) // Skip Growatt (device=3) and installations with data collection disabled
.ToList();
foreach (var installation in installations)
@ -75,6 +75,13 @@ public static class DailyIngestionService
/// </summary>
public static async Task IngestInstallationAsync(Int64 installationId)
{
var installation = Db.GetInstallationById(installationId);
if (installation is null || !installation.DataCollectionEnabled)
{
Console.WriteLine($"[DailyIngestion] Skipping installation {installationId} (data collection disabled).");
return;
}
await TryIngestFromJson(installationId);
IngestFromXlsx(installationId);
}
@ -88,6 +95,11 @@ public static class DailyIngestionService
{
var installation = Db.GetInstallationById(installationId);
if (installation is null) return;
if (!installation.DataCollectionEnabled)
{
Console.WriteLine($"[DailyIngestion] Skipping date-range ingest for installation {installationId} (data collection disabled).");
return;
}
var newDaily = 0;
var newHourly = 0;

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.Product == (Int32)ProductType.SodistorePro) && i.Device != 3) // Skip Growatt (device=3)
.Where(i => (i.Product == (Int32)ProductType.SodioHome || i.Product == (Int32)ProductType.SodistorePro) && i.Device != 3 && i.DataCollectionEnabled) // Skip Growatt (device=3) and installations with data collection disabled
.ToList();
var generated = 0;

View File

@ -762,6 +762,28 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
/>
</div>
<div>
<FormControl fullWidth sx={{ marginLeft: 1, marginTop: 1, marginBottom: 1, width: 440 }}>
<InputLabel sx={{ fontSize: 14, backgroundColor: 'white' }}>
<FormattedMessage id="dataCollectionEnabled" defaultMessage="Data Collection" />
</InputLabel>
<Select
name="dataCollectionEnabled"
value={formValues.dataCollectionEnabled === false ? 'no' : 'yes'}
onChange={(e) =>
setFormValues({
...formValues,
dataCollectionEnabled: e.target.value === 'yes'
})
}
inputProps={{ readOnly: !canEdit }}
>
<MenuItem value="yes"><FormattedMessage id="yes" defaultMessage="Yes" /></MenuItem>
<MenuItem value="no"><FormattedMessage id="no" defaultMessage="No" /></MenuItem>
</Select>
</FormControl>
</div>
<div>
<FormControl fullWidth sx={{ marginLeft: 1, marginTop: 1, marginBottom: 1, width: 440 }}>
<InputLabel sx={{ fontSize: 14, backgroundColor: 'white' }}>

View File

@ -209,46 +209,60 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
marginLeft: '15px'
}}
>
{status === -1 ? (
<CancelIcon
{installation.dataCollectionEnabled === false ? (
<div
style={{
width: '23px',
height: '23px',
color: 'red',
borderRadius: '50%'
width: '20px',
height: '20px',
marginLeft: '2px',
borderRadius: '50%',
backgroundColor: 'grey'
}}
/>
) : (
''
)}
<>
{status === -1 ? (
<CancelIcon
style={{
width: '23px',
height: '23px',
color: 'red',
borderRadius: '50%'
}}
/>
) : (
''
)}
{status === -2 ? (
<CircularProgress
size={20}
sx={{
color: '#f7b34d'
}}
/>
) : (
''
)}
{status === -2 ? (
<CircularProgress
size={20}
sx={{
color: '#f7b34d'
}}
/>
) : (
''
)}
<div
style={{
width: '20px',
height: '20px',
marginLeft: '2px',
borderRadius: '50%',
backgroundColor:
status === 2
? 'red'
: status === 1
? 'orange'
: status === -1 || status === -2
? 'transparent'
: 'green'
}}
/>
<div
style={{
width: '20px',
height: '20px',
marginLeft: '2px',
borderRadius: '50%',
backgroundColor:
status === 2
? 'red'
: status === 1
? 'orange'
: status === -1 || status === -2
? 'transparent'
: 'green'
}}
/>
</>
)}
{installation.testingMode && (
<BuildIcon
style={{

View File

@ -62,6 +62,7 @@ function SodioHomeInstallation(props: singleInstallationProps) {
const [currentTab, setCurrentTab] = useState<string>(undefined);
const [values, setValues] = useState<JSONRecordData | null>(null);
const status = props.current_installation.status;
const dataCollectionDisabled = props.current_installation.dataCollectionEnabled === false;
const [
failedToCommunicateWithInstallation,
setFailedToCommunicateWithInstallation
@ -417,46 +418,60 @@ function SodioHomeInstallation(props: singleInstallationProps) {
marginTop: '-10px'
}}
>
{status === -1 ? (
<CancelIcon
{dataCollectionDisabled ? (
<div
style={{
width: '23px',
height: '23px',
color: 'red',
borderRadius: '50%'
width: '20px',
height: '20px',
marginLeft: '2px',
borderRadius: '50%',
backgroundColor: 'grey'
}}
/>
) : (
''
)}
<>
{status === -1 ? (
<CancelIcon
style={{
width: '23px',
height: '23px',
color: 'red',
borderRadius: '50%'
}}
/>
) : (
''
)}
{status === -2 ? (
<CircularProgress
size={20}
sx={{
color: '#f7b34d'
}}
/>
) : (
''
)}
{status === -2 ? (
<CircularProgress
size={20}
sx={{
color: '#f7b34d'
}}
/>
) : (
''
)}
<div
style={{
width: '20px',
height: '20px',
marginLeft: '2px',
borderRadius: '50%',
backgroundColor:
status === 2
? 'red'
: status === 1
? 'orange'
: status === -1 || status === -2
? 'transparent'
: 'green'
}}
/>
<div
style={{
width: '20px',
height: '20px',
marginLeft: '2px',
borderRadius: '50%',
backgroundColor:
status === 2
? 'red'
: status === 1
? 'orange'
: status === -1 || status === -2
? 'transparent'
: 'green'
}}
/>
</>
)}
{props.current_installation.testingMode && (
<BuildIcon
@ -521,7 +536,7 @@ function SodioHomeInstallation(props: singleInstallationProps) {
}
/>
{currentUser.userType !== UserType.client && (
{currentUser.userType !== UserType.client && !dataCollectionDisabled && (
<Route
path={routes.log}
element={
@ -534,19 +549,21 @@ function SodioHomeInstallation(props: singleInstallationProps) {
/>
)}
<Route
path={routes.live}
element={
<TopologySodistoreHome
values={values}
connected={connected}
loading={loading}
batteryClusterNumber={props.current_installation.batteryClusterNumber}
></TopologySodistoreHome>
}
/>
{!dataCollectionDisabled && (
<Route
path={routes.live}
element={
<TopologySodistoreHome
values={values}
connected={connected}
loading={loading}
batteryClusterNumber={props.current_installation.batteryClusterNumber}
></TopologySodistoreHome>
}
/>
)}
{currentUser.userType !== UserType.client && (
{currentUser.userType !== UserType.client && !dataCollectionDisabled && (
<Route
path={routes.batteryview + '/*'}
element={
@ -573,7 +590,7 @@ function SodioHomeInstallation(props: singleInstallationProps) {
/>
)}
{currentUser.userType == UserType.admin && (
{currentUser.userType == UserType.admin && !dataCollectionDisabled && (
<Route
path={routes.configuration}
element={
@ -600,21 +617,23 @@ function SodioHomeInstallation(props: singleInstallationProps) {
/>
)} */}
<Route
path={routes.overview}
element={
<Overview
s3Credentials={s3Credentials}
id={props.current_installation.id}
device={props.current_installation.device}
product={props.current_installation.product}
connected={connected}
loading={loading}
/>
}
/>
{!dataCollectionDisabled && (
<Route
path={routes.overview}
element={
<Overview
s3Credentials={s3Credentials}
id={props.current_installation.id}
device={props.current_installation.device}
product={props.current_installation.product}
connected={connected}
loading={loading}
/>
}
/>
)}
{props.current_installation.device !== 3 && (
{props.current_installation.device !== 3 && !dataCollectionDisabled && (
<Route
path={routes.report}
element={

View File

@ -35,6 +35,7 @@ function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros)
vpnIp: '',
installationModel: '',
externalEms: 'No',
dataCollectionEnabled: true,
...(isSodistorePro ? { device: 4 } : {}),
});
const [inverterCount, setInverterCount] = useState('');
@ -249,6 +250,46 @@ function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros)
</>
)}
<div>
<FormControl
fullWidth
sx={{
marginTop: 1,
marginBottom: 1,
width: 390
}}
>
<InputLabel
sx={{
fontSize: 14,
backgroundColor: 'white'
}}
>
<FormattedMessage
id="dataCollectionEnabled"
defaultMessage="Data Collection"
/>
</InputLabel>
<Select
name="dataCollectionEnabled"
value={formValues.dataCollectionEnabled ? 'yes' : 'no'}
onChange={(e) =>
setFormValues({
...formValues,
dataCollectionEnabled: e.target.value === 'yes'
})
}
>
<MenuItem value="yes">
<FormattedMessage id="yes" defaultMessage="Yes" />
</MenuItem>
<MenuItem value="no">
<FormattedMessage id="no" defaultMessage="No" />
</MenuItem>
</Select>
</FormControl>
</div>
</Box>
<div
style={{

View File

@ -275,6 +275,12 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
const isGrowatt = currentInstallation?.device === 3
|| (installations.length === 1 && installations[0].device === 3);
// When data collection is disabled, only navigation, info, history, tickets, documents remain.
const dataCollectionDisabled =
currentInstallation?.dataCollectionEnabled === false
|| (installations.length === 1 && installations[0].dataCollectionEnabled === false);
const allowedWhenDisabled = ['list', 'tree', 'information', 'history', 'installationTickets', 'documents'];
const tabs = inInstallationView && currentUser.userType == UserType.admin
? [
{
@ -471,6 +477,7 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
>
{tabs
.filter((tab) => !(isGrowatt && tab.value === 'report'))
.filter((tab) => !dataCollectionDisabled || allowedWhenDisabled.includes(tab.value))
.map((tab) => (
<Tab
key={tab.value}
@ -544,6 +551,7 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
>
{singleInstallationTabs
.filter((tab) => !(isGrowatt && tab.value === 'report'))
.filter((tab) => !dataCollectionDisabled || allowedWhenDisabled.includes(tab.value))
.map((tab) => (
<Tab
key={tab.value}

View File

@ -160,50 +160,64 @@ function CustomTreeItem(props: CustomTreeItemProps) {
{props.node.type === 'Installation' && (
<div>
{status === -1 ? (
<CancelIcon
{(props.node as any).dataCollectionEnabled === false ? (
<div
style={{
width: '23px',
height: '23px',
color: 'red',
width: '20px',
height: '20px',
borderRadius: '50%',
marginLeft: '30px',
marginTop: '30px'
marginLeft: '17px',
backgroundColor: 'grey'
}}
/>
) : (
''
)}
<>
{status === -1 ? (
<CancelIcon
style={{
width: '23px',
height: '23px',
color: 'red',
borderRadius: '50%',
marginLeft: '30px',
marginTop: '30px'
}}
/>
) : (
''
)}
{status === -2 ? (
<CircularProgress
size={20}
sx={{
color: '#f7b34d',
marginLeft: '22px',
marginTop: '30px'
}}
/>
) : (
''
)}
{status === -2 ? (
<CircularProgress
size={20}
sx={{
color: '#f7b34d',
marginLeft: '22px',
marginTop: '30px'
}}
/>
) : (
''
)}
<div
style={{
width: '20px',
height: '20px',
borderRadius: '50%',
marginLeft: '17px',
backgroundColor:
status === 2
? 'red'
: status === 1
? 'orange'
: status === -1 || status === -2
? 'transparent'
: 'green'
}}
/>
<div
style={{
width: '20px',
height: '20px',
borderRadius: '50%',
marginLeft: '17px',
backgroundColor:
status === 2
? 'red'
: status === 1
? 'orange'
: status === -1 || status === -2
? 'transparent'
: 'green'
}}
/>
</>
)}
</div>
)}
</div>

View File

@ -36,6 +36,7 @@ export interface I_Installation extends I_S3Credentials {
product: number;
device: number;
testingMode?: boolean;
dataCollectionEnabled?: boolean;
status?: number;
serialNumber?: string;
networkProvider: string;

View File

@ -86,6 +86,9 @@
"externalEmsOther": "Externes EMS (angeben)",
"emsNo": "Nein",
"emsOther": "Andere",
"yes": "Ja",
"no": "Nein",
"dataCollectionEnabled": "Datenerfassung",
"generalInfo": "Allgemeine Informationen",
"installationSetup": "Installationseinrichtung",
"couplingType": "AC/DC-Kopplung",

View File

@ -68,6 +68,9 @@
"externalEmsOther": "External EMS (specify)",
"emsNo": "No",
"emsOther": "Other",
"yes": "Yes",
"no": "No",
"dataCollectionEnabled": "Data Collection",
"generalInfo": "General Info",
"installationSetup": "Installation Setup",
"couplingType": "AC/DC Coupling",

View File

@ -80,6 +80,9 @@
"externalEmsOther": "EMS externe (préciser)",
"emsNo": "Non",
"emsOther": "Autre",
"yes": "Oui",
"no": "Non",
"dataCollectionEnabled": "Collecte de données",
"generalInfo": "Informations générales",
"installationSetup": "Configuration de l'installation",
"couplingType": "Couplage AC/DC",

View File

@ -68,6 +68,9 @@
"externalEmsOther": "EMS esterno (specificare)",
"emsNo": "No",
"emsOther": "Altro",
"yes": "Sì",
"no": "No",
"dataCollectionEnabled": "Raccolta dati",
"generalInfo": "Informazioni generali",
"installationSetup": "Configurazione installazione",
"couplingType": "Accoppiamento AC/DC",