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:
maziggy
2026-01-30 09:46:03 +01:00
parent 48c349bed1
commit 4c2cef64b3
17 changed files with 982 additions and 338 deletions
+3
View File
@@ -376,6 +376,7 @@ async def export_backup(
"schedule_on_time": plug.schedule_on_time,
"schedule_off_time": plug.schedule_off_time,
"show_in_switchbar": plug.show_in_switchbar,
"show_on_printer_card": plug.show_on_printer_card,
}
)
backup["included"].append("smart_plugs")
@@ -1295,6 +1296,7 @@ async def import_backup(
existing.schedule_on_time = plug_data.get("schedule_on_time")
existing.schedule_off_time = plug_data.get("schedule_off_time")
existing.show_in_switchbar = plug_data.get("show_in_switchbar", False)
existing.show_on_printer_card = plug_data.get("show_on_printer_card", True)
restored["smart_plugs"] += 1
else:
skipped["smart_plugs"] += 1
@@ -1324,6 +1326,7 @@ async def import_backup(
schedule_on_time=plug_data.get("schedule_on_time"),
schedule_off_time=plug_data.get("schedule_off_time"),
show_in_switchbar=plug_data.get("show_in_switchbar", False),
show_on_printer_card=plug_data.get("show_on_printer_card", True),
)
db.add(plug)
restored["smart_plugs"] += 1
+115 -14
View File
@@ -56,9 +56,21 @@ async def create_smart_plug(
raise HTTPException(400, "Printer not found")
# Check if printer already has a plug assigned
result = await db.execute(select(SmartPlug).where(SmartPlug.printer_id == data.printer_id))
if result.scalar_one_or_none():
raise HTTPException(400, "This printer already has a smart plug assigned")
# Scripts can coexist with other plugs (they're for multi-device control, not power on/off)
is_script = data.plug_type == "homeassistant" and data.ha_entity_id and data.ha_entity_id.startswith("script.")
if not is_script:
# For non-script plugs, check there's no other non-script plug assigned
result = await db.execute(select(SmartPlug).where(SmartPlug.printer_id == data.printer_id))
existing = result.scalar_one_or_none()
if existing:
# Allow if existing plug is a script
existing_is_script = (
existing.plug_type == "homeassistant"
and existing.ha_entity_id
and existing.ha_entity_id.startswith("script.")
)
if not existing_is_script:
raise HTTPException(400, "This printer already has a smart plug assigned")
plug = SmartPlug(**data.model_dump())
db.add(plug)
@@ -74,12 +86,48 @@ async def create_smart_plug(
@router.get("/by-printer/{printer_id}", response_model=SmartPlugResponse | None)
async def get_smart_plug_by_printer(printer_id: int, db: AsyncSession = Depends(get_db)):
"""Get the smart plug assigned to a printer."""
"""Get the main smart plug assigned to a printer.
When multiple plugs are assigned (e.g., a regular plug + script),
returns the main (non-script) plug for power control.
"""
result = await db.execute(select(SmartPlug).where(SmartPlug.printer_id == printer_id))
plug = result.scalar_one_or_none()
if not plug:
plugs = result.scalars().all()
if not plugs:
return None
return plug
# If multiple plugs, prefer the non-script one (main power plug)
for plug in plugs:
is_script = plug.plug_type == "homeassistant" and plug.ha_entity_id and plug.ha_entity_id.startswith("script.")
if not is_script:
return plug
# All are scripts, return the first one
return plugs[0]
@router.get("/by-printer/{printer_id}/scripts", response_model=list[SmartPlugResponse])
async def get_script_plugs_by_printer(printer_id: int, db: AsyncSession = Depends(get_db)):
"""Get all HA script plugs assigned to a printer.
Returns only script entities (script.*) for the printer that have
show_on_printer_card enabled.
Used to display "Run Script" buttons alongside the main power plug.
"""
result = await db.execute(select(SmartPlug).where(SmartPlug.printer_id == printer_id))
plugs = result.scalars().all()
# Filter to only scripts with show_on_printer_card enabled
scripts = [
plug
for plug in plugs
if plug.plug_type == "homeassistant"
and plug.ha_entity_id
and plug.ha_entity_id.startswith("script.")
and plug.show_on_printer_card
]
return scripts
# Tasmota Discovery Endpoints
@@ -287,14 +335,29 @@ async def update_smart_plug(
raise HTTPException(400, "Printer not found")
# Check if that printer already has a different plug assigned
result = await db.execute(
select(SmartPlug).where(
SmartPlug.printer_id == new_printer_id,
SmartPlug.id != plug_id,
# Scripts can coexist with other plugs
# Determine if the plug being updated is/will be a script
new_entity_id = update_data.get("ha_entity_id", plug.ha_entity_id)
new_plug_type = update_data.get("plug_type", plug.plug_type)
is_script = new_plug_type == "homeassistant" and new_entity_id and new_entity_id.startswith("script.")
if not is_script:
result = await db.execute(
select(SmartPlug).where(
SmartPlug.printer_id == new_printer_id,
SmartPlug.id != plug_id,
)
)
)
if result.scalar_one_or_none():
raise HTTPException(400, "This printer already has a smart plug assigned")
existing = result.scalar_one_or_none()
if existing:
# Allow if existing plug is a script
existing_is_script = (
existing.plug_type == "homeassistant"
and existing.ha_entity_id
and existing.ha_entity_id.startswith("script.")
)
if not existing_is_script:
raise HTTPException(400, "This printer already has a smart plug assigned")
for field, value in update_data.items():
setattr(plug, field, value)
@@ -376,6 +439,13 @@ async def control_smart_plug(
plug.last_checked = datetime.utcnow()
await db.commit()
# Trigger associated scripts if this is a main (non-script) plug
is_main_plug = not (
plug.plug_type == "homeassistant" and plug.ha_entity_id and plug.ha_entity_id.startswith("script.")
)
if is_main_plug and plug.printer_id and expected_state:
await trigger_associated_scripts(plug.printer_id, expected_state, db)
# MQTT relay - publish smart plug state change
if expected_state:
try:
@@ -401,6 +471,37 @@ async def control_smart_plug(
return {"success": True, "action": control.action}
async def trigger_associated_scripts(printer_id: int, plug_state: str, db: AsyncSession):
"""Trigger scripts linked to a printer based on main plug state change.
When the main plug turns ON, triggers scripts with auto_on=True.
When the main plug turns OFF, triggers scripts with auto_off=True.
"""
result = await db.execute(select(SmartPlug).where(SmartPlug.printer_id == printer_id))
plugs = result.scalars().all()
# Find scripts that should be triggered
for plug in plugs:
is_script = plug.plug_type == "homeassistant" and plug.ha_entity_id and plug.ha_entity_id.startswith("script.")
if not is_script:
continue
should_trigger = False
if plug_state == "ON" and plug.auto_on:
should_trigger = True
logger.info(f"Auto-triggering script '{plug.name}' on printer power-on")
elif plug_state == "OFF" and plug.auto_off:
should_trigger = True
logger.info(f"Auto-triggering script '{plug.name}' on printer power-off")
if should_trigger:
try:
service = await _get_service_for_plug(plug, db)
await service.turn_on(plug) # Scripts are triggered by calling turn_on
except Exception as e:
logger.error(f"Failed to trigger script '{plug.name}': {e}")
@router.get("/{plug_id}/status", response_model=SmartPlugStatus)
async def get_plug_status(plug_id: int, db: AsyncSession = Depends(get_db)):
"""Get current plug status from device including energy data."""