Просмотр исходного кода

Added option to track energy only per print or in total

Martin Ziegler 9 месяцев назад
Родитель
Сommit
301eec98fb

+ 32 - 9
backend/app/api/routes/archives.py

@@ -209,16 +209,39 @@ async def get_archive_stats(db: AsyncSession = Depends(get_db)):
         for printer_key, accs in printer_accuracies.items():
         for printer_key, accs in printer_accuracies.items():
             accuracy_by_printer[printer_key] = round(sum(accs) / len(accs), 1)
             accuracy_by_printer[printer_key] = round(sum(accs) / len(accs), 1)
 
 
-    # Energy totals
-    energy_kwh_result = await db.execute(
-        select(func.sum(PrintArchive.energy_kwh))
-    )
-    total_energy_kwh = energy_kwh_result.scalar() or 0
+    # Energy totals - check which mode to use
+    from backend.app.api.routes.settings import get_setting
+    energy_tracking_mode = await get_setting(db, "energy_tracking_mode") or "total"
+    energy_cost_per_kwh_str = await get_setting(db, "energy_cost_per_kwh")
+    energy_cost_per_kwh = float(energy_cost_per_kwh_str) if energy_cost_per_kwh_str else 0.15
+
+    if energy_tracking_mode == "total":
+        # Total mode: sum up 'total' counter from all smart plugs (lifetime consumption)
+        from backend.app.models.smart_plug import SmartPlug
+        from backend.app.services.tasmota import tasmota_service
+
+        plugs_result = await db.execute(select(SmartPlug))
+        plugs = list(plugs_result.scalars().all())
+
+        total_energy_kwh = 0.0
+        for plug in plugs:
+            energy = await tasmota_service.get_energy(plug)
+            if energy and energy.get("total") is not None:
+                total_energy_kwh += energy["total"]
+
+        total_energy_kwh = round(total_energy_kwh, 3)
+        total_energy_cost = round(total_energy_kwh * energy_cost_per_kwh, 2)
+    else:
+        # Print mode: sum up per-print energy from archives
+        energy_kwh_result = await db.execute(
+            select(func.sum(PrintArchive.energy_kwh))
+        )
+        total_energy_kwh = energy_kwh_result.scalar() or 0
 
 
-    energy_cost_result = await db.execute(
-        select(func.sum(PrintArchive.energy_cost))
-    )
-    total_energy_cost = energy_cost_result.scalar() or 0
+        energy_cost_result = await db.execute(
+            select(func.sum(PrintArchive.energy_cost))
+        )
+        total_energy_cost = energy_cost_result.scalar() or 0
 
 
     return ArchiveStats(
     return ArchiveStats(
         total_prints=total_prints,
         total_prints=total_prints,

+ 47 - 40
backend/app/main.py

@@ -555,49 +555,56 @@ async def on_print_complete(printer_id: int, data: dict):
             "status": status,
             "status": status,
         })
         })
 
 
-    # Calculate energy used for this print
+    # Calculate energy used for this print (always per-print: end - start)
     try:
     try:
         starting_kwh = _print_energy_start.pop(archive_id, None)
         starting_kwh = _print_energy_start.pop(archive_id, None)
