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 ReadRoleId { get; set; } = "";
public String WriteRoleId { get; set; } = ""; public String WriteRoleId { get; set; } = "";
public Boolean TestingMode { get; set; } = false; public Boolean TestingMode { get; set; } = false;
public Boolean DataCollectionEnabled { get; set; } = true;
public int Status { get; set; } = -1; public int Status { get; set; } = -1;
public int Product { get; set; } = (int)ProductType.Salimax; public int Product { get; set; } = (int)ProductType.Salimax;
public int Device { get; set; } = 0; 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 = 'fr' WHERE Language = 'french'");
Connection.Execute("UPDATE User SET Language = 'it' WHERE Language = 'italian'"); 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 // 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 = 'InnovEnergy'");
Connection.Execute("UPDATE Folder SET Name = 'inesco energy' WHERE Name = 'inesco Energy'"); 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 cutoffTimestamp = DateTimeOffset.UtcNow.AddYears(-1).ToUnixTimeSeconds();
var cutoffKey = cutoffTimestamp.ToString(); 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}"); 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..."); Console.WriteLine($"[DailyIngestion] Starting ingestion for all SodioHome installations...");
var installations = Db.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(); .ToList();
foreach (var installation in installations) foreach (var installation in installations)
@ -75,6 +75,13 @@ public static class DailyIngestionService
/// </summary> /// </summary>
public static async Task IngestInstallationAsync(Int64 installationId) 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); await TryIngestFromJson(installationId);
IngestFromXlsx(installationId); IngestFromXlsx(installationId);
} }
@ -88,6 +95,11 @@ public static class DailyIngestionService
{ {
var installation = Db.GetInstallationById(installationId); var installation = Db.GetInstallationById(installationId);
if (installation is null) return; 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 newDaily = 0;
var newHourly = 0; var newHourly = 0;

View File

@ -106,7 +106,7 @@ public static class ReportAggregationService
Console.WriteLine("[ReportAggregation] Running Monday weekly report generation..."); Console.WriteLine("[ReportAggregation] Running Monday weekly report generation...");
var installations = Db.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(); .ToList();
var generated = 0; var generated = 0;

View File

@ -762,6 +762,28 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
/> />
</div> </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> <div>
<FormControl fullWidth sx={{ marginLeft: 1, marginTop: 1, marginBottom: 1, width: 440 }}> <FormControl fullWidth sx={{ marginLeft: 1, marginTop: 1, marginBottom: 1, width: 440 }}>
<InputLabel sx={{ fontSize: 14, backgroundColor: 'white' }}> <InputLabel sx={{ fontSize: 14, backgroundColor: 'white' }}>

View File

@ -209,6 +209,18 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
marginLeft: '15px' marginLeft: '15px'
}} }}
> >
{installation.dataCollectionEnabled === false ? (
<div
style={{
width: '20px',
height: '20px',
marginLeft: '2px',
borderRadius: '50%',
backgroundColor: 'grey'
}}
/>
) : (
<>
{status === -1 ? ( {status === -1 ? (
<CancelIcon <CancelIcon
style={{ style={{
@ -249,6 +261,8 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
: 'green' : 'green'
}} }}
/> />
</>
)}
{installation.testingMode && ( {installation.testingMode && (
<BuildIcon <BuildIcon
style={{ style={{

View File

@ -62,6 +62,7 @@ function SodioHomeInstallation(props: singleInstallationProps) {
const [currentTab, setCurrentTab] = useState<string>(undefined); const [currentTab, setCurrentTab] = useState<string>(undefined);
const [values, setValues] = useState<JSONRecordData | null>(null); const [values, setValues] = useState<JSONRecordData | null>(null);
const status = props.current_installation.status; const status = props.current_installation.status;
const dataCollectionDisabled = props.current_installation.dataCollectionEnabled === false;
const [ const [
failedToCommunicateWithInstallation, failedToCommunicateWithInstallation,
setFailedToCommunicateWithInstallation setFailedToCommunicateWithInstallation
@ -417,6 +418,18 @@ function SodioHomeInstallation(props: singleInstallationProps) {
marginTop: '-10px' marginTop: '-10px'
}} }}
> >
{dataCollectionDisabled ? (
<div
style={{
width: '20px',
height: '20px',
marginLeft: '2px',
borderRadius: '50%',
backgroundColor: 'grey'
}}
/>
) : (
<>
{status === -1 ? ( {status === -1 ? (
<CancelIcon <CancelIcon
style={{ style={{
@ -457,6 +470,8 @@ function SodioHomeInstallation(props: singleInstallationProps) {
: 'green' : 'green'
}} }}
/> />
</>
)}
{props.current_installation.testingMode && ( {props.current_installation.testingMode && (
<BuildIcon <BuildIcon
@ -521,7 +536,7 @@ function SodioHomeInstallation(props: singleInstallationProps) {
} }
/> />
{currentUser.userType !== UserType.client && ( {currentUser.userType !== UserType.client && !dataCollectionDisabled && (
<Route <Route
path={routes.log} path={routes.log}
element={ element={
@ -534,6 +549,7 @@ function SodioHomeInstallation(props: singleInstallationProps) {
/> />
)} )}
{!dataCollectionDisabled && (
<Route <Route
path={routes.live} path={routes.live}
element={ element={
@ -545,8 +561,9 @@ function SodioHomeInstallation(props: singleInstallationProps) {
></TopologySodistoreHome> ></TopologySodistoreHome>
} }
/> />
)}
{currentUser.userType !== UserType.client && ( {currentUser.userType !== UserType.client && !dataCollectionDisabled && (
<Route <Route
path={routes.batteryview + '/*'} path={routes.batteryview + '/*'}
element={ element={
@ -573,7 +590,7 @@ function SodioHomeInstallation(props: singleInstallationProps) {
/> />
)} )}
{currentUser.userType == UserType.admin && ( {currentUser.userType == UserType.admin && !dataCollectionDisabled && (
<Route <Route
path={routes.configuration} path={routes.configuration}
element={ element={
@ -600,6 +617,7 @@ function SodioHomeInstallation(props: singleInstallationProps) {
/> />
)} */} )} */}
{!dataCollectionDisabled && (
<Route <Route
path={routes.overview} path={routes.overview}
element={ element={
@ -613,8 +631,9 @@ function SodioHomeInstallation(props: singleInstallationProps) {
/> />
} }
/> />
)}
{props.current_installation.device !== 3 && ( {props.current_installation.device !== 3 && !dataCollectionDisabled && (
<Route <Route
path={routes.report} path={routes.report}
element={ element={

View File

@ -35,6 +35,7 @@ function SodistorehomeInstallationForm(props: SodistorehomeInstallationFormPros)
vpnIp: '', vpnIp: '',
installationModel: '', installationModel: '',
externalEms: 'No', externalEms: 'No',
dataCollectionEnabled: true,
...(isSodistorePro ? { device: 4 } : {}), ...(isSodistorePro ? { device: 4 } : {}),
}); });
const [inverterCount, setInverterCount] = useState(''); 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> </Box>
<div <div
style={{ style={{

View File

@ -275,6 +275,12 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
const isGrowatt = currentInstallation?.device === 3 const isGrowatt = currentInstallation?.device === 3
|| (installations.length === 1 && installations[0].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 const tabs = inInstallationView && currentUser.userType == UserType.admin
? [ ? [
{ {
@ -471,6 +477,7 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
> >
{tabs {tabs
.filter((tab) => !(isGrowatt && tab.value === 'report')) .filter((tab) => !(isGrowatt && tab.value === 'report'))
.filter((tab) => !dataCollectionDisabled || allowedWhenDisabled.includes(tab.value))
.map((tab) => ( .map((tab) => (
<Tab <Tab
key={tab.value} key={tab.value}
@ -544,6 +551,7 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
> >
{singleInstallationTabs {singleInstallationTabs
.filter((tab) => !(isGrowatt && tab.value === 'report')) .filter((tab) => !(isGrowatt && tab.value === 'report'))
.filter((tab) => !dataCollectionDisabled || allowedWhenDisabled.includes(tab.value))
.map((tab) => ( .map((tab) => (
<Tab <Tab
key={tab.value} key={tab.value}

View File

@ -160,6 +160,18 @@ function CustomTreeItem(props: CustomTreeItemProps) {
{props.node.type === 'Installation' && ( {props.node.type === 'Installation' && (
<div> <div>
{(props.node as any).dataCollectionEnabled === false ? (
<div
style={{
width: '20px',
height: '20px',
borderRadius: '50%',
marginLeft: '17px',
backgroundColor: 'grey'
}}
/>
) : (
<>
{status === -1 ? ( {status === -1 ? (
<CancelIcon <CancelIcon
style={{ style={{
@ -204,6 +216,8 @@ function CustomTreeItem(props: CustomTreeItemProps) {
: 'green' : 'green'
}} }}
/> />
</>
)}
</div> </div>
)} )}
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

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