From 9e81f246d8bc9c53d59d84a64c8787d7ff856c0e Mon Sep 17 00:00:00 2001 From: ManuelW Date: Mon, 23 Mar 2026 10:09:14 +0100 Subject: [PATCH] Feature/multiple switches per prtinter (#786) Feature/multiple switches per prtinter (#786) --- backend/app/api/routes/smart_plugs.py | 17 ++ frontend/src/api/client.ts | 1 + frontend/src/components/AddSmartPlugModal.tsx | 4 +- frontend/src/components/SmartPlugCard.tsx | 5 +- frontend/src/i18n/locales/de.ts | 10 +- frontend/src/i18n/locales/en.ts | 10 +- frontend/src/i18n/locales/fr.ts | 6 +- frontend/src/i18n/locales/it.ts | 6 +- frontend/src/i18n/locales/ja.ts | 10 +- frontend/src/i18n/locales/pt-BR.ts | 10 +- frontend/src/i18n/locales/zh-CN.ts | 12 +- frontend/src/pages/PrintersPage.tsx | 282 ++++++++---------- 12 files changed, 185 insertions(+), 188 deletions(-) diff --git a/backend/app/api/routes/smart_plugs.py b/backend/app/api/routes/smart_plugs.py index 40cb69312..51e3fcdc3 100644 --- a/backend/app/api/routes/smart_plugs.py +++ b/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 diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 01f6864ac..5ce2e8b2c 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -3277,6 +3277,7 @@ export const api = { getSmartPlug: (id: number) => request(`/smart-plugs/${id}`), getSmartPlugByPrinter: (printerId: number) => request(`/smart-plugs/by-printer/${printerId}`), getScriptPlugsByPrinter: (printerId: number) => request(`/smart-plugs/by-printer/${printerId}/scripts`), + getAllSmartPlugsByPrinter: (printerId: number) => request(`/smart-plugs/by-printer/${printerId}/all`), createSmartPlug: (data: SmartPlugCreate) => request('/smart-plugs/', { method: 'POST', diff --git a/frontend/src/components/AddSmartPlugModal.tsx b/frontend/src/components/AddSmartPlugModal.tsx index e6fdddeae..183bc2d1c 100644 --- a/frontend/src/components/AddSmartPlugModal.tsx +++ b/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) => { diff --git a/frontend/src/components/SmartPlugCard.tsx b/frontend/src/components/SmartPlugCard.tsx index b78856743..3e34103e6 100644 --- a/frontend/src/components/SmartPlugCard.tsx +++ b/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] }); } }, }); diff --git a/frontend/src/i18n/locales/de.ts b/frontend/src/i18n/locales/de.ts index 2b19d7978..7cc37c030 100644 --- a/frontend/src/i18n/locales/de.ts +++ b/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 diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index d7d091a5c..0142ff953 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/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 diff --git a/frontend/src/i18n/locales/fr.ts b/frontend/src/i18n/locales/fr.ts index 67b027d3f..3646d8ada 100644 --- a/frontend/src/i18n/locales/fr.ts +++ b/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 diff --git a/frontend/src/i18n/locales/it.ts b/frontend/src/i18n/locales/it.ts index ad295f2a1..6ff1c9e3c 100644 --- a/frontend/src/i18n/locales/it.ts +++ b/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 diff --git a/frontend/src/i18n/locales/ja.ts b/frontend/src/i18n/locales/ja.ts index c6283197a..c7df021e0 100644 --- a/frontend/src/i18n/locales/ja.ts +++ b/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 diff --git a/frontend/src/i18n/locales/pt-BR.ts b/frontend/src/i18n/locales/pt-BR.ts index c9ab8a433..169cb2659 100644 --- a/frontend/src/i18n/locales/pt-BR.ts +++ b/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 diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index 59440f916..d5460d1de 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/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 diff --git a/frontend/src/pages/PrintersPage.tsx b/frontend/src/pages/PrintersPage.tsx index 530fac87d..656b5437e 100644 --- a/frontend/src/pages/PrintersPage.tsx +++ b/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(null); + const [showPowerOffConfirm, setShowPowerOffConfirm] = useState(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 smart plugs for this printer + const { data: smartPlugs } = useQuery({ + queryKey: ['smartPlugsByPrinter', printer.id], + queryFn: () => api.getAllSmartPlugsByPrinter(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 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' && ( -
-
- {/* Plug name and status */} -
- - {smartPlug.name} - {plugStatus && ( - - {plugStatus.state || '?'} - {plugStatus.state === 'ON' && plugStatus.energy?.power != null && ( - · {plugStatus.energy.power}W + {smartPlugs && smartPlugs.length > 0 && viewMode === 'expanded' && ( +
+ {smartPlugs.map((plug, index) => { + const plugStatus = plugStatusResults[index]?.data; + const isScript = plug.plug_type === 'homeassistant' && plug.ha_entity_id?.startsWith('script.'); + return ( +
+
+ + {plug.name} + {plugStatus && ( + + {plugStatus.state || '?'} + {plugStatus.state === 'ON' && plugStatus.energy?.power != null && ( + · {plugStatus.energy.power}W + )} + )} - - )} -
- - {/* Spacer */} -
- - {/* Power buttons */} -
- - -
- - {/* Auto-off toggle */} -
- - -
-
- - {/* HA entity buttons row */} - {scriptPlugs && scriptPlugs.length > 0 && ( -
- - HA: -
- {scriptPlugs.map(script => { - const isScript = script.ha_entity_id?.startsWith('script.'); - return ( - - ); - })} +
+
+ {isScript ? ( + + ) : ( +
+ + +
+ )} + {!isScript && ( +
+ + +
+ )}
-
- )} + ); + })}
)} @@ -4514,36 +4494,36 @@ function PrinterCard({ )} {/* Power On Confirmation */} - {showPowerOnConfirm && smartPlug && ( + {showPowerOnConfirm !== null && ( 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 && ( 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)} /> )}