-        logger.info(f"[ENERGY] Print complete for archive {archive_id}, starting_kwh={starting_kwh}, tracked_archives={list(_print_energy_start.keys())}")
-        if starting_kwh is not None:
-            async with async_session() as db:
-                # Get smart plug for this printer (SmartPlug is imported at module level)
-                plug_result = await db.execute(
-                    select(SmartPlug).where(SmartPlug.printer_id == printer_id)
-                )
-                plug = plug_result.scalar_one_or_none()
-
-                if plug:
-                    energy = await tasmota_service.get_energy(plug)
-                    logger.info(f"[ENERGY] Print complete - ending energy response: {energy}")
-                    if energy and energy.get("total") is not None:
-                        ending_kwh = energy["total"]
-                        energy_used = round(ending_kwh - starting_kwh, 4)
-                        logger.info(f"[ENERGY] Calculated: ending={ending_kwh}, starting={starting_kwh}, used={energy_used}")
-
-                        # Get energy cost per kWh from settings (default to 0.15)
-                        from backend.app.api.routes.settings import get_setting
-                        energy_cost_per_kwh = await get_setting(db, "energy_cost_per_kwh")
-                        cost_per_kwh = float(energy_cost_per_kwh) if energy_cost_per_kwh else 0.15
-                        energy_cost = round(energy_used * cost_per_kwh, 2)
-
-                        # Update archive with energy data
-                        from backend.app.models.archive import PrintArchive
-                        result = await db.execute(
-                            select(PrintArchive).where(PrintArchive.id == archive_id)
-                        )
-                        archive = result.scalar_one_or_none()
-                        if archive:
-                            archive.energy_kwh = energy_used
-                            archive.energy_cost = energy_cost
-                            await db.commit()
-                            logger.info(f"[ENERGY] Saved to archive {archive_id}: {energy_used} kWh, cost={energy_cost}")
-                        else:
-                            logger.warning(f"[ENERGY] Archive {archive_id} not found when saving energy")
-                    else:
-                        logger.warning(f"[ENERGY] No 'total' in ending energy response")
+        logger.info(f"[ENERGY] Print complete for archive {archive_id}, starting_kwh={starting_kwh}")
+
+        async with async_session() as db:
+            # Get smart plug for this printer (SmartPlug is imported at module level)
+            plug_result = await db.execute(
+                select(SmartPlug).where(SmartPlug.printer_id == printer_id)
+            )
+            plug = plug_result.scalar_one_or_none()
+
+            if plug:
+                energy = await tasmota_service.get_energy(plug)
+                logger.info(f"[ENERGY] Print complete - energy response: {energy}")
+
+                energy_used = None
+
+                # Calculate per-print energy: end total - start total
+                if starting_kwh is not None and energy and energy.get("total") is not None:
+                    ending_kwh = energy["total"]
+                    energy_used = round(ending_kwh - starting_kwh, 4)
+                    logger.info(f"[ENERGY] Per-print energy: ending={ending_kwh}, starting={starting_kwh}, used={energy_used}")
+                elif starting_kwh is None:
+                    logger.info(f"[ENERGY] No starting energy recorded for this archive")
                 else:
                 else:
-                    logger.info(f"[ENERGY] No smart plug found for printer {printer_id} at print complete")
+                    logger.warning(f"[ENERGY] No 'total' in ending energy response")
+
+                if energy_used is not None and energy_used >= 0:
+                    # Get energy cost per kWh from settings (default to 0.15)
+                    from backend.app.api.routes.settings import get_setting
+                    energy_cost_per_kwh = await get_setting(db, "energy_cost_per_kwh")
+                    cost_per_kwh = float(energy_cost_per_kwh) if energy_cost_per_kwh else 0.15
+                    energy_cost = round(energy_used * cost_per_kwh, 2)
+
+                    # Update archive with energy data
+                    from backend.app.models.archive import PrintArchive
+                    result = await db.execute(
+                        select(PrintArchive).where(PrintArchive.id == archive_id)
+                    )
+                    archive = result.scalar_one_or_none()
+                    if archive:
+                        archive.energy_kwh = energy_used
+                        archive.energy_cost = energy_cost
+                        await db.commit()
+                        logger.info(f"[ENERGY] Saved to archive {archive_id}: {energy_used} kWh, cost={energy_cost}")
+                    else:
+                        logger.warning(f"[ENERGY] Archive {archive_id} not found when saving energy")
+            else:
+                logger.info(f"[ENERGY] No smart plug found for printer {printer_id} at print complete")
     except Exception as e:
     except Exception as e:
         import logging
         import logging
         logging.getLogger(__name__).warning(f"Failed to calculate energy: {e}")
         logging.getLogger(__name__).warning(f"Failed to calculate energy: {e}")

+ 2 - 0
backend/app/schemas/settings.py

@@ -10,6 +10,7 @@ class AppSettings(BaseModel):
     default_filament_cost: float = Field(default=25.0, description="Default filament cost per kg")
     default_filament_cost: float = Field(default=25.0, description="Default filament cost per kg")
     currency: str = Field(default="USD", description="Currency for cost tracking")
     currency: str = Field(default="USD", description="Currency for cost tracking")
     energy_cost_per_kwh: float = Field(default=0.15, description="Electricity cost per kWh for energy tracking")
     energy_cost_per_kwh: float = Field(default=0.15, description="Electricity cost per kWh for energy tracking")
