Added power switch and automation controls to printer card

This commit is contained in:
Martin Ziegler
2025-11-29 11:01:29 +01:00
parent 526de63acd
commit a2b889485d
8 changed files with 657 additions and 447 deletions
+1
View File
@@ -519,6 +519,7 @@ export const api = {
// Smart Plugs
getSmartPlugs: () => request<SmartPlug[]>('/smart-plugs/'),
getSmartPlug: (id: number) => request<SmartPlug>(`/smart-plugs/${id}`),
getSmartPlugByPrinter: (printerId: number) => request<SmartPlug | null>(`/smart-plugs/by-printer/${printerId}`),
createSmartPlug: (data: SmartPlugCreate) =>
request<SmartPlug>('/smart-plugs/', {
method: 'POST',
+34 -2
View File
@@ -15,6 +15,8 @@ interface SmartPlugCardProps {
export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
const queryClient = useQueryClient();
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [showPowerOnConfirm, setShowPowerOnConfirm] = useState(false);
const [showPowerOffConfirm, setShowPowerOffConfirm] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
// Fetch current status
@@ -108,7 +110,7 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
size="sm"
variant={isOn ? 'primary' : 'secondary'}
disabled={!isReachable || isPending}
onClick={() => controlMutation.mutate('on')}
onClick={() => setShowPowerOnConfirm(true)}
className="flex-1"
>
{isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <Power className="w-4 h-4" />}
@@ -118,7 +120,7 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
size="sm"
variant={!isOn ? 'primary' : 'secondary'}
disabled={!isReachable || isPending}
onClick={() => controlMutation.mutate('off')}
onClick={() => setShowPowerOffConfirm(true)}
className="flex-1"
>
{isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <PowerOff className="w-4 h-4" />}
@@ -291,6 +293,36 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
onCancel={() => setShowDeleteConfirm(false)}
/>
)}
{/* Power On Confirmation */}
{showPowerOnConfirm && (
<ConfirmModal
title="Turn On Smart Plug"
message={`Are you sure you want to turn on "${plug.name}"?`}
confirmText="Turn On"
variant="default"
onConfirm={() => {
controlMutation.mutate('on');
setShowPowerOnConfirm(false);
}}
onCancel={() => setShowPowerOnConfirm(false)}
/>
)}
{/* Power Off Confirmation */}
{showPowerOffConfirm && (
<ConfirmModal
title="Turn Off Smart Plug"
message={`Are you sure you want to turn off "${plug.name}"? This will cut power to the connected device.`}
confirmText="Turn Off"
variant="danger"
onConfirm={() => {
controlMutation.mutate('off');
setShowPowerOffConfirm(false);
}}
onCancel={() => setShowPowerOffConfirm(false)}
/>
)}
</>
);
}
+200 -35
View File
@@ -13,6 +13,9 @@ import {
HardDrive,
AlertTriangle,
Terminal,
Power,
PowerOff,
Zap,
} from 'lucide-react';
import { api } from '../api/client';
import type { Printer, PrinterCreate } from '../api/client';
@@ -83,6 +86,8 @@ function PrinterCard({ printer, hideIfDisconnected }: { printer: Printer; hideIf
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [showFileManager, setShowFileManager] = useState(false);
const [showMQTTDebug, setShowMQTTDebug] = useState(false);
const [showPowerOnConfirm, setShowPowerOnConfirm] = useState(false);
const [showPowerOffConfirm, setShowPowerOffConfirm] = useState(false);
const { data: status } = useQuery({
queryKey: ['printerStatus', printer.id],
@@ -90,6 +95,20 @@ function PrinterCard({ printer, hideIfDisconnected }: { printer: Printer; hideIf
refetchInterval: 30000, // Fallback polling, WebSocket handles real-time
});
// Fetch smart plug for this printer
const { data: smartPlug } = useQuery({
queryKey: ['smartPlugByPrinter', printer.id],
queryFn: () => api.getSmartPlugByPrinter(printer.id),
});
// Fetch smart plug status if plug exists
const { data: plugStatus } = useQuery({
queryKey: ['smartPlugStatus', smartPlug?.id],
queryFn: () => smartPlug ? api.getSmartPlugStatus(smartPlug.id) : null,
enabled: !!smartPlug,
refetchInterval: 30000,
});
// Determine if this card should be hidden
const shouldHide = hideIfDisconnected && status && !status.connected;
@@ -107,6 +126,23 @@ function PrinterCard({ printer, hideIfDisconnected }: { printer: Printer; hideIf
},
});
// 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] });
},
});
const toggleAutoOffMutation = useMutation({
mutationFn: (enabled: boolean) =>
smartPlug ? api.updateSmartPlug(smartPlug.id, { auto_off: enabled }) : Promise.reject('No plug'),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['smartPlugByPrinter', printer.id] });
},
});
if (shouldHide) {
return null;
}
@@ -229,44 +265,63 @@ function PrinterCard({ printer, hideIfDisconnected }: { printer: Printer; hideIf
</p>
</div>
{/* Current Print */}
{status.current_print && status.state === 'RUNNING' && (
<div className="mb-4 p-3 bg-bambu-dark rounded-lg">
<div className="flex gap-3">
{/* Cover Image */}
<CoverImage url={status.cover_url} printName={status.subtask_name || status.current_print || undefined} />
{/* Print Info */}
<div className="flex-1 min-w-0">
<p className="text-sm text-bambu-gray mb-1">Printing</p>
<p className="text-white text-sm mb-2 truncate">
{status.subtask_name || status.current_print}
</p>
<div className="flex items-center justify-between text-sm">
<div className="flex-1 bg-bambu-dark-tertiary rounded-full h-2 mr-3">
<div
className="bg-bambu-green h-2 rounded-full transition-all"
style={{ width: `${status.progress || 0}%` }}
/>
{/* Current Print or Idle Placeholder */}
<div className="mb-4 p-3 bg-bambu-dark rounded-lg">
<div className="flex gap-3">
{/* Cover Image */}
<CoverImage
url={status.state === 'RUNNING' ? status.cover_url : undefined}
printName={status.state === 'RUNNING' ? (status.subtask_name || status.current_print || undefined) : undefined}
/>
{/* Print Info */}
<div className="flex-1 min-w-0">
{status.current_print && status.state === 'RUNNING' ? (
<>
<p className="text-sm text-bambu-gray mb-1">Printing</p>
<p className="text-white text-sm mb-2 truncate">
{status.subtask_name || status.current_print}
</p>
<div className="flex items-center justify-between text-sm">
<div className="flex-1 bg-bambu-dark-tertiary rounded-full h-2 mr-3">
<div
className="bg-bambu-green h-2 rounded-full transition-all"
style={{ width: `${status.progress || 0}%` }}
/>
</div>
<span className="text-white">{Math.round(status.progress || 0)}%</span>
</div>
<span className="text-white">{Math.round(status.progress || 0)}%</span>
</div>
<div className="flex items-center gap-3 mt-2 text-xs text-bambu-gray">
{status.remaining_time != null && status.remaining_time > 0 && (
<span className="flex items-center gap-1">
<Clock className="w-3 h-3" />
{formatTime(status.remaining_time * 60)}
</span>
)}
{status.layer_num != null && status.total_layers != null && status.total_layers > 0 && (
<span>
Layer {status.layer_num}/{status.total_layers}
</span>
)}
</div>
</div>
<div className="flex items-center gap-3 mt-2 text-xs text-bambu-gray">
{status.remaining_time != null && status.remaining_time > 0 && (
<span className="flex items-center gap-1">
<Clock className="w-3 h-3" />
{formatTime(status.remaining_time * 60)}
</span>
)}
{status.layer_num != null && status.total_layers != null && status.total_layers > 0 && (
<span>
Layer {status.layer_num}/{status.total_layers}
</span>
)}
</div>
</>
) : (
<>
<p className="text-sm text-bambu-gray mb-1">Status</p>
<p className="text-white text-sm mb-2 capitalize">
{status.state?.toLowerCase() || 'Idle'}
</p>
<div className="flex items-center justify-between text-sm">
<div className="flex-1 bg-bambu-dark-tertiary rounded-full h-2 mr-3">
<div className="bg-bambu-dark-tertiary h-2 rounded-full" />
</div>
<span className="text-bambu-gray">—</span>
</div>
<p className="text-xs text-bambu-gray mt-2">Ready to print</p>
</>
)}
</div>
</div>
)}
</div>
{/* Temperatures */}
{status.temperatures && (
@@ -299,6 +354,82 @@ function PrinterCard({ printer, hideIfDisconnected }: { printer: Printer; hideIf
</>
)}
{/* Smart Plug Controls */}
{smartPlug && (
<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 || '?'}
</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'}
className={`px-2 py-1 text-xs rounded transition-colors flex items-center gap-1 ${
plugStatus?.state === 'ON'
? 'bg-bambu-green text-white'
: 'bg-bambu-dark text-bambu-gray hover:text-white hover:bg-bambu-dark-tertiary'
}`}
>
<Power className="w-3 h-3" />
On
</button>
<button
onClick={() => setShowPowerOffConfirm(true)}
disabled={powerControlMutation.isPending || plugStatus?.state === 'OFF'}
className={`px-2 py-1 text-xs rounded transition-colors flex items-center gap-1 ${
plugStatus?.state === 'OFF'
? 'bg-red-500/30 text-red-400'
: 'bg-bambu-dark text-bambu-gray hover:text-white hover:bg-bambu-dark-tertiary'
}`}
>
<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 text-bambu-gray hidden sm:inline">Auto-off</span>
<button
onClick={() => toggleAutoOffMutation.mutate(!smartPlug.auto_off)}
disabled={toggleAutoOffMutation.isPending}
title="Auto power-off after print"
className={`relative w-9 h-5 rounded-full transition-colors flex-shrink-0 ${
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 ? 'translate-x-4' : 'translate-x-0'
}`}
/>
</button>
</div>
</div>
</div>
)}
{/* Connection Info & Actions */}
<div className="mt-4 pt-4 border-t border-bambu-dark-tertiary flex items-center justify-between">
<div className="text-xs text-bambu-gray">
@@ -334,6 +465,40 @@ function PrinterCard({ printer, hideIfDisconnected }: { printer: Printer; hideIf
onClose={() => setShowMQTTDebug(false)}
/>
)}
{/* Power On Confirmation */}
{showPowerOnConfirm && smartPlug && (
<ConfirmModal
title="Power On Printer"
message={`Are you sure you want to turn ON the power for "${printer.name}"?`}
confirmText="Power On"
variant="default"
onConfirm={() => {
powerControlMutation.mutate('on');
setShowPowerOnConfirm(false);
}}
onCancel={() => setShowPowerOnConfirm(false)}
/>
)}
{/* Power Off Confirmation */}
{showPowerOffConfirm && smartPlug && (
<ConfirmModal
title="Power Off Printer"
message={
status?.state === 'RUNNING'
? `WARNING: "${printer.name}" is currently printing! Are you sure you want to turn OFF the power? This will interrupt the print and may damage the printer.`
: `Are you sure you want to turn OFF the power for "${printer.name}"?`
}
confirmText="Power Off"
variant="danger"
onConfirm={() => {
powerControlMutation.mutate('off');
setShowPowerOffConfirm(false);
}}
onCancel={() => setShowPowerOffConfirm(false)}
/>
)}
</Card>
);
}