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
+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>
)}
</>
)}