mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 11:56:01 +02:00
Add script automation and visibility options (#176)
Enhance Home Assistant script support with automation triggers and printer card visibility control. - Script automation: Run scripts automatically when main plug turns on/off - Show/hide scripts on printer cards (configurable per script) - Scripts appear in dedicated row on printer cards with quick-run buttons - Toast notification when triggering scripts from settings/sidebar Closes #176
This commit is contained in:
@@ -842,7 +842,7 @@ export interface SmartPlug {
|
||||
name: string;
|
||||
plug_type: 'tasmota' | 'homeassistant';
|
||||
ip_address: string | null; // Required for Tasmota
|
||||
ha_entity_id: string | null; // Required for Home Assistant (e.g., "switch.printer_plug")
|
||||
ha_entity_id: string | null; // Required for Home Assistant (e.g., "switch.printer_plug", "script.turn_on_printer")
|
||||
// Home Assistant energy sensor entities (optional)
|
||||
ha_power_entity: string | null;
|
||||
ha_energy_today_entity: string | null;
|
||||
@@ -865,8 +865,9 @@ export interface SmartPlug {
|
||||
schedule_enabled: boolean;
|
||||
schedule_on_time: string | null;
|
||||
schedule_off_time: string | null;
|
||||
// Switchbar visibility
|
||||
// Visibility options
|
||||
show_in_switchbar: boolean;
|
||||
show_on_printer_card: boolean; // For scripts: show on printer card
|
||||
// Status
|
||||
last_state: string | null;
|
||||
last_checked: string | null;
|
||||
@@ -901,8 +902,9 @@ export interface SmartPlugCreate {
|
||||
schedule_enabled?: boolean;
|
||||
schedule_on_time?: string | null;
|
||||
schedule_off_time?: string | null;
|
||||
// Switchbar visibility
|
||||
// Visibility options
|
||||
show_in_switchbar?: boolean;
|
||||
show_on_printer_card?: boolean;
|
||||
}
|
||||
|
||||
export interface SmartPlugUpdate {
|
||||
@@ -931,8 +933,9 @@ export interface SmartPlugUpdate {
|
||||
schedule_enabled?: boolean;
|
||||
schedule_on_time?: string | null;
|
||||
schedule_off_time?: string | null;
|
||||
// Switchbar visibility
|
||||
// Visibility options
|
||||
show_in_switchbar?: boolean;
|
||||
show_on_printer_card?: boolean;
|
||||
}
|
||||
|
||||
// Home Assistant entity for smart plug selection
|
||||
@@ -940,7 +943,7 @@ export interface HAEntity {
|
||||
entity_id: string;
|
||||
friendly_name: string;
|
||||
state: string | null;
|
||||
domain: string; // "switch", "light", "input_boolean"
|
||||
domain: string; // "switch", "light", "input_boolean", "script"
|
||||
}
|
||||
|
||||
// Home Assistant sensor entity for energy monitoring
|
||||
@@ -2394,6 +2397,7 @@ export const api = {
|
||||
getSmartPlugs: () => request<SmartPlug[]>('/smart-plugs/'),
|
||||
getSmartPlug: (id: number) => request<SmartPlug>(`/smart-plugs/${id}`),
|
||||
getSmartPlugByPrinter: (printerId: number) => request<SmartPlug | null>(`/smart-plugs/by-printer/${printerId}`),
|
||||
getScriptPlugsByPrinter: (printerId: number) => request<SmartPlug[]>(`/smart-plugs/by-printer/${printerId}/scripts`),
|
||||
createSmartPlug: (data: SmartPlugCreate) =>
|
||||
request<SmartPlug>('/smart-plugs/', {
|
||||
method: 'POST',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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 } from 'lucide-react';
|
||||
import { X, Save, Loader2, Wifi, WifiOff, CheckCircle, Bell, Clock, LayoutGrid, Search, Plug, Power, Home, Play, Eye } from 'lucide-react';
|
||||
import { api } from '../api/client';
|
||||
import type { SmartPlug, SmartPlugCreate, SmartPlugUpdate, DiscoveredTasmotaDevice } from '../api/client';
|
||||
import { Button } from './Button';
|
||||
@@ -51,6 +51,10 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
|
||||
const [testResult, setTestResult] = useState<{ success: boolean; state?: string | null; device_name?: string | null } | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Automation settings
|
||||
const [autoOn, setAutoOn] = useState(plug?.auto_on ?? true);
|
||||
const [autoOff, setAutoOff] = useState(plug?.auto_off ?? true);
|
||||
|
||||
// Power alert settings
|
||||
const [powerAlertEnabled, setPowerAlertEnabled] = useState(plug?.power_alert_enabled || false);
|
||||
const [powerAlertHigh, setPowerAlertHigh] = useState<string>(plug?.power_alert_high?.toString() || '');
|
||||
@@ -61,8 +65,9 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
|
||||
const [scheduleOnTime, setScheduleOnTime] = useState<string>(plug?.schedule_on_time || '');
|
||||
const [scheduleOffTime, setScheduleOffTime] = useState<string>(plug?.schedule_off_time || '');
|
||||
|
||||
// Switchbar visibility
|
||||
// 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);
|
||||
@@ -235,6 +240,10 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
|
||||
mutationFn: (data: SmartPlugCreate) => api.createSmartPlug(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['smart-plugs'] });
|
||||
// Also invalidate script plugs queries for printer cards
|
||||
queryClient.invalidateQueries({ predicate: (query) =>
|
||||
Array.isArray(query.queryKey) && query.queryKey[0] === 'scriptPlugsByPrinter'
|
||||
});
|
||||
onClose();
|
||||
},
|
||||
onError: (err: Error) => {
|
||||
@@ -247,6 +256,10 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
|
||||
mutationFn: (data: SmartPlugUpdate) => api.updateSmartPlug(plug!.id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['smart-plugs'] });
|
||||
// Also invalidate script plugs queries for printer cards
|
||||
queryClient.invalidateQueries({ predicate: (query) =>
|
||||
Array.isArray(query.queryKey) && query.queryKey[0] === 'scriptPlugsByPrinter'
|
||||
});
|
||||
onClose();
|
||||
},
|
||||
onError: (err: Error) => {
|
||||
@@ -254,11 +267,17 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
|
||||
},
|
||||
});
|
||||
|
||||
// Check if selected entity is a script (scripts allow multiple plugs per printer)
|
||||
const isScript = plugType === 'homeassistant' && haEntityId?.startsWith('script.');
|
||||
|
||||
// Filter out printers that already have a plug assigned (except current plug's printer)
|
||||
const availablePrinters = printers?.filter(p => {
|
||||
const hasPlug = existingPlugs?.some(ep => ep.printer_id === p.id && ep.id !== plug?.id);
|
||||
return !hasPlug;
|
||||
});
|
||||
// Scripts can link to any printer (they're for multi-device control)
|
||||
const availablePrinters = isScript
|
||||
? printers
|
||||
: printers?.filter(p => {
|
||||
const hasPlug = existingPlugs?.some(ep => ep.printer_id === p.id && ep.id !== plug?.id);
|
||||
return !hasPlug;
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -291,6 +310,9 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
|
||||
username: plugType === 'tasmota' ? (username.trim() || null) : null,
|
||||
password: plugType === 'tasmota' ? (password.trim() || null) : null,
|
||||
printer_id: printerId,
|
||||
// Automation
|
||||
auto_on: autoOn,
|
||||
auto_off: autoOff,
|
||||
// Power alerts
|
||||
power_alert_enabled: powerAlertEnabled,
|
||||
power_alert_high: powerAlertHigh ? parseFloat(powerAlertHigh) : null,
|
||||
@@ -299,8 +321,9 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
|
||||
schedule_enabled: scheduleEnabled,
|
||||
schedule_on_time: scheduleOnTime || null,
|
||||
schedule_off_time: scheduleOffTime || null,
|
||||
// Switchbar
|
||||
// Visibility options
|
||||
show_in_switchbar: showInSwitchbar,
|
||||
show_on_printer_card: showOnPrinterCard,
|
||||
};
|
||||
|
||||
if (isEditing) {
|
||||
@@ -582,15 +605,15 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
|
||||
<p className="text-xs text-bambu-gray mt-1">
|
||||
{debouncedSearch
|
||||
? `Searching all entities (${availableEntities.length} found)`
|
||||
: `Showing switch, light, input_boolean (${availableEntities.length} available)`}
|
||||
: `Showing switch, light, input_boolean, script (${availableEntities.length} available)`}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
|
||||
{/* Energy Monitoring Section (Optional) */}
|
||||
{haEntityId && haSensorEntities && haSensorEntities.length > 0 && (
|
||||
{/* Energy Monitoring Section (Optional) - hidden for scripts */}
|
||||
{haEntityId && !isScript && 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">Energy Monitoring (Optional)</p>
|
||||
@@ -975,110 +998,183 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs text-bambu-gray mt-1">
|
||||
Linking enables automatic on/off when prints start/complete
|
||||
{isScript
|
||||
? 'Link to a printer to enable auto-run triggers'
|
||||
: 'Linking enables automatic on/off when prints start/complete'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Power Alerts */}
|
||||
<div className="border-t border-bambu-dark-tertiary pt-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Bell className="w-4 h-4 text-bambu-green" />
|
||||
<span className="text-white font-medium">Power Alerts</span>
|
||||
{/* Script Options - only show for HA scripts */}
|
||||
{isScript && (
|
||||
<div className="border-t border-bambu-dark-tertiary pt-4 space-y-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Play className="w-4 h-4 text-bambu-green" />
|
||||
<span className="text-white font-medium">Script Automation</span>
|
||||
</div>
|
||||
|
||||
{/* Auto-run when printer turns on */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span className="text-white">Run when printer turns on</span>
|
||||
<p className="text-xs text-bambu-gray">Execute script when main plug is switched on</p>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoOn}
|
||||
onChange={(e) => setAutoOn(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>
|
||||
|
||||
{/* Auto-run when printer turns off */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span className="text-white">Run when printer turns off</span>
|
||||
<p className="text-xs text-bambu-gray">Execute script when main plug is switched off</p>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoOff}
|
||||
onChange={(e) => setAutoOff(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>
|
||||
<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">Alert if above (W)</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"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Power Alerts - hidden for scripts (no power monitoring) */}
|
||||
{!isScript && (
|
||||
<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">Power Alerts</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">Alert if above (W)</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">Alert if below (W)</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">
|
||||
Get notified when power consumption crosses these thresholds. Leave empty to disable that direction.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Schedule - hidden for scripts */}
|
||||
{!isScript && (
|
||||
<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">Daily Schedule</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">Turn On at</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">Turn Off at</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">
|
||||
Automatically turn the plug on/off at these times daily. Leave empty to skip that action.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Visibility Options */}
|
||||
<div className="border-t border-bambu-dark-tertiary pt-4 space-y-4">
|
||||
{/* Show on printer card - only for scripts */}
|
||||
{isScript && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Eye className="w-4 h-4 text-bambu-green" />
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-1">Alert if below (W)</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"
|
||||
/>
|
||||
<span className="text-white font-medium">Show on Printer Card</span>
|
||||
<p className="text-xs text-bambu-gray">Display script button on printer card</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-bambu-gray">
|
||||
Get notified when power consumption crosses these thresholds. Leave empty to disable that direction.
|
||||
</p>
|
||||
<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>
|
||||
|
||||
{/* Schedule */}
|
||||
<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">Daily Schedule</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">Turn On at</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">Turn Off at</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">
|
||||
Automatically turn the plug on/off at these times daily. Leave empty to skip that action.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Switchbar Visibility */}
|
||||
<div className="border-t border-bambu-dark-tertiary pt-4">
|
||||
{/* Show in Switchbar */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<LayoutGrid className="w-4 h-4 text-bambu-green" />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
|
||||
import { Plug, Power, PowerOff, Loader2, Trash2, Settings2, Thermometer, Clock, Wifi, WifiOff, Edit2, Bell, Calendar, LayoutGrid, ExternalLink, Home } from 'lucide-react';
|
||||
import { Plug, Power, PowerOff, Loader2, Trash2, Settings2, Thermometer, Clock, Wifi, WifiOff, Edit2, Bell, Calendar, LayoutGrid, ExternalLink, Home, Play, Eye } from 'lucide-react';
|
||||
import { api } from '../api/client';
|
||||
import type { SmartPlug, SmartPlugUpdate } from '../api/client';
|
||||
import { Card, CardContent } from './Card';
|
||||
@@ -55,12 +55,24 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
|
||||
|
||||
return { previousStatus };
|
||||
},
|
||||
onSuccess: (_data, action) => {
|
||||
// Show toast for script triggers
|
||||
const isScriptPlug = plug.plug_type === 'homeassistant' && plug.ha_entity_id?.startsWith('script.');
|
||||
if (isScriptPlug && action === 'on') {
|
||||
showToast(`Script "${plug.name}" triggered`, 'success');
|
||||
}
|
||||
},
|
||||
onError: (_err, action, context) => {
|
||||
// Rollback on error
|
||||
if (context?.previousStatus) {
|
||||
queryClient.setQueryData(['smart-plug-status', plug.id], context.previousStatus);
|
||||
}
|
||||
showToast(`Failed to turn ${action} "${plug.name}"`, 'error');
|
||||
const isScriptPlug = plug.plug_type === 'homeassistant' && plug.ha_entity_id?.startsWith('script.');
|
||||
if (isScriptPlug) {
|
||||
showToast(`Failed to trigger script "${plug.name}"`, 'error');
|
||||
} else {
|
||||
showToast(`Failed to turn ${action} "${plug.name}"`, 'error');
|
||||
}
|
||||
},
|
||||
onSettled: () => {
|
||||
// Refetch after a short delay to get actual state
|
||||
@@ -80,6 +92,10 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
|
||||
if (plug.printer_id) {
|
||||
queryClient.invalidateQueries({ queryKey: ['smartPlugByPrinter', plug.printer_id] });
|
||||
}
|
||||
// Invalidate script plugs queries for printer cards
|
||||
queryClient.invalidateQueries({ predicate: (query) =>
|
||||
Array.isArray(query.queryKey) && query.queryKey[0] === 'scriptPlugsByPrinter'
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -88,6 +104,10 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
|
||||
mutationFn: () => api.deleteSmartPlug(plug.id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['smart-plugs'] });
|
||||
// Also invalidate script plugs queries for printer cards
|
||||
queryClient.invalidateQueries({ predicate: (query) =>
|
||||
Array.isArray(query.queryKey) && query.queryKey[0] === 'scriptPlugsByPrinter'
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -95,6 +115,9 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
|
||||
const isReachable = status?.reachable ?? false;
|
||||
const isPending = controlMutation.isPending;
|
||||
|
||||
// Check if this is a HA script entity (scripts can only be triggered, not toggled)
|
||||
const isScript = plug.plug_type === 'homeassistant' && plug.ha_entity_id?.startsWith('script.');
|
||||
|
||||
// Generate admin URL with auto-login credentials (Tasmota only)
|
||||
const getAdminUrl = () => {
|
||||
if (plug.plug_type !== 'tasmota' || !plug.ip_address) return null;
|
||||
@@ -113,31 +136,46 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
|
||||
<Card className="relative">
|
||||
<CardContent className="p-4">
|
||||
{/* Header Row */}
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-2 rounded-lg ${isReachable ? (isOn ? 'bg-bambu-green/20' : 'bg-bambu-dark') : 'bg-red-500/20'}`}>
|
||||
{plug.plug_type === 'homeassistant' ? (
|
||||
<div className="flex items-start justify-between gap-2 mb-3">
|
||||
<div className="flex items-center gap-3 min-w-0 flex-1">
|
||||
<div className={`p-2 rounded-lg flex-shrink-0 ${isReachable ? ((isOn || isScript) ? 'bg-bambu-green/20' : 'bg-bambu-dark') : 'bg-red-500/20'}`}>
|
||||
{isScript ? (
|
||||
<Play className={`w-5 h-5 ${isReachable ? 'text-bambu-green' : 'text-red-400'}`} />
|
||||
) : plug.plug_type === 'homeassistant' ? (
|
||||
<Home className={`w-5 h-5 ${isReachable ? (isOn ? 'text-bambu-green' : 'text-bambu-gray') : 'text-red-400'}`} />
|
||||
) : (
|
||||
<Plug className={`w-5 h-5 ${isReachable ? (isOn ? 'text-bambu-green' : 'text-bambu-gray') : 'text-red-400'}`} />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-white">{plug.name}</h3>
|
||||
<p className="text-sm text-bambu-gray">
|
||||
<div className="min-w-0">
|
||||
<h3 className="font-medium text-white truncate">{plug.name}</h3>
|
||||
<p className="text-sm text-bambu-gray truncate" title={plug.plug_type === 'homeassistant' ? plug.ha_entity_id ?? undefined : plug.ip_address ?? undefined}>
|
||||
{plug.plug_type === 'homeassistant' ? plug.ha_entity_id : plug.ip_address}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status indicator */}
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
<div className="flex flex-col items-end gap-1 flex-shrink-0">
|
||||
{statusLoading ? (
|
||||
<Loader2 className="w-4 h-4 text-bambu-gray animate-spin" />
|
||||
) : isScript ? (
|
||||
/* Script entities: show badge and Ready status stacked */
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
<span className="flex items-center gap-1 px-2 py-0.5 bg-blue-500/20 text-blue-400 text-xs rounded-full">
|
||||
<Play className="w-3 h-3" />
|
||||
Script
|
||||
</span>
|
||||
<span className={`text-sm ${isReachable ? 'text-status-ok' : 'text-status-error'}`}>
|
||||
{isReachable ? 'Ready' : 'Offline'}
|
||||
</span>
|
||||
</div>
|
||||
) : isReachable ? (
|
||||
<div className="flex items-center gap-1 text-sm">
|
||||
<Wifi className="w-4 h-4 text-status-ok" />
|
||||
<span className={isOn ? 'text-status-ok' : 'text-bambu-gray'}>{status?.state || 'Unknown'}</span>
|
||||
<span className={isOn ? 'text-status-ok' : 'text-bambu-gray'}>
|
||||
{status?.state || 'Unknown'}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1 text-sm text-status-error">
|
||||
@@ -193,26 +231,43 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
|
||||
|
||||
{/* Quick Controls */}
|
||||
<div className="flex gap-2 mb-3">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={isOn ? 'primary' : 'secondary'}
|
||||
disabled={!isReachable || isPending}
|
||||
onClick={() => setShowPowerOnConfirm(true)}
|
||||
className="flex-1"
|
||||
>
|
||||
{isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <Power className="w-4 h-4" />}
|
||||
On
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={!isOn ? 'primary' : 'secondary'}
|
||||
disabled={!isReachable || isPending}
|
||||
onClick={() => setShowPowerOffConfirm(true)}
|
||||
className="flex-1"
|
||||
>
|
||||
{isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <PowerOff className="w-4 h-4" />}
|
||||
Off
|
||||
</Button>
|
||||
{isScript ? (
|
||||
/* Script entities: single "Run" button */
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
disabled={!isReachable || isPending}
|
||||
onClick={() => setShowPowerOnConfirm(true)}
|
||||
className="flex-1"
|
||||
>
|
||||
{isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <Play className="w-4 h-4" />}
|
||||
Run Script
|
||||
</Button>
|
||||
) : (
|
||||
/* Regular entities: On/Off buttons */
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={isOn ? 'primary' : 'secondary'}
|
||||
disabled={!isReachable || isPending}
|
||||
onClick={() => setShowPowerOnConfirm(true)}
|
||||
className="flex-1"
|
||||
>
|
||||
{isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <Power className="w-4 h-4" />}
|
||||
On
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={!isOn ? 'primary' : 'secondary'}
|
||||
disabled={!isReachable || isPending}
|
||||
onClick={() => setShowPowerOffConfirm(true)}
|
||||
className="flex-1"
|
||||
>
|
||||
{isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <PowerOff className="w-4 h-4" />}
|
||||
Off
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Toggle Settings Panel */}
|
||||
@@ -230,6 +285,28 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
|
||||
{/* Expanded Settings */}
|
||||
{isExpanded && (
|
||||
<div className="pt-3 border-t border-bambu-dark-tertiary space-y-4">
|
||||
{/* Show on Printer Card Toggle - only for scripts */}
|
||||
{isScript && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Eye className="w-4 h-4 text-bambu-green" />
|
||||
<div>
|
||||
<p className="text-sm text-white">Show on Printer Card</p>
|
||||
<p className="text-xs text-bambu-gray">Display script button on printer card</p>
|
||||
</div>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={plug.show_on_printer_card}
|
||||
onChange={(e) => updateMutation.mutate({ show_on_printer_card: e.target.checked })}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-9 h-5 bg-bambu-dark-tertiary peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-bambu-green"></div>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show in Switchbar Toggle */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -267,11 +344,13 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Auto On */}
|
||||
{/* Auto On / Run when printer turns on */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-white">Auto On</p>
|
||||
<p className="text-xs text-bambu-gray">Turn on when print starts</p>
|
||||
<p className="text-sm text-white">{isScript ? 'Run when printer turns on' : 'Auto On'}</p>
|
||||
<p className="text-xs text-bambu-gray">
|
||||
{isScript ? 'Execute script when main plug is switched on' : 'Turn on when print starts'}
|
||||
</p>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
@@ -284,11 +363,13 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Auto Off */}
|
||||
{/* Auto Off / Run when printer turns off */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-white">Auto Off</p>
|
||||
<p className="text-xs text-bambu-gray">Turn off when print completes (one-shot)</p>
|
||||
<p className="text-sm text-white">{isScript ? 'Run when printer turns off' : 'Auto Off'}</p>
|
||||
<p className="text-xs text-bambu-gray">
|
||||
{isScript ? 'Execute script when main plug is switched off' : 'Turn off when print completes (one-shot)'}
|
||||
</p>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
@@ -301,8 +382,8 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Delay Mode */}
|
||||
{plug.auto_off && (
|
||||
{/* Delay Mode - hidden for script entities */}
|
||||
{plug.auto_off && !isScript && (
|
||||
<div className="space-y-3 pl-4 border-l-2 border-bambu-dark-tertiary">
|
||||
<div>
|
||||
<p className="text-sm text-white mb-2">Turn Off Delay Mode</p>
|
||||
@@ -401,12 +482,14 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Power On Confirmation */}
|
||||
{/* Power On / Run Script Confirmation */}
|
||||
{showPowerOnConfirm && (
|
||||
<ConfirmModal
|
||||
title="Turn On Smart Plug"
|
||||
message={`Are you sure you want to turn on "${plug.name}"?`}
|
||||
confirmText="Turn On"
|
||||
title={isScript ? "Run Script" : "Turn On Smart Plug"}
|
||||
message={isScript
|
||||
? `Are you sure you want to run the script "${plug.name}"?`
|
||||
: `Are you sure you want to turn on "${plug.name}"?`}
|
||||
confirmText={isScript ? "Run" : "Turn On"}
|
||||
variant="default"
|
||||
onConfirm={() => {
|
||||
controlMutation.mutate('on');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Plug, Power, PowerOff, Loader2, Wifi, WifiOff, Zap } from 'lucide-react';
|
||||
import { Plug, Power, PowerOff, Loader2, Wifi, WifiOff, Zap, Play } from 'lucide-react';
|
||||
import { api } from '../api/client';
|
||||
import type { SmartPlug } from '../api/client';
|
||||
import { ConfirmModal } from './ConfirmModal';
|
||||
@@ -32,6 +32,9 @@ function SwitchItem({ plug }: { plug: SmartPlug }) {
|
||||
const isReachable = status?.reachable ?? false;
|
||||
const isPending = controlMutation.isPending;
|
||||
|
||||
// Check if this is a HA script entity
|
||||
const isScript = plug.plug_type === 'homeassistant' && plug.ha_entity_id?.startsWith('script.');
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (confirmAction) {
|
||||
controlMutation.mutate(confirmAction);
|
||||
@@ -43,18 +46,33 @@ function SwitchItem({ plug }: { plug: SmartPlug }) {
|
||||
<>
|
||||
<div className="flex items-center justify-between py-2 px-3 hover:bg-bambu-dark-tertiary rounded-lg transition-colors">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`p-1.5 rounded ${isReachable ? (isOn ? 'bg-bambu-green/20' : 'bg-bambu-dark') : 'bg-red-500/20'}`}>
|
||||
<Plug className={`w-4 h-4 ${isReachable ? (isOn ? 'text-bambu-green' : 'text-bambu-gray') : 'text-red-400'}`} />
|
||||
<div className={`p-1.5 rounded ${isReachable ? ((isOn || isScript) ? 'bg-bambu-green/20' : 'bg-bambu-dark') : 'bg-red-500/20'}`}>
|
||||
{isScript ? (
|
||||
<Play className={`w-4 h-4 ${isReachable ? 'text-bambu-green' : 'text-red-400'}`} />
|
||||
) : (
|
||||
<Plug className={`w-4 h-4 ${isReachable ? (isOn ? 'text-bambu-green' : 'text-bambu-gray') : 'text-red-400'}`} />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-white font-medium">{plug.name}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-sm text-white font-medium">{plug.name}</p>
|
||||
{isScript && (
|
||||
<span className="px-1.5 py-0.5 bg-blue-500/20 text-blue-400 text-[10px] rounded-full">Script</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
{statusLoading ? (
|
||||
<Loader2 className="w-3 h-3 text-bambu-gray animate-spin" />
|
||||
) : isScript ? (
|
||||
<span className={isReachable ? 'text-status-ok' : 'text-status-error'}>
|
||||
{isReachable ? 'Ready' : 'Offline'}
|
||||
</span>
|
||||
) : isReachable ? (
|
||||
<>
|
||||
<Wifi className="w-3 h-3 text-status-ok" />
|
||||
<span className={isOn ? 'text-status-ok' : 'text-bambu-gray'}>{status?.state || 'Unknown'}</span>
|
||||
<span className={isOn ? 'text-status-ok' : 'text-bambu-gray'}>
|
||||
{status?.state || 'Unknown'}
|
||||
</span>
|
||||
{status?.energy?.power !== null && status?.energy?.power !== undefined && (
|
||||
<>
|
||||
<span className="text-bambu-gray mx-1">|</span>
|
||||
@@ -74,38 +92,57 @@ function SwitchItem({ plug }: { plug: SmartPlug }) {
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
onClick={() => setConfirmAction('on')}
|
||||
disabled={!isReachable || isPending}
|
||||
className={`p-1.5 rounded transition-colors ${
|
||||
isOn
|
||||
? 'bg-bambu-green text-white'
|
||||
: 'bg-bambu-dark text-bambu-gray hover:text-white'
|
||||
} disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||
title="Turn On"
|
||||
>
|
||||
{isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <Power className="w-4 h-4" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setConfirmAction('off')}
|
||||
disabled={!isReachable || isPending}
|
||||
className={`p-1.5 rounded transition-colors ${
|
||||
!isOn && isReachable
|
||||
? 'bg-bambu-dark-tertiary text-white'
|
||||
: 'bg-bambu-dark text-bambu-gray hover:text-white'
|
||||
} disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||
title="Turn Off"
|
||||
>
|
||||
{isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <PowerOff className="w-4 h-4" />}
|
||||
</button>
|
||||
{isScript ? (
|
||||
/* Script: single Run button */
|
||||
<button
|
||||
onClick={() => setConfirmAction('on')}
|
||||
disabled={!isReachable || isPending}
|
||||
className="p-1.5 rounded transition-colors bg-bambu-green text-white disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title="Run Script"
|
||||
>
|
||||
{isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <Play className="w-4 h-4" />}
|
||||
</button>
|
||||
) : (
|
||||
/* Regular: On/Off buttons */
|
||||
<>
|
||||
<button
|
||||
onClick={() => setConfirmAction('on')}
|
||||
disabled={!isReachable || isPending}
|
||||
className={`p-1.5 rounded transition-colors ${
|
||||
isOn
|
||||
? 'bg-bambu-green text-white'
|
||||
: 'bg-bambu-dark text-bambu-gray hover:text-white'
|
||||
} disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||
title="Turn On"
|
||||
>
|
||||
{isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <Power className="w-4 h-4" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setConfirmAction('off')}
|
||||
disabled={!isReachable || isPending}
|
||||
className={`p-1.5 rounded transition-colors ${
|
||||
!isOn && isReachable
|
||||
? 'bg-bambu-dark-tertiary text-white'
|
||||
: 'bg-bambu-dark text-bambu-gray hover:text-white'
|
||||
} disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||
title="Turn Off"
|
||||
>
|
||||
{isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <PowerOff className="w-4 h-4" />}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{confirmAction && (
|
||||
<ConfirmModal
|
||||
title={`Turn ${confirmAction === 'on' ? 'On' : 'Off'} Smart Plug`}
|
||||
message={`Are you sure you want to turn ${confirmAction === 'on' ? 'on' : 'off'} "${plug.name}"?`}
|
||||
confirmText={confirmAction === 'on' ? 'Turn On' : 'Turn Off'}
|
||||
title={isScript && confirmAction === 'on'
|
||||
? 'Run Script'
|
||||
: `Turn ${confirmAction === 'on' ? 'On' : 'Off'} Smart Plug`}
|
||||
message={isScript && confirmAction === 'on'
|
||||
? `Are you sure you want to run the script "${plug.name}"?`
|
||||
: `Are you sure you want to turn ${confirmAction === 'on' ? 'on' : 'off'} "${plug.name}"?`}
|
||||
confirmText={isScript && confirmAction === 'on' ? 'Run' : (confirmAction === 'on' ? 'Turn On' : 'Turn Off')}
|
||||
variant={confirmAction === 'off' ? 'warning' : 'default'}
|
||||
onConfirm={handleConfirm}
|
||||
onCancel={() => setConfirmAction(null)}
|
||||
|
||||
@@ -1093,6 +1093,12 @@ function PrinterCard({
|
||||
queryFn: () => api.getSmartPlugByPrinter(printer.id),
|
||||
});
|
||||
|
||||
// Fetch script plugs for this printer (for multi-device control)
|
||||
const { data: scriptPlugs } = useQuery({
|
||||
queryKey: ['scriptPlugsByPrinter', printer.id],
|
||||
queryFn: () => api.getScriptPlugsByPrinter(printer.id),
|
||||
});
|
||||
|
||||
// Fetch smart plug status if plug exists (faster refresh for energy monitoring)
|
||||
const { data: plugStatus } = useQuery({
|
||||
queryKey: ['smartPlugStatus', smartPlug?.id],
|
||||
@@ -1155,6 +1161,15 @@ function PrinterCard({
|
||||
},
|
||||
});
|
||||
|
||||
// Run script mutation
|
||||
const runScriptMutation = useMutation({
|
||||
mutationFn: (scriptId: number) => api.controlSmartPlug(scriptId, 'on'),
|
||||
onSuccess: () => {
|
||||
showToast('Script triggered');
|
||||
},
|
||||
onError: (error: Error) => showToast(error.message || 'Failed to run script', 'error'),
|
||||
});
|
||||
|
||||
// Print control mutations
|
||||
const stopPrintMutation = useMutation({
|
||||
mutationFn: () => api.stopPrint(printer.id),
|
||||
@@ -2683,6 +2698,28 @@ function PrinterCard({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Script buttons row */}
|
||||
{scriptPlugs && scriptPlugs.length > 0 && (
|
||||
<div className="flex items-center gap-2 mt-2 pt-2 border-t border-bambu-dark-tertiary/50">
|
||||
<Play className="w-3.5 h-3.5 text-blue-400 flex-shrink-0" />
|
||||
<span className="text-xs text-bambu-gray">Scripts:</span>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{scriptPlugs.map(script => (
|
||||
<button
|
||||
key={script.id}
|
||||
onClick={() => runScriptMutation.mutate(script.id)}
|
||||
disabled={runScriptMutation.isPending}
|
||||
title={`Run ${script.ha_entity_id}`}
|
||||
className="px-2 py-0.5 text-xs bg-blue-500/20 text-blue-400 hover:bg-blue-500/30 rounded transition-colors flex items-center gap-1"
|
||||
>
|
||||
<Play className="w-2.5 h-2.5" />
|
||||
{script.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1643,7 +1643,7 @@ export function SettingsPage() {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-sm text-bambu-gray">
|
||||
Connect to Home Assistant to control smart plugs via HA's REST API. Supports switch, light, and input_boolean entities.
|
||||
Connect to Home Assistant to control smart plugs via HA's REST API. Supports switch, light, input_boolean, and script entities.
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
Reference in New Issue
Block a user