+    energy_tracking_mode: str = Field(default="total", description="Energy display mode on stats: 'print' shows sum of per-print energy, 'total' shows lifetime plug consumption")
 
 
 
 
 class AppSettingsUpdate(BaseModel):
 class AppSettingsUpdate(BaseModel):
@@ -21,3 +22,4 @@ class AppSettingsUpdate(BaseModel):
     default_filament_cost: float | None = None
     default_filament_cost: float | None = None
     currency: str | None = None
     currency: str | None = None
     energy_cost_per_kwh: float | None = None
     energy_cost_per_kwh: float | None = None
+    energy_tracking_mode: str | None = None

+ 1 - 0
frontend/src/api/client.ts

@@ -151,6 +151,7 @@ export interface AppSettings {
   default_filament_cost: number;
   default_filament_cost: number;
   currency: string;
   currency: string;
   energy_cost_per_kwh: number;
   energy_cost_per_kwh: number;
+  energy_tracking_mode: 'print' | 'total';
 }
 }
 
 
 export type AppSettingsUpdate = Partial<AppSettings>;
 export type AppSettingsUpdate = Partial<AppSettings>;

+ 20 - 2
frontend/src/pages/SettingsPage.tsx

@@ -59,7 +59,8 @@ export function SettingsPage() {
         settings.capture_finish_photo !== localSettings.capture_finish_photo ||
         settings.capture_finish_photo !== localSettings.capture_finish_photo ||
         settings.default_filament_cost !== localSettings.default_filament_cost ||
         settings.default_filament_cost !== localSettings.default_filament_cost ||
         settings.currency !== localSettings.currency ||
         settings.currency !== localSettings.currency ||
-        settings.energy_cost_per_kwh !== localSettings.energy_cost_per_kwh;
+        settings.energy_cost_per_kwh !== localSettings.energy_cost_per_kwh ||
+        settings.energy_tracking_mode !== localSettings.energy_tracking_mode;
       setHasChanges(changed);
       setHasChanges(changed);
     }
     }
   }, [settings, localSettings]);
   }, [settings, localSettings]);
@@ -72,6 +73,8 @@ export function SettingsPage() {
       setHasChanges(false);
       setHasChanges(false);
       setShowSaved(true);
       setShowSaved(true);
       setTimeout(() => setShowSaved(false), 2000);
       setTimeout(() => setShowSaved(false), 2000);
+      // Invalidate archive stats to reflect energy tracking mode change
+      queryClient.invalidateQueries({ queryKey: ['archiveStats'] });
     },
     },
   });
   });
 
 
@@ -249,8 +252,23 @@ export function SettingsPage() {
                   }
                   }
                   className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
                   className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
                 />
                 />
+              </div>
+              <div>
+                <label className="block text-sm text-bambu-gray mb-1">
+                  Energy display mode
+                </label>
+                <select
+                  value={localSettings.energy_tracking_mode || 'total'}
+                  onChange={(e) => updateSetting('energy_tracking_mode', e.target.value as 'print' | 'total')}
+                  className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
+                >
+                  <option value="print">Prints Only</option>
+                  <option value="total">Total Consumption</option>
+                </select>
                 <p className="text-xs text-bambu-gray mt-1">
                 <p className="text-xs text-bambu-gray mt-1">
-                  Used for tracking energy costs per print via smart plugs
+                  {localSettings.energy_tracking_mode === 'print'
+                    ? 'Dashboard shows sum of energy used during prints'
+                    : 'Dashboard shows lifetime energy from smart plugs'}
                 </p>
                 </p>
               </div>
               </div>
             </CardContent>
             </CardContent>

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/index-C2qF9pCa.js


+ 1 - 1
static/index.html

@@ -7,7 +7,7 @@
     <link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png" />
     <link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png" />
     <link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png" />
     <link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png" />
     <link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png" />
     <link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png" />
-    <script type="module" crossorigin src="/assets/index-MvaVdELS.js"></script>
+    <script type="module" crossorigin src="/assets/index-C2qF9pCa.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-B9G6rzsh.css">
     <link rel="stylesheet" crossorigin href="/assets/index-B9G6rzsh.css">
   </head>
   </head>
   <body>
   <body>

Некоторые файлы не были показаны из-за большого количества измененных файлов