Files
bambuddy/frontend/src/components/AddSmartPlugModal.tsx
T
maziggy 76adf70fd4 Add separate power/energy URLs and multipliers for REST smart plugs (#472)
REST/Webhook smart plugs can now fetch power and energy data from
  individual URLs instead of requiring all values in a single status
  response. Each value falls back to the shared Status URL when no
  separate URL is set, preserving backward compatibility. Added power
  and energy multipliers for unit conversion (e.g. 0.001 for Wh→kWh).
2026-04-03 08:31:08 +02:00

1655 lines
84 KiB
TypeScript

import { useState, useEffect, useRef } from 'react';
import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
import { X, Save, Loader2, Wifi, WifiOff, CheckCircle, Bell, Clock, LayoutGrid, Search, Plug, Power, Home, Radio, Eye, Globe } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { api } from '../api/client';
import type { SmartPlug, SmartPlugCreate, SmartPlugUpdate, DiscoveredTasmotaDevice } from '../api/client';
import { Button } from './Button';
interface AddSmartPlugModalProps {
plug?: SmartPlug | null;
onClose: () => void;
}
export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
const { t } = useTranslation();
const queryClient = useQueryClient();
const isEditing = !!plug;
// Plug type selection
const [plugType, setPlugType] = useState<'tasmota' | 'homeassistant' | 'mqtt' | 'rest'>(plug?.plug_type || 'tasmota');
const [name, setName] = useState(plug?.name || '');
// Tasmota fields
const [ipAddress, setIpAddress] = useState(plug?.ip_address || '');
const [username, setUsername] = useState(plug?.username || '');
const [password, setPassword] = useState(plug?.password || '');
// Home Assistant fields
const [haEntityId, setHaEntityId] = useState(plug?.ha_entity_id || '');
// MQTT fields - Power
const [mqttPowerTopic, setMqttPowerTopic] = useState(plug?.mqtt_power_topic || plug?.mqtt_topic || '');
const [mqttPowerPath, setMqttPowerPath] = useState(plug?.mqtt_power_path || '');
const [mqttPowerMultiplier, setMqttPowerMultiplier] = useState<string>(
(plug?.mqtt_power_multiplier ?? plug?.mqtt_multiplier ?? 1).toString()
);
// MQTT fields - Energy
const [mqttEnergyTopic, setMqttEnergyTopic] = useState(plug?.mqtt_energy_topic || '');
const [mqttEnergyPath, setMqttEnergyPath] = useState(plug?.mqtt_energy_path || '');
const [mqttEnergyMultiplier, setMqttEnergyMultiplier] = useState<string>(
(plug?.mqtt_energy_multiplier ?? 1).toString()
);
// MQTT fields - State
const [mqttStateTopic, setMqttStateTopic] = useState(plug?.mqtt_state_topic || '');
const [mqttStatePath, setMqttStatePath] = useState(plug?.mqtt_state_path || '');
const [mqttStateOnValue, setMqttStateOnValue] = useState(plug?.mqtt_state_on_value || '');
// REST fields
const [restOnUrl, setRestOnUrl] = useState(plug?.rest_on_url || '');
const [restOnBody, setRestOnBody] = useState(plug?.rest_on_body || '');
const [restOffUrl, setRestOffUrl] = useState(plug?.rest_off_url || '');
const [restOffBody, setRestOffBody] = useState(plug?.rest_off_body || '');
const [restMethod, setRestMethod] = useState(plug?.rest_method || 'POST');
const [restHeaders, setRestHeaders] = useState(plug?.rest_headers || '');
const [restStatusUrl, setRestStatusUrl] = useState(plug?.rest_status_url || '');
const [restStatusPath, setRestStatusPath] = useState(plug?.rest_status_path || '');
const [restStatusOnValue, setRestStatusOnValue] = useState(plug?.rest_status_on_value || '');
const [restPowerUrl, setRestPowerUrl] = useState(plug?.rest_power_url || '');
const [restPowerPath, setRestPowerPath] = useState(plug?.rest_power_path || '');
const [restPowerMultiplier, setRestPowerMultiplier] = useState<string>((plug?.rest_power_multiplier ?? 1).toString());
const [restEnergyUrl, setRestEnergyUrl] = useState(plug?.rest_energy_url || '');
const [restEnergyPath, setRestEnergyPath] = useState(plug?.rest_energy_path || '');
const [restEnergyMultiplier, setRestEnergyMultiplier] = useState<string>((plug?.rest_energy_multiplier ?? 1).toString());
// HA energy sensor entities (optional)
const [haPowerEntity, setHaPowerEntity] = useState(plug?.ha_power_entity || '');
const [haEnergyTodayEntity, setHaEnergyTodayEntity] = useState(plug?.ha_energy_today_entity || '');
const [haEnergyTotalEntity, setHaEnergyTotalEntity] = useState(plug?.ha_energy_total_entity || '');
// HA entity search
const [haEntitySearch, setHaEntitySearch] = useState('');
const [debouncedSearch, setDebouncedSearch] = useState('');
const [isEntityDropdownOpen, setIsEntityDropdownOpen] = useState(false);
const entityDropdownRef = useRef<HTMLDivElement>(null);
// Energy sensor search states
const [powerSensorSearch, setPowerSensorSearch] = useState('');
const [isPowerDropdownOpen, setIsPowerDropdownOpen] = useState(false);
const powerDropdownRef = useRef<HTMLDivElement>(null);
const [energyTodaySearch, setEnergyTodaySearch] = useState('');
const [isEnergyTodayDropdownOpen, setIsEnergyTodayDropdownOpen] = useState(false);
const energyTodayDropdownRef = useRef<HTMLDivElement>(null);
const [energyTotalSearch, setEnergyTotalSearch] = useState('');
const [isEnergyTotalDropdownOpen, setIsEnergyTotalDropdownOpen] = useState(false);
const energyTotalDropdownRef = useRef<HTMLDivElement>(null);
const [printerId, setPrinterId] = useState<number | null>(plug?.printer_id || null);
const [testResult, setTestResult] = useState<{ success: boolean; state?: string | null; device_name?: string | null } | null>(null);
const [error, setError] = useState<string | null>(null);
// Power alert settings
const [powerAlertEnabled, setPowerAlertEnabled] = useState(plug?.power_alert_enabled || false);
const [powerAlertHigh, setPowerAlertHigh] = useState<string>(plug?.power_alert_high?.toString() || '');
const [powerAlertLow, setPowerAlertLow] = useState<string>(plug?.power_alert_low?.toString() || '');
// Schedule settings
const [scheduleEnabled, setScheduleEnabled] = useState(plug?.schedule_enabled || false);
const [scheduleOnTime, setScheduleOnTime] = useState<string>(plug?.schedule_on_time || '');
const [scheduleOffTime, setScheduleOffTime] = useState<string>(plug?.schedule_off_time || '');
// Visibility options
const [showInSwitchbar, setShowInSwitchbar] = useState(plug?.show_in_switchbar || false);
const [showOnPrinterCard, setShowOnPrinterCard] = useState(plug?.show_on_printer_card ?? true);
// Discovery state
const [isScanning, setIsScanning] = useState(false);
const [scanProgress, setScanProgress] = useState({ scanned: 0, total: 0 });
const [discoveredDevices, setDiscoveredDevices] = useState<DiscoveredTasmotaDevice[]>([]);
const scanPollRef = useRef<NodeJS.Timeout | null>(null);
// Fetch printers for linking
const { data: printers } = useQuery({
queryKey: ['printers'],
queryFn: api.getPrinters,
});
// Fetch existing plugs to check for conflicts
const { data: existingPlugs } = useQuery({
queryKey: ['smart-plugs'],
queryFn: api.getSmartPlugs,
});
// Fetch settings to check if HA is configured
const { data: settings } = useQuery({
queryKey: ['settings'],
queryFn: api.getSettings,
});
// Check if HA is properly configured
const haConfigured = !!(settings?.ha_enabled && settings?.ha_url && settings?.ha_token);
// Debounce search input
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearch(haEntitySearch);
}, 300);
return () => clearTimeout(timer);
}, [haEntitySearch]);
// Close dropdowns when clicking outside
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (entityDropdownRef.current && !entityDropdownRef.current.contains(e.target as Node)) {
setIsEntityDropdownOpen(false);
}
if (powerDropdownRef.current && !powerDropdownRef.current.contains(e.target as Node)) {
setIsPowerDropdownOpen(false);
}
if (energyTodayDropdownRef.current && !energyTodayDropdownRef.current.contains(e.target as Node)) {
setIsEnergyTodayDropdownOpen(false);
}
if (energyTotalDropdownRef.current && !energyTotalDropdownRef.current.contains(e.target as Node)) {
setIsEnergyTotalDropdownOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
// Fetch Home Assistant entities when in HA mode AND HA is configured
const { data: haEntities, isLoading: haEntitiesLoading, error: haEntitiesError } = useQuery({
queryKey: ['ha-entities', debouncedSearch],
queryFn: () => api.getHAEntities(debouncedSearch || undefined),
enabled: plugType === 'homeassistant' && haConfigured,
retry: false,
staleTime: 0,
});
// Fetch Home Assistant sensor entities for energy monitoring
const { data: haSensorEntities } = useQuery({
queryKey: ['ha-sensor-entities'],
queryFn: api.getHASensorEntities,
enabled: plugType === 'homeassistant' && haConfigured,
retry: false,
staleTime: 0,
});
// Close on Escape key and cleanup scan polling
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
if (scanPollRef.current) {
clearInterval(scanPollRef.current);
}
};
}, [onClose]);
// Start scanning for Tasmota devices (auto-detects network)
const startScan = async () => {
setIsScanning(true);
setDiscoveredDevices([]);
setScanProgress({ scanned: 0, total: 0 });
setError(null);
try {
await api.startTasmotaScan();
// Poll function to fetch status and devices
const pollStatus = async () => {
try {
const status = await api.getTasmotaScanStatus();
setScanProgress({ scanned: status.scanned, total: status.total });
const devices = await api.getDiscoveredTasmotaDevices();
setDiscoveredDevices(devices);
if (!status.running) {
setIsScanning(false);
if (scanPollRef.current) {
clearInterval(scanPollRef.current);
scanPollRef.current = null;
}
}
} catch (e) {
console.error('Polling error:', e);
}
};
// Poll immediately, then every 500ms
await pollStatus();
scanPollRef.current = setInterval(pollStatus, 500);
} catch (err) {
setIsScanning(false);
const errorMsg = err instanceof Error ? err.message : (typeof err === 'string' ? err : JSON.stringify(err));
setError(errorMsg || t('smartPlugs.failedToStartScan'));
}
};
// Stop scanning
const stopScan = async () => {
try {
await api.stopTasmotaScan();
} catch {
// Ignore stop errors
}
setIsScanning(false);
if (scanPollRef.current) {
clearInterval(scanPollRef.current);
scanPollRef.current = null;
}
};
// Select a discovered device
const selectDevice = (device: DiscoveredTasmotaDevice) => {
setIpAddress(device.ip_address);
setName(device.name);
setTestResult(null);
};
// Test connection mutation
const testMutation = useMutation({
mutationFn: () => api.testSmartPlugConnection(ipAddress, username || null, password || null),
onSuccess: (result) => {
setTestResult(result);
setError(null);
// Auto-fill name from device if empty
if (!name && result.device_name) {
setName(result.device_name);
}
},
onError: (err: Error) => {
setTestResult(null);
setError(err.message);
},
});
// Create mutation
const createMutation = useMutation({
mutationFn: (data: SmartPlugCreate) => api.createSmartPlug(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['smart-plugs'] });
// Also invalidate printer card HA entity queries
queryClient.invalidateQueries({ queryKey: ['scriptPlugsByPrinter'] });
onClose();
},
onError: (err: Error) => {
setError(err.message);
},
});
// Update mutation
const updateMutation = useMutation({
mutationFn: (data: SmartPlugUpdate) => api.updateSmartPlug(plug!.id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['smart-plugs'] });
// Also invalidate printer card HA entity queries
queryClient.invalidateQueries({ queryKey: ['scriptPlugsByPrinter'] });
onClose();
},
onError: (err: Error) => {
setError(err.message);
},
});
// For Tasmota plugs, only one per printer (physical device)
// For HA scripts, allow multiple per printer
const availablePrinters = printers?.filter(p => {
if (plugType === 'tasmota') {
const hasTasmotaPlug = existingPlugs?.some(
ep => ep.printer_id === p.id && ep.id !== plug?.id && ep.plug_type === 'tasmota'
);
return !hasTasmotaPlug;
}
// HA scripts can have multiple per printer
return true;
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setError(null);
if (!name.trim()) {
setError(t('smartPlugs.nameRequired'));
return;
}
if (plugType === 'tasmota' && !ipAddress.trim()) {
setError('IP address is required for Tasmota plugs');
return;
}
if (plugType === 'homeassistant' && !haEntityId) {
setError(t('smartPlugs.entityRequired'));
return;
}
if (plugType === 'mqtt') {
// Check that at least one topic is configured (path is optional)
const hasPower = mqttPowerTopic.trim();
const hasEnergy = mqttEnergyTopic.trim();
const hasState = mqttStateTopic.trim();
if (!hasPower && !hasEnergy && !hasState) {
setError(t('smartPlugs.mqttTopicRequired'));
return;
}
}
if (plugType === 'rest') {
if (!restOnUrl.trim() && !restOffUrl.trim()) {
setError(t('smartPlugs.restUrlRequired'));
return;
}
}
const data = {
name: name.trim(),
plug_type: plugType,
ip_address: plugType === 'tasmota' ? ipAddress.trim() : null,
ha_entity_id: plugType === 'homeassistant' ? haEntityId : null,
// HA energy sensor entities (optional)
ha_power_entity: plugType === 'homeassistant' ? (haPowerEntity || null) : null,
ha_energy_today_entity: plugType === 'homeassistant' ? (haEnergyTodayEntity || null) : null,
ha_energy_total_entity: plugType === 'homeassistant' ? (haEnergyTotalEntity || null) : null,
// MQTT power fields
mqtt_power_topic: plugType === 'mqtt' ? (mqttPowerTopic.trim() || null) : null,
mqtt_power_path: plugType === 'mqtt' ? (mqttPowerPath.trim() || null) : null,
mqtt_power_multiplier: plugType === 'mqtt' ? (parseFloat(mqttPowerMultiplier) || 1) : 1,
// MQTT energy fields
mqtt_energy_topic: plugType === 'mqtt' ? (mqttEnergyTopic.trim() || null) : null,
mqtt_energy_path: plugType === 'mqtt' ? (mqttEnergyPath.trim() || null) : null,
mqtt_energy_multiplier: plugType === 'mqtt' ? (parseFloat(mqttEnergyMultiplier) || 1) : 1,
// MQTT state fields
mqtt_state_topic: plugType === 'mqtt' ? (mqttStateTopic.trim() || null) : null,
mqtt_state_path: plugType === 'mqtt' ? (mqttStatePath.trim() || null) : null,
mqtt_state_on_value: plugType === 'mqtt' ? (mqttStateOnValue.trim() || null) : null,
// REST fields
rest_on_url: plugType === 'rest' ? (restOnUrl.trim() || null) : null,
rest_on_body: plugType === 'rest' ? (restOnBody.trim() || null) : null,
rest_off_url: plugType === 'rest' ? (restOffUrl.trim() || null) : null,
rest_off_body: plugType === 'rest' ? (restOffBody.trim() || null) : null,
rest_method: plugType === 'rest' ? restMethod : null,
rest_headers: plugType === 'rest' ? (restHeaders.trim() || null) : null,
rest_status_url: plugType === 'rest' ? (restStatusUrl.trim() || null) : null,
rest_status_path: plugType === 'rest' ? (restStatusPath.trim() || null) : null,
rest_status_on_value: plugType === 'rest' ? (restStatusOnValue.trim() || null) : null,
rest_power_url: plugType === 'rest' ? (restPowerUrl.trim() || null) : null,
rest_power_path: plugType === 'rest' ? (restPowerPath.trim() || null) : null,
rest_power_multiplier: plugType === 'rest' ? (parseFloat(restPowerMultiplier) || 1) : 1,
rest_energy_url: plugType === 'rest' ? (restEnergyUrl.trim() || null) : null,
rest_energy_path: plugType === 'rest' ? (restEnergyPath.trim() || null) : null,
rest_energy_multiplier: plugType === 'rest' ? (parseFloat(restEnergyMultiplier) || 1) : 1,
username: plugType === 'tasmota' ? (username.trim() || null) : null,
password: plugType === 'tasmota' ? (password.trim() || null) : null,
printer_id: printerId,
// Power alerts
power_alert_enabled: powerAlertEnabled,
power_alert_high: powerAlertHigh ? parseFloat(powerAlertHigh) : null,
power_alert_low: powerAlertLow ? parseFloat(powerAlertLow) : null,
// Schedule
schedule_enabled: scheduleEnabled,
schedule_on_time: scheduleOnTime || null,
schedule_off_time: scheduleOffTime || null,
// Visibility
show_in_switchbar: showInSwitchbar,
show_on_printer_card: showOnPrinterCard,
};
if (isEditing) {
updateMutation.mutate(data);
} else {
createMutation.mutate(data);
}
};
const isPending = createMutation.isPending || updateMutation.isPending;
return (
<div
className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4"
onClick={onClose}
>
<div
className="bg-bambu-dark-secondary rounded-xl border border-bambu-dark-tertiary w-full max-w-md max-h-[90vh] flex flex-col"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-bambu-dark-tertiary flex-shrink-0">
<h2 className="text-lg font-semibold text-white">
{isEditing ? t('smartPlugs.editTitle') : t('smartPlugs.addTitle')}
</h2>
<button
onClick={onClose}
className="text-bambu-gray hover:text-white transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Form */}
<form onSubmit={handleSubmit} className="p-6 space-y-4 overflow-y-auto">
{error && (
<div className="p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-sm text-red-400">
{error}
</div>
)}
{/* Plug Type Selector - only show when not editing */}
{!isEditing && (
<div className="flex gap-2 mb-2">
<button
type="button"
onClick={() => {
setPlugType('tasmota');
setTestResult(null);
setError(null);
}}
className={`flex-1 flex items-center justify-center gap-2 px-3 py-2.5 rounded-lg font-medium transition-colors ${
plugType === 'tasmota'
? 'bg-bambu-green text-white'
: 'bg-bambu-dark text-bambu-gray hover:text-white border border-bambu-dark-tertiary'
}`}
>
<Plug className="w-4 h-4" />
Tasmota
</button>
<button
type="button"
onClick={() => {
setPlugType('homeassistant');
setTestResult(null);
setError(null);
}}
className={`flex-1 flex items-center justify-center gap-2 px-3 py-2.5 rounded-lg font-medium transition-colors ${
plugType === 'homeassistant'
? 'bg-bambu-green text-white'
: 'bg-bambu-dark text-bambu-gray hover:text-white border border-bambu-dark-tertiary'
}`}
>
<Home className="w-4 h-4" />
HA
</button>
<button
type="button"
onClick={() => {
setPlugType('mqtt');
setTestResult(null);
setError(null);
}}
className={`flex-1 flex items-center justify-center gap-2 px-3 py-2.5 rounded-lg font-medium transition-colors ${
plugType === 'mqtt'
? 'bg-bambu-green text-white'
: 'bg-bambu-dark text-bambu-gray hover:text-white border border-bambu-dark-tertiary'
}`}
>
<Radio className="w-4 h-4" />
MQTT
</button>
<button
type="button"
onClick={() => {
setPlugType('rest');
setTestResult(null);
setError(null);
}}
className={`flex-1 flex items-center justify-center gap-2 px-3 py-2.5 rounded-lg font-medium transition-colors ${
plugType === 'rest'
? 'bg-bambu-green text-white'
: 'bg-bambu-dark text-bambu-gray hover:text-white border border-bambu-dark-tertiary'
}`}
>
<Globe className="w-4 h-4" />
REST
</button>
</div>
)}
{/* Discovery Section - only show when not editing and Tasmota is selected */}
{!isEditing && plugType === 'tasmota' && (
<div className="space-y-3">
{/* Scan button - auto-detects network */}
{isScanning ? (
<Button type="button" variant="secondary" onClick={stopScan} className="w-full">
<X className="w-4 h-4" />
{t('smartPlugs.stopScanning')}
</Button>
) : (
<Button type="button" variant="primary" onClick={startScan} className="w-full">
<Search className="w-4 h-4" />
{t('smartPlugs.discoverTasmota')}
</Button>
)}
{/* Progress bar */}
{isScanning && scanProgress.total > 0 && (
<div className="space-y-1">
<div className="flex justify-between text-xs text-bambu-gray">
<span>{t('smartPlugs.addSmartPlug.scanningNetwork')}</span>
<span>{scanProgress.scanned} / {scanProgress.total}</span>
</div>
<div className="w-full bg-bambu-dark-tertiary rounded-full h-2">
<div
className="bg-bambu-green h-2 rounded-full transition-all duration-300"
style={{ width: `${(scanProgress.scanned / scanProgress.total) * 100}%` }}
/>
</div>
</div>
)}
{/* Discovered devices */}
{discoveredDevices.length > 0 && (
<div className="space-y-2">
<p className="text-xs text-bambu-gray">{t('smartPlugs.foundDevices', { count: discoveredDevices.length })}</p>
<div className="max-h-40 overflow-y-auto space-y-1">
{discoveredDevices.map((device) => (
<button
key={device.ip_address}
type="button"
onClick={() => selectDevice(device)}
className="w-full flex items-center justify-between p-2 bg-bambu-dark hover:bg-bambu-dark-tertiary rounded-lg transition-colors text-left border border-bambu-dark-tertiary"
>
<div className="flex items-center gap-2">
<Plug className="w-4 h-4 text-bambu-green" />
<div>
<p className="text-sm text-white">{device.name}</p>
<p className="text-xs text-bambu-gray">{device.ip_address}</p>
</div>
</div>
{device.state && (
<span className={`flex items-center gap-1 text-xs ${
device.state === 'ON' ? 'text-bambu-green' : 'text-bambu-gray'
}`}>
<Power className="w-3 h-3" />
{device.state}
</span>
)}
</button>
))}
</div>
</div>
)}
{!isScanning && discoveredDevices.length === 0 && scanProgress.total > 0 && (
<p className="text-xs text-bambu-gray text-center py-2">
{t('smartPlugs.noDevicesFound')}
</p>
)}
</div>
)}
{/* Home Assistant Entity Selector - only show when HA is selected */}
{plugType === 'homeassistant' && (
<div className="space-y-3">
{/* HA not configured */}
{!haConfigured && (
<div className="space-y-3">
<div className="p-3 bg-yellow-500/20 border border-yellow-500/50 rounded-lg text-sm text-yellow-400">
{t('smartPlugs.haNotConfigured')}{' '}
<span className="font-medium">{t('smartPlugs.haSettingsPath')}</span>
</div>
<div>
<label className="block text-sm text-bambu-gray mb-1 opacity-50">{t('smartPlugs.selectEntity')}</label>
<select
disabled
className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-bambu-gray cursor-not-allowed opacity-50"
>
<option>{t('smartPlugs.addSmartPlug.chooseEntity')}</option>
</select>
</div>
</div>
)}
{/* HA configured - show loading/entities */}
{haConfigured && (
<>
{haEntitiesLoading && (
<div className="flex items-center justify-center py-4 text-bambu-gray">
<Loader2 className="w-5 h-5 animate-spin mr-2" />
{t('smartPlugs.loadingEntities')}
</div>
)}
{haEntitiesError && (
<div className="p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-sm text-red-400">
{t('smartPlugs.failedToLoadEntities', { error: (haEntitiesError as Error).message })}
</div>
)}
{/* Searchable Entity Dropdown */}
{(() => {
// Filter out entities already configured (except current plug when editing)
const configuredEntityIds = existingPlugs
?.filter(p => p.ha_entity_id && p.id !== plug?.id)
.map(p => p.ha_entity_id) || [];
const availableEntities = (haEntities || []).filter(e => !configuredEntityIds.includes(e.entity_id));
const selectedEntity = haEntities?.find(e => e.entity_id === haEntityId);
return (
<div ref={entityDropdownRef} className="relative">
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.selectEntity')}</label>
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
<input
type="text"
value={isEntityDropdownOpen ? haEntitySearch : (selectedEntity ? `${selectedEntity.friendly_name} (${selectedEntity.entity_id})` : '')}
onChange={(e) => {
setHaEntitySearch(e.target.value);
if (!isEntityDropdownOpen) setIsEntityDropdownOpen(true);
}}
onFocus={() => {
setIsEntityDropdownOpen(true);
setHaEntitySearch('');
}}
placeholder={t('smartPlugs.addSmartPlug.placeholders.searchEntities')}
className="w-full pl-9 pr-8 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
/>
{haEntityId && !isEntityDropdownOpen && (
<button
type="button"
onClick={() => {
setHaEntityId('');
setHaEntitySearch('');
}}
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 hover:bg-bambu-dark-tertiary rounded"
>
<X className="w-4 h-4 text-bambu-gray hover:text-white" />
</button>
)}
{haEntitiesLoading && (
<Loader2 className="absolute right-2 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray animate-spin" />
)}
</div>
{/* Dropdown */}
{isEntityDropdownOpen && (
<div className="absolute z-50 w-full mt-1 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg shadow-lg max-h-60 overflow-y-auto">
{haEntitiesLoading && (
<div className="px-3 py-2 text-sm text-bambu-gray flex items-center gap-2">
<Loader2 className="w-4 h-4 animate-spin" />
{t('smartPlugs.loading')}
</div>
)}
{!haEntitiesLoading && availableEntities.length === 0 && (
<div className="px-3 py-2 text-sm text-bambu-gray">
{debouncedSearch
? t('smartPlugs.noEntitiesMatching', { search: debouncedSearch })
: t('smartPlugs.noEntitiesAvailable')}
</div>
)}
{!haEntitiesLoading && availableEntities.map((entity) => (
<button
key={entity.entity_id}
type="button"
onClick={() => {
setHaEntityId(entity.entity_id);
setIsEntityDropdownOpen(false);
setHaEntitySearch('');
// Auto-fill name
if (!name) {
setName(entity.friendly_name);
}
}}
className={`w-full px-3 py-2 text-left text-sm hover:bg-bambu-dark-tertiary transition-colors ${
entity.entity_id === haEntityId ? 'bg-bambu-green/20 text-bambu-green' : 'text-white'
}`}
>
<div className="font-medium">{entity.friendly_name}</div>
<div className="text-xs text-bambu-gray flex items-center justify-between">
<span>{entity.entity_id}</span>
<span className={entity.state === 'on' ? 'text-bambu-green' : ''}>{entity.state}</span>
</div>
</button>
))}
</div>
)}
<p className="text-xs text-bambu-gray mt-1">
{debouncedSearch
? t('smartPlugs.searchingEntities', { count: availableEntities.length })
: t('smartPlugs.showingEntities', { count: availableEntities.length })}
</p>
</div>
);
})()}
{/* Energy Monitoring Section (Optional) */}
{haEntityId && haSensorEntities && haSensorEntities.length > 0 && (
<div className="border-t border-bambu-dark-tertiary pt-4 mt-4 space-y-3">
<div>
<p className="text-white font-medium mb-1">{t('smartPlugs.energyMonitoringOptional')}</p>
<p className="text-xs text-bambu-gray mb-3">
{t('smartPlugs.energyMonitoringHint')}
</p>
</div>
{/* Power Sensor (W) */}
{(() => {
const powerSensors = haSensorEntities.filter(s =>
s.unit_of_measurement === 'W' || s.unit_of_measurement === 'kW' || s.unit_of_measurement === 'mW'
);
const filteredPowerSensors = powerSensorSearch
? powerSensors.filter(s =>
s.entity_id.toLowerCase().includes(powerSensorSearch.toLowerCase()) ||
s.friendly_name.toLowerCase().includes(powerSensorSearch.toLowerCase())
)
: powerSensors;
const selectedPowerSensor = haSensorEntities.find(s => s.entity_id === haPowerEntity);
return (
<div ref={powerDropdownRef} className="relative">
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.powerSensorW')}</label>
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
<input
type="text"
value={isPowerDropdownOpen ? powerSensorSearch : (selectedPowerSensor ? `${selectedPowerSensor.friendly_name} (${selectedPowerSensor.state} ${selectedPowerSensor.unit_of_measurement})` : '')}
onChange={(e) => {
setPowerSensorSearch(e.target.value);
if (!isPowerDropdownOpen) setIsPowerDropdownOpen(true);
}}
onFocus={() => {
setIsPowerDropdownOpen(true);
setPowerSensorSearch('');
}}
placeholder={t('smartPlugs.addSmartPlug.placeholders.searchPowerSensors')}
className="w-full pl-9 pr-8 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
/>
{haPowerEntity && !isPowerDropdownOpen && (
<button
type="button"
onClick={() => {
setHaPowerEntity('');
setPowerSensorSearch('');
}}
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 hover:bg-bambu-dark-tertiary rounded"
>
<X className="w-4 h-4 text-bambu-gray hover:text-white" />
</button>
)}
</div>
{isPowerDropdownOpen && (
<div className="absolute z-50 w-full mt-1 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg shadow-lg max-h-48 overflow-y-auto">
<button
type="button"
onClick={() => {
setHaPowerEntity('');
setIsPowerDropdownOpen(false);
setPowerSensorSearch('');
}}
className="w-full px-3 py-2 text-left text-sm text-bambu-gray hover:bg-bambu-dark-tertiary"
>
{t('smartPlugs.none')}
</button>
{filteredPowerSensors.map((sensor) => (
<button
key={sensor.entity_id}
type="button"
onClick={() => {
setHaPowerEntity(sensor.entity_id);
setIsPowerDropdownOpen(false);
setPowerSensorSearch('');
}}
className={`w-full px-3 py-2 text-left text-sm hover:bg-bambu-dark-tertiary ${
sensor.entity_id === haPowerEntity ? 'bg-bambu-green/20 text-bambu-green' : 'text-white'
}`}
>
<div className="font-medium">{sensor.friendly_name}</div>
<div className="text-xs text-bambu-gray">{sensor.entity_id} • {sensor.state} {sensor.unit_of_measurement}</div>
</button>
))}
{filteredPowerSensors.length === 0 && (
<div className="px-3 py-2 text-sm text-bambu-gray">{t('smartPlugs.noMatchingSensors')}</div>
)}
</div>
)}
</div>
);
})()}
{/* Energy Today (kWh) */}
{(() => {
const energySensors = haSensorEntities.filter(s =>
s.unit_of_measurement === 'kWh' || s.unit_of_measurement === 'Wh' || s.unit_of_measurement === 'MWh'
);
const filteredEnergySensors = energyTodaySearch
? energySensors.filter(s =>
s.entity_id.toLowerCase().includes(energyTodaySearch.toLowerCase()) ||
s.friendly_name.toLowerCase().includes(energyTodaySearch.toLowerCase())
)
: energySensors;
const selectedSensor = haSensorEntities.find(s => s.entity_id === haEnergyTodayEntity);
return (
<div ref={energyTodayDropdownRef} className="relative">
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.energyTodayKwh')}</label>
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
<input
type="text"
value={isEnergyTodayDropdownOpen ? energyTodaySearch : (selectedSensor ? `${selectedSensor.friendly_name} (${selectedSensor.state} ${selectedSensor.unit_of_measurement})` : '')}
onChange={(e) => {
setEnergyTodaySearch(e.target.value);
if (!isEnergyTodayDropdownOpen) setIsEnergyTodayDropdownOpen(true);
}}
onFocus={() => {
setIsEnergyTodayDropdownOpen(true);
setEnergyTodaySearch('');
}}
placeholder={t('smartPlugs.addSmartPlug.placeholders.searchEnergySensors')}
className="w-full pl-9 pr-8 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
/>
{haEnergyTodayEntity && !isEnergyTodayDropdownOpen && (
<button
type="button"
onClick={() => {
setHaEnergyTodayEntity('');
setEnergyTodaySearch('');
}}
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 hover:bg-bambu-dark-tertiary rounded"
>
<X className="w-4 h-4 text-bambu-gray hover:text-white" />
</button>
)}
</div>
{isEnergyTodayDropdownOpen && (
<div className="absolute z-50 w-full mt-1 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg shadow-lg max-h-48 overflow-y-auto">
<button
type="button"
onClick={() => {
setHaEnergyTodayEntity('');
setIsEnergyTodayDropdownOpen(false);
setEnergyTodaySearch('');
}}
className="w-full px-3 py-2 text-left text-sm text-bambu-gray hover:bg-bambu-dark-tertiary"
>
{t('smartPlugs.none')}
</button>
{filteredEnergySensors.map((sensor) => (
<button
key={sensor.entity_id}
type="button"
onClick={() => {
setHaEnergyTodayEntity(sensor.entity_id);
setIsEnergyTodayDropdownOpen(false);
setEnergyTodaySearch('');
}}
className={`w-full px-3 py-2 text-left text-sm hover:bg-bambu-dark-tertiary ${
sensor.entity_id === haEnergyTodayEntity ? 'bg-bambu-green/20 text-bambu-green' : 'text-white'
}`}
>
<div className="font-medium">{sensor.friendly_name}</div>
<div className="text-xs text-bambu-gray">{sensor.entity_id} • {sensor.state} {sensor.unit_of_measurement}</div>
</button>
))}
{filteredEnergySensors.length === 0 && (
<div className="px-3 py-2 text-sm text-bambu-gray">{t('smartPlugs.noMatchingSensors')}</div>
)}
</div>
)}
</div>
);
})()}
{/* Total Energy (kWh) */}
{(() => {
const energySensors = haSensorEntities.filter(s =>
s.unit_of_measurement === 'kWh' || s.unit_of_measurement === 'Wh' || s.unit_of_measurement === 'MWh'
);
const filteredEnergySensors = energyTotalSearch
? energySensors.filter(s =>
s.entity_id.toLowerCase().includes(energyTotalSearch.toLowerCase()) ||
s.friendly_name.toLowerCase().includes(energyTotalSearch.toLowerCase())
)
: energySensors;
const selectedSensor = haSensorEntities.find(s => s.entity_id === haEnergyTotalEntity);
return (
<div ref={energyTotalDropdownRef} className="relative">
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.totalEnergyKwh')}</label>
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
<input
type="text"
value={isEnergyTotalDropdownOpen ? energyTotalSearch : (selectedSensor ? `${selectedSensor.friendly_name} (${selectedSensor.state} ${selectedSensor.unit_of_measurement})` : '')}
onChange={(e) => {
setEnergyTotalSearch(e.target.value);
if (!isEnergyTotalDropdownOpen) setIsEnergyTotalDropdownOpen(true);
}}
onFocus={() => {
setIsEnergyTotalDropdownOpen(true);
setEnergyTotalSearch('');
}}
placeholder={t('smartPlugs.addSmartPlug.placeholders.searchEnergySensors')}
className="w-full pl-9 pr-8 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
/>
{haEnergyTotalEntity && !isEnergyTotalDropdownOpen && (
<button
type="button"
onClick={() => {
setHaEnergyTotalEntity('');
setEnergyTotalSearch('');
}}
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 hover:bg-bambu-dark-tertiary rounded"
>
<X className="w-4 h-4 text-bambu-gray hover:text-white" />
</button>
)}
</div>
{isEnergyTotalDropdownOpen && (
<div className="absolute z-50 w-full mt-1 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg shadow-lg max-h-48 overflow-y-auto">
<button
type="button"
onClick={() => {
setHaEnergyTotalEntity('');
setIsEnergyTotalDropdownOpen(false);
setEnergyTotalSearch('');
}}
className="w-full px-3 py-2 text-left text-sm text-bambu-gray hover:bg-bambu-dark-tertiary"
>
{t('smartPlugs.none')}
</button>
{filteredEnergySensors.map((sensor) => (
<button
key={sensor.entity_id}
type="button"
onClick={() => {
setHaEnergyTotalEntity(sensor.entity_id);
setIsEnergyTotalDropdownOpen(false);
setEnergyTotalSearch('');
}}
className={`w-full px-3 py-2 text-left text-sm hover:bg-bambu-dark-tertiary ${
sensor.entity_id === haEnergyTotalEntity ? 'bg-bambu-green/20 text-bambu-green' : 'text-white'
}`}
>
<div className="font-medium">{sensor.friendly_name}</div>
<div className="text-xs text-bambu-gray">{sensor.entity_id} • {sensor.state} {sensor.unit_of_measurement}</div>
</button>
))}
{filteredEnergySensors.length === 0 && (
<div className="px-3 py-2 text-sm text-bambu-gray">{t('smartPlugs.noMatchingSensors')}</div>
)}
</div>
)}
</div>
);
})()}
</div>
)}
</>
)}
</div>
)}
{/* MQTT Configuration - only show when MQTT is selected */}
{plugType === 'mqtt' && (
<div className="space-y-3">
{/* MQTT broker not configured */}
{!settings?.mqtt_broker && (
<div className="p-3 bg-yellow-500/20 border border-yellow-500/50 rounded-lg text-sm text-yellow-400">
{t('smartPlugs.mqttNotConfigured')}{' '}
<span className="font-medium">{t('smartPlugs.mqttSettingsPath')}</span>
{' '}{t('smartPlugs.mqttNotConfiguredSuffix')}
</div>
)}
{/* MQTT broker configured - show fields */}
{settings?.mqtt_broker && (
<>
<div className="p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg text-sm text-blue-300">
<p className="font-medium mb-1">{t('smartPlugs.monitorOnly')}</p>
<p className="text-xs opacity-80">
{t('smartPlugs.mqttMonitorOnlyDescription')}
</p>
</div>
{/* Power Section */}
<div className="space-y-3 p-3 bg-bambu-dark rounded-lg border border-bambu-dark-tertiary">
<p className="text-white font-medium text-sm">{t('smartPlugs.powerMonitoring')}</p>
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.topic')}</label>
<input
type="text"
value={mqttPowerTopic}
onChange={(e) => setMqttPowerTopic(e.target.value)}
placeholder="zigbee2mqtt/shelly-working-room"
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.jsonPath')}</label>
<input
type="text"
value={mqttPowerPath}
onChange={(e) => setMqttPowerPath(e.target.value)}
placeholder="power_l1"
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
/>
</div>
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.multiplier')}</label>
<input
type="text"
value={mqttPowerMultiplier}
onChange={(e) => setMqttPowerMultiplier(e.target.value)}
placeholder="1"
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
/>
</div>
</div>
<p className="text-xs text-bambu-gray" style={{ whiteSpace: 'pre-line' }}>
{t('smartPlugs.mqttPowerHint')}
</p>
</div>
{/* Energy Section */}
<div className="space-y-3 p-3 bg-bambu-dark rounded-lg border border-bambu-dark-tertiary">
<p className="text-white font-medium text-sm">{t('smartPlugs.energyMonitoring')} <span className="text-bambu-gray font-normal">({t('smartPlugs.optional')})</span></p>
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.topic')}</label>
<input
type="text"
value={mqttEnergyTopic}
onChange={(e) => setMqttEnergyTopic(e.target.value)}
placeholder="Same as power topic, or different"
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.jsonPath')}</label>
<input
type="text"
value={mqttEnergyPath}
onChange={(e) => setMqttEnergyPath(e.target.value)}
placeholder="energy_l1"
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
/>
</div>
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.multiplier')}</label>
<input
type="text"
value={mqttEnergyMultiplier}
onChange={(e) => setMqttEnergyMultiplier(e.target.value)}
placeholder="1"
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
/>
</div>
</div>
<p className="text-xs text-bambu-gray" style={{ whiteSpace: 'pre-line' }}>
{t('smartPlugs.mqttEnergyHint')}
</p>
</div>
{/* State Section */}
<div className="space-y-3 p-3 bg-bambu-dark rounded-lg border border-bambu-dark-tertiary">
<p className="text-white font-medium text-sm">{t('smartPlugs.stateMonitoring')} <span className="text-bambu-gray font-normal">({t('smartPlugs.optional')})</span></p>
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.topic')}</label>
<input
type="text"
value={mqttStateTopic}
onChange={(e) => setMqttStateTopic(e.target.value)}
placeholder="Same as power topic, or different"
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.jsonPath')}</label>
<input
type="text"
value={mqttStatePath}
onChange={(e) => setMqttStatePath(e.target.value)}
placeholder="state_l1"
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
/>
</div>
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.onValue')}</label>
<input
type="text"
value={mqttStateOnValue}
onChange={(e) => setMqttStateOnValue(e.target.value)}
placeholder={t('smartPlugs.addSmartPlug.placeholders.mqttStateOnValue')}
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
/>
</div>
</div>
<p className="text-xs text-bambu-gray" style={{ whiteSpace: 'pre-line' }}>
{t('smartPlugs.mqttStateHint')}
</p>
</div>
</>
)}
</div>
)}
{/* REST API Section */}
{plugType === 'rest' && (
<div className="space-y-3">
{/* Control Section */}
<div className="space-y-3 p-3 bg-bambu-dark rounded-lg border border-bambu-dark-tertiary">
<p className="text-white font-medium text-sm">{t('smartPlugs.restControl')}</p>
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restMethod')}</label>
<select
value={restMethod}
onChange={(e) => setRestMethod(e.target.value)}
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
>
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="PATCH">PATCH</option>
</select>
</div>
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restOnUrl')}</label>
<input
type="text"
value={restOnUrl}
onChange={(e) => { setRestOnUrl(e.target.value); setTestResult(null); }}
placeholder="http://openhab:8080/rest/items/MyPlug"
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
/>
</div>
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restOnBody')} <span className="text-bambu-gray font-normal">({t('smartPlugs.optional')})</span></label>
<input
type="text"
value={restOnBody}
onChange={(e) => setRestOnBody(e.target.value)}
placeholder={t('smartPlugs.restBodyHint')}
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
/>
</div>
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restOffUrl')}</label>
<input
type="text"
value={restOffUrl}
onChange={(e) => { setRestOffUrl(e.target.value); setTestResult(null); }}
placeholder="http://openhab:8080/rest/items/MyPlug"
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
/>
</div>
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restOffBody')} <span className="text-bambu-gray font-normal">({t('smartPlugs.optional')})</span></label>
<input
type="text"
value={restOffBody}
onChange={(e) => setRestOffBody(e.target.value)}
placeholder={t('smartPlugs.restBodyHint')}
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
/>
</div>
</div>
{/* Headers Section */}
<div className="space-y-3 p-3 bg-bambu-dark rounded-lg border border-bambu-dark-tertiary">
<p className="text-white font-medium text-sm">{t('smartPlugs.restHeaders')} <span className="text-bambu-gray font-normal">({t('smartPlugs.optional')})</span></p>
<div>
<textarea
value={restHeaders}
onChange={(e) => setRestHeaders(e.target.value)}
placeholder={t('smartPlugs.restHeadersHint')}
rows={2}
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none font-mono text-sm"
/>
</div>
</div>
{/* Status Polling Section (optional) */}
<div className="space-y-3 p-3 bg-bambu-dark rounded-lg border border-bambu-dark-tertiary">
<p className="text-white font-medium text-sm">{t('smartPlugs.stateMonitoring')} <span className="text-bambu-gray font-normal">({t('smartPlugs.optional')})</span></p>
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restStatusUrl')}</label>
<input
type="text"
value={restStatusUrl}
onChange={(e) => setRestStatusUrl(e.target.value)}
placeholder={t('smartPlugs.restStatusHint')}
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restStatusPath')}</label>
<input
type="text"
value={restStatusPath}
onChange={(e) => setRestStatusPath(e.target.value)}
placeholder={t('smartPlugs.restPathHint')}
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
/>
</div>
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restStatusOnValue')}</label>
<input
type="text"
value={restStatusOnValue}
onChange={(e) => setRestStatusOnValue(e.target.value)}
placeholder="ON"
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
/>
</div>
</div>
</div>
{/* Energy Monitoring (optional) */}
<div className="space-y-3 p-3 bg-bambu-dark rounded-lg border border-bambu-dark-tertiary">
<p className="text-white font-medium text-sm">{t('smartPlugs.energyMonitoring')} <span className="text-bambu-gray font-normal">({t('smartPlugs.optional')})</span></p>
{/* Power */}
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restPowerUrl')} <span className="text-bambu-gray font-normal">({t('smartPlugs.optional')})</span></label>
<input
type="text"
value={restPowerUrl}
onChange={(e) => setRestPowerUrl(e.target.value)}
placeholder={t('smartPlugs.restPowerUrlHint')}
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restPowerPath')}</label>
<input
type="text"
value={restPowerPath}
onChange={(e) => setRestPowerPath(e.target.value)}
placeholder={t('smartPlugs.restPathHint')}
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
/>
</div>
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restPowerMultiplier')}</label>
<input
type="text"
value={restPowerMultiplier}
onChange={(e) => setRestPowerMultiplier(e.target.value)}
placeholder="1"
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
/>
</div>
</div>
{/* Energy */}
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restEnergyUrl')} <span className="text-bambu-gray font-normal">({t('smartPlugs.optional')})</span></label>
<input
type="text"
value={restEnergyUrl}
onChange={(e) => setRestEnergyUrl(e.target.value)}
placeholder={t('smartPlugs.restEnergyUrlHint')}
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restEnergyPath')}</label>
<input
type="text"
value={restEnergyPath}
onChange={(e) => setRestEnergyPath(e.target.value)}
placeholder={t('smartPlugs.restPathHint')}
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
/>
</div>
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restEnergyMultiplier')}</label>
<input
type="text"
value={restEnergyMultiplier}
onChange={(e) => setRestEnergyMultiplier(e.target.value)}
placeholder="1"
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
/>
</div>
</div>
<p className="text-xs text-bambu-gray">
{t('smartPlugs.restEnergyHint')}
</p>
</div>
{/* Test Connection */}
{(restOnUrl.trim() || restOffUrl.trim()) && (
<div className="flex gap-2">
<Button
type="button"
variant="secondary"
onClick={async () => {
setTestResult(null);
try {
const url = restOnUrl.trim() || restOffUrl.trim();
const result = await api.testRESTConnection(url, restMethod, restHeaders.trim() || null);
setTestResult({ success: result.success });
if (!result.success) {
setError(result.error || t('smartPlugs.addSmartPlug.connectionFailed'));
}
} catch {
setTestResult({ success: false });
setError(t('smartPlugs.addSmartPlug.connectionFailed'));
}
}}
className="w-full"
>
<Wifi className="w-4 h-4" />
{t('smartPlugs.testConnection')}
</Button>
</div>
)}
{testResult && (
<div className={`flex items-center gap-2 text-sm ${testResult.success ? 'text-green-400' : 'text-red-400'}`}>
{testResult.success ? <CheckCircle className="w-4 h-4" /> : <WifiOff className="w-4 h-4" />}
{testResult.success ? t('smartPlugs.connectionSuccess') : t('smartPlugs.addSmartPlug.connectionFailed')}
</div>
)}
</div>
)}
{/* IP Address - only show for Tasmota */}
{plugType === 'tasmota' && (
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.ipAddress')}</label>
<div className="flex gap-2">
<input
type="text"
value={ipAddress}
onChange={(e) => {
setIpAddress(e.target.value);
setTestResult(null);
}}
placeholder="192.168.1.100"
className="flex-1 px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
/>
<Button
type="button"
variant="secondary"
onClick={() => testMutation.mutate()}
disabled={!ipAddress.trim() || testMutation.isPending}
>
{testMutation.isPending ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Wifi className="w-4 h-4" />
)}
{t('smartPlugs.test')}
</Button>
</div>
</div>
)}
{/* Test Result - only show for Tasmota */}
{plugType === 'tasmota' && testResult && (
<div className={`p-3 rounded-lg flex items-center gap-2 ${
testResult.success
? 'bg-bambu-green/20 border border-bambu-green/50 text-bambu-green'
: 'bg-red-500/20 border border-red-500/50 text-red-400'
}`}>
{testResult.success ? (
<>
<CheckCircle className="w-5 h-5" />
<div>
<p className="font-medium">{t('smartPlugs.connectedResult')}</p>
<p className="text-sm opacity-80">
{testResult.device_name && t('smartPlugs.deviceLabel', { name: testResult.device_name })}
{t('smartPlugs.stateLabel', { state: testResult.state })}
</p>
</div>
</>
) : (
<>
<WifiOff className="w-5 h-5" />
<span>{t('smartPlugs.addSmartPlug.connectionFailed')}</span>
</>
)}
</div>
)}
{/* Name */}
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.nameLabel')}</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder={t('smartPlugs.addSmartPlug.placeholders.plugName')}
className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
/>
</div>
{/* Authentication (optional) - only show for Tasmota */}
{plugType === 'tasmota' && (
<>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.username')}</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="admin"
className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
/>
</div>
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.password')}</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="********"
className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
/>
</div>
</div>
<p className="text-xs text-bambu-gray -mt-2">
{t('smartPlugs.authHint')}
</p>
</>
)}
{/* Link to Printer - not shown for MQTT plugs (monitor-only) */}
{plugType !== 'mqtt' && (
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.linkToPrinter')}</label>
<select
value={printerId ?? ''}
onChange={(e) => setPrinterId(e.target.value ? Number(e.target.value) : null)}
className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
>
<option value="">{t('smartPlugs.noPrinter')}</option>
{availablePrinters?.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
</select>
<p className="text-xs text-bambu-gray mt-1">
{t('smartPlugs.linkingDescription')}
</p>
</div>
)}
{/* Power Alerts */}
<div className="border-t border-bambu-dark-tertiary pt-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Bell className="w-4 h-4 text-bambu-green" />
<span className="text-white font-medium">{t('smartPlugs.powerAlerts')}</span>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={powerAlertEnabled}
onChange={(e) => setPowerAlertEnabled(e.target.checked)}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-bambu-dark-tertiary peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-bambu-green"></div>
</label>
</div>
{powerAlertEnabled && (
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.alertAbove')}</label>
<input
type="number"
value={powerAlertHigh}
onChange={(e) => setPowerAlertHigh(e.target.value)}
placeholder="e.g. 200"
min="0"
max="5000"
className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
/>
</div>
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.alertBelow')}</label>
<input
type="number"
value={powerAlertLow}
onChange={(e) => setPowerAlertLow(e.target.value)}
placeholder="e.g. 10"
min="0"
max="5000"
className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
/>
</div>
</div>
<p className="text-xs text-bambu-gray">
{t('smartPlugs.alertDescription')}
</p>
</div>
)}
</div>
{/* Schedule - not shown for MQTT plugs (monitor-only) */}
{plugType !== 'mqtt' && (
<div className="border-t border-bambu-dark-tertiary pt-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Clock className="w-4 h-4 text-bambu-green" />
<span className="text-white font-medium">{t('smartPlugs.dailySchedule')}</span>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={scheduleEnabled}
onChange={(e) => setScheduleEnabled(e.target.checked)}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-bambu-dark-tertiary peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-bambu-green"></div>
</label>
</div>
{scheduleEnabled && (
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.turnOnAt')}</label>
<input
type="time"
value={scheduleOnTime}
onChange={(e) => setScheduleOnTime(e.target.value)}
className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
/>
</div>
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.turnOffAt')}</label>
<input
type="time"
value={scheduleOffTime}
onChange={(e) => setScheduleOffTime(e.target.value)}
className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
/>
</div>
</div>
<p className="text-xs text-bambu-gray">
{t('smartPlugs.scheduleDescription')}
</p>
</div>
)}
</div>
)}
{/* Switchbar Visibility */}
<div className="border-t border-bambu-dark-tertiary pt-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<LayoutGrid className="w-4 h-4 text-bambu-green" />
<div>
<span className="text-white font-medium">{t('smartPlugs.showInSwitchbar')}</span>
<p className="text-xs text-bambu-gray">{t('smartPlugs.quickAccessSidebar')}</p>
</div>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={showInSwitchbar}
onChange={(e) => setShowInSwitchbar(e.target.checked)}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-bambu-dark-tertiary peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-bambu-green"></div>
</label>
</div>
</div>
{/* Printer Card Visibility - only for HA entities */}
{plugType === 'homeassistant' && (
<div className="border-t border-bambu-dark-tertiary pt-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Eye className="w-4 h-4 text-bambu-green" />
<div>
<span className="text-white font-medium">{t('smartPlugs.showOnPrinterCard')}</span>
<p className="text-xs text-bambu-gray">{t('smartPlugs.displayOnPrinterCard')}</p>
</div>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={showOnPrinterCard}
onChange={(e) => setShowOnPrinterCard(e.target.checked)}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-bambu-dark-tertiary peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-bambu-green"></div>
</label>
</div>
</div>
)}
{/* Actions */}
<div className="flex gap-3 pt-2">
<Button
type="button"
variant="secondary"
onClick={onClose}
className="flex-1"
>
{t('smartPlugs.cancel')}
</Button>
<Button
type="submit"
disabled={isPending}
className="flex-1"
>
{isPending ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Save className="w-4 h-4" />
)}
{isEditing ? t('smartPlugs.save') : t('smartPlugs.add')}
</Button>
</div>
</form>
</div>
</div>
);
}