AddSmartPlugModal.tsx 84 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654
  1. import { useState, useEffect, useRef } from 'react';
  2. import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
  3. import { X, Save, Loader2, Wifi, WifiOff, CheckCircle, Bell, Clock, LayoutGrid, Search, Plug, Power, Home, Radio, Eye, Globe } from 'lucide-react';
  4. import { useTranslation } from 'react-i18next';
  5. import { api } from '../api/client';
  6. import type { SmartPlug, SmartPlugCreate, SmartPlugUpdate, DiscoveredTasmotaDevice } from '../api/client';
  7. import { Button } from './Button';
  8. interface AddSmartPlugModalProps {
  9. plug?: SmartPlug | null;
  10. onClose: () => void;
  11. }
  12. export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
  13. const { t } = useTranslation();
  14. const queryClient = useQueryClient();
  15. const isEditing = !!plug;
  16. // Plug type selection
  17. const [plugType, setPlugType] = useState<'tasmota' | 'homeassistant' | 'mqtt' | 'rest'>(plug?.plug_type || 'tasmota');
  18. const [name, setName] = useState(plug?.name || '');
  19. // Tasmota fields
  20. const [ipAddress, setIpAddress] = useState(plug?.ip_address || '');
  21. const [username, setUsername] = useState(plug?.username || '');
  22. const [password, setPassword] = useState(plug?.password || '');
  23. // Home Assistant fields
  24. const [haEntityId, setHaEntityId] = useState(plug?.ha_entity_id || '');
  25. // MQTT fields - Power
  26. const [mqttPowerTopic, setMqttPowerTopic] = useState(plug?.mqtt_power_topic || plug?.mqtt_topic || '');
  27. const [mqttPowerPath, setMqttPowerPath] = useState(plug?.mqtt_power_path || '');
  28. const [mqttPowerMultiplier, setMqttPowerMultiplier] = useState<string>(
  29. (plug?.mqtt_power_multiplier ?? plug?.mqtt_multiplier ?? 1).toString()
  30. );
  31. // MQTT fields - Energy
  32. const [mqttEnergyTopic, setMqttEnergyTopic] = useState(plug?.mqtt_energy_topic || '');
  33. const [mqttEnergyPath, setMqttEnergyPath] = useState(plug?.mqtt_energy_path || '');
  34. const [mqttEnergyMultiplier, setMqttEnergyMultiplier] = useState<string>(
  35. (plug?.mqtt_energy_multiplier ?? 1).toString()
  36. );
  37. // MQTT fields - State
  38. const [mqttStateTopic, setMqttStateTopic] = useState(plug?.mqtt_state_topic || '');
  39. const [mqttStatePath, setMqttStatePath] = useState(plug?.mqtt_state_path || '');
  40. const [mqttStateOnValue, setMqttStateOnValue] = useState(plug?.mqtt_state_on_value || '');
  41. // REST fields
  42. const [restOnUrl, setRestOnUrl] = useState(plug?.rest_on_url || '');
  43. const [restOnBody, setRestOnBody] = useState(plug?.rest_on_body || '');
  44. const [restOffUrl, setRestOffUrl] = useState(plug?.rest_off_url || '');
  45. const [restOffBody, setRestOffBody] = useState(plug?.rest_off_body || '');
  46. const [restMethod, setRestMethod] = useState(plug?.rest_method || 'POST');
  47. const [restHeaders, setRestHeaders] = useState(plug?.rest_headers || '');
  48. const [restStatusUrl, setRestStatusUrl] = useState(plug?.rest_status_url || '');
  49. const [restStatusPath, setRestStatusPath] = useState(plug?.rest_status_path || '');
  50. const [restStatusOnValue, setRestStatusOnValue] = useState(plug?.rest_status_on_value || '');
  51. const [restPowerUrl, setRestPowerUrl] = useState(plug?.rest_power_url || '');
  52. const [restPowerPath, setRestPowerPath] = useState(plug?.rest_power_path || '');
  53. const [restPowerMultiplier, setRestPowerMultiplier] = useState<string>((plug?.rest_power_multiplier ?? 1).toString());
  54. const [restEnergyUrl, setRestEnergyUrl] = useState(plug?.rest_energy_url || '');
  55. const [restEnergyPath, setRestEnergyPath] = useState(plug?.rest_energy_path || '');
  56. const [restEnergyMultiplier, setRestEnergyMultiplier] = useState<string>((plug?.rest_energy_multiplier ?? 1).toString());
  57. // HA energy sensor entities (optional)
  58. const [haPowerEntity, setHaPowerEntity] = useState(plug?.ha_power_entity || '');
  59. const [haEnergyTodayEntity, setHaEnergyTodayEntity] = useState(plug?.ha_energy_today_entity || '');
  60. const [haEnergyTotalEntity, setHaEnergyTotalEntity] = useState(plug?.ha_energy_total_entity || '');
  61. // HA entity search
  62. const [haEntitySearch, setHaEntitySearch] = useState('');
  63. const [debouncedSearch, setDebouncedSearch] = useState('');
  64. const [isEntityDropdownOpen, setIsEntityDropdownOpen] = useState(false);
  65. const entityDropdownRef = useRef<HTMLDivElement>(null);
  66. // Energy sensor search states
  67. const [powerSensorSearch, setPowerSensorSearch] = useState('');
  68. const [isPowerDropdownOpen, setIsPowerDropdownOpen] = useState(false);
  69. const powerDropdownRef = useRef<HTMLDivElement>(null);
  70. const [energyTodaySearch, setEnergyTodaySearch] = useState('');
  71. const [isEnergyTodayDropdownOpen, setIsEnergyTodayDropdownOpen] = useState(false);
  72. const energyTodayDropdownRef = useRef<HTMLDivElement>(null);
  73. const [energyTotalSearch, setEnergyTotalSearch] = useState('');
  74. const [isEnergyTotalDropdownOpen, setIsEnergyTotalDropdownOpen] = useState(false);
  75. const energyTotalDropdownRef = useRef<HTMLDivElement>(null);
  76. const [printerId, setPrinterId] = useState<number | null>(plug?.printer_id || null);
  77. const [testResult, setTestResult] = useState<{ success: boolean; state?: string | null; device_name?: string | null } | null>(null);
  78. const [error, setError] = useState<string | null>(null);
  79. // Power alert settings
  80. const [powerAlertEnabled, setPowerAlertEnabled] = useState(plug?.power_alert_enabled || false);
  81. const [powerAlertHigh, setPowerAlertHigh] = useState<string>(plug?.power_alert_high?.toString() || '');
  82. const [powerAlertLow, setPowerAlertLow] = useState<string>(plug?.power_alert_low?.toString() || '');
  83. // Schedule settings
  84. const [scheduleEnabled, setScheduleEnabled] = useState(plug?.schedule_enabled || false);
  85. const [scheduleOnTime, setScheduleOnTime] = useState<string>(plug?.schedule_on_time || '');
  86. const [scheduleOffTime, setScheduleOffTime] = useState<string>(plug?.schedule_off_time || '');
  87. // Visibility options
  88. const [showInSwitchbar, setShowInSwitchbar] = useState(plug?.show_in_switchbar || false);
  89. const [showOnPrinterCard, setShowOnPrinterCard] = useState(plug?.show_on_printer_card ?? true);
  90. // Discovery state
  91. const [isScanning, setIsScanning] = useState(false);
  92. const [scanProgress, setScanProgress] = useState({ scanned: 0, total: 0 });
  93. const [discoveredDevices, setDiscoveredDevices] = useState<DiscoveredTasmotaDevice[]>([]);
  94. const scanPollRef = useRef<NodeJS.Timeout | null>(null);
  95. // Fetch printers for linking
  96. const { data: printers } = useQuery({
  97. queryKey: ['printers'],
  98. queryFn: api.getPrinters,
  99. });
  100. // Fetch existing plugs to check for conflicts
  101. const { data: existingPlugs } = useQuery({
  102. queryKey: ['smart-plugs'],
  103. queryFn: api.getSmartPlugs,
  104. });
  105. // Fetch settings to check if HA is configured
  106. const { data: settings } = useQuery({
  107. queryKey: ['settings'],
  108. queryFn: api.getSettings,
  109. });
  110. // Check if HA is properly configured
  111. const haConfigured = !!(settings?.ha_enabled && settings?.ha_url && settings?.ha_token);
  112. // Debounce search input
  113. useEffect(() => {
  114. const timer = setTimeout(() => {
  115. setDebouncedSearch(haEntitySearch);
  116. }, 300);
  117. return () => clearTimeout(timer);
  118. }, [haEntitySearch]);
  119. // Close dropdowns when clicking outside
  120. useEffect(() => {
  121. const handleClickOutside = (e: MouseEvent) => {
  122. if (entityDropdownRef.current && !entityDropdownRef.current.contains(e.target as Node)) {
  123. setIsEntityDropdownOpen(false);
  124. }
  125. if (powerDropdownRef.current && !powerDropdownRef.current.contains(e.target as Node)) {
  126. setIsPowerDropdownOpen(false);
  127. }
  128. if (energyTodayDropdownRef.current && !energyTodayDropdownRef.current.contains(e.target as Node)) {
  129. setIsEnergyTodayDropdownOpen(false);
  130. }
  131. if (energyTotalDropdownRef.current && !energyTotalDropdownRef.current.contains(e.target as Node)) {
  132. setIsEnergyTotalDropdownOpen(false);
  133. }
  134. };
  135. document.addEventListener('mousedown', handleClickOutside);
  136. return () => document.removeEventListener('mousedown', handleClickOutside);
  137. }, []);
  138. // Fetch Home Assistant entities when in HA mode AND HA is configured
  139. const { data: haEntities, isLoading: haEntitiesLoading, error: haEntitiesError } = useQuery({
  140. queryKey: ['ha-entities', debouncedSearch],
  141. queryFn: () => api.getHAEntities(debouncedSearch || undefined),
  142. enabled: plugType === 'homeassistant' && haConfigured,
  143. retry: false,
  144. staleTime: 0,
  145. });
  146. // Fetch Home Assistant sensor entities for energy monitoring
  147. const { data: haSensorEntities } = useQuery({
  148. queryKey: ['ha-sensor-entities'],
  149. queryFn: api.getHASensorEntities,
  150. enabled: plugType === 'homeassistant' && haConfigured,
  151. retry: false,
  152. staleTime: 0,
  153. });
  154. // Close on Escape key and cleanup scan polling
  155. useEffect(() => {
  156. const handleKeyDown = (e: KeyboardEvent) => {
  157. if (e.key === 'Escape') onClose();
  158. };
  159. window.addEventListener('keydown', handleKeyDown);
  160. return () => {
  161. window.removeEventListener('keydown', handleKeyDown);
  162. if (scanPollRef.current) {
  163. clearInterval(scanPollRef.current);
  164. }
  165. };
  166. }, [onClose]);
  167. // Start scanning for Tasmota devices (auto-detects network)
  168. const startScan = async () => {
  169. setIsScanning(true);
  170. setDiscoveredDevices([]);
  171. setScanProgress({ scanned: 0, total: 0 });
  172. setError(null);
  173. try {
  174. await api.startTasmotaScan();
  175. // Poll function to fetch status and devices
  176. const pollStatus = async () => {
  177. try {
  178. const status = await api.getTasmotaScanStatus();
  179. setScanProgress({ scanned: status.scanned, total: status.total });
  180. const devices = await api.getDiscoveredTasmotaDevices();
  181. setDiscoveredDevices(devices);
  182. if (!status.running) {
  183. setIsScanning(false);
  184. if (scanPollRef.current) {
  185. clearInterval(scanPollRef.current);
  186. scanPollRef.current = null;
  187. }
  188. }
  189. } catch (e) {
  190. console.error('Polling error:', e);
  191. }
  192. };
  193. // Poll immediately, then every 500ms
  194. await pollStatus();
  195. scanPollRef.current = setInterval(pollStatus, 500);
  196. } catch (err) {
  197. setIsScanning(false);
  198. const errorMsg = err instanceof Error ? err.message : (typeof err === 'string' ? err : JSON.stringify(err));
  199. setError(errorMsg || t('smartPlugs.failedToStartScan'));
  200. }
  201. };
  202. // Stop scanning
  203. const stopScan = async () => {
  204. try {
  205. await api.stopTasmotaScan();
  206. } catch {
  207. // Ignore stop errors
  208. }
  209. setIsScanning(false);
  210. if (scanPollRef.current) {
  211. clearInterval(scanPollRef.current);
  212. scanPollRef.current = null;
  213. }
  214. };
  215. // Select a discovered device
  216. const selectDevice = (device: DiscoveredTasmotaDevice) => {
  217. setIpAddress(device.ip_address);
  218. setName(device.name);
  219. setTestResult(null);
  220. };
  221. // Test connection mutation
  222. const testMutation = useMutation({
  223. mutationFn: () => api.testSmartPlugConnection(ipAddress, username || null, password || null),
  224. onSuccess: (result) => {
  225. setTestResult(result);
  226. setError(null);
  227. // Auto-fill name from device if empty
  228. if (!name && result.device_name) {
  229. setName(result.device_name);
  230. }
  231. },
  232. onError: (err: Error) => {
  233. setTestResult(null);
  234. setError(err.message);
  235. },
  236. });
  237. // Create mutation
  238. const createMutation = useMutation({
  239. mutationFn: (data: SmartPlugCreate) => api.createSmartPlug(data),
  240. onSuccess: () => {
  241. queryClient.invalidateQueries({ queryKey: ['smart-plugs'] });
  242. // Also invalidate printer card HA entity queries
  243. queryClient.invalidateQueries({ queryKey: ['scriptPlugsByPrinter'] });
  244. onClose();
  245. },
  246. onError: (err: Error) => {
  247. setError(err.message);
  248. },
  249. });
  250. // Update mutation
  251. const updateMutation = useMutation({
  252. mutationFn: (data: SmartPlugUpdate) => api.updateSmartPlug(plug!.id, data),
  253. onSuccess: () => {
  254. queryClient.invalidateQueries({ queryKey: ['smart-plugs'] });
  255. // Also invalidate printer card HA entity queries
  256. queryClient.invalidateQueries({ queryKey: ['scriptPlugsByPrinter'] });
  257. onClose();
  258. },
  259. onError: (err: Error) => {
  260. setError(err.message);
  261. },
  262. });
  263. // For Tasmota plugs, only one per printer (physical device)
  264. // For HA scripts, allow multiple per printer
  265. const availablePrinters = printers?.filter(p => {
  266. if (plugType === 'tasmota') {
  267. const hasTasmotaPlug = existingPlugs?.some(
  268. ep => ep.printer_id === p.id && ep.id !== plug?.id && ep.plug_type === 'tasmota'
  269. );
  270. return !hasTasmotaPlug;
  271. }
  272. // HA scripts can have multiple per printer
  273. return true;
  274. });
  275. const handleSubmit = (e: React.FormEvent) => {
  276. e.preventDefault();
  277. setError(null);
  278. if (!name.trim()) {
  279. setError(t('smartPlugs.nameRequired'));
  280. return;
  281. }
  282. if (plugType === 'tasmota' && !ipAddress.trim()) {
  283. setError('IP address is required for Tasmota plugs');
  284. return;
  285. }
  286. if (plugType === 'homeassistant' && !haEntityId) {
  287. setError(t('smartPlugs.entityRequired'));
  288. return;
  289. }
  290. if (plugType === 'mqtt') {
  291. // Check that at least one topic is configured (path is optional)
  292. const hasPower = mqttPowerTopic.trim();
  293. const hasEnergy = mqttEnergyTopic.trim();
  294. const hasState = mqttStateTopic.trim();
  295. if (!hasPower && !hasEnergy && !hasState) {
  296. setError(t('smartPlugs.mqttTopicRequired'));
  297. return;
  298. }
  299. }
  300. if (plugType === 'rest') {
  301. if (!restOnUrl.trim() && !restOffUrl.trim()) {
  302. setError(t('smartPlugs.restUrlRequired'));
  303. return;
  304. }
  305. }
  306. const data = {
  307. name: name.trim(),
  308. plug_type: plugType,
  309. ip_address: plugType === 'tasmota' ? ipAddress.trim() : null,
  310. ha_entity_id: plugType === 'homeassistant' ? haEntityId : null,
  311. // HA energy sensor entities (optional)
  312. ha_power_entity: plugType === 'homeassistant' ? (haPowerEntity || null) : null,
  313. ha_energy_today_entity: plugType === 'homeassistant' ? (haEnergyTodayEntity || null) : null,
  314. ha_energy_total_entity: plugType === 'homeassistant' ? (haEnergyTotalEntity || null) : null,
  315. // MQTT power fields
  316. mqtt_power_topic: plugType === 'mqtt' ? (mqttPowerTopic.trim() || null) : null,
  317. mqtt_power_path: plugType === 'mqtt' ? (mqttPowerPath.trim() || null) : null,
  318. mqtt_power_multiplier: plugType === 'mqtt' ? (parseFloat(mqttPowerMultiplier) || 1) : 1,
  319. // MQTT energy fields
  320. mqtt_energy_topic: plugType === 'mqtt' ? (mqttEnergyTopic.trim() || null) : null,
  321. mqtt_energy_path: plugType === 'mqtt' ? (mqttEnergyPath.trim() || null) : null,
  322. mqtt_energy_multiplier: plugType === 'mqtt' ? (parseFloat(mqttEnergyMultiplier) || 1) : 1,
  323. // MQTT state fields
  324. mqtt_state_topic: plugType === 'mqtt' ? (mqttStateTopic.trim() || null) : null,
  325. mqtt_state_path: plugType === 'mqtt' ? (mqttStatePath.trim() || null) : null,
  326. mqtt_state_on_value: plugType === 'mqtt' ? (mqttStateOnValue.trim() || null) : null,
  327. // REST fields
  328. rest_on_url: plugType === 'rest' ? (restOnUrl.trim() || null) : null,
  329. rest_on_body: plugType === 'rest' ? (restOnBody.trim() || null) : null,
  330. rest_off_url: plugType === 'rest' ? (restOffUrl.trim() || null) : null,
  331. rest_off_body: plugType === 'rest' ? (restOffBody.trim() || null) : null,
  332. rest_method: plugType === 'rest' ? restMethod : null,
  333. rest_headers: plugType === 'rest' ? (restHeaders.trim() || null) : null,
  334. rest_status_url: plugType === 'rest' ? (restStatusUrl.trim() || null) : null,
  335. rest_status_path: plugType === 'rest' ? (restStatusPath.trim() || null) : null,
  336. rest_status_on_value: plugType === 'rest' ? (restStatusOnValue.trim() || null) : null,
  337. rest_power_url: plugType === 'rest' ? (restPowerUrl.trim() || null) : null,
  338. rest_power_path: plugType === 'rest' ? (restPowerPath.trim() || null) : null,
  339. rest_power_multiplier: plugType === 'rest' ? (parseFloat(restPowerMultiplier) || 1) : 1,
  340. rest_energy_url: plugType === 'rest' ? (restEnergyUrl.trim() || null) : null,
  341. rest_energy_path: plugType === 'rest' ? (restEnergyPath.trim() || null) : null,
  342. rest_energy_multiplier: plugType === 'rest' ? (parseFloat(restEnergyMultiplier) || 1) : 1,
  343. username: plugType === 'tasmota' ? (username.trim() || null) : null,
  344. password: plugType === 'tasmota' ? (password.trim() || null) : null,
  345. printer_id: printerId,
  346. // Power alerts
  347. power_alert_enabled: powerAlertEnabled,
  348. power_alert_high: powerAlertHigh ? parseFloat(powerAlertHigh) : null,
  349. power_alert_low: powerAlertLow ? parseFloat(powerAlertLow) : null,
  350. // Schedule
  351. schedule_enabled: scheduleEnabled,
  352. schedule_on_time: scheduleOnTime || null,
  353. schedule_off_time: scheduleOffTime || null,
  354. // Visibility
  355. show_in_switchbar: showInSwitchbar,
  356. show_on_printer_card: showOnPrinterCard,
  357. };
  358. if (isEditing) {
  359. updateMutation.mutate(data);
  360. } else {
  361. createMutation.mutate(data);
  362. }
  363. };
  364. const isPending = createMutation.isPending || updateMutation.isPending;
  365. return (
  366. <div
  367. className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4"
  368. onClick={onClose}
  369. >
  370. <div
  371. className="bg-bambu-dark-secondary rounded-xl border border-bambu-dark-tertiary w-full max-w-md max-h-[90vh] flex flex-col"
  372. onClick={(e) => e.stopPropagation()}
  373. >
  374. {/* Header */}
  375. <div className="flex items-center justify-between px-6 py-4 border-b border-bambu-dark-tertiary flex-shrink-0">
  376. <h2 className="text-lg font-semibold text-white">
  377. {isEditing ? t('smartPlugs.editTitle') : t('smartPlugs.addTitle')}
  378. </h2>
  379. <button
  380. onClick={onClose}
  381. className="text-bambu-gray hover:text-white transition-colors"
  382. >
  383. <X className="w-5 h-5" />
  384. </button>
  385. </div>
  386. {/* Form */}
  387. <form onSubmit={handleSubmit} className="p-6 space-y-4 overflow-y-auto">
  388. {error && (
  389. <div className="p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-sm text-red-400">
  390. {error}
  391. </div>
  392. )}
  393. {/* Plug Type Selector - only show when not editing */}
  394. {!isEditing && (
  395. <div className="flex gap-2 mb-2">
  396. <button
  397. type="button"
  398. onClick={() => {
  399. setPlugType('tasmota');
  400. setTestResult(null);
  401. setError(null);
  402. }}
  403. className={`flex-1 flex items-center justify-center gap-2 px-3 py-2.5 rounded-lg font-medium transition-colors ${
  404. plugType === 'tasmota'
  405. ? 'bg-bambu-green text-white'
  406. : 'bg-bambu-dark text-bambu-gray hover:text-white border border-bambu-dark-tertiary'
  407. }`}
  408. >
  409. <Plug className="w-4 h-4" />
  410. Tasmota
  411. </button>
  412. <button
  413. type="button"
  414. onClick={() => {
  415. setPlugType('homeassistant');
  416. setTestResult(null);
  417. setError(null);
  418. }}
  419. className={`flex-1 flex items-center justify-center gap-2 px-3 py-2.5 rounded-lg font-medium transition-colors ${
  420. plugType === 'homeassistant'
  421. ? 'bg-bambu-green text-white'
  422. : 'bg-bambu-dark text-bambu-gray hover:text-white border border-bambu-dark-tertiary'
  423. }`}
  424. >
  425. <Home className="w-4 h-4" />
  426. HA
  427. </button>
  428. <button
  429. type="button"
  430. onClick={() => {
  431. setPlugType('mqtt');
  432. setTestResult(null);
  433. setError(null);
  434. }}
  435. className={`flex-1 flex items-center justify-center gap-2 px-3 py-2.5 rounded-lg font-medium transition-colors ${
  436. plugType === 'mqtt'
  437. ? 'bg-bambu-green text-white'
  438. : 'bg-bambu-dark text-bambu-gray hover:text-white border border-bambu-dark-tertiary'
  439. }`}
  440. >
  441. <Radio className="w-4 h-4" />
  442. MQTT
  443. </button>
  444. <button
  445. type="button"
  446. onClick={() => {
  447. setPlugType('rest');
  448. setTestResult(null);
  449. setError(null);
  450. }}
  451. className={`flex-1 flex items-center justify-center gap-2 px-3 py-2.5 rounded-lg font-medium transition-colors ${
  452. plugType === 'rest'
  453. ? 'bg-bambu-green text-white'
  454. : 'bg-bambu-dark text-bambu-gray hover:text-white border border-bambu-dark-tertiary'
  455. }`}
  456. >
  457. <Globe className="w-4 h-4" />
  458. REST
  459. </button>
  460. </div>
  461. )}
  462. {/* Discovery Section - only show when not editing and Tasmota is selected */}
  463. {!isEditing && plugType === 'tasmota' && (
  464. <div className="space-y-3">
  465. {/* Scan button - auto-detects network */}
  466. {isScanning ? (
  467. <Button type="button" variant="secondary" onClick={stopScan} className="w-full">
  468. <X className="w-4 h-4" />
  469. {t('smartPlugs.stopScanning')}
  470. </Button>
  471. ) : (
  472. <Button type="button" variant="primary" onClick={startScan} className="w-full">
  473. <Search className="w-4 h-4" />
  474. {t('smartPlugs.discoverTasmota')}
  475. </Button>
  476. )}
  477. {/* Progress bar */}
  478. {isScanning && scanProgress.total > 0 && (
  479. <div className="space-y-1">
  480. <div className="flex justify-between text-xs text-bambu-gray">
  481. <span>{t('smartPlugs.addSmartPlug.scanningNetwork')}</span>
  482. <span>{scanProgress.scanned} / {scanProgress.total}</span>
  483. </div>
  484. <div className="w-full bg-bambu-dark-tertiary rounded-full h-2">
  485. <div
  486. className="bg-bambu-green h-2 rounded-full transition-all duration-300"
  487. style={{ width: `${(scanProgress.scanned / scanProgress.total) * 100}%` }}
  488. />
  489. </div>
  490. </div>
  491. )}
  492. {/* Discovered devices */}
  493. {discoveredDevices.length > 0 && (
  494. <div className="space-y-2">
  495. <p className="text-xs text-bambu-gray">{t('smartPlugs.foundDevices', { count: discoveredDevices.length })}</p>
  496. <div className="max-h-40 overflow-y-auto space-y-1">
  497. {discoveredDevices.map((device) => (
  498. <button
  499. key={device.ip_address}
  500. type="button"
  501. onClick={() => selectDevice(device)}
  502. 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"
  503. >
  504. <div className="flex items-center gap-2">
  505. <Plug className="w-4 h-4 text-bambu-green" />
  506. <div>
  507. <p className="text-sm text-white">{device.name}</p>
  508. <p className="text-xs text-bambu-gray">{device.ip_address}</p>
  509. </div>
  510. </div>
  511. {device.state && (
  512. <span className={`flex items-center gap-1 text-xs ${
  513. device.state === 'ON' ? 'text-bambu-green' : 'text-bambu-gray'
  514. }`}>
  515. <Power className="w-3 h-3" />
  516. {device.state}
  517. </span>
  518. )}
  519. </button>
  520. ))}
  521. </div>
  522. </div>
  523. )}
  524. {!isScanning && discoveredDevices.length === 0 && scanProgress.total > 0 && (
  525. <p className="text-xs text-bambu-gray text-center py-2">
  526. {t('smartPlugs.noDevicesFound')}
  527. </p>
  528. )}
  529. </div>
  530. )}
  531. {/* Home Assistant Entity Selector - only show when HA is selected */}
  532. {plugType === 'homeassistant' && (
  533. <div className="space-y-3">
  534. {/* HA not configured */}
  535. {!haConfigured && (
  536. <div className="space-y-3">
  537. <div className="p-3 bg-yellow-500/20 border border-yellow-500/50 rounded-lg text-sm text-yellow-400">
  538. {t('smartPlugs.haNotConfigured')}{' '}
  539. <span className="font-medium">{t('smartPlugs.haSettingsPath')}</span>
  540. </div>
  541. <div>
  542. <label className="block text-sm text-bambu-gray mb-1 opacity-50">{t('smartPlugs.selectEntity')}</label>
  543. <select
  544. disabled
  545. 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"
  546. >
  547. <option>{t('smartPlugs.addSmartPlug.chooseEntity')}</option>
  548. </select>
  549. </div>
  550. </div>
  551. )}
  552. {/* HA configured - show loading/entities */}
  553. {haConfigured && (
  554. <>
  555. {haEntitiesLoading && (
  556. <div className="flex items-center justify-center py-4 text-bambu-gray">
  557. <Loader2 className="w-5 h-5 animate-spin mr-2" />
  558. {t('smartPlugs.loadingEntities')}
  559. </div>
  560. )}
  561. {haEntitiesError && (
  562. <div className="p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-sm text-red-400">
  563. {t('smartPlugs.failedToLoadEntities', { error: (haEntitiesError as Error).message })}
  564. </div>
  565. )}
  566. {/* Searchable Entity Dropdown */}
  567. {(() => {
  568. // Filter out entities already configured (except current plug when editing)
  569. const configuredEntityIds = existingPlugs
  570. ?.filter(p => p.ha_entity_id && p.id !== plug?.id)
  571. .map(p => p.ha_entity_id) || [];
  572. const availableEntities = (haEntities || []).filter(e => !configuredEntityIds.includes(e.entity_id));
  573. const selectedEntity = haEntities?.find(e => e.entity_id === haEntityId);
  574. return (
  575. <div ref={entityDropdownRef} className="relative">
  576. <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.selectEntity')}</label>
  577. <div className="relative">
  578. <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
  579. <input
  580. type="text"
  581. value={isEntityDropdownOpen ? haEntitySearch : (selectedEntity ? `${selectedEntity.friendly_name} (${selectedEntity.entity_id})` : '')}
  582. onChange={(e) => {
  583. setHaEntitySearch(e.target.value);
  584. if (!isEntityDropdownOpen) setIsEntityDropdownOpen(true);
  585. }}
  586. onFocus={() => {
  587. setIsEntityDropdownOpen(true);
  588. setHaEntitySearch('');
  589. }}
  590. placeholder={t('smartPlugs.addSmartPlug.placeholders.searchEntities')}
  591. 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"
  592. />
  593. {haEntityId && !isEntityDropdownOpen && (
  594. <button
  595. type="button"
  596. onClick={() => {
  597. setHaEntityId('');
  598. setHaEntitySearch('');
  599. }}
  600. className="absolute right-2 top-1/2 -translate-y-1/2 p-1 hover:bg-bambu-dark-tertiary rounded"
  601. >
  602. <X className="w-4 h-4 text-bambu-gray hover:text-white" />
  603. </button>
  604. )}
  605. {haEntitiesLoading && (
  606. <Loader2 className="absolute right-2 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray animate-spin" />
  607. )}
  608. </div>
  609. {/* Dropdown */}
  610. {isEntityDropdownOpen && (
  611. <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">
  612. {haEntitiesLoading && (
  613. <div className="px-3 py-2 text-sm text-bambu-gray flex items-center gap-2">
  614. <Loader2 className="w-4 h-4 animate-spin" />
  615. {t('smartPlugs.loading')}
  616. </div>
  617. )}
  618. {!haEntitiesLoading && availableEntities.length === 0 && (
  619. <div className="px-3 py-2 text-sm text-bambu-gray">
  620. {debouncedSearch
  621. ? t('smartPlugs.noEntitiesMatching', { search: debouncedSearch })
  622. : t('smartPlugs.noEntitiesAvailable')}
  623. </div>
  624. )}
  625. {!haEntitiesLoading && availableEntities.map((entity) => (
  626. <button
  627. key={entity.entity_id}
  628. type="button"
  629. onClick={() => {
  630. setHaEntityId(entity.entity_id);
  631. setIsEntityDropdownOpen(false);
  632. setHaEntitySearch('');
  633. // Auto-fill name
  634. if (!name) {
  635. setName(entity.friendly_name);
  636. }
  637. }}
  638. className={`w-full px-3 py-2 text-left text-sm hover:bg-bambu-dark-tertiary transition-colors ${
  639. entity.entity_id === haEntityId ? 'bg-bambu-green/20 text-bambu-green' : 'text-white'
  640. }`}
  641. >
  642. <div className="font-medium">{entity.friendly_name}</div>
  643. <div className="text-xs text-bambu-gray flex items-center justify-between">
  644. <span>{entity.entity_id}</span>
  645. <span className={entity.state === 'on' ? 'text-bambu-green' : ''}>{entity.state}</span>
  646. </div>
  647. </button>
  648. ))}
  649. </div>
  650. )}
  651. <p className="text-xs text-bambu-gray mt-1">
  652. {debouncedSearch
  653. ? t('smartPlugs.searchingEntities', { count: availableEntities.length })
  654. : t('smartPlugs.showingEntities', { count: availableEntities.length })}
  655. </p>
  656. </div>
  657. );
  658. })()}
  659. {/* Energy Monitoring Section (Optional) */}
  660. {haEntityId && haSensorEntities && haSensorEntities.length > 0 && (
  661. <div className="border-t border-bambu-dark-tertiary pt-4 mt-4 space-y-3">
  662. <div>
  663. <p className="text-white font-medium mb-1">{t('smartPlugs.energyMonitoringOptional')}</p>
  664. <p className="text-xs text-bambu-gray mb-3">
  665. {t('smartPlugs.energyMonitoringHint')}
  666. </p>
  667. </div>
  668. {/* Power Sensor (W) */}
  669. {(() => {
  670. const powerSensors = haSensorEntities.filter(s =>
  671. s.unit_of_measurement === 'W' || s.unit_of_measurement === 'kW' || s.unit_of_measurement === 'mW'
  672. );
  673. const filteredPowerSensors = powerSensorSearch
  674. ? powerSensors.filter(s =>
  675. s.entity_id.toLowerCase().includes(powerSensorSearch.toLowerCase()) ||
  676. s.friendly_name.toLowerCase().includes(powerSensorSearch.toLowerCase())
  677. )
  678. : powerSensors;
  679. const selectedPowerSensor = haSensorEntities.find(s => s.entity_id === haPowerEntity);
  680. return (
  681. <div ref={powerDropdownRef} className="relative">
  682. <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.powerSensorW')}</label>
  683. <div className="relative">
  684. <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
  685. <input
  686. type="text"
  687. value={isPowerDropdownOpen ? powerSensorSearch : (selectedPowerSensor ? `${selectedPowerSensor.friendly_name} (${selectedPowerSensor.state} ${selectedPowerSensor.unit_of_measurement})` : '')}
  688. onChange={(e) => {
  689. setPowerSensorSearch(e.target.value);
  690. if (!isPowerDropdownOpen) setIsPowerDropdownOpen(true);
  691. }}
  692. onFocus={() => {
  693. setIsPowerDropdownOpen(true);
  694. setPowerSensorSearch('');
  695. }}
  696. placeholder={t('smartPlugs.addSmartPlug.placeholders.searchPowerSensors')}
  697. 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"
  698. />
  699. {haPowerEntity && !isPowerDropdownOpen && (
  700. <button
  701. type="button"
  702. onClick={() => {
  703. setHaPowerEntity('');
  704. setPowerSensorSearch('');
  705. }}
  706. className="absolute right-2 top-1/2 -translate-y-1/2 p-1 hover:bg-bambu-dark-tertiary rounded"
  707. >
  708. <X className="w-4 h-4 text-bambu-gray hover:text-white" />
  709. </button>
  710. )}
  711. </div>
  712. {isPowerDropdownOpen && (
  713. <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">
  714. <button
  715. type="button"
  716. onClick={() => {
  717. setHaPowerEntity('');
  718. setIsPowerDropdownOpen(false);
  719. setPowerSensorSearch('');
  720. }}
  721. className="w-full px-3 py-2 text-left text-sm text-bambu-gray hover:bg-bambu-dark-tertiary"
  722. >
  723. {t('smartPlugs.none')}
  724. </button>
  725. {filteredPowerSensors.map((sensor) => (
  726. <button
  727. key={sensor.entity_id}
  728. type="button"
  729. onClick={() => {
  730. setHaPowerEntity(sensor.entity_id);
  731. setIsPowerDropdownOpen(false);
  732. setPowerSensorSearch('');
  733. }}
  734. className={`w-full px-3 py-2 text-left text-sm hover:bg-bambu-dark-tertiary ${
  735. sensor.entity_id === haPowerEntity ? 'bg-bambu-green/20 text-bambu-green' : 'text-white'
  736. }`}
  737. >
  738. <div className="font-medium">{sensor.friendly_name}</div>
  739. <div className="text-xs text-bambu-gray">{sensor.entity_id} • {sensor.state} {sensor.unit_of_measurement}</div>
  740. </button>
  741. ))}
  742. {filteredPowerSensors.length === 0 && (
  743. <div className="px-3 py-2 text-sm text-bambu-gray">{t('smartPlugs.noMatchingSensors')}</div>
  744. )}
  745. </div>
  746. )}
  747. </div>
  748. );
  749. })()}
  750. {/* Energy Today (kWh) */}
  751. {(() => {
  752. const energySensors = haSensorEntities.filter(s =>
  753. s.unit_of_measurement === 'kWh' || s.unit_of_measurement === 'Wh' || s.unit_of_measurement === 'MWh'
  754. );
  755. const filteredEnergySensors = energyTodaySearch
  756. ? energySensors.filter(s =>
  757. s.entity_id.toLowerCase().includes(energyTodaySearch.toLowerCase()) ||
  758. s.friendly_name.toLowerCase().includes(energyTodaySearch.toLowerCase())
  759. )
  760. : energySensors;
  761. const selectedSensor = haSensorEntities.find(s => s.entity_id === haEnergyTodayEntity);
  762. return (
  763. <div ref={energyTodayDropdownRef} className="relative">
  764. <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.energyTodayKwh')}</label>
  765. <div className="relative">
  766. <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
  767. <input
  768. type="text"
  769. value={isEnergyTodayDropdownOpen ? energyTodaySearch : (selectedSensor ? `${selectedSensor.friendly_name} (${selectedSensor.state} ${selectedSensor.unit_of_measurement})` : '')}
  770. onChange={(e) => {
  771. setEnergyTodaySearch(e.target.value);
  772. if (!isEnergyTodayDropdownOpen) setIsEnergyTodayDropdownOpen(true);
  773. }}
  774. onFocus={() => {
  775. setIsEnergyTodayDropdownOpen(true);
  776. setEnergyTodaySearch('');
  777. }}
  778. placeholder={t('smartPlugs.addSmartPlug.placeholders.searchEnergySensors')}
  779. 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"
  780. />
  781. {haEnergyTodayEntity && !isEnergyTodayDropdownOpen && (
  782. <button
  783. type="button"
  784. onClick={() => {
  785. setHaEnergyTodayEntity('');
  786. setEnergyTodaySearch('');
  787. }}
  788. className="absolute right-2 top-1/2 -translate-y-1/2 p-1 hover:bg-bambu-dark-tertiary rounded"
  789. >
  790. <X className="w-4 h-4 text-bambu-gray hover:text-white" />
  791. </button>
  792. )}
  793. </div>
  794. {isEnergyTodayDropdownOpen && (
  795. <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">
  796. <button
  797. type="button"
  798. onClick={() => {
  799. setHaEnergyTodayEntity('');
  800. setIsEnergyTodayDropdownOpen(false);
  801. setEnergyTodaySearch('');
  802. }}
  803. className="w-full px-3 py-2 text-left text-sm text-bambu-gray hover:bg-bambu-dark-tertiary"
  804. >
  805. {t('smartPlugs.none')}
  806. </button>
  807. {filteredEnergySensors.map((sensor) => (
  808. <button
  809. key={sensor.entity_id}
  810. type="button"
  811. onClick={() => {
  812. setHaEnergyTodayEntity(sensor.entity_id);
  813. setIsEnergyTodayDropdownOpen(false);
  814. setEnergyTodaySearch('');
  815. }}
  816. className={`w-full px-3 py-2 text-left text-sm hover:bg-bambu-dark-tertiary ${
  817. sensor.entity_id === haEnergyTodayEntity ? 'bg-bambu-green/20 text-bambu-green' : 'text-white'
  818. }`}
  819. >
  820. <div className="font-medium">{sensor.friendly_name}</div>
  821. <div className="text-xs text-bambu-gray">{sensor.entity_id} • {sensor.state} {sensor.unit_of_measurement}</div>
  822. </button>
  823. ))}
  824. {filteredEnergySensors.length === 0 && (
  825. <div className="px-3 py-2 text-sm text-bambu-gray">{t('smartPlugs.noMatchingSensors')}</div>
  826. )}
  827. </div>
  828. )}
  829. </div>
  830. );
  831. })()}
  832. {/* Total Energy (kWh) */}
  833. {(() => {
  834. const energySensors = haSensorEntities.filter(s =>
  835. s.unit_of_measurement === 'kWh' || s.unit_of_measurement === 'Wh' || s.unit_of_measurement === 'MWh'
  836. );
  837. const filteredEnergySensors = energyTotalSearch
  838. ? energySensors.filter(s =>
  839. s.entity_id.toLowerCase().includes(energyTotalSearch.toLowerCase()) ||
  840. s.friendly_name.toLowerCase().includes(energyTotalSearch.toLowerCase())
  841. )
  842. : energySensors;
  843. const selectedSensor = haSensorEntities.find(s => s.entity_id === haEnergyTotalEntity);
  844. return (
  845. <div ref={energyTotalDropdownRef} className="relative">
  846. <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.totalEnergyKwh')}</label>
  847. <div className="relative">
  848. <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
  849. <input
  850. type="text"
  851. value={isEnergyTotalDropdownOpen ? energyTotalSearch : (selectedSensor ? `${selectedSensor.friendly_name} (${selectedSensor.state} ${selectedSensor.unit_of_measurement})` : '')}
  852. onChange={(e) => {
  853. setEnergyTotalSearch(e.target.value);
  854. if (!isEnergyTotalDropdownOpen) setIsEnergyTotalDropdownOpen(true);
  855. }}
  856. onFocus={() => {
  857. setIsEnergyTotalDropdownOpen(true);
  858. setEnergyTotalSearch('');
  859. }}
  860. placeholder={t('smartPlugs.addSmartPlug.placeholders.searchEnergySensors')}
  861. 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"
  862. />
  863. {haEnergyTotalEntity && !isEnergyTotalDropdownOpen && (
  864. <button
  865. type="button"
  866. onClick={() => {
  867. setHaEnergyTotalEntity('');
  868. setEnergyTotalSearch('');
  869. }}
  870. className="absolute right-2 top-1/2 -translate-y-1/2 p-1 hover:bg-bambu-dark-tertiary rounded"
  871. >
  872. <X className="w-4 h-4 text-bambu-gray hover:text-white" />
  873. </button>
  874. )}
  875. </div>
  876. {isEnergyTotalDropdownOpen && (
  877. <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">
  878. <button
  879. type="button"
  880. onClick={() => {
  881. setHaEnergyTotalEntity('');
  882. setIsEnergyTotalDropdownOpen(false);
  883. setEnergyTotalSearch('');
  884. }}
  885. className="w-full px-3 py-2 text-left text-sm text-bambu-gray hover:bg-bambu-dark-tertiary"
  886. >
  887. {t('smartPlugs.none')}
  888. </button>
  889. {filteredEnergySensors.map((sensor) => (
  890. <button
  891. key={sensor.entity_id}
  892. type="button"
  893. onClick={() => {
  894. setHaEnergyTotalEntity(sensor.entity_id);
  895. setIsEnergyTotalDropdownOpen(false);
  896. setEnergyTotalSearch('');
  897. }}
  898. className={`w-full px-3 py-2 text-left text-sm hover:bg-bambu-dark-tertiary ${
  899. sensor.entity_id === haEnergyTotalEntity ? 'bg-bambu-green/20 text-bambu-green' : 'text-white'
  900. }`}
  901. >
  902. <div className="font-medium">{sensor.friendly_name}</div>
  903. <div className="text-xs text-bambu-gray">{sensor.entity_id} • {sensor.state} {sensor.unit_of_measurement}</div>
  904. </button>
  905. ))}
  906. {filteredEnergySensors.length === 0 && (
  907. <div className="px-3 py-2 text-sm text-bambu-gray">{t('smartPlugs.noMatchingSensors')}</div>
  908. )}
  909. </div>
  910. )}
  911. </div>
  912. );
  913. })()}
  914. </div>
  915. )}
  916. </>
  917. )}
  918. </div>
  919. )}
  920. {/* MQTT Configuration - only show when MQTT is selected */}
  921. {plugType === 'mqtt' && (
  922. <div className="space-y-3">
  923. {/* MQTT broker not configured */}
  924. {!settings?.mqtt_broker && (
  925. <div className="p-3 bg-yellow-500/20 border border-yellow-500/50 rounded-lg text-sm text-yellow-400">
  926. {t('smartPlugs.mqttNotConfigured')}{' '}
  927. <span className="font-medium">{t('smartPlugs.mqttSettingsPath')}</span>
  928. {' '}{t('smartPlugs.mqttNotConfiguredSuffix')}
  929. </div>
  930. )}
  931. {/* MQTT broker configured - show fields */}
  932. {settings?.mqtt_broker && (
  933. <>
  934. <div className="p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg text-sm text-blue-300">
  935. <p className="font-medium mb-1">{t('smartPlugs.monitorOnly')}</p>
  936. <p className="text-xs opacity-80">
  937. {t('smartPlugs.mqttMonitorOnlyDescription')}
  938. </p>
  939. </div>
  940. {/* Power Section */}
  941. <div className="space-y-3 p-3 bg-bambu-dark rounded-lg border border-bambu-dark-tertiary">
  942. <p className="text-white font-medium text-sm">{t('smartPlugs.powerMonitoring')}</p>
  943. <div>
  944. <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.topic')}</label>
  945. <input
  946. type="text"
  947. value={mqttPowerTopic}
  948. onChange={(e) => setMqttPowerTopic(e.target.value)}
  949. placeholder="zigbee2mqtt/shelly-working-room"
  950. 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"
  951. />
  952. </div>
  953. <div className="grid grid-cols-2 gap-3">
  954. <div>
  955. <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.jsonPath')}</label>
  956. <input
  957. type="text"
  958. value={mqttPowerPath}
  959. onChange={(e) => setMqttPowerPath(e.target.value)}
  960. placeholder="power_l1"
  961. 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"
  962. />
  963. </div>
  964. <div>
  965. <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.multiplier')}</label>
  966. <input
  967. type="text"
  968. value={mqttPowerMultiplier}
  969. onChange={(e) => setMqttPowerMultiplier(e.target.value)}
  970. placeholder="1"
  971. 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"
  972. />
  973. </div>
  974. </div>
  975. <p className="text-xs text-bambu-gray" style={{ whiteSpace: 'pre-line' }}>
  976. {t('smartPlugs.mqttPowerHint')}
  977. </p>
  978. </div>
  979. {/* Energy Section */}
  980. <div className="space-y-3 p-3 bg-bambu-dark rounded-lg border border-bambu-dark-tertiary">
  981. <p className="text-white font-medium text-sm">{t('smartPlugs.energyMonitoring')} <span className="text-bambu-gray font-normal">({t('smartPlugs.optional')})</span></p>
  982. <div>
  983. <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.topic')}</label>
  984. <input
  985. type="text"
  986. value={mqttEnergyTopic}
  987. onChange={(e) => setMqttEnergyTopic(e.target.value)}
  988. placeholder="Same as power topic, or different"
  989. 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"
  990. />
  991. </div>
  992. <div className="grid grid-cols-2 gap-3">
  993. <div>
  994. <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.jsonPath')}</label>
  995. <input
  996. type="text"
  997. value={mqttEnergyPath}
  998. onChange={(e) => setMqttEnergyPath(e.target.value)}
  999. placeholder="energy_l1"
  1000. 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"
  1001. />
  1002. </div>
  1003. <div>
  1004. <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.multiplier')}</label>
  1005. <input
  1006. type="text"
  1007. value={mqttEnergyMultiplier}
  1008. onChange={(e) => setMqttEnergyMultiplier(e.target.value)}
  1009. placeholder="1"
  1010. 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"
  1011. />
  1012. </div>
  1013. </div>
  1014. <p className="text-xs text-bambu-gray" style={{ whiteSpace: 'pre-line' }}>
  1015. {t('smartPlugs.mqttEnergyHint')}
  1016. </p>
  1017. </div>
  1018. {/* State Section */}
  1019. <div className="space-y-3 p-3 bg-bambu-dark rounded-lg border border-bambu-dark-tertiary">
  1020. <p className="text-white font-medium text-sm">{t('smartPlugs.stateMonitoring')} <span className="text-bambu-gray font-normal">({t('smartPlugs.optional')})</span></p>
  1021. <div>
  1022. <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.topic')}</label>
  1023. <input
  1024. type="text"
  1025. value={mqttStateTopic}
  1026. onChange={(e) => setMqttStateTopic(e.target.value)}
  1027. placeholder="Same as power topic, or different"
  1028. 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"
  1029. />
  1030. </div>
  1031. <div className="grid grid-cols-2 gap-3">
  1032. <div>
  1033. <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.jsonPath')}</label>
  1034. <input
  1035. type="text"
  1036. value={mqttStatePath}
  1037. onChange={(e) => setMqttStatePath(e.target.value)}
  1038. placeholder="state_l1"
  1039. 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"
  1040. />
  1041. </div>
  1042. <div>
  1043. <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.onValue')}</label>
  1044. <input
  1045. type="text"
  1046. value={mqttStateOnValue}
  1047. onChange={(e) => setMqttStateOnValue(e.target.value)}
  1048. placeholder={t('smartPlugs.addSmartPlug.placeholders.mqttStateOnValue')}
  1049. 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"
  1050. />
  1051. </div>
  1052. </div>
  1053. <p className="text-xs text-bambu-gray" style={{ whiteSpace: 'pre-line' }}>
  1054. {t('smartPlugs.mqttStateHint')}
  1055. </p>
  1056. </div>
  1057. </>
  1058. )}
  1059. </div>
  1060. )}
  1061. {/* REST API Section */}
  1062. {plugType === 'rest' && (
  1063. <div className="space-y-3">
  1064. {/* Control Section */}
  1065. <div className="space-y-3 p-3 bg-bambu-dark rounded-lg border border-bambu-dark-tertiary">
  1066. <p className="text-white font-medium text-sm">{t('smartPlugs.restControl')}</p>
  1067. <div>
  1068. <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restMethod')}</label>
  1069. <select
  1070. value={restMethod}
  1071. onChange={(e) => setRestMethod(e.target.value)}
  1072. 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"
  1073. >
  1074. <option value="GET">GET</option>
  1075. <option value="POST">POST</option>
  1076. <option value="PUT">PUT</option>
  1077. <option value="PATCH">PATCH</option>
  1078. </select>
  1079. </div>
  1080. <div>
  1081. <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restOnUrl')}</label>
  1082. <input
  1083. type="text"
  1084. value={restOnUrl}
  1085. onChange={(e) => { setRestOnUrl(e.target.value); setTestResult(null); }}
  1086. placeholder="http://openhab:8080/rest/items/MyPlug"
  1087. 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"
  1088. />
  1089. </div>
  1090. <div>
  1091. <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>
  1092. <input
  1093. type="text"
  1094. value={restOnBody}
  1095. onChange={(e) => setRestOnBody(e.target.value)}
  1096. placeholder={t('smartPlugs.restBodyHint')}
  1097. 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"
  1098. />
  1099. </div>
  1100. <div>
  1101. <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restOffUrl')}</label>
  1102. <input
  1103. type="text"
  1104. value={restOffUrl}
  1105. onChange={(e) => { setRestOffUrl(e.target.value); setTestResult(null); }}
  1106. placeholder="http://openhab:8080/rest/items/MyPlug"
  1107. 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"
  1108. />
  1109. </div>
  1110. <div>
  1111. <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>
  1112. <input
  1113. type="text"
  1114. value={restOffBody}
  1115. onChange={(e) => setRestOffBody(e.target.value)}
  1116. placeholder={t('smartPlugs.restBodyHint')}
  1117. 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"
  1118. />
  1119. </div>
  1120. </div>
  1121. {/* Headers Section */}
  1122. <div className="space-y-3 p-3 bg-bambu-dark rounded-lg border border-bambu-dark-tertiary">
  1123. <p className="text-white font-medium text-sm">{t('smartPlugs.restHeaders')} <span className="text-bambu-gray font-normal">({t('smartPlugs.optional')})</span></p>
  1124. <div>
  1125. <textarea
  1126. value={restHeaders}
  1127. onChange={(e) => setRestHeaders(e.target.value)}
  1128. placeholder={t('smartPlugs.restHeadersHint')}
  1129. rows={2}
  1130. 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"
  1131. />
  1132. </div>
  1133. </div>
  1134. {/* Status Polling Section (optional) */}
  1135. <div className="space-y-3 p-3 bg-bambu-dark rounded-lg border border-bambu-dark-tertiary">
  1136. <p className="text-white font-medium text-sm">{t('smartPlugs.stateMonitoring')} <span className="text-bambu-gray font-normal">({t('smartPlugs.optional')})</span></p>
  1137. <div>
  1138. <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restStatusUrl')}</label>
  1139. <input
  1140. type="text"
  1141. value={restStatusUrl}
  1142. onChange={(e) => setRestStatusUrl(e.target.value)}
  1143. placeholder={t('smartPlugs.restStatusHint')}
  1144. 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"
  1145. />
  1146. </div>
  1147. <div className="grid grid-cols-2 gap-3">
  1148. <div>
  1149. <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restStatusPath')}</label>
  1150. <input
  1151. type="text"
  1152. value={restStatusPath}
  1153. onChange={(e) => setRestStatusPath(e.target.value)}
  1154. placeholder={t('smartPlugs.restPathHint')}
  1155. 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"
  1156. />
  1157. </div>
  1158. <div>
  1159. <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restStatusOnValue')}</label>
  1160. <input
  1161. type="text"
  1162. value={restStatusOnValue}
  1163. onChange={(e) => setRestStatusOnValue(e.target.value)}
  1164. placeholder="ON"
  1165. 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"
  1166. />
  1167. </div>
  1168. </div>
  1169. </div>
  1170. {/* Energy Monitoring (optional) */}
  1171. <div className="space-y-3 p-3 bg-bambu-dark rounded-lg border border-bambu-dark-tertiary">
  1172. <p className="text-white font-medium text-sm">{t('smartPlugs.energyMonitoring')} <span className="text-bambu-gray font-normal">({t('smartPlugs.optional')})</span></p>
  1173. {/* Power */}
  1174. <div>
  1175. <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>
  1176. <input
  1177. type="text"
  1178. value={restPowerUrl}
  1179. onChange={(e) => setRestPowerUrl(e.target.value)}
  1180. placeholder={t('smartPlugs.restPowerUrlHint')}
  1181. 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"
  1182. />
  1183. </div>
  1184. <div className="grid grid-cols-2 gap-3">
  1185. <div>
  1186. <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restPowerPath')}</label>
  1187. <input
  1188. type="text"
  1189. value={restPowerPath}
  1190. onChange={(e) => setRestPowerPath(e.target.value)}
  1191. placeholder={t('smartPlugs.restPathHint')}
  1192. 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"
  1193. />
  1194. </div>
  1195. <div>
  1196. <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restPowerMultiplier')}</label>
  1197. <input
  1198. type="text"
  1199. value={restPowerMultiplier}
  1200. onChange={(e) => setRestPowerMultiplier(e.target.value)}
  1201. placeholder="1"
  1202. 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"
  1203. />
  1204. </div>
  1205. </div>
  1206. {/* Energy */}
  1207. <div>
  1208. <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>
  1209. <input
  1210. type="text"
  1211. value={restEnergyUrl}
  1212. onChange={(e) => setRestEnergyUrl(e.target.value)}
  1213. placeholder={t('smartPlugs.restEnergyUrlHint')}
  1214. 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"
  1215. />
  1216. </div>
  1217. <div className="grid grid-cols-2 gap-3">
  1218. <div>
  1219. <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restEnergyPath')}</label>
  1220. <input
  1221. type="text"
  1222. value={restEnergyPath}
  1223. onChange={(e) => setRestEnergyPath(e.target.value)}
  1224. placeholder={t('smartPlugs.restPathHint')}
  1225. 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"
  1226. />
  1227. </div>
  1228. <div>
  1229. <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restEnergyMultiplier')}</label>
  1230. <input
  1231. type="text"
  1232. value={restEnergyMultiplier}
  1233. onChange={(e) => setRestEnergyMultiplier(e.target.value)}
  1234. placeholder="1"
  1235. 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"
  1236. />
  1237. </div>
  1238. </div>
  1239. <p className="text-xs text-bambu-gray">
  1240. {t('smartPlugs.restEnergyHint')}
  1241. </p>
  1242. </div>
  1243. {/* Test Connection */}
  1244. {(restOnUrl.trim() || restOffUrl.trim()) && (
  1245. <div className="flex gap-2">
  1246. <Button
  1247. type="button"
  1248. variant="secondary"
  1249. onClick={async () => {
  1250. setTestResult(null);
  1251. try {
  1252. const url = restOnUrl.trim() || restOffUrl.trim();
  1253. const result = await api.testRESTConnection(url, restMethod, restHeaders.trim() || null);
  1254. setTestResult({ success: result.success });
  1255. if (!result.success) {
  1256. setError(result.error || t('smartPlugs.addSmartPlug.connectionFailed'));
  1257. }
  1258. } catch {
  1259. setTestResult({ success: false });
  1260. setError(t('smartPlugs.addSmartPlug.connectionFailed'));
  1261. }
  1262. }}
  1263. className="w-full"
  1264. >
  1265. <Wifi className="w-4 h-4" />
  1266. {t('smartPlugs.testConnection')}
  1267. </Button>
  1268. </div>
  1269. )}
  1270. {testResult && (
  1271. <div className={`flex items-center gap-2 text-sm ${testResult.success ? 'text-green-400' : 'text-red-400'}`}>
  1272. {testResult.success ? <CheckCircle className="w-4 h-4" /> : <WifiOff className="w-4 h-4" />}
  1273. {testResult.success ? t('smartPlugs.connectionSuccess') : t('smartPlugs.addSmartPlug.connectionFailed')}
  1274. </div>
  1275. )}
  1276. </div>
  1277. )}
  1278. {/* IP Address - only show for Tasmota */}
  1279. {plugType === 'tasmota' && (
  1280. <div>
  1281. <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.ipAddress')}</label>
  1282. <div className="flex gap-2">
  1283. <input
  1284. type="text"
  1285. value={ipAddress}
  1286. onChange={(e) => {
  1287. setIpAddress(e.target.value);
  1288. setTestResult(null);
  1289. }}
  1290. placeholder="192.168.1.100"
  1291. 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"
  1292. />
  1293. <Button
  1294. type="button"
  1295. variant="secondary"
  1296. onClick={() => testMutation.mutate()}
  1297. disabled={!ipAddress.trim() || testMutation.isPending}
  1298. >
  1299. {testMutation.isPending ? (
  1300. <Loader2 className="w-4 h-4 animate-spin" />
  1301. ) : (
  1302. <Wifi className="w-4 h-4" />
  1303. )}
  1304. {t('smartPlugs.test')}
  1305. </Button>
  1306. </div>
  1307. </div>
  1308. )}
  1309. {/* Test Result - only show for Tasmota */}
  1310. {plugType === 'tasmota' && testResult && (
  1311. <div className={`p-3 rounded-lg flex items-center gap-2 ${
  1312. testResult.success
  1313. ? 'bg-bambu-green/20 border border-bambu-green/50 text-bambu-green'
  1314. : 'bg-red-500/20 border border-red-500/50 text-red-400'
  1315. }`}>
  1316. {testResult.success ? (
  1317. <>
  1318. <CheckCircle className="w-5 h-5" />
  1319. <div>
  1320. <p className="font-medium">{t('smartPlugs.connectedResult')}</p>
  1321. <p className="text-sm opacity-80">
  1322. {testResult.device_name && t('smartPlugs.deviceLabel', { name: testResult.device_name })}
  1323. {t('smartPlugs.stateLabel', { state: testResult.state })}
  1324. </p>
  1325. </div>
  1326. </>
  1327. ) : (
  1328. <>
  1329. <WifiOff className="w-5 h-5" />
  1330. <span>{t('smartPlugs.addSmartPlug.connectionFailed')}</span>
  1331. </>
  1332. )}
  1333. </div>
  1334. )}
  1335. {/* Name */}
  1336. <div>
  1337. <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.nameLabel')}</label>
  1338. <input
  1339. type="text"
  1340. value={name}
  1341. onChange={(e) => setName(e.target.value)}
  1342. placeholder={t('smartPlugs.addSmartPlug.placeholders.plugName')}
  1343. 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"
  1344. />
  1345. </div>
  1346. {/* Authentication (optional) - only show for Tasmota */}
  1347. {plugType === 'tasmota' && (
  1348. <>
  1349. <div className="grid grid-cols-2 gap-3">
  1350. <div>
  1351. <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.username')}</label>
  1352. <input
  1353. type="text"
  1354. value={username}
  1355. onChange={(e) => setUsername(e.target.value)}
  1356. placeholder="admin"
  1357. 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"
  1358. />
  1359. </div>
  1360. <div>
  1361. <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.password')}</label>
  1362. <input
  1363. type="password"
  1364. value={password}
  1365. onChange={(e) => setPassword(e.target.value)}
  1366. placeholder="********"
  1367. 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"
  1368. />
  1369. </div>
  1370. </div>
  1371. <p className="text-xs text-bambu-gray -mt-2">
  1372. {t('smartPlugs.authHint')}
  1373. </p>
  1374. </>
  1375. )}
  1376. {/* Link to Printer - not shown for MQTT plugs (monitor-only) */}
  1377. {plugType !== 'mqtt' && (
  1378. <div>
  1379. <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.linkToPrinter')}</label>
  1380. <select
  1381. value={printerId ?? ''}
  1382. onChange={(e) => setPrinterId(e.target.value ? Number(e.target.value) : null)}
  1383. 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"
  1384. >
  1385. <option value="">{t('smartPlugs.noPrinter')}</option>
  1386. {availablePrinters?.map((p) => (
  1387. <option key={p.id} value={p.id}>
  1388. {p.name}
  1389. </option>
  1390. ))}
  1391. </select>
  1392. <p className="text-xs text-bambu-gray mt-1">
  1393. {t('smartPlugs.linkingDescription')}
  1394. </p>
  1395. </div>
  1396. )}
  1397. {/* Power Alerts */}
  1398. <div className="border-t border-bambu-dark-tertiary pt-4">
  1399. <div className="flex items-center justify-between mb-3">
  1400. <div className="flex items-center gap-2">
  1401. <Bell className="w-4 h-4 text-bambu-green" />
  1402. <span className="text-white font-medium">{t('smartPlugs.powerAlerts')}</span>
  1403. </div>
  1404. <label className="relative inline-flex items-center cursor-pointer">
  1405. <input
  1406. type="checkbox"
  1407. checked={powerAlertEnabled}
  1408. onChange={(e) => setPowerAlertEnabled(e.target.checked)}
  1409. className="sr-only peer"
  1410. />
  1411. <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>
  1412. </label>
  1413. </div>
  1414. {powerAlertEnabled && (
  1415. <div className="space-y-3">
  1416. <div className="grid grid-cols-2 gap-3">
  1417. <div>
  1418. <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.alertAbove')}</label>
  1419. <input
  1420. type="number"
  1421. value={powerAlertHigh}
  1422. onChange={(e) => setPowerAlertHigh(e.target.value)}
  1423. placeholder="e.g. 200"
  1424. min="0"
  1425. max="5000"
  1426. 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"
  1427. />
  1428. </div>
  1429. <div>
  1430. <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.alertBelow')}</label>
  1431. <input
  1432. type="number"
  1433. value={powerAlertLow}
  1434. onChange={(e) => setPowerAlertLow(e.target.value)}
  1435. placeholder="e.g. 10"
  1436. min="0"
  1437. max="5000"
  1438. 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"
  1439. />
  1440. </div>
  1441. </div>
  1442. <p className="text-xs text-bambu-gray">
  1443. {t('smartPlugs.alertDescription')}
  1444. </p>
  1445. </div>
  1446. )}
  1447. </div>
  1448. {/* Schedule - not shown for MQTT plugs (monitor-only) */}
  1449. {plugType !== 'mqtt' && (
  1450. <div className="border-t border-bambu-dark-tertiary pt-4">
  1451. <div className="flex items-center justify-between mb-3">
  1452. <div className="flex items-center gap-2">
  1453. <Clock className="w-4 h-4 text-bambu-green" />
  1454. <span className="text-white font-medium">{t('smartPlugs.dailySchedule')}</span>
  1455. </div>
  1456. <label className="relative inline-flex items-center cursor-pointer">
  1457. <input
  1458. type="checkbox"
  1459. checked={scheduleEnabled}
  1460. onChange={(e) => setScheduleEnabled(e.target.checked)}
  1461. className="sr-only peer"
  1462. />
  1463. <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>
  1464. </label>
  1465. </div>
  1466. {scheduleEnabled && (
  1467. <div className="space-y-3">
  1468. <div className="grid grid-cols-2 gap-3">
  1469. <div>
  1470. <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.turnOnAt')}</label>
  1471. <input
  1472. type="time"
  1473. value={scheduleOnTime}
  1474. onChange={(e) => setScheduleOnTime(e.target.value)}
  1475. 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"
  1476. />
  1477. </div>
  1478. <div>
  1479. <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.turnOffAt')}</label>
  1480. <input
  1481. type="time"
  1482. value={scheduleOffTime}
  1483. onChange={(e) => setScheduleOffTime(e.target.value)}
  1484. 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"
  1485. />
  1486. </div>
  1487. </div>
  1488. <p className="text-xs text-bambu-gray">
  1489. {t('smartPlugs.scheduleDescription')}
  1490. </p>
  1491. </div>
  1492. )}
  1493. </div>
  1494. )}
  1495. {/* Switchbar Visibility */}
  1496. <div className="border-t border-bambu-dark-tertiary pt-4">
  1497. <div className="flex items-center justify-between">
  1498. <div className="flex items-center gap-2">
  1499. <LayoutGrid className="w-4 h-4 text-bambu-green" />
  1500. <div>
  1501. <span className="text-white font-medium">{t('smartPlugs.showInSwitchbar')}</span>
  1502. <p className="text-xs text-bambu-gray">{t('smartPlugs.quickAccessSidebar')}</p>
  1503. </div>
  1504. </div>
  1505. <label className="relative inline-flex items-center cursor-pointer">
  1506. <input
  1507. type="checkbox"
  1508. checked={showInSwitchbar}
  1509. onChange={(e) => setShowInSwitchbar(e.target.checked)}
  1510. className="sr-only peer"
  1511. />
  1512. <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>
  1513. </label>
  1514. </div>
  1515. </div>
  1516. {/* Printer Card Visibility - only for HA entities */}
  1517. {plugType === 'homeassistant' && (
  1518. <div className="border-t border-bambu-dark-tertiary pt-4">
  1519. <div className="flex items-center justify-between">
  1520. <div className="flex items-center gap-2">
  1521. <Eye className="w-4 h-4 text-bambu-green" />
  1522. <div>
  1523. <span className="text-white font-medium">{t('smartPlugs.showOnPrinterCard')}</span>
  1524. <p className="text-xs text-bambu-gray">{t('smartPlugs.displayOnPrinterCard')}</p>
  1525. </div>
  1526. </div>
  1527. <label className="relative inline-flex items-center cursor-pointer">
  1528. <input
  1529. type="checkbox"
  1530. checked={showOnPrinterCard}
  1531. onChange={(e) => setShowOnPrinterCard(e.target.checked)}
  1532. className="sr-only peer"
  1533. />
  1534. <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>
  1535. </label>
  1536. </div>
  1537. </div>
  1538. )}
  1539. {/* Actions */}
  1540. <div className="flex gap-3 pt-2">
  1541. <Button
  1542. type="button"
  1543. variant="secondary"
  1544. onClick={onClose}
  1545. className="flex-1"
  1546. >
  1547. {t('smartPlugs.cancel')}
  1548. </Button>
  1549. <Button
  1550. type="submit"
  1551. disabled={isPending}
  1552. className="flex-1"
  1553. >
  1554. {isPending ? (
  1555. <Loader2 className="w-4 h-4 animate-spin" />
  1556. ) : (
  1557. <Save className="w-4 h-4" />
  1558. )}
  1559. {isEditing ? t('smartPlugs.save') : t('smartPlugs.add')}
  1560. </Button>
  1561. </div>
  1562. </form>
  1563. </div>
  1564. </div>
  1565. );
  1566. }