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
+12 -2
View File
@@ -346,6 +346,12 @@ async def run_migrations(conn):
except OperationalError:
pass # Already applied
# Migration: Add auto_off_persistent column to smart_plugs (keep auto-off enabled between prints)
try:
await conn.execute(text("ALTER TABLE smart_plugs ADD COLUMN auto_off_persistent BOOLEAN DEFAULT 0"))
except OperationalError:
pass # Already applied
# Migration: Add AMS alarm notification columns to notification_providers
try:
await conn.execute(text("ALTER TABLE notification_providers ADD COLUMN on_ams_humidity_high BOOLEAN DEFAULT 0"))
@@ -597,6 +603,7 @@ async def run_migrations(conn):
enabled BOOLEAN NOT NULL DEFAULT 1,
auto_on BOOLEAN NOT NULL DEFAULT 1,
auto_off BOOLEAN NOT NULL DEFAULT 1,
auto_off_persistent BOOLEAN NOT NULL DEFAULT 0,
off_delay_mode VARCHAR(20) NOT NULL DEFAULT 'time',
off_delay_minutes INTEGER NOT NULL DEFAULT 5,
off_temp_threshold INTEGER NOT NULL DEFAULT 70,
@@ -625,7 +632,8 @@ async def run_migrations(conn):
INSERT INTO smart_plugs_new
SELECT id, name, ip_address,
COALESCE(plug_type, 'tasmota'), ha_entity_id, printer_id,
enabled, auto_on, auto_off, off_delay_mode, off_delay_minutes, off_temp_threshold,
enabled, auto_on, auto_off, COALESCE(auto_off_persistent, 0),
off_delay_mode, off_delay_minutes, off_temp_threshold,
username, password, power_alert_enabled, power_alert_high, power_alert_low,
power_alert_last_triggered, schedule_enabled, schedule_on_time, schedule_off_time,
COALESCE(show_in_switchbar, 0), last_state, last_checked, auto_off_executed,
@@ -894,6 +902,7 @@ async def run_migrations(conn):
enabled BOOLEAN NOT NULL DEFAULT 1,
auto_on BOOLEAN NOT NULL DEFAULT 1,
auto_off BOOLEAN NOT NULL DEFAULT 1,
auto_off_persistent BOOLEAN NOT NULL DEFAULT 0,
off_delay_mode VARCHAR(20) NOT NULL DEFAULT 'time',
off_delay_minutes INTEGER NOT NULL DEFAULT 5,
off_temp_threshold INTEGER NOT NULL DEFAULT 70,
@@ -923,7 +932,8 @@ async def run_migrations(conn):
INSERT INTO smart_plugs_temp
SELECT id, name, ip_address, plug_type, ha_entity_id, ha_power_entity,
ha_energy_today_entity, ha_energy_total_entity, printer_id, enabled,
auto_on, auto_off, off_delay_mode, off_delay_minutes, off_temp_threshold,
auto_on, auto_off, COALESCE(auto_off_persistent, 0),
off_delay_mode, off_delay_minutes, off_temp_threshold,
username, password, power_alert_enabled, power_alert_high, power_alert_low,
power_alert_last_triggered, schedule_enabled, schedule_on_time, schedule_off_time,
show_in_switchbar, last_state, last_checked, auto_off_executed,
+1
View File
@@ -57,6 +57,7 @@ class SmartPlug(Base):
enabled: Mapped[bool] = mapped_column(Boolean, default=True)
auto_on: Mapped[bool] = mapped_column(Boolean, default=True) # Turn on at print start
auto_off: Mapped[bool] = mapped_column(Boolean, default=True) # Turn off at print complete/fail
auto_off_persistent: Mapped[bool] = mapped_column(Boolean, default=False) # Keep auto-off enabled between prints
# Turn-off delay mode: "time" or "temperature"
off_delay_mode: Mapped[str] = mapped_column(String(20), default="time")
+2
View File
@@ -48,6 +48,7 @@ class SmartPlugBase(BaseModel):
enabled: bool = True
auto_on: bool = True
auto_off: bool = True
auto_off_persistent: bool = False
off_delay_mode: Literal["time", "temperature"] = "time"
off_delay_minutes: int = Field(default=5, ge=0, le=60)
off_temp_threshold: int = Field(default=70, ge=30, le=150)
@@ -115,6 +116,7 @@ class SmartPlugUpdate(BaseModel):
enabled: bool | None = None
auto_on: bool | None = None
auto_off: bool | None = None
auto_off_persistent: bool | None = None
off_delay_mode: Literal["time", "temperature"] | None = None
off_delay_minutes: int | None = Field(default=None, ge=0, le=60)
off_temp_threshold: int | None = Field(default=None, ge=30, le=150)
+7 -3
View File
@@ -416,7 +416,7 @@ class SmartPlugManager:
logger.warning("Failed to update plug %s pending state: %s", plug_id, e)
async def _mark_auto_off_executed(self, plug_id: int):
"""Disable auto-off after it was executed (one-shot behavior)."""
"""Disable auto-off after it was executed (one-shot behavior unless persistent)."""
try:
from backend.app.core.database import async_session
from backend.app.models.smart_plug import SmartPlug
@@ -425,14 +425,18 @@ class SmartPlugManager:
result = await db.execute(select(SmartPlug).where(SmartPlug.id == plug_id))
plug = result.scalar_one_or_none()
if plug:
plug.auto_off = False # Disable auto-off (one-shot behavior)
if not plug.auto_off_persistent:
plug.auto_off = False # Disable auto-off (one-shot behavior)
plug.auto_off_executed = False # Reset the flag
plug.auto_off_pending = False # Clear pending state
plug.auto_off_pending_since = None
plug.last_state = "OFF"
plug.last_checked = datetime.now(timezone.utc)
await db.commit()
logger.info("Auto-off executed and disabled for plug %s", plug_id)
if plug.auto_off_persistent:
logger.info("Auto-off executed for plug %s (persistent, stays enabled)", plug_id)
else:
logger.info("Auto-off executed and disabled for plug %s", plug_id)
except Exception as e:
logger.warning("Failed to update plug %s after auto-off: %s", plug_id, e)