mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-04 05:01:37 +02:00
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).
265 lines
11 KiB
Python
265 lines
11 KiB
Python
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_url: str | None = Field(default=None, max_length=500)
|
|
rest_power_path: str | None = Field(default=None, max_length=200)
|
|
rest_power_multiplier: float = Field(default=1.0, ge=0.0001, le=10000)
|
|
rest_energy_url: str | None = Field(default=None, max_length=500)
|
|
rest_energy_path: str | None = Field(default=None, max_length=200)
|
|
rest_energy_multiplier: float = Field(default=1.0, ge=0.0001, le=10000)
|
|
|
|
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_url: str | None = None
|
|
rest_power_path: str | None = None
|
|
rest_power_multiplier: float | None = Field(default=None, ge=0.0001, le=10000)
|
|
rest_energy_url: str | None = None
|
|
rest_energy_path: str | None = None
|
|
rest_energy_multiplier: float | None = Field(default=None, ge=0.0001, le=10000)
|
|
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
|