printer_ha_sensor.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. """Schemas for Home Assistant entities bound to a printer (#1148, #448)."""
  2. from datetime import datetime
  3. from typing import Literal
  4. from pydantic import BaseModel, Field, model_validator
  5. class PrinterHASensorBase(BaseModel):
  6. printer_id: int
  7. name: str = Field(..., min_length=1, max_length=100)
  8. entity_id: str = Field(..., pattern=r"^(binary_sensor|sensor)\.[a-z0-9_]+$")
  9. kind: Literal["binary", "numeric"] = "binary"
  10. device_class: str | None = Field(default=None, max_length=32)
  11. unit: str | None = Field(default=None, max_length=16)
  12. alert_state: Literal["on", "off"] | None = None
  13. alert_above: float | None = None
  14. alert_below: float | None = None
  15. block_print: bool = False
  16. notify_on_alert: bool = False
  17. show_on_printer_card: bool = True
  18. sort_order: int = Field(default=0, ge=0, le=999)
  19. @model_validator(mode="after")
  20. def validate_kind_matches_entity(self) -> "PrinterHASensorBase":
  21. domain = self.entity_id.split(".")[0]
  22. expected = "binary" if domain == "binary_sensor" else "numeric"
  23. if self.kind != expected:
  24. raise ValueError(f"kind must be '{expected}' for a {domain} entity")
  25. # Alert fields are per-kind: a threshold on a door contact and an
  26. # on/off alert on a thermometer are both configuration the poller
  27. # would silently ignore, so reject them at the edge instead.
  28. if self.kind == "binary" and (self.alert_above is not None or self.alert_below is not None):
  29. raise ValueError("alert_above/alert_below only apply to numeric sensors")
  30. if self.kind == "numeric" and self.alert_state is not None:
  31. raise ValueError("alert_state only applies to binary sensors")
  32. if self.alert_above is not None and self.alert_below is not None and self.alert_below >= self.alert_above:
  33. raise ValueError("alert_below must be lower than alert_above")
  34. # An interlock or a notification with nothing to trigger on would never
  35. # fire — that reads as a broken feature, not as a no-op.
  36. if (self.block_print or self.notify_on_alert) and not self._has_alert_condition():
  37. raise ValueError("block_print and notify_on_alert require an alert condition")
  38. return self
  39. def _has_alert_condition(self) -> bool:
  40. return self.alert_state is not None or self.alert_above is not None or self.alert_below is not None
  41. class PrinterHASensorCreate(PrinterHASensorBase):
  42. pass
  43. class PrinterHASensorUpdate(BaseModel):
  44. """Partial update. Validated against the merged row in the route, because
  45. the per-kind rules above need fields this payload may not carry."""
  46. name: str | None = Field(default=None, min_length=1, max_length=100)
  47. entity_id: str | None = Field(default=None, pattern=r"^(binary_sensor|sensor)\.[a-z0-9_]+$")
  48. kind: Literal["binary", "numeric"] | None = None
  49. device_class: str | None = Field(default=None, max_length=32)
  50. unit: str | None = Field(default=None, max_length=16)
  51. alert_state: Literal["on", "off"] | None = None
  52. alert_above: float | None = None
  53. alert_below: float | None = None
  54. block_print: bool | None = None
  55. notify_on_alert: bool | None = None
  56. show_on_printer_card: bool | None = None
  57. sort_order: int | None = Field(default=None, ge=0, le=999)
  58. class PrinterHASensorResponse(PrinterHASensorBase):
  59. id: int
  60. last_state: str | None = None
  61. last_changed: datetime | None = None
  62. last_checked: datetime | None = None
  63. created_at: datetime
  64. updated_at: datetime
  65. class Config:
  66. from_attributes = True
  67. class PrinterHASensorReading(BaseModel):
  68. """One sensor's live state, as the printer card renders it."""
  69. id: int
  70. name: str
  71. entity_id: str
  72. kind: str
  73. device_class: str | None = None
  74. unit: str | None = None
  75. # Raw HA state: "on"/"off" for binary, the numeric string for sensors.
  76. # None when the entity is unavailable or has not been polled yet.
  77. state: str | None = None
  78. value: float | None = None # numeric sensors only, parsed from state
  79. alerting: bool = False
  80. block_print: bool = False
  81. reachable: bool = True
  82. last_changed: datetime | None = None
  83. class HADisplayEntity(BaseModel):
  84. """A bindable entity, as offered by the picker."""
  85. entity_id: str
  86. friendly_name: str
  87. state: str | None = None
  88. domain: str
  89. device_class: str | None = None
  90. unit_of_measurement: str | None = None