Przeglądaj źródła

Feature/multiple switches per prtinter (#786)

Feature/multiple switches per prtinter (#786)
ManuelW 2 miesięcy temu
rodzic
commit
9e81f246d8

+ 17 - 0
backend/app/api/routes/smart_plugs.py

@@ -201,6 +201,23 @@ async def get_script_plugs_by_printer(
     return ha_entities
 
 
+@router.get("/by-printer/{printer_id}/all", response_model=list[SmartPlugResponse])
+async def get_all_smart_plugs_by_printer(
+    printer_id: int,
+    db: AsyncSession = Depends(get_db),
+    _: User | None = RequirePermissionIfAuthEnabled(Permission.SMART_PLUGS_READ),
+):
+    """Get all smart plugs assigned to a printer that should appear on the printer card.
+
+    Returns power plugs (tasmota, mqtt) plus HA entities with show_on_printer_card enabled.
+    """
+    result = await db.execute(select(SmartPlug).where(SmartPlug.printer_id == printer_id))
+    plugs = result.scalars().all()
+    return [
+        plug for plug in plugs if plug.plug_type != "homeassistant" or (plug.ha_entity_id and plug.show_on_printer_card)
+    ]
+
+
 # Tasmota Discovery Endpoints
 # NOTE: These must be defined BEFORE /{plug_id} routes to avoid path conflicts
 

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

@@ -3277,6 +3277,7 @@ export const api = {
   getSmartPlug: (id: number) => request<SmartPlug>(`/smart-plugs/${id}`),
   getSmartPlugByPrinter: (printerId: number) => request<SmartPlug | null>(`/smart-plugs/by-printer/${printerId}`),
   getScriptPlugsByPrinter: (printerId: number) => request<SmartPlug[]>(`/smart-plugs/by-printer/${printerId}/scripts`),
+  getAllSmartPlugsByPrinter: (printerId: number) => request<SmartPlug[]>(`/smart-plugs/by-printer/${printerId}/all`),
   createSmartPlug: (data: SmartPlugCreate) =>
     request<SmartPlug>('/smart-plugs/', {
       method: 'POST',

+ 2 - 2
frontend/src/components/AddSmartPlugModal.tsx

@@ -255,7 +255,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
     onSuccess: () => {
       queryClient.invalidateQueries({ queryKey: ['smart-plugs'] });
       // Also invalidate printer card HA entity queries
-      queryClient.invalidateQueries({ queryKey: ['scriptPlugsByPrinter'] });
+      queryClient.invalidateQueries({ queryKey: ['smartPlugsByPrinter'] });
       onClose();
     },
     onError: (err: Error) => {
@@ -269,7 +269,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
     onSuccess: () => {
       queryClient.invalidateQueries({ queryKey: ['smart-plugs'] });
       // Also invalidate printer card HA entity queries
-      queryClient.invalidateQueries({ queryKey: ['scriptPlugsByPrinter'] });
+      queryClient.invalidateQueries({ queryKey: ['smartPlugsByPrinter'] });
       onClose();
     },
     onError: (err: Error) => {

+ 2 - 3
frontend/src/components/SmartPlugCard.tsx

@@ -80,8 +80,7 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
       queryClient.invalidateQueries({ queryKey: ['smart-plugs'] });
       // Also invalidate printer-specific smart plug queries to keep PrintersPage in sync
       if (plug.printer_id) {
-        queryClient.invalidateQueries({ queryKey: ['smartPlugByPrinter', plug.printer_id] });
-        queryClient.invalidateQueries({ queryKey: ['scriptPlugsByPrinter', plug.printer_id] });
+        queryClient.invalidateQueries({ queryKey: ['smartPlugsByPrinter', plug.printer_id] });
       }
     },
   });
@@ -93,7 +92,7 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
       queryClient.invalidateQueries({ queryKey: ['smart-plugs'] });
       // Also invalidate printer card HA entity queries
       if (plug.printer_id) {
-        queryClient.invalidateQueries({ queryKey: ['scriptPlugsByPrinter', plug.printer_id] });
+        queryClient.invalidateQueries({ queryKey: ['smartPlugsByPrinter', plug.printer_id] });
       }
     },
   });

+ 5 - 5
frontend/src/i18n/locales/de.ts

@@ -374,12 +374,12 @@ export default {
       resumeTitle: 'Druck fortsetzen',
       resumeMessage: 'Möchten Sie den Druck auf "{{name}}" fortsetzen?',
       resumeButton: 'Druck fortsetzen',
-      powerOnTitle: 'Drucker einschalten',
-      powerOnMessage: 'Möchten Sie die Stromversorgung für "{{name}}" wirklich EINSCHALTEN?',
+      powerOnTitle: 'Schalter einschalten',
+      powerOnMessage: 'Möchten Sie "{{name}}" wirklich EINSCHALTEN?',
       powerOnButton: 'Einschalten',
-      powerOffTitle: 'Drucker ausschalten',
-      powerOffMessage: 'Möchten Sie die Stromversorgung für "{{name}}" wirklich AUSSCHALTEN?',
-      powerOffWarning: 'WARNUNG: "{{name}}" druckt gerade! Möchten Sie die Stromversorgung wirklich AUSSCHALTEN? Dies unterbricht den Druck und kann den Drucker beschädigen.',
+      powerOffTitle: 'Schalter ausschalten',
+      powerOffMessage: 'Möchten Sie "{{name}}" wirklich AUSSCHALTEN?',
+      powerOffWarning: 'WARNUNG: "{{name}}" druckt gerade! Möchten Sie wirklich AUSSCHALTEN? Dies unterbricht den Druck und kann den Drucker beschädigen.',
       powerOffButton: 'Ausschalten',
     },
     // Discovery

+ 5 - 5
frontend/src/i18n/locales/en.ts

@@ -374,12 +374,12 @@ export default {
       resumeTitle: 'Resume Print',
       resumeMessage: 'Are you sure you want to resume the print on "{{name}}"?',
       resumeButton: 'Resume Print',
-      powerOnTitle: 'Power On Printer',
-      powerOnMessage: 'Are you sure you want to turn ON the power for "{{name}}"?',
+      powerOnTitle: 'Power On Switch',
+      powerOnMessage: 'Are you sure you want to turn ON "{{name}}"?',
       powerOnButton: 'Power On',
-      powerOffTitle: 'Power Off Printer',
-      powerOffMessage: 'Are you sure you want to turn OFF the power for "{{name}}"?',
-      powerOffWarning: 'WARNING: "{{name}}" is currently printing! Are you sure you want to turn OFF the power? This will interrupt the print and may damage the printer.',
+      powerOffTitle: 'Power Off Switch',
+      powerOffMessage: 'Are you sure you want to turn OFF "{{name}}"?',
+      powerOffWarning: 'WARNING: "{{name}}" is currently printing! Are you sure you want to turn it OFF? This will interrupt the print and may damage the printer.',
       powerOffButton: 'Power Off',
     },
     // Discovery

+ 3 - 3
frontend/src/i18n/locales/fr.ts

@@ -374,12 +374,12 @@ export default {
       resumeTitle: 'Reprendre l\'impression',
       resumeMessage: 'Reprendre l\'impression sur "{{name}}" ?',
       resumeButton: 'Reprendre',
-      powerOnTitle: 'Allumer l\'imprimante',
+      powerOnTitle: 'Allumer Interrupteur',
       powerOnMessage: 'Allumer "{{name}}" ?',
       powerOnButton: 'Allumer',
-      powerOffTitle: 'Éteindre l\'imprimante',
+      powerOffTitle: 'Éteindre Interrupteur',
       powerOffMessage: 'Éteindre "{{name}}" ?',
-      powerOffWarning: 'ATTENTION : "{{name}}" imprime ! L\'éteindre maintenant peut endommager l\'imprimante.',
+      powerOffWarning: 'ATTENTION : "{{name}}" imprime actuellement ! Êtes-vous sûr de vouloir l\'éteindre ? Cela interrompra l\'impression et pourrait endommager l\'imprimante.',
       powerOffButton: 'Éteindre',
     },
     // Discovery

+ 3 - 3
frontend/src/i18n/locales/it.ts

@@ -374,12 +374,12 @@ export default {
       resumeTitle: 'Riprendi Stampa',
       resumeMessage: 'Sei sicuro di riprendere la stampa su "{{name}}"?',
       resumeButton: 'Riprendi Stampa',
-      powerOnTitle: 'Accendi Stampante',
+      powerOnTitle: 'Accendi Interruttore',
       powerOnMessage: 'Sei sicuro di accendere "{{name}}"?',
       powerOnButton: 'Accendi',
-      powerOffTitle: 'Spegni Stampante',
+      powerOffTitle: 'Spegni Interruttore',
       powerOffMessage: 'Sei sicuro di spegnere "{{name}}"?',
-      powerOffWarning: 'AVVISO: "{{name}}" sta stampando! Sei sicuro di spegnere? Questo interromperà la stampa e potrebbe danneggiare la stampante.',
+      powerOffWarning: 'AVVISO: "{{name}}" sta stampando! Sei sicuro di spegnerlo? Questo interromperà la stampa e potrebbe danneggiare la stampante.',
       powerOffButton: 'Spegni',
     },
     // Discovery

+ 5 - 5
frontend/src/i18n/locales/ja.ts

@@ -373,12 +373,12 @@ export default {
       resumeTitle: '印刷を再開',
       resumeMessage: '「{{name}}」の印刷を再開しますか?',
       resumeButton: '印刷を再開',
-      powerOnTitle: 'プリンターの電源をオン',
-      powerOnMessage: '「{{name}}」の電源をオンにしますか?',
+      powerOnTitle: 'スイッチをオン',
+      powerOnMessage: '「{{name}}」をオンにしますか?',
       powerOnButton: '電源オン',
-      powerOffTitle: 'プリンターの電源をオフ',
-      powerOffMessage: '「{{name}}」の電源をオフにしますか?',
-      powerOffWarning: '警告: 「{{name}}」は現在印刷中です!電源をオフにしますか?印刷が中断され、プリンターが損傷する可能性があります。',
+      powerOffTitle: 'スイッチをオフ',
+      powerOffMessage: '「{{name}}」をオフにしますか?',
+      powerOffWarning: '警告: 「{{name}}」は現在印刷中です!オフにしますか?印刷が中断され、プリンターが損傷する可能性があります。',
       powerOffButton: '電源オフ',
     },
     // Discovery

+ 5 - 5
frontend/src/i18n/locales/pt-BR.ts

@@ -374,12 +374,12 @@ export default {
       resumeTitle: 'Retomar Impressão',
       resumeMessage: 'Tem certeza de que deseja retomar a impressão em "{{name}}"?',
       resumeButton: 'Retomar Impressão',
-      powerOnTitle: 'Ligar Impressora',
-      powerOnMessage: 'Tem certeza de que deseja ligar a impressora "{{name}}"?',
+      powerOnTitle: 'Ligar Interruptor',
+      powerOnMessage: 'Tem certeza de que deseja ligar "{{name}}"?',
       powerOnButton: 'Ligar',
-      powerOffTitle: 'Desligar Impressora',
-      powerOffMessage: 'Tem certeza de que deseja desligar a impressora "{{name}}"?',
-      powerOffWarning: 'AVISO: "{{name}}" está imprimindo no momento! Tem certeza de que deseja desligar a impressora? Isso interromperá a impressão e pode danificar a impressora.',
+      powerOffTitle: 'Desligar Interruptor',
+      powerOffMessage: 'Tem certeza de que deseja desligar "{{name}}"?',
+      powerOffWarning: 'AVISO: "{{name}}" está imprimindo! Tem certeza de que deseja desligá-lo? Isso interromperá a impressão e pode danificar a impressora.',
       powerOffButton: 'Desligar',
     },
     // Discovery

+ 6 - 6
frontend/src/i18n/locales/zh-CN.ts

@@ -374,12 +374,12 @@ export default {
       resumeTitle: '继续打印',
       resumeMessage: '确定要继续"{{name}}"上的打印吗?',
       resumeButton: '继续打印',
-      powerOnTitle: '开启打印机',
-      powerOnMessage: '确定要开"{{name}}"的电源吗?',
-      powerOnButton: '开',
-      powerOffTitle: '关闭打印机',
-      powerOffMessage: '确定要关闭"{{name}}"的电源吗?',
-      powerOffWarning: '警告:"{{name}}"正在打印中!确定要关闭电源吗?这将中断打印并可能损坏打印机。',
+      powerOnTitle: '开启开关',
+      powerOnMessage: '确定要开"{{name}}"吗?',
+      powerOnButton: '开',
+      powerOffTitle: '关闭开关',
+      powerOffMessage: '确定要关闭"{{name}}"吗?',
+      powerOffWarning: '警告:"{{name}}"正在打印中!确定要关闭吗?这将中断打印并可能损坏打印机。',
       powerOffButton: '关机',
     },
     // Discovery

+ 131 - 151
frontend/src/pages/PrintersPage.tsx

@@ -1,5 +1,5 @@
 import { useState, useEffect, useMemo, useRef, useCallback } from 'react';
-import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { useQuery, useQueries, useMutation, useQueryClient } from '@tanstack/react-query';
 import { useTranslation } from 'react-i18next';
 import { useTheme } from '../contexts/ThemeContext';
 import { useAuth } from '../contexts/AuthContext';
@@ -41,7 +41,6 @@ import {
   CheckCircle,
   XCircle,
   User,
-  Home,
   Printer as PrinterIcon,
   Info,
   Cable,
@@ -1554,8 +1553,8 @@ function PrinterCard({
   const [showEditModal, setShowEditModal] = useState(false);
   const [showFileManager, setShowFileManager] = useState(false);
   const [showMQTTDebug, setShowMQTTDebug] = useState(false);
-  const [showPowerOnConfirm, setShowPowerOnConfirm] = useState(false);
-  const [showPowerOffConfirm, setShowPowerOffConfirm] = useState(false);
+  const [showPowerOnConfirm, setShowPowerOnConfirm] = useState<number | null>(null);
+  const [showPowerOffConfirm, setShowPowerOffConfirm] = useState<number | null>(null);
   const [showHMSModal, setShowHMSModal] = useState(false);
   const [showStopConfirm, setShowStopConfirm] = useState(false);
   const [showPauseConfirm, setShowPauseConfirm] = useState(false);
@@ -1780,24 +1779,19 @@ function PrinterCard({
     ? currentTrayNow
     : cachedTrayNow.current;
 
-  // Fetch smart plug for this printer
-  const { data: smartPlug } = useQuery({
-    queryKey: ['smartPlugByPrinter', printer.id],
-    queryFn: () => api.getSmartPlugByPrinter(printer.id),
-  });
-
-  // Fetch script plugs for this printer (for multi-device control)
-  const { data: scriptPlugs } = useQuery({
-    queryKey: ['scriptPlugsByPrinter', printer.id],
-    queryFn: () => api.getScriptPlugsByPrinter(printer.id),
+  // Fetch smart plugs for this printer
+  const { data: smartPlugs } = useQuery({
+    queryKey: ['smartPlugsByPrinter', printer.id],
+    queryFn: () => api.getAllSmartPlugsByPrinter(printer.id),
   });
 
-  // Fetch smart plug status if plug exists (faster refresh for energy monitoring)
-  const { data: plugStatus } = useQuery({
-    queryKey: ['smartPlugStatus', smartPlug?.id],
-    queryFn: () => smartPlug ? api.getSmartPlugStatus(smartPlug.id) : null,
-    enabled: !!smartPlug,
-    refetchInterval: 10000, // 10 seconds for real-time power display
+  // Fetch smart plug status for all plugs (faster refresh for energy monitoring)
+  const plugStatusResults = useQueries({
+    queries: (smartPlugs || []).map(plug => ({
+      queryKey: ['smartPlugStatus', plug.id],
+      queryFn: () => api.getSmartPlugStatus(plug.id),
+      refetchInterval: 10000, // 10 seconds for real-time power display
+    })),
   });
 
   // Fetch queue count for this printer
@@ -1892,26 +1886,26 @@ function PrinterCard({
 
   // Smart plug control mutations
   const powerControlMutation = useMutation({
-    mutationFn: (action: 'on' | 'off') =>
-      smartPlug ? api.controlSmartPlug(smartPlug.id, action) : Promise.reject('No plug'),
-    onSuccess: () => {
-      queryClient.invalidateQueries({ queryKey: ['smartPlugStatus', smartPlug?.id] });
+    mutationFn: ({ plugId, action }: { plugId: number; action: 'on' | 'off' }) =>
+      api.controlSmartPlug(plugId, action),
+    onSuccess: (_data, variables) => {
+      queryClient.invalidateQueries({ queryKey: ['smartPlugsByPrinter', printer.id] });
+      queryClient.invalidateQueries({ queryKey: ['smartPlugStatus', variables.plugId] });
     },
   });
 
   const toggleAutoOffMutation = useMutation({
-    mutationFn: (enabled: boolean) =>
-      smartPlug ? api.updateSmartPlug(smartPlug.id, { auto_off: enabled }) : Promise.reject('No plug'),
+    mutationFn: ({ plugId, enabled }: { plugId: number; enabled: boolean }) =>
+      api.updateSmartPlug(plugId, { auto_off: enabled }),
     onSuccess: () => {
-      queryClient.invalidateQueries({ queryKey: ['smartPlugByPrinter', printer.id] });
-      // Also invalidate the smart-plugs list to keep Settings page in sync
+      queryClient.invalidateQueries({ queryKey: ['smartPlugsByPrinter', printer.id] });
       queryClient.invalidateQueries({ queryKey: ['smart-plugs'] });
     },
   });
 
-  // Run HA entity mutation — scripts use 'on' (trigger), switches use 'toggle'
+  // Run HA script mutation
   const runScriptMutation = useMutation({
-    mutationFn: ({ id, action }: { id: number; action: 'on' | 'toggle' }) => api.controlSmartPlug(id, action),
+    mutationFn: (id: number) => api.controlSmartPlug(id, 'on'),
     onSuccess: () => {
       showToast(t('printers.toast.scriptTriggered'));
     },
@@ -3969,118 +3963,104 @@ function PrinterCard({
         )}
 
         {/* Smart Plug Controls - hidden in compact mode */}
-        {smartPlug && viewMode === 'expanded' && (
-          <div className="mt-4 pt-4 border-t border-bambu-dark-tertiary">
-            <div className="flex items-center gap-3">
-              {/* Plug name and status */}
-              <div className="flex items-center gap-2 min-w-0">
-                <Zap className="w-4 h-4 text-bambu-gray flex-shrink-0" />
-                <span className="text-sm text-white truncate">{smartPlug.name}</span>
-                {plugStatus && (
-                  <span
-                    className={`text-xs px-1.5 py-0.5 rounded flex-shrink-0 ${
-                      plugStatus.state === 'ON'
-                        ? 'bg-bambu-green/20 text-bambu-green'
-                        : plugStatus.state === 'OFF'
-                        ? 'bg-red-500/20 text-red-400'
-                        : 'bg-bambu-gray/20 text-bambu-gray'
-                    }`}
-                  >
-                    {plugStatus.state || '?'}
-                    {plugStatus.state === 'ON' && plugStatus.energy?.power != null && (
-                      <span className="text-yellow-400 ml-1.5">· {plugStatus.energy.power}W</span>
+        {smartPlugs && smartPlugs.length > 0 && viewMode === 'expanded' && (
+          <div className="mt-4 pt-4 border-t border-bambu-dark-tertiary space-y-2">
+            {smartPlugs.map((plug, index) => {
+              const plugStatus = plugStatusResults[index]?.data;
+              const isScript = plug.plug_type === 'homeassistant' && plug.ha_entity_id?.startsWith('script.');
+              return (
+                <div key={plug.id} className="flex items-center gap-3">
+                  <div className="flex items-center gap-2 min-w-0">
+                    <Zap className="w-4 h-4 text-bambu-gray flex-shrink-0" />
+                    <span className="text-sm text-white truncate">{plug.name}</span>
+                    {plugStatus && (
+                      <span className={`text-xs px-1.5 py-0.5 rounded flex-shrink-0 ${
+                        plugStatus.state === 'ON'
+                          ? 'bg-bambu-green/20 text-bambu-green'
+                          : plugStatus.state === 'OFF'
+                          ? 'bg-red-500/20 text-red-400'
+                          : 'bg-bambu-gray/20 text-bambu-gray'
+                      }`}>
+                        {plugStatus.state || '?'}
+                        {plugStatus.state === 'ON' && plugStatus.energy?.power != null && (
+                          <span className="text-yellow-400 ml-1.5">· {plugStatus.energy.power}W</span>
+                        )}
+                      </span>
                     )}
-                  </span>
-                )}
-              </div>
-
-              {/* Spacer */}
-              <div className="flex-1" />
-
-              {/* Power buttons */}
-              <div className="flex items-center gap-1">
-                <button
-                  onClick={() => setShowPowerOnConfirm(true)}
-                  disabled={powerControlMutation.isPending || plugStatus?.state === 'ON' || !hasPermission('smart_plugs:control')}
-                  className={`px-2 py-1 text-xs rounded transition-colors flex items-center gap-1 ${
-                    !hasPermission('smart_plugs:control')
-                      ? 'bg-bambu-dark text-bambu-gray/50 cursor-not-allowed'
-                      : plugStatus?.state === 'ON'
-                        ? 'bg-bambu-green text-white'
-                        : 'bg-bambu-dark text-bambu-gray hover:text-white hover:bg-bambu-dark-tertiary'
-                  }`}
-                  title={!hasPermission('smart_plugs:control') ? t('printers.permission.noSmartPlugControl') : undefined}
-                >
-                  <Power className="w-3 h-3" />
-                  On
-                </button>
-                <button
-                  onClick={() => setShowPowerOffConfirm(true)}
-                  disabled={powerControlMutation.isPending || plugStatus?.state === 'OFF' || !hasPermission('smart_plugs:control')}
-                  className={`px-2 py-1 text-xs rounded transition-colors flex items-center gap-1 ${
-                    !hasPermission('smart_plugs:control')
-                      ? 'bg-bambu-dark text-bambu-gray/50 cursor-not-allowed'
-                      : plugStatus?.state === 'OFF'
-                        ? 'bg-red-500/30 text-red-400'
-                        : 'bg-bambu-dark text-bambu-gray hover:text-white hover:bg-bambu-dark-tertiary'
-                  }`}
-                  title={!hasPermission('smart_plugs:control') ? t('printers.permission.noSmartPlugControl') : undefined}
-                >
-                  <PowerOff className="w-3 h-3" />
-                  Off
-                </button>
-              </div>
-
-              {/* Auto-off toggle */}
-              <div className="flex items-center gap-2 flex-shrink-0">
-                <span className={`text-xs hidden sm:inline ${smartPlug.auto_off_executed ? 'text-bambu-green' : 'text-bambu-gray'}`}>
-                  {smartPlug.auto_off_executed ? 'Auto-off done' : 'Auto-off'}
-                </span>
-                <button
-                  onClick={() => toggleAutoOffMutation.mutate(!smartPlug.auto_off)}
-                  disabled={toggleAutoOffMutation.isPending || smartPlug.auto_off_executed || !hasPermission('smart_plugs:control')}
-                  title={!hasPermission('smart_plugs:control') ? t('printers.permission.noSmartPlugControl') : (smartPlug.auto_off_executed ? t('printers.autoOffExecuted') : t('printers.autoOffAfterPrint'))}
-                  className={`relative w-9 h-5 rounded-full transition-colors flex-shrink-0 ${
-                    !hasPermission('smart_plugs:control')
-                      ? 'bg-bambu-dark-tertiary/50 cursor-not-allowed'
-                      : smartPlug.auto_off_executed
-                        ? 'bg-bambu-green/50 cursor-not-allowed'
-                        : smartPlug.auto_off ? 'bg-bambu-green' : 'bg-bambu-dark-tertiary'
-                  }`}
-                >
-                  <span
-                    className={`absolute top-[2px] left-[2px] w-4 h-4 bg-white rounded-full transition-transform ${
-                      smartPlug.auto_off || smartPlug.auto_off_executed ? 'translate-x-4' : 'translate-x-0'
-                    }`}
-                  />
-                </button>
-              </div>
-            </div>
-
-            {/* HA entity buttons row */}
-            {scriptPlugs && scriptPlugs.length > 0 && (
-              <div className="flex items-center gap-2 mt-2 pt-2 border-t border-bambu-dark-tertiary/50">
-                <Home className="w-3.5 h-3.5 text-blue-400 flex-shrink-0" />
-                <span className="text-xs text-bambu-gray">HA:</span>
-                <div className="flex flex-wrap gap-1">
-                  {scriptPlugs.map(script => {
-                    const isScript = script.ha_entity_id?.startsWith('script.');
-                    return (
-                      <button
-                        key={script.id}
-                        onClick={() => runScriptMutation.mutate({ id: script.id, action: isScript ? 'on' : 'toggle' })}
-                        disabled={runScriptMutation.isPending}
-                        title={`${isScript ? 'Run' : 'Toggle'} ${script.ha_entity_id}`}
-                        className="px-2 py-0.5 text-xs bg-blue-500/20 text-blue-400 hover:bg-blue-500/30 rounded transition-colors flex items-center gap-1"
-                      >
-                        <Play className="w-2.5 h-2.5" />
-                        {script.name}
-                      </button>
-                    );
-                  })}
+                  </div>
+                  <div className="flex-1" />
+                  {isScript ? (
+                    <button
+                      onClick={() => runScriptMutation.mutate(plug.id)}
+                      disabled={runScriptMutation.isPending || !hasPermission('smart_plugs:control')}
+                      className="px-2 py-1 text-xs rounded transition-colors flex items-center gap-1 bg-blue-500/20 text-blue-400 hover:bg-blue-500/30"
+                      title={!hasPermission('smart_plugs:control') ? t('printers.permission.noSmartPlugControl') : 'Run script'}
+                    >
+                      <Play className="w-3 h-3" />
+                      Run
+                    </button>
+                  ) : (
+                  <div className="flex items-center gap-1">
+                    <button
+                      onClick={() => setShowPowerOnConfirm(plug.id)}
+                      disabled={powerControlMutation.isPending || plugStatus?.state === 'ON' || !hasPermission('smart_plugs:control')}
+                      className={`px-2 py-1 text-xs rounded transition-colors flex items-center gap-1 ${
+                        !hasPermission('smart_plugs:control')
+                          ? 'bg-bambu-dark text-bambu-gray/50 cursor-not-allowed'
+                          : plugStatus?.state === 'ON'
+                          ? 'bg-bambu-green text-white'
+                          : 'bg-bambu-dark text-bambu-gray hover:text-white hover:bg-bambu-dark-tertiary'
+                      }`}
+                      title={!hasPermission('smart_plugs:control') ? t('printers.permission.noSmartPlugControl') : undefined}
+                    >
+                      <Power className="w-3 h-3" />
+                      On
+                    </button>
+                    <button
+                      onClick={() => setShowPowerOffConfirm(plug.id)}
+                      disabled={powerControlMutation.isPending || plugStatus?.state === 'OFF' || !hasPermission('smart_plugs:control')}
+                      className={`px-2 py-1 text-xs rounded transition-colors flex items-center gap-1 ${
+                        !hasPermission('smart_plugs:control')
+                          ? 'bg-bambu-dark text-bambu-gray/50 cursor-not-allowed'
+                          : plugStatus?.state === 'OFF'
+                          ? 'bg-red-500/30 text-red-400'
+                          : 'bg-bambu-dark text-bambu-gray hover:text-white hover:bg-bambu-dark-tertiary'
+                      }`}
+                      title={!hasPermission('smart_plugs:control') ? t('printers.permission.noSmartPlugControl') : undefined}
+                    >
+                      <PowerOff className="w-3 h-3" />
+                      Off
+                    </button>
+                  </div>
+                  )}
+                  {!isScript && (
+                  <div className="flex items-center gap-2 flex-shrink-0">
+                    <span className={`text-xs hidden sm:inline ${plug.auto_off_executed ? 'text-bambu-green' : 'text-bambu-gray'}`}>
+                      {plug.auto_off_executed ? 'Auto-off done' : 'Auto-off'}
+                    </span>
+                    <button
+                      onClick={() => toggleAutoOffMutation.mutate({ plugId: plug.id, enabled: !plug.auto_off })}
+                      disabled={toggleAutoOffMutation.isPending || plug.auto_off_executed || !hasPermission('smart_plugs:control')}
+                      title={!hasPermission('smart_plugs:control') ? t('printers.permission.noSmartPlugControl') : (plug.auto_off_executed ? t('printers.autoOffExecuted') : t('printers.autoOffAfterPrint'))}
+                      className={`relative w-9 h-5 rounded-full transition-colors flex-shrink-0 ${
+                        !hasPermission('smart_plugs:control')
+                          ? 'bg-bambu-dark-tertiary/50 cursor-not-allowed'
+                          : plug.auto_off_executed
+                          ? 'bg-bambu-green/50 cursor-not-allowed'
+                          : plug.auto_off ? 'bg-bambu-green' : 'bg-bambu-dark-tertiary'
+                      }`}
+                    >
+                      <span
+                        className={`absolute top-[2px] left-[2px] w-4 h-4 bg-white rounded-full transition-transform ${
+                          plug.auto_off || plug.auto_off_executed ? 'translate-x-4' : 'translate-x-0'
+                        }`}
+                      />
+                    </button>
+                  </div>
+                  )}
                 </div>
-              </div>
-            )}
+              );
+            })}
           </div>
         )}
 
@@ -4514,36 +4494,36 @@ function PrinterCard({
       )}
 
       {/* Power On Confirmation */}
-      {showPowerOnConfirm && smartPlug && (
+      {showPowerOnConfirm !== null && (
         <ConfirmModal
           title={t('printers.confirm.powerOnTitle')}
-          message={t('printers.confirm.powerOnMessage', { name: printer.name })}
+          message={t('printers.confirm.powerOnMessage', { name: smartPlugs?.find(p => p.id === showPowerOnConfirm)?.name || '' })}
           confirmText={t('printers.confirm.powerOnButton')}
           variant="default"
           onConfirm={() => {
-            powerControlMutation.mutate('on');
-            setShowPowerOnConfirm(false);
+            powerControlMutation.mutate({ plugId: showPowerOnConfirm, action: 'on' });
+            setShowPowerOnConfirm(null);
           }}
-          onCancel={() => setShowPowerOnConfirm(false)}
+          onCancel={() => setShowPowerOnConfirm(null)}
         />
       )}
 
       {/* Power Off Confirmation */}
-      {showPowerOffConfirm && smartPlug && (
+      {showPowerOffConfirm !== null && (
         <ConfirmModal
           title={t('printers.confirm.powerOffTitle')}
           message={
             status?.state === 'RUNNING'
-              ? t('printers.confirm.powerOffWarning', { name: printer.name })
-              : t('printers.confirm.powerOffMessage', { name: printer.name })
+              ? t('printers.confirm.powerOffWarning', { name: smartPlugs?.find(p => p.id === showPowerOffConfirm)?.name || '' })
+              : t('printers.confirm.powerOffMessage', { name: smartPlugs?.find(p => p.id === showPowerOffConfirm)?.name || '' })
           }
           confirmText={t('printers.confirm.powerOffButton')}
           variant="danger"
           onConfirm={() => {
-            powerControlMutation.mutate('off');
-            setShowPowerOffConfirm(false);
+            powerControlMutation.mutate({ plugId: showPowerOffConfirm, action: 'off' });
+            setShowPowerOffConfirm(null);
           }}
-          onCancel={() => setShowPowerOffConfirm(false)}
+          onCancel={() => setShowPowerOffConfirm(null)}
         />
       )}