import { useState } from 'react'; import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query'; import { Plug, Power, PowerOff, Loader2, Trash2, Settings2, Thermometer, Clock, Wifi, WifiOff, Edit2, Bell, Calendar, LayoutGrid, ExternalLink, Home, Radio, Eye } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { api } from '../api/client'; import type { SmartPlug, SmartPlugUpdate } from '../api/client'; import { Card, CardContent } from './Card'; import { Button } from './Button'; import { ConfirmModal } from './ConfirmModal'; import { useToast } from '../contexts/ToastContext'; interface SmartPlugCardProps { plug: SmartPlug; onEdit: (plug: SmartPlug) => void; } export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) { const { t } = useTranslation(); const queryClient = useQueryClient(); const { showToast } = useToast(); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [showPowerOnConfirm, setShowPowerOnConfirm] = useState(false); const [showPowerOffConfirm, setShowPowerOffConfirm] = useState(false); const [isExpanded, setIsExpanded] = useState(false); // Fetch current status const { data: status, isLoading: statusLoading } = useQuery({ queryKey: ['smart-plug-status', plug.id], queryFn: () => api.getSmartPlugStatus(plug.id), refetchInterval: 30000, // Refresh every 30 seconds }); // Fetch printers for linking const { data: printers } = useQuery({ queryKey: ['printers'], queryFn: api.getPrinters, }); const linkedPrinter = printers?.find(p => p.id === plug.printer_id); // Control mutation with optimistic updates const controlMutation = useMutation({ mutationFn: (action: 'on' | 'off' | 'toggle') => api.controlSmartPlug(plug.id, action), onMutate: async (action) => { // Cancel any outgoing refetches await queryClient.cancelQueries({ queryKey: ['smart-plug-status', plug.id] }); // Snapshot the previous value const previousStatus = queryClient.getQueryData(['smart-plug-status', plug.id]); // Optimistically update to the new value const newState = action === 'on' ? 'ON' : action === 'off' ? 'OFF' : (status?.state === 'ON' ? 'OFF' : 'ON'); queryClient.setQueryData(['smart-plug-status', plug.id], (old: typeof status) => ({ ...old, state: newState, })); return { previousStatus }; }, onError: (_err, action, context) => { // Rollback on error if (context?.previousStatus) { queryClient.setQueryData(['smart-plug-status', plug.id], context.previousStatus); } showToast(`Failed to turn ${action} "${plug.name}"`, 'error'); }, onSettled: () => { // Refetch after a short delay to get actual state setTimeout(() => { queryClient.invalidateQueries({ queryKey: ['smart-plug-status', plug.id] }); queryClient.invalidateQueries({ queryKey: ['smart-plugs'] }); }, 1000); }, }); // Update mutation const updateMutation = useMutation({ mutationFn: (data: SmartPlugUpdate) => api.updateSmartPlug(plug.id, data), onSuccess: () => { 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] }); } }, }); // Delete mutation const deleteMutation = useMutation({ mutationFn: () => api.deleteSmartPlug(plug.id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['smart-plugs'] }); // Also invalidate printer card HA entity queries if (plug.printer_id) { queryClient.invalidateQueries({ queryKey: ['scriptPlugsByPrinter', plug.printer_id] }); } }, }); const isOn = status?.state === 'ON'; // For MQTT plugs, consider reachable if we have power data (even if backend says not reachable) const hasMqttData = plug.plug_type === 'mqtt' && (status?.energy?.power !== null && status?.energy?.power !== undefined); const isReachable = (status?.reachable ?? false) || hasMqttData; const isPending = controlMutation.isPending; // Generate admin URL with auto-login credentials (Tasmota only) const getAdminUrl = () => { if (plug.plug_type !== 'tasmota' || !plug.ip_address) return null; const ip = plug.ip_address; if (plug.username && plug.password) { // Use HTTP Basic Auth in URL for auto-login return `http://${encodeURIComponent(plug.username)}:${encodeURIComponent(plug.password)}@${ip}/`; } return `http://${ip}/`; }; const adminUrl = getAdminUrl(); return ( <> {/* Header Row */}
{plug.plug_type === 'mqtt' ? ( ) : plug.plug_type === 'homeassistant' ? ( ) : ( )}

