| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730 |
- 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';
- import { SECRET_PLACEHOLDER, selectPlaceholderOnFocus } from '../utils/secrets';
- 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 || '');
- // A saved secret does not follow a changed address: it must be entered again.
- const passwordNeedsReentry = password === SECRET_PLACEHOLDER && ipAddress !== (plug?.ip_address || '');
- const headersNeedReentry =
- restHeaders === SECRET_PLACEHOLDER &&
- (restOnUrl !== (plug?.rest_on_url || '') ||
- restOffUrl !== (plug?.rest_off_url || '') ||
- restStatusUrl !== (plug?.rest_status_url || '') ||
- restPowerUrl !== (plug?.rest_power_url || '') ||
- restEnergyUrl !== (plug?.rest_energy_url || ''));
- const [restEnergyPath, setRestEnergyPath] = useState(plug?.rest_energy_path || '');
- const [restEnergyMultiplier, setRestEnergyMultiplier] = useState<string>((plug?.rest_energy_multiplier ?? 1).toString());
- const [restEnergyTotalPath, setRestEnergyTotalPath] = useState(plug?.rest_energy_total_path || '');
- const [restEnergyTotalMultiplier, setRestEnergyTotalMultiplier] = useState<string>((plug?.rest_energy_total_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);
- // #2629: defaults to true (a plug linked to a printer usually powers it);
- // users turn it off for accessories like a filter fan or chamber light.
- const [controlsPrinterPower, setControlsPrinterPower] = useState(plug?.controls_printer_power ?? true);
- 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, plug?.id),
- 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,
- rest_energy_total_path: plugType === 'rest' ? (restEnergyTotalPath.trim() || null) : null,
- rest_energy_total_multiplier: plugType === 'rest' ? (parseFloat(restEnergyTotalMultiplier) || 1) : 1,
- username: plugType === 'tasmota' ? (username.trim() || null) : null,
- password: plugType === 'tasmota' ? (password.trim() || null) : null,
- printer_id: printerId,
- controls_printer_power: controlsPrinterPower,
- // 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-100 dark:bg-red-500/20 border border-red-400 dark:border-red-500/50 rounded-lg text-sm text-red-700 dark: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-100 dark:bg-yellow-500/20 border border-yellow-400 dark:border-yellow-500/50 rounded-lg text-sm text-yellow-700 dark: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-100 dark:bg-red-500/20 border border-red-400 dark:border-red-500/50 rounded-lg text-sm text-red-700 dark: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-100 dark:bg-yellow-500/20 border border-yellow-400 dark:border-yellow-500/50 rounded-lg text-sm text-yellow-700 dark: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-50 dark:bg-blue-500/10 border border-blue-300 dark:border-blue-500/30 rounded-lg text-sm text-blue-700 dark: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)}
- onFocus={selectPlaceholderOnFocus}
- 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"
- />
- {restHeaders === SECRET_PLACEHOLDER && (
- <p className={`text-xs mt-1 ${headersNeedReentry ? 'text-yellow-400' : 'text-bambu-gray'}`}>
- {t(headersNeedReentry ? 'common.secretReenter' : 'common.secretKept')}
- </p>
- )}
- </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>
- {/* Lifetime counter (#2539) — the only energy figure a Shelly has. */}
- <div className="grid grid-cols-2 gap-3">
- <div>
- <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restEnergyTotalPath')}</label>
- <input
- type="text"
- value={restEnergyTotalPath}
- onChange={(e) => setRestEnergyTotalPath(e.target.value)}
- placeholder={t('smartPlugs.restEnergyTotalPathHint')}
- 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.restEnergyTotalMultiplier')}</label>
- <input
- type="text"
- value={restEnergyTotalMultiplier}
- onChange={(e) => setRestEnergyTotalMultiplier(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.restEnergyTotalHint')}
- </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, plug?.id);
- 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-700 dark:text-green-400' : 'text-red-700 dark: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-100 dark:bg-red-500/20 border border-red-400 dark:border-red-500/50 text-red-700 dark: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)}
- onFocus={selectPlaceholderOnFocus}
- 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"
- />
- {password === SECRET_PLACEHOLDER && (
- <p className={`text-xs mt-1 ${passwordNeedsReentry ? 'text-yellow-400' : 'text-bambu-gray'}`}>
- {t(passwordNeedsReentry ? 'common.secretReenter' : 'common.secretKept')}
- </p>
- )}
- </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>
- {/* Whether the plug feeds the printer itself, or is an accessory
- that merely follows the print cycle (#2629). */}
- {printerId !== null && (
- <div className="flex items-center justify-between mt-3">
- <div className="pr-3">
- <p className="text-sm text-white">{t('smartPlugs.controlsPrinterPower')}</p>
- <p className="text-xs text-bambu-gray">{t('smartPlugs.controlsPrinterPowerDescription')}</p>
- </div>
- <label className="relative inline-flex items-center cursor-pointer shrink-0">
- <input
- type="checkbox"
- checked={controlsPrinterPower}
- onChange={(e) => setControlsPrinterPower(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>
- )}
- {/* 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>
- );
- }
|