Files
bambuddy/backend/app/models/smart_plug.py
T
maziggy 76adf70fd4 Add separate power/energy URLs and multipliers for REST smart plugs (#472)
REST/Webhook smart plugs can now fetch power and energy data from
  individual URLs instead of requiring all values in a single status
  response. Each value falls back to the shared Status URL when no
  separate URL is set, preserving backward compatibility. Added power
  and energy multipliers for unit conversion (e.g. 0.001 for Wh→kWh).
2026-04-03 08:31:08 +02:00

128 lines
7.6 KiB
Python

from datetime import datetime
from sqlalchemy import Boolean, DateTime, Float, ForeignKey, Integer, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.app.core.database import Base
class SmartPlug(Base):
"""Smart plug for printer power control (Tasmota, Home Assistant, MQTT, or REST)."""
__tablename__ = "smart_plugs"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(100))
ip_address: Mapped[str | None] = mapped_column(String(45), nullable=True) # IPv4/IPv6 (required for Tasmota)
# Plug type: "tasmota" (default), "homeassistant", "mqtt", or "rest"
plug_type: Mapped[str] = mapped_column(String(20), default="tasmota")
# Home Assistant entity ID (e.g., "switch.printer_plug")
ha_entity_id: Mapped[str | None] = mapped_column(String(100), nullable=True)
# Home Assistant energy sensor entities (optional, for separate energy sensors)
ha_power_entity: Mapped[str | None] = mapped_column(String(100), nullable=True) # sensor.xxx_power
ha_energy_today_entity: Mapped[str | None] = mapped_column(String(100), nullable=True) # sensor.xxx_today
ha_energy_total_entity: Mapped[str | None] = mapped_column(String(100), nullable=True) # sensor.xxx_total
# MQTT plug fields (required when plug_type="mqtt")
# Legacy field - kept for backward compatibility, now use mqtt_power_topic
mqtt_topic: Mapped[str | None] = mapped_column(
String(200), nullable=True
) # e.g., "zigbee2mqtt/shelly-working-room" (deprecated, use mqtt_power_topic)
# Power monitoring
mqtt_power_topic: Mapped[str | None] = mapped_column(String(200), nullable=True) # Topic for power data
mqtt_power_path: Mapped[str | None] = mapped_column(String(100), nullable=True) # e.g., "power_l1" or "data.power"
mqtt_power_multiplier: Mapped[float] = mapped_column(Float, default=1.0) # Unit conversion for power
# Energy monitoring
mqtt_energy_topic: Mapped[str | None] = mapped_column(String(200), nullable=True) # Topic for energy data
mqtt_energy_path: Mapped[str | None] = mapped_column(String(100), nullable=True) # e.g., "energy_l1"
mqtt_energy_multiplier: Mapped[float] = mapped_column(Float, default=1.0) # Unit conversion for energy
# State monitoring
mqtt_state_topic: Mapped[str | None] = mapped_column(String(200), nullable=True) # Topic for state data
mqtt_state_path: Mapped[str | None] = mapped_column(String(100), nullable=True) # e.g., "state_l1" for ON/OFF
mqtt_state_on_value: Mapped[str | None] = mapped_column(
String(50), nullable=True
) # What value means "ON" (e.g., "ON", "true", "1")
# Legacy multiplier - kept for backward compatibility
mqtt_multiplier: Mapped[float] = mapped_column(Float, default=1.0) # Deprecated, use mqtt_power_multiplier
# REST/Webhook plug fields (required when plug_type="rest")
# Control URLs
rest_on_url: Mapped[str | None] = mapped_column(String(500), nullable=True) # Full URL to turn ON
rest_on_body: Mapped[str | None] = mapped_column(Text, nullable=True) # Request body for ON
rest_off_url: Mapped[str | None] = mapped_column(String(500), nullable=True) # Full URL to turn OFF
rest_off_body: Mapped[str | None] = mapped_column(Text, nullable=True) # Request body for OFF
rest_method: Mapped[str | None] = mapped_column(String(10), nullable=True) # HTTP method: POST, PUT, GET
rest_headers: Mapped[str | None] = mapped_column(Text, nullable=True) # JSON string of custom headers
# Status polling (optional)
rest_status_url: Mapped[str | None] = mapped_column(String(500), nullable=True) # GET endpoint for state
rest_status_path: Mapped[str | None] = mapped_column(String(200), nullable=True) # JSON path to state value
rest_status_on_value: Mapped[str | None] = mapped_column(String(50), nullable=True) # What value means ON
# Energy monitoring (optional — can use separate URLs or extract from status response)
rest_power_url: Mapped[str | None] = mapped_column(String(500), nullable=True) # Separate URL for power data
rest_power_path: Mapped[str | None] = mapped_column(String(200), nullable=True) # JSON path for power (watts)
rest_power_multiplier: Mapped[float] = mapped_column(Float, default=1.0) # Unit conversion for power
rest_energy_url: Mapped[str | None] = mapped_column(String(500), nullable=True) # Separate URL for energy data
rest_energy_path: Mapped[str | None] = mapped_column(String(200), nullable=True) # JSON path for energy (kWh)
rest_energy_multiplier: Mapped[float] = mapped_column(
Float, default=1.0
) # Unit conversion (e.g., 0.001 for Wh→kWh)
# Link to printer (multiple plugs/scripts can be linked to one printer)
printer_id: Mapped[int | None] = mapped_column(ForeignKey("printers.id", ondelete="SET NULL"), nullable=True)
# Automation settings
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")
off_delay_minutes: Mapped[int] = mapped_column(Integer, default=5) # For time mode
off_temp_threshold: Mapped[int] = mapped_column(Integer, default=70) # For temp mode (°C)
# Optional auth (some Tasmota configs require it)
username: Mapped[str | None] = mapped_column(String(50), nullable=True)
password: Mapped[str | None] = mapped_column(String(100), nullable=True)
# Power alerts
power_alert_enabled: Mapped[bool] = mapped_column(Boolean, default=False)
power_alert_high: Mapped[float | None] = mapped_column(Float, nullable=True) # Alert when power > this (watts)
power_alert_low: Mapped[float | None] = mapped_column(Float, nullable=True) # Alert when power < this (watts)
power_alert_last_triggered: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) # Cooldown tracking
# Schedule (time-based on/off)
schedule_enabled: Mapped[bool] = mapped_column(Boolean, default=False)
schedule_on_time: Mapped[str | None] = mapped_column(String(5), nullable=True) # "HH:MM" format
schedule_off_time: Mapped[str | None] = mapped_column(String(5), nullable=True) # "HH:MM" format
# Switchbar visibility
show_in_switchbar: Mapped[bool] = mapped_column(Boolean, default=False)
# Printer card visibility (for scripts)
show_on_printer_card: Mapped[bool] = mapped_column(Boolean, default=True)
# Status tracking
last_state: Mapped[str | None] = mapped_column(String(10), nullable=True) # "ON"/"OFF"
last_checked: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
auto_off_executed: Mapped[bool] = mapped_column(Boolean, default=False) # True when auto-off was triggered
auto_off_pending: Mapped[bool] = mapped_column(Boolean, default=False) # True when waiting for cooldown
auto_off_pending_since: Mapped[datetime | None] = mapped_column(
DateTime, nullable=True
) # When auto-off was scheduled
# Timestamps
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), onupdate=func.now())
# Relationship
printer: Mapped["Printer"] = relationship(back_populates="smart_plugs")
from backend.app.models.printer import Printer # noqa: E402