Files
the-other-dude/backend/app/models/wireless_link.py
Jason Staack a71df2af29 feat(13-02): add wireless_links table migration, ORM model, register both models
- Migration 033 creates wireless_links with state machine, missed_polls, RLS
- WirelessLink model with LinkState enum (discovered/active/degraded/down/stale)
- Register DeviceInterface, WirelessLink, LinkState in models __init__

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 06:02:14 -05:00

86 lines
2.9 KiB
Python

"""WirelessLink model -- AP-to-CPE link state tracking for link discovery."""
import uuid
from datetime import datetime
from enum import Enum
from sqlalchemy import DateTime, ForeignKey, Integer, String, UniqueConstraint, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base
class LinkState(str, Enum):
"""Link health state machine values."""
DISCOVERED = "discovered"
ACTIVE = "active"
DEGRADED = "degraded"
DOWN = "down"
STALE = "stale"
class WirelessLink(Base):
__tablename__ = "wireless_links"
__table_args__ = (
UniqueConstraint("ap_device_id", "cpe_device_id", name="uq_wireless_links_ap_cpe"),
)
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
primary_key=True,
default=uuid.uuid4,
server_default=func.gen_random_uuid(),
)
ap_device_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("devices.id", ondelete="CASCADE"),
nullable=False,
)
cpe_device_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("devices.id", ondelete="CASCADE"),
nullable=False,
)
tenant_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("tenants.id", ondelete="CASCADE"),
nullable=False,
)
interface: Mapped[str | None] = mapped_column(String(255), nullable=True)
client_mac: Mapped[str] = mapped_column(String(17), nullable=False)
signal_strength: Mapped[int | None] = mapped_column(Integer, nullable=True)
tx_ccq: Mapped[int | None] = mapped_column(Integer, nullable=True)
tx_rate: Mapped[str | None] = mapped_column(String(50), nullable=True)
rx_rate: Mapped[str | None] = mapped_column(String(50), nullable=True)
state: Mapped[str] = mapped_column(
String(20),
nullable=False,
default=LinkState.DISCOVERED.value,
)
missed_polls: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
discovered_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
last_seen: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
# Relationships
ap_device: Mapped["Device"] = relationship( # type: ignore[name-defined]
"Device", foreign_keys=[ap_device_id]
)
cpe_device: Mapped["Device"] = relationship( # type: ignore[name-defined]
"Device", foreign_keys=[cpe_device_id]
)
def __repr__(self) -> str:
return (
f"<WirelessLink id={self.id} ap={self.ap_device_id} "
f"cpe={self.cpe_device_id} state={self.state!r}>"
)