{plug.name}

{plug.plug_type === 'mqtt' ? plug.mqtt_topic : plug.plug_type === 'homeassistant' ? plug.ha_entity_id : plug.ip_address}

{/* Status indicator */}
{statusLoading ? ( ) : plug.plug_type === 'mqtt' ? ( /* MQTT plugs - show badge and checkmark when receiving data */
MQTT {isReachable && }
) : plug.plug_type === 'homeassistant' ? (
HA {isReachable ? (status?.state || '?') : 'Offline'}
) : isReachable ? (
{status?.state || 'Unknown'}
) : (
{t('smartPlugs.offline')}
)} {/* Admin page link - only for Tasmota */} {adminUrl && ( {t('smartPlugs.admin')} )}
{/* Linked Printer */} {linkedPrinter && (
Linked to: {linkedPrinter.name}
)} {/* Feature Badges */} {(plug.power_alert_enabled || plug.schedule_enabled || plug.plug_type === 'mqtt') && (
{plug.plug_type === 'mqtt' && ( Monitor Only )} {plug.power_alert_enabled && ( Alerts )} {plug.schedule_enabled && ( {plug.schedule_on_time && plug.schedule_off_time ? `${plug.schedule_on_time} - ${plug.schedule_off_time}` : plug.schedule_on_time ? `On ${plug.schedule_on_time}` : `Off ${plug.schedule_off_time}`} )}
)} {/* Quick Controls - hidden for MQTT plugs (monitor-only) */} {plug.plug_type !== 'mqtt' && (
)} {/* Energy display for MQTT plugs */} {plug.plug_type === 'mqtt' && status?.energy && (
{status.energy.power !== null && status.energy.power !== undefined && (

{Math.round(status.energy.power)}W

Power

)} {status.energy.today !== null && status.energy.today !== undefined && (

{status.energy.today.toFixed(2)}

kWh Today

)}
)} {/* Toggle Settings Panel */} {/* Expanded Settings */} {isExpanded && (
{/* Show in Switchbar Toggle */}

Show in Switchbar

Quick access from sidebar

{/* Automation controls - only for controllable plugs (not MQTT) */} {plug.plug_type !== 'mqtt' && ( <> {/* Enabled Toggle */}

Enabled

Enable automation for this plug

{/* Auto On */}

Auto On

Turn on when print starts

{/* Auto Off */}

Auto Off

Turn off when print completes (one-shot)

{/* Delay Mode */} {plug.auto_off && (

Turn Off Delay Mode

{plug.off_delay_mode === 'time' ? (
updateMutation.mutate({ off_delay_minutes: parseInt(e.target.value) || 5 })} className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white text-sm focus:border-bambu-green focus:outline-none" />
) : (
updateMutation.mutate({ off_temp_threshold: parseInt(e.target.value) || 70 })} className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white text-sm focus:border-bambu-green focus:outline-none" />

Turns off when nozzle cools below this temperature

)}
)} )} {/* Action Buttons */}
)}
{/* Delete Confirmation */} {showDeleteConfirm && ( { deleteMutation.mutate(); setShowDeleteConfirm(false); }} onCancel={() => setShowDeleteConfirm(false)} /> )} {/* Power On Confirmation */} {showPowerOnConfirm && ( { controlMutation.mutate('on'); setShowPowerOnConfirm(false); }} onCancel={() => setShowPowerOnConfirm(false)} /> )} {/* Power Off Confirmation */} {showPowerOffConfirm && ( { controlMutation.mutate('off'); setShowPowerOffConfirm(false); }} onCancel={() => setShowPowerOffConfirm(false)} /> )} ); }