feat(smart-plugs): auto-off after AMS drying completes (#1349)

Reporter Kyobinoyo asked for the equivalent of the existing
  print-finish auto-off but triggered when AMS drying ends.

  Two new SmartPlug columns: auto_off_after_drying (default false),
  off_delay_after_drying_minutes (default 10 — AMS chamber is hot
  post-cycle so longer cooldown than the print-finish default of 5).
  SQLite + Postgres migrations both idempotent.

  Trigger lives in BambuMQTTClient — per-AMS _previous_dry_times
  tracks the dry_time > 0 → 0 falling edge and fires a new
  on_drying_complete(ams_id) callback. Plumbed through
  PrinterManager.set_drying_complete_callback to
  SmartPlugManager.on_drying_complete(printer_id, db), which walks
  linked plugs and respects the per-plug toggle. Catches queue,
  ambient and manual drying identically because it observes firmware
  state, not scheduler intent.

  Frontend: single "Auto Off After Drying" toggle + delay input on
  the smart plug card, next to the existing print-finish auto-off
  section.

  Per-AMS plug routing (separate plug for AMS only, per-AMS targeting
  on dual-AMS printers) deferred — Bambuddy's plug model is
  plug→printer, so the trigger fires whenever any AMS on the linked
  printer finishes a cycle.
This commit is contained in:
maziggy
2026-05-17 12:26:42 +02:00
parent b486880477
commit 6f2cec5eb3
22 changed files with 459 additions and 67 deletions
+9
View File
@@ -1447,6 +1447,9 @@ export interface SmartPlug {
off_delay_mode: 'time' | 'temperature';
off_delay_minutes: number;
off_temp_threshold: number;
// #1349: auto-off after AMS drying completes.
auto_off_after_drying: boolean;
off_delay_after_drying_minutes: number;
username: string | null;
password: string | null;
// Power alerts
@@ -1518,6 +1521,9 @@ export interface SmartPlugCreate {
off_delay_mode?: 'time' | 'temperature';
off_delay_minutes?: number;
off_temp_threshold?: number;
// #1349
auto_off_after_drying?: boolean;
off_delay_after_drying_minutes?: number;
username?: string | null;
password?: string | null;
// Power alerts
@@ -1581,6 +1587,9 @@ export interface SmartPlugUpdate {
off_delay_mode?: 'time' | 'temperature';
off_delay_minutes?: number;
off_temp_threshold?: number;
// #1349
auto_off_after_drying?: boolean;
off_delay_after_drying_minutes?: number;
username?: string | null;
password?: string | null;
// Power alerts
+37
View File
@@ -209,6 +209,7 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
</div>
)}
{/* Feature Badges */}
{(plug.power_alert_enabled || plug.schedule_enabled || plug.plug_type === 'mqtt') && (
<div className="flex flex-wrap gap-1.5 mb-3">
@@ -448,6 +449,42 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
)}
</div>
)}
{/* Auto Off After Drying (#1349) — independent of the
print-finish auto-off above. Uses its own delay because
the AMS chamber is hot post-cycle and users often want
more cooldown than the print-finish default. Fires when
any AMS attached to the linked printer finishes a dry
cycle. */}
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-white">{t('smartPlugs.autoOffAfterDrying')}</p>
<p className="text-xs text-bambu-gray">{t('smartPlugs.autoOffAfterDryingDescription')}</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={plug.auto_off_after_drying}
onChange={(e) => updateMutation.mutate({ auto_off_after_drying: 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>
{plug.auto_off_after_drying && (
<div className="pl-4 border-l-2 border-bambu-dark-tertiary">
<label className="block text-xs text-bambu-gray mb-1">{t('smartPlugs.delayAfterDryingMinutes')}</label>
<input
type="number"
min="0"
max="120"
value={plug.off_delay_after_drying_minutes}
onChange={(e) => updateMutation.mutate({ off_delay_after_drying_minutes: parseInt(e.target.value) || 10 })}
className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white text-sm focus:border-bambu-green focus:outline-none"
/>
</div>
)}
</>
)}
+3
View File
@@ -4462,6 +4462,9 @@ export default {
autoOffDescription: 'Ausschalten wenn Druck abgeschlossen (einmalig)',
autoOffPersistent: 'Aktiviert lassen',
autoOffPersistentDescription: 'Zwischen Drucken aktiviert bleiben statt einmalig',
autoOffAfterDrying: 'Automatisch aus nach Trocknung',
autoOffAfterDryingDescription: 'Ausschalten, wenn AMS-Trocknung abgeschlossen ist',
delayAfterDryingMinutes: 'Verzögerung nach Trocknung (Minuten)',
turnOffDelayMode: 'Ausschaltverzögerungsmodus',
time: 'Zeit',
temp: 'Temp.',
+3
View File
@@ -4471,6 +4471,9 @@ export default {
autoOffDescription: 'Turn off when print completes (one-shot)',
autoOffPersistent: 'Keep Enabled',
autoOffPersistentDescription: 'Stay enabled between prints instead of one-shot',
autoOffAfterDrying: 'Auto Off After Drying',
autoOffAfterDryingDescription: 'Turn off when AMS drying completes',
delayAfterDryingMinutes: 'Drying delay (minutes)',
turnOffDelayMode: 'Turn Off Delay Mode',
time: 'Time',
temp: 'Temp',
+3
View File
@@ -4452,6 +4452,9 @@ export default {
autoOffDescription: 'Éteindre à la fin de l\'impression (unique)',
autoOffPersistent: 'Garder activé',
autoOffPersistentDescription: 'Rester activé entre les impressions au lieu d\'une seule fois',
autoOffAfterDrying: 'Arrêt auto après séchage',
autoOffAfterDryingDescription: 'Éteindre à la fin du séchage de l\'AMS',
delayAfterDryingMinutes: 'Délai après séchage (minutes)',
turnOffDelayMode: 'Mode de délai d\'extinction',
time: 'Temps',
temp: 'Temp.',
+3
View File
@@ -4451,6 +4451,9 @@ export default {
autoOffDescription: 'Spegni quando la stampa è completata (una tantum)',
autoOffPersistent: 'Mantieni attivo',
autoOffPersistentDescription: 'Resta attivo tra le stampe invece di una tantum',
autoOffAfterDrying: 'Spegni dopo asciugatura',
autoOffAfterDryingDescription: 'Spegni al termine dell\'asciugatura AMS',
delayAfterDryingMinutes: 'Ritardo dopo asciugatura (minuti)',
turnOffDelayMode: 'Modalità ritardo spegnimento',
time: 'Tempo',
temp: 'Temp.',
+3
View File
@@ -4463,6 +4463,9 @@ export default {
autoOffDescription: '印刷完了時にオフにする(ワンショット)',
autoOffPersistent: '有効のまま維持',
autoOffPersistentDescription: 'ワンショットではなく印刷間で有効のまま維持',
autoOffAfterDrying: '乾燥完了後に自動オフ',
autoOffAfterDryingDescription: 'AMSの乾燥が完了したらオフにする',
delayAfterDryingMinutes: '乾燥後の遅延(分)',
turnOffDelayMode: 'オフ遅延モード',
time: '時間',
temp: '温度',
+3
View File
@@ -4451,6 +4451,9 @@ export default {
autoOffDescription: 'Desligar quando a impressão terminar (única vez)',
autoOffPersistent: 'Manter ativado',
autoOffPersistentDescription: 'Permanecer ativado entre impressões em vez de única vez',
autoOffAfterDrying: 'Desligar Após Secagem',
autoOffAfterDryingDescription: 'Desligar quando a secagem do AMS terminar',
delayAfterDryingMinutes: 'Atraso após secagem (minutos)',
turnOffDelayMode: 'Modo de atraso para desligar',
time: 'Tempo',
temp: 'Temp.',
+3
View File
@@ -4451,6 +4451,9 @@ export default {
autoOffDescription: '打印完成时关闭(一次性)',
autoOffPersistent: '保持启用',
autoOffPersistentDescription: '在打印之间保持启用而非一次性',
autoOffAfterDrying: '干燥完成后自动关闭',
autoOffAfterDryingDescription: 'AMS 干燥完成后关闭',
delayAfterDryingMinutes: '干燥后延迟(分钟)',
turnOffDelayMode: '关闭延迟模式',
time: '时间',
temp: '温度',
+3
View File
@@ -4451,6 +4451,9 @@ export default {
autoOffDescription: '列印完成時關閉(一次性)',
autoOffPersistent: '保持啟用',
autoOffPersistentDescription: '在列印之間保持啟用而非一次性',
autoOffAfterDrying: '乾燥完成後自動關閉',
autoOffAfterDryingDescription: 'AMS 乾燥完成後關閉',
delayAfterDryingMinutes: '乾燥後延遲(分鐘)',
turnOffDelayMode: '關閉延遲模式',
time: '時間',
temp: '溫度',