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, Globe } 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(t('smartPlugs.failedToTurn', { action, name: 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.plug_type === 'rest' ? ( ) : ( )}

{plug.name}

{plug.plug_type === 'mqtt' ? plug.mqtt_topic : plug.plug_type === 'homeassistant' ? plug.ha_entity_id : plug.plug_type === 'rest' ? (plug.rest_on_url || plug.rest_off_url) : 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 || '?') : t('smartPlugs.offline')}
) : plug.plug_type === 'rest' ? (
REST {isReachable ? (status?.state || '?') : t('smartPlugs.offline')}
) : isReachable ? (
{status?.state || t('smartPlugs.unknown')}
) : (
{t('smartPlugs.offline')}
)} {/* Admin page link - only for Tasmota */} {adminUrl && ( {t('smartPlugs.admin')} )}
{/* Linked Printer */} {linkedPrinter && (
{t('smartPlugs.linkedTo')} {linkedPrinter.name}
)} {/* Feature Badges */} {(plug.power_alert_enabled || plug.schedule_enabled || plug.plug_type === 'mqtt') && (
{plug.plug_type === 'mqtt' && ( {t('smartPlugs.monitorOnly')} )} {plug.power_alert_enabled && ( {t('smartPlugs.alerts')} )} {plug.schedule_enabled && ( {plug.schedule_on_time && plug.schedule_off_time ? `${plug.schedule_on_time} - ${plug.schedule_off_time}` : plug.schedule_on_time ? t('smartPlugs.scheduleOn', { time: plug.schedule_on_time }) : t('smartPlugs.scheduleOff', { time: 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

{t('smartPlugs.power')}

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

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

{t('smartPlugs.kwhToday')}

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

{t('smartPlugs.showInSwitchbar')}

{t('smartPlugs.quickAccessSidebar')}

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

{t('smartPlugs.enabled')}

{t('smartPlugs.enableAutomation')}

{/* Auto On */}

{t('smartPlugs.autoOn')}

{t('smartPlugs.autoOnDescription')}

{/* Auto Off */}

{t('smartPlugs.autoOff')}

{t('smartPlugs.autoOffDescription')}

{/* Auto Off Persistent */} {plug.auto_off && (

{t('smartPlugs.autoOffPersistent')}

{t('smartPlugs.autoOffPersistentDescription')}

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

{t('smartPlugs.turnOffDelayMode')}

{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" />

{t('smartPlugs.tempThresholdDescription')}

)}
)} {/* Auto Off After Drying (#1349) — independent of the print-finish auto-off above. Uses its own delay because the AMS chamber is hot post-cycle and users often want more cooldown than the print-finish default. Fires when any AMS attached to the linked printer finishes a dry cycle. */}

{t('smartPlugs.autoOffAfterDrying')}

{t('smartPlugs.autoOffAfterDryingDescription')}

{plug.auto_off_after_drying && (
updateMutation.mutate({ off_delay_after_drying_minutes: parseInt(e.target.value) || 10 })} 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" />
)} )} {/* 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)} /> )} ); }