| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256 |
- from datetime import datetime
- from typing import Literal
- from pydantic import BaseModel, Field, model_validator
- class SmartPlugBase(BaseModel):
- name: str = Field(..., min_length=1, max_length=100)
- plug_type: Literal["tasmota", "homeassistant", "mqtt", "rest"] = "tasmota"
- # Tasmota fields (required when plug_type="tasmota")
- ip_address: str | None = Field(default=None, pattern=r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$")
- username: str | None = None
- password: str | None = None
- # Home Assistant fields (required when plug_type="homeassistant")
- ha_entity_id: str | None = Field(default=None, pattern=r"^(switch|light|input_boolean|script)\.[a-z0-9_]+$")
- # Home Assistant energy sensor entities (optional, for separate energy sensors)
- ha_power_entity: str | None = Field(default=None, pattern=r"^sensor\.[a-z0-9_]+$")
- ha_energy_today_entity: str | None = Field(default=None, pattern=r"^sensor\.[a-z0-9_]+$")
- ha_energy_total_entity: str | None = Field(default=None, pattern=r"^sensor\.[a-z0-9_]+$")
- # MQTT fields (required when plug_type="mqtt")
- # Legacy field - kept for backward compatibility
- mqtt_topic: str | None = Field(default=None, max_length=200) # Deprecated, use mqtt_power_topic
- # Power monitoring
- mqtt_power_topic: str | None = Field(default=None, max_length=200) # Topic for power data
- mqtt_power_path: str | None = Field(default=None, max_length=100) # e.g., "power_l1" or "data.power"
- mqtt_power_multiplier: float = Field(default=1.0, ge=0.0001, le=10000) # Unit conversion for power
- # Energy monitoring
- mqtt_energy_topic: str | None = Field(default=None, max_length=200) # Topic for energy data
- mqtt_energy_path: str | None = Field(default=None, max_length=100) # e.g., "energy_l1"
- mqtt_energy_multiplier: float = Field(default=1.0, ge=0.0001, le=10000) # Unit conversion for energy
- # State monitoring
- mqtt_state_topic: str | None = Field(default=None, max_length=200) # Topic for state data
- mqtt_state_path: str | None = Field(default=None, max_length=100) # e.g., "state_l1" for ON/OFF
- mqtt_state_on_value: str | None = Field(
- default=None, max_length=50
- ) # What value means "ON" (e.g., "ON", "true", "1")
- # Legacy multiplier - kept for backward compatibility
- mqtt_multiplier: float = Field(default=1.0, ge=0.0001, le=10000) # Deprecated, use mqtt_power_multiplier
- # REST/Webhook fields (required when plug_type="rest")
- rest_on_url: str | None = Field(default=None, max_length=500)
- rest_on_body: str | None = None
- rest_off_url: str | None = Field(default=None, max_length=500)
- rest_off_body: str | None = None
- rest_method: Literal["GET", "POST", "PUT", "PATCH"] | None = None
- rest_headers: str | None = None # JSON string of custom headers
- rest_status_url: str | None = Field(default=None, max_length=500)
- rest_status_path: str | None = Field(default=None, max_length=200)
- rest_status_on_value: str | None = Field(default=None, max_length=50)
- rest_power_path: str | None = Field(default=None, max_length=200)
- rest_energy_path: str | None = Field(default=None, max_length=200)
- printer_id: int | None = None
- 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)
- # Power alerts
- power_alert_enabled: bool = False
- power_alert_high: float | None = Field(default=None, ge=0, le=5000) # Alert when power > this (watts)
- power_alert_low: float | None = Field(default=None, ge=0, le=5000) # Alert when power < this (watts)
- # Schedule
- schedule_enabled: bool = False
- schedule_on_time: str | None = Field(default=None, pattern=r"^([01]\d|2[0-3]):[0-5]\d$") # HH:MM format
- schedule_off_time: str | None = Field(default=None, pattern=r"^([01]\d|2[0-3]):[0-5]\d$") # HH:MM format
- # Visibility options
- show_in_switchbar: bool = False
- show_on_printer_card: bool = True # For scripts: show on printer card
- @model_validator(mode="after")
- def validate_plug_type_fields(self) -> "SmartPlugBase":
- if self.plug_type == "tasmota" and not self.ip_address:
- raise ValueError("ip_address is required for Tasmota plugs")
- if self.plug_type == "homeassistant" and not self.ha_entity_id:
- raise ValueError("ha_entity_id is required for Home Assistant plugs")
- if self.plug_type == "mqtt":
- # Determine the effective power topic (new field takes priority, fall back to legacy)
- power_topic = self.mqtt_power_topic or self.mqtt_topic
- # Path is optional - if not set, raw MQTT payload value will be used
- has_power = bool(power_topic)
- has_energy = bool(self.mqtt_energy_topic)
- has_state = bool(self.mqtt_state_topic)
- # At least one data source must be configured (path is optional)
- if not has_power and not has_energy and not has_state:
- raise ValueError("At least one MQTT topic must be configured for power, energy, or state monitoring")
- if self.plug_type == "rest":
- if not self.rest_on_url and not self.rest_off_url:
- raise ValueError("At least one of ON URL or OFF URL is required for REST plugs")
- return self
- class SmartPlugCreate(SmartPlugBase):
- pass
- class SmartPlugUpdate(BaseModel):
- name: str | None = None
- plug_type: Literal["tasmota", "homeassistant", "mqtt", "rest"] | None = None
- ip_address: str | None = None
- ha_entity_id: str | None = None
- # Home Assistant energy sensor entities (optional)
- ha_power_entity: str | None = None
- ha_energy_today_entity: str | None = None
- ha_energy_total_entity: str | None = None
- # MQTT fields (legacy)
- mqtt_topic: str | None = None
- mqtt_multiplier: float | None = Field(default=None, ge=0.0001, le=10000)
- # MQTT power fields
- mqtt_power_topic: str | None = None
- mqtt_power_path: str | None = None
- mqtt_power_multiplier: float | None = Field(default=None, ge=0.0001, le=10000)
- # MQTT energy fields
- mqtt_energy_topic: str | None = None
- mqtt_energy_path: str | None = None
- mqtt_energy_multiplier: float | None = Field(default=None, ge=0.0001, le=10000)
- # MQTT state fields
- mqtt_state_topic: str | None = None
- mqtt_state_path: str | None = None
- mqtt_state_on_value: str | None = None
- # REST fields
- rest_on_url: str | None = None
- rest_on_body: str | None = None
- rest_off_url: str | None = None
- rest_off_body: str | None = None
- rest_method: Literal["GET", "POST", "PUT", "PATCH"] | None = None
- rest_headers: str | None = None
- rest_status_url: str | None = None
- rest_status_path: str | None = None
- rest_status_on_value: str | None = None
- rest_power_path: str | None = None
- rest_energy_path: str | None = None
- printer_id: int | None = None
- 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)
- username: str | None = None
- password: str | None = None
- # Power alerts
- power_alert_enabled: bool | None = None
- power_alert_high: float | None = Field(default=None, ge=0, le=5000)
- power_alert_low: float | None = Field(default=None, ge=0, le=5000)
- # Schedule
- schedule_enabled: bool | None = None
- schedule_on_time: str | None = Field(default=None, pattern=r"^([01]\d|2[0-3]):[0-5]\d$")
- schedule_off_time: str | None = Field(default=None, pattern=r"^([01]\d|2[0-3]):[0-5]\d$")
- # Visibility options
- show_in_switchbar: bool | None = None
- show_on_printer_card: bool | None = None
- class SmartPlugResponse(SmartPlugBase):
- id: int
- last_state: str | None = None
- last_checked: datetime | None = None
- auto_off_executed: bool = False # True when auto-off was triggered after print
- power_alert_last_triggered: datetime | None = None
- created_at: datetime
- updated_at: datetime
- class Config:
- from_attributes = True
- class SmartPlugControl(BaseModel):
- action: Literal["on", "off", "toggle"]
- class SmartPlugEnergy(BaseModel):
- """Energy monitoring data from a smart plug."""
- power: float | None = None # Current watts
- voltage: float | None = None # Volts
- current: float | None = None # Amps
- today: float | None = None # kWh used today
- yesterday: float | None = None # kWh used yesterday
- total: float | None = None # Total kWh
- factor: float | None = None # Power factor (0-1)
- apparent_power: float | None = None # VA
- reactive_power: float | None = None # VAr
- class SmartPlugStatus(BaseModel):
- state: str | None = None # "ON", "OFF", or None if unreachable
- reachable: bool = True
- device_name: str | None = None
- energy: SmartPlugEnergy | None = None # Energy data if available
- class SmartPlugTestConnection(BaseModel):
- ip_address: str = Field(..., pattern=r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$")
- username: str | None = None
- password: str | None = None
- # Home Assistant schemas
- class HATestConnectionRequest(BaseModel):
- """Request to test Home Assistant connection."""
- url: str = Field(..., min_length=1)
- token: str = Field(..., min_length=1)
- class HATestConnectionResponse(BaseModel):
- """Response from HA connection test."""
- success: bool
- message: str | None = None
- error: str | None = None
- class HAEntity(BaseModel):
- """A Home Assistant entity that can be used as a smart plug."""
- entity_id: str
- friendly_name: str
- state: str | None = None
- domain: str # "switch", "light", "input_boolean", "script"
- class HASensorEntity(BaseModel):
- """A Home Assistant sensor entity for energy monitoring."""
- entity_id: str
- friendly_name: str
- state: str | None = None
- unit_of_measurement: str | None = None # "W", "kW", "kWh", "Wh"
- class RESTTestConnectionRequest(BaseModel):
- """Request to test a REST smart plug connection."""
- url: str = Field(..., min_length=1)
- method: str = Field(default="GET")
- headers: str | None = None # JSON string of custom headers
- class RESTTestConnectionResponse(BaseModel):
- """Response from REST connection test."""
- success: bool
- error: str | None = None
|