smart_plug.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. from datetime import datetime
  2. from typing import Literal
  3. from pydantic import BaseModel, Field, model_validator
  4. class SmartPlugBase(BaseModel):
  5. name: str = Field(..., min_length=1, max_length=100)
  6. plug_type: Literal["tasmota", "homeassistant"] = "tasmota"
  7. # Tasmota fields (required when plug_type="tasmota")
  8. ip_address: str | None = Field(default=None, pattern=r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$")
  9. username: str | None = None
  10. password: str | None = None
  11. # Home Assistant fields (required when plug_type="homeassistant")
  12. ha_entity_id: str | None = Field(default=None, pattern=r"^(switch|light|input_boolean|script)\.[a-z0-9_]+$")
  13. # Home Assistant energy sensor entities (optional, for separate energy sensors)
  14. ha_power_entity: str | None = Field(default=None, pattern=r"^sensor\.[a-z0-9_]+$")
  15. ha_energy_today_entity: str | None = Field(default=None, pattern=r"^sensor\.[a-z0-9_]+$")
  16. ha_energy_total_entity: str | None = Field(default=None, pattern=r"^sensor\.[a-z0-9_]+$")
  17. printer_id: int | None = None
  18. enabled: bool = True
  19. auto_on: bool = True
  20. auto_off: bool = True
  21. off_delay_mode: Literal["time", "temperature"] = "time"
  22. off_delay_minutes: int = Field(default=5, ge=0, le=60)
  23. off_temp_threshold: int = Field(default=70, ge=30, le=150)
  24. # Power alerts
  25. power_alert_enabled: bool = False
  26. power_alert_high: float | None = Field(default=None, ge=0, le=5000) # Alert when power > this (watts)
  27. power_alert_low: float | None = Field(default=None, ge=0, le=5000) # Alert when power < this (watts)
  28. # Schedule
  29. schedule_enabled: bool = False
  30. schedule_on_time: str | None = Field(default=None, pattern=r"^([01]\d|2[0-3]):[0-5]\d$") # HH:MM format
  31. schedule_off_time: str | None = Field(default=None, pattern=r"^([01]\d|2[0-3]):[0-5]\d$") # HH:MM format
  32. # Visibility options
  33. show_in_switchbar: bool = False
  34. show_on_printer_card: bool = True # For scripts: show on printer card
  35. @model_validator(mode="after")
  36. def validate_plug_type_fields(self) -> "SmartPlugBase":
  37. if self.plug_type == "tasmota" and not self.ip_address:
  38. raise ValueError("ip_address is required for Tasmota plugs")
  39. if self.plug_type == "homeassistant" and not self.ha_entity_id:
  40. raise ValueError("ha_entity_id is required for Home Assistant plugs")
  41. return self
  42. class SmartPlugCreate(SmartPlugBase):
  43. pass
  44. class SmartPlugUpdate(BaseModel):
  45. name: str | None = None
  46. plug_type: Literal["tasmota", "homeassistant"] | None = None
  47. ip_address: str | None = None
  48. ha_entity_id: str | None = None
  49. # Home Assistant energy sensor entities (optional)
  50. ha_power_entity: str | None = None
  51. ha_energy_today_entity: str | None = None
  52. ha_energy_total_entity: str | None = None
  53. printer_id: int | None = None
  54. enabled: bool | None = None
  55. auto_on: bool | None = None
  56. auto_off: bool | None = None
  57. off_delay_mode: Literal["time", "temperature"] | None = None
  58. off_delay_minutes: int | None = Field(default=None, ge=0, le=60)
  59. off_temp_threshold: int | None = Field(default=None, ge=30, le=150)
  60. username: str | None = None
  61. password: str | None = None
  62. # Power alerts
  63. power_alert_enabled: bool | None = None
  64. power_alert_high: float | None = Field(default=None, ge=0, le=5000)
  65. power_alert_low: float | None = Field(default=None, ge=0, le=5000)
  66. # Schedule
  67. schedule_enabled: bool | None = None
  68. schedule_on_time: str | None = Field(default=None, pattern=r"^([01]\d|2[0-3]):[0-5]\d$")
  69. schedule_off_time: str | None = Field(default=None, pattern=r"^([01]\d|2[0-3]):[0-5]\d$")
  70. # Visibility options
  71. show_in_switchbar: bool | None = None
  72. show_on_printer_card: bool | None = None
  73. class SmartPlugResponse(SmartPlugBase):
  74. id: int
  75. last_state: str | None = None
  76. last_checked: datetime | None = None
  77. auto_off_executed: bool = False # True when auto-off was triggered after print
  78. power_alert_last_triggered: datetime | None = None
  79. created_at: datetime
  80. updated_at: datetime
  81. class Config:
  82. from_attributes = True
  83. class SmartPlugControl(BaseModel):
  84. action: Literal["on", "off", "toggle"]
  85. class SmartPlugEnergy(BaseModel):
  86. """Energy monitoring data from a smart plug."""
  87. power: float | None = None # Current watts
  88. voltage: float | None = None # Volts
  89. current: float | None = None # Amps
  90. today: float | None = None # kWh used today
  91. yesterday: float | None = None # kWh used yesterday
  92. total: float | None = None # Total kWh
  93. factor: float | None = None # Power factor (0-1)
  94. apparent_power: float | None = None # VA
  95. reactive_power: float | None = None # VAr
  96. class SmartPlugStatus(BaseModel):
  97. state: str | None = None # "ON", "OFF", or None if unreachable
  98. reachable: bool = True
  99. device_name: str | None = None
  100. energy: SmartPlugEnergy | None = None # Energy data if available
  101. class SmartPlugTestConnection(BaseModel):
  102. ip_address: str = Field(..., pattern=r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$")
  103. username: str | None = None
  104. password: str | None = None
  105. # Home Assistant schemas
  106. class HATestConnectionRequest(BaseModel):
  107. """Request to test Home Assistant connection."""
  108. url: str = Field(..., min_length=1)
  109. token: str = Field(..., min_length=1)
  110. class HATestConnectionResponse(BaseModel):
  111. """Response from HA connection test."""
  112. success: bool
  113. message: str | None = None
  114. error: str | None = None
  115. class HAEntity(BaseModel):
  116. """A Home Assistant entity that can be used as a smart plug."""
  117. entity_id: str
  118. friendly_name: str
  119. state: str | None = None
  120. domain: str # "switch", "light", "input_boolean", "script"
  121. class HASensorEntity(BaseModel):
  122. """A Home Assistant sensor entity for energy monitoring."""
  123. entity_id: str
  124. friendly_name: str
  125. state: str | None = None
  126. unit_of_measurement: str | None = None # "W", "kW", "kWh", "Wh"