Add persistent auto-off option for smart plugs (#826)

Auto-off now has a "Keep Enabled" toggle that keeps it active between
  prints instead of disabling after each use (one-shot). Useful for HA
  accessories like BentoBox filters that should always power off after
  prints. Default behavior (one-shot) is unchanged.
This commit is contained in:
maziggy
2026-03-27 07:50:47 +01:00
parent 94c6696469
commit c6f62f9cd9
18 changed files with 257 additions and 14 deletions
@@ -43,6 +43,7 @@ const createMockPlug = (overrides: Partial<SmartPlug> = {}): SmartPlug => ({
enabled: true,
auto_on: true,
auto_off: true,
auto_off_persistent: false,
off_delay_mode: 'time',
off_delay_minutes: 5,
off_temp_threshold: 70,
@@ -241,6 +242,51 @@ describe('SmartPlugCard', () => {
});
});
describe('persistent auto-off', () => {
it('shows Keep Enabled toggle when auto_off is enabled', async () => {
const user = userEvent.setup();
const plug = createMockPlug({ auto_off: true, auto_off_persistent: false });
render(<SmartPlugCard plug={plug} onEdit={mockOnEdit} />);
await user.click(screen.getByText('Automation Settings'));
await waitFor(() => {
expect(screen.getByText('Keep Enabled')).toBeInTheDocument();
expect(screen.getByText('Stay enabled between prints instead of one-shot')).toBeInTheDocument();
});
});
it('does not show Keep Enabled toggle when auto_off is disabled', async () => {
const user = userEvent.setup();
const plug = createMockPlug({ auto_off: false });
render(<SmartPlugCard plug={plug} onEdit={mockOnEdit} />);
await user.click(screen.getByText('Automation Settings'));
await waitFor(() => {
expect(screen.queryByText('Keep Enabled')).not.toBeInTheDocument();
});
});
it('shows Keep Enabled toggle for HA plugs with auto_off enabled', async () => {
const user = userEvent.setup();
const plug = createMockPlug({
plug_type: 'homeassistant',
ip_address: null,
ha_entity_id: 'switch.bentobox_filter',
auto_off: true,
auto_off_persistent: true,
});
render(<SmartPlugCard plug={plug} onEdit={mockOnEdit} />);
await user.click(screen.getByText('Automation Settings'));
await waitFor(() => {
expect(screen.getByText('Keep Enabled')).toBeInTheDocument();
});
});
});
describe('disabled state', () => {
it('renders plug even when disabled', () => {
const plug = createMockPlug({ enabled: false });
+3
View File
@@ -1074,6 +1074,7 @@ export interface SmartPlug {
enabled: boolean;
auto_on: boolean;
auto_off: boolean;
auto_off_persistent: boolean;
off_delay_mode: 'time' | 'temperature';
off_delay_minutes: number;
off_temp_threshold: number;
@@ -1128,6 +1129,7 @@ export interface SmartPlugCreate {
enabled?: boolean;
auto_on?: boolean;
auto_off?: boolean;
auto_off_persistent?: boolean;
off_delay_mode?: 'time' | 'temperature';
off_delay_minutes?: number;
off_temp_threshold?: number;
@@ -1174,6 +1176,7 @@ export interface SmartPlugUpdate {
enabled?: boolean;
auto_on?: boolean;
auto_off?: boolean;
auto_off_persistent?: boolean;
off_delay_mode?: 'time' | 'temperature';
off_delay_minutes?: number;
off_temp_threshold?: number;
+19
View File
@@ -361,6 +361,25 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
</label>
</div>
{/* Auto Off Persistent */}
{plug.auto_off && (
<div className="flex items-center justify-between pl-4 border-l-2 border-bambu-dark-tertiary">
<div>
<p className="text-sm text-white">{t('smartPlugs.autoOffPersistent')}</p>
<p className="text-xs text-bambu-gray">{t('smartPlugs.autoOffPersistentDescription')}</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={plug.auto_off_persistent}
onChange={(e) => updateMutation.mutate({ auto_off_persistent: 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>
)}
{/* Delay Mode */}
{plug.auto_off && (
<div className="space-y-3 pl-4 border-l-2 border-bambu-dark-tertiary">
+2
View File
@@ -3671,6 +3671,8 @@ export default {
autoOnDescription: 'Einschalten wenn Druck startet',
autoOff: 'Auto Aus',
autoOffDescription: 'Ausschalten wenn Druck abgeschlossen (einmalig)',
autoOffPersistent: 'Aktiviert lassen',
autoOffPersistentDescription: 'Zwischen Drucken aktiviert bleiben statt einmalig',
turnOffDelayMode: 'Ausschaltverzögerungsmodus',
time: 'Zeit',
temp: 'Temp',
+2
View File
@@ -3676,6 +3676,8 @@ export default {
autoOnDescription: 'Turn on when print starts',
autoOff: 'Auto Off',
autoOffDescription: 'Turn off when print completes (one-shot)',
autoOffPersistent: 'Keep Enabled',
autoOffPersistentDescription: 'Stay enabled between prints instead of one-shot',
turnOffDelayMode: 'Turn Off Delay Mode',
time: 'Time',
temp: 'Temp',
+2
View File
@@ -3663,6 +3663,8 @@ export default {
autoOnDescription: 'Allumer au démarrage de l\'impression',
autoOff: 'Auto Off',
autoOffDescription: 'Éteindre à la fin de l\'impression (unique)',
autoOffPersistent: 'Garder activé',
autoOffPersistentDescription: 'Rester activé entre les impressions au lieu d\'une seule fois',
turnOffDelayMode: 'Mode de délai d\'extinction',
time: 'Temps',
temp: 'Temp',
+2
View File
@@ -3662,6 +3662,8 @@ export default {
autoOnDescription: 'Accendi quando inizia la stampa',
autoOff: 'Auto Off',
autoOffDescription: 'Spegni quando la stampa è completata (una tantum)',
autoOffPersistent: 'Mantieni attivo',
autoOffPersistentDescription: 'Resta attivo tra le stampe invece di una tantum',
turnOffDelayMode: 'Modalità ritardo spegnimento',
time: 'Tempo',
temp: 'Temp',
+2
View File
@@ -3675,6 +3675,8 @@ export default {
autoOnDescription: '印刷開始時にオンにする',
autoOff: '自動オフ',
autoOffDescription: '印刷完了時にオフにする(ワンショット)',
autoOffPersistent: '有効のまま維持',
autoOffPersistentDescription: 'ワンショットではなく印刷間で有効のまま維持',
turnOffDelayMode: 'オフ遅延モード',
time: '時間',
temp: '温度',
+2
View File
@@ -3662,6 +3662,8 @@ export default {
autoOnDescription: 'Ligar quando a impressão iniciar',
autoOff: 'Auto Desligar',
autoOffDescription: 'Desligar quando a impressão terminar (única vez)',
autoOffPersistent: 'Manter ativado',
autoOffPersistentDescription: 'Permanecer ativado entre impressões em vez de única vez',
turnOffDelayMode: 'Modo de atraso para desligar',
time: 'Tempo',
temp: 'Temp',
+2
View File
@@ -3662,6 +3662,8 @@ export default {
autoOnDescription: '打印开始时开启',
autoOff: '自动关闭',
autoOffDescription: '打印完成时关闭(一次性)',
autoOffPersistent: '保持启用',
autoOffPersistentDescription: '在打印之间保持启用而非一次性',
turnOffDelayMode: '关闭延迟模式',
time: '时间',
temp: '温度',