ad4fcd1cfd
First-party plugins for Fabled Scryer (https://github.com/bvandeusen/fabledscryer). traefik: Prometheus metrics scraping, stats history, access log parsing (GeoIP optional) unifi: UniFi controller integration — WAN health, devices, clients, DPI, events ups: UPS monitoring via NUT (Network UPS Tools), Ansible shutdown automation Each plugin follows the Fabled Scryer plugin contract: setup(app), get_scheduled_tasks(), get_blueprint() index.yaml lists all three plugins in the catalog format. Populate checksum_sha256 entries once release zips are published via GitHub Releases. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
183 lines
10 KiB
Python
183 lines
10 KiB
Python
# plugins/unifi/models.py
|
|
from __future__ import annotations
|
|
import uuid
|
|
from datetime import datetime, timezone
|
|
from sqlalchemy import Boolean, DateTime, Float, Integer, String, Text
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
from fabledscryer.models.base import Base
|
|
|
|
|
|
# ── Time-series (one row per scrape) ──────────────────────────────────────────
|
|
|
|
class UnifiWanStat(Base):
|
|
"""WAN interface health — one row per scrape."""
|
|
__tablename__ = "unifi_wan_stats"
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
scraped_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|
|
is_up: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
wan_ip: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
latency_ms: Mapped[float | None] = mapped_column(Float, nullable=True)
|
|
rx_bytes_rate: Mapped[float | None] = mapped_column(Float, nullable=True)
|
|
tx_bytes_rate: Mapped[float | None] = mapped_column(Float, nullable=True)
|
|
uptime_seconds: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
|
|
|
|
class UnifiClientSnapshot(Base):
|
|
"""Aggregated client counts + top talkers — one row per scrape."""
|
|
__tablename__ = "unifi_client_snapshots"
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
scraped_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|
|
total_clients: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
wireless_clients: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
wired_clients: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
guest_clients: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
top_clients_json: Mapped[str] = mapped_column(Text, nullable=False, default="[]")
|
|
# JSON: [{"mac","hostname","ip","type","tx_rate","rx_rate","signal","essid"}]
|
|
|
|
|
|
class UnifiWlanStat(Base):
|
|
"""Per-SSID/radio stats — one row per scrape per SSID+radio combination."""
|
|
__tablename__ = "unifi_wlan_stats"
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
scraped_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|
|
ssid: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
radio: Mapped[str | None] = mapped_column(String(8), nullable=True) # ng=2.4GHz, na=5GHz, 6e=6GHz
|
|
num_sta: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
tx_bytes: Mapped[float | None] = mapped_column(Float, nullable=True)
|
|
rx_bytes: Mapped[float | None] = mapped_column(Float, nullable=True)
|
|
channel: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
satisfaction: Mapped[int | None] = mapped_column(Integer, nullable=True) # 0-100
|
|
|
|
|
|
class UnifiDpiSnapshot(Base):
|
|
"""DPI application/category traffic snapshot — one row per scrape (JSON blob)."""
|
|
__tablename__ = "unifi_dpi_snapshots"
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
scraped_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|
|
# JSON: [{"cat_id", "cat_name", "rx_bytes", "tx_bytes"}] aggregated across all clients
|
|
by_category_json: Mapped[str] = mapped_column(Text, nullable=False, default="[]")
|
|
# JSON: [{"mac", "hostname", "top_cats": [{"cat_name", "bytes"}]}]
|
|
by_client_json: Mapped[str] = mapped_column(Text, nullable=False, default="[]")
|
|
|
|
|
|
# ── Upserted inventory (keyed by UniFi ID or MAC) ─────────────────────────────
|
|
|
|
class UnifiDevice(Base):
|
|
"""Network infrastructure devices — upserted by MAC each scrape."""
|
|
__tablename__ = "unifi_devices"
|
|
|
|
mac: Mapped[str] = mapped_column(String(32), primary_key=True)
|
|
name: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
model: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
device_type: Mapped[str | None] = mapped_column(String(16), nullable=True)
|
|
ip: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
state: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
uptime_seconds: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
last_seen: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
version: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
scraped_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|
|
|
|
|
|
class UnifiKnownClient(Base):
|
|
"""All historically seen network clients — upserted by MAC."""
|
|
__tablename__ = "unifi_known_clients"
|
|
|
|
mac: Mapped[str] = mapped_column(String(32), primary_key=True)
|
|
hostname: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
name: Mapped[str | None] = mapped_column(String(255), nullable=True) # user-set alias
|
|
ip: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
mac_vendor: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
|
note: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
is_blocked: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
first_seen: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
last_seen: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
scraped_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|
|
|
|
|
|
class UnifiEvent(Base):
|
|
"""Site event log — upserted by UniFi event ID to avoid duplicates."""
|
|
__tablename__ = "unifi_events"
|
|
|
|
unifi_id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
event_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
event_key: Mapped[str] = mapped_column(String(64), nullable=False) # e.g. EVT_WU_Connected
|
|
category: Mapped[str] = mapped_column(String(16), nullable=False) # wlan, wired, ap, ids, wan, system
|
|
message: Mapped[str] = mapped_column(Text, nullable=False)
|
|
source_mac: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
|
source_ip: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
details_json: Mapped[str] = mapped_column(Text, nullable=False, default="{}")
|
|
|
|
|
|
class UnifiAlarm(Base):
|
|
"""Active alarms — upserted by UniFi alarm ID."""
|
|
__tablename__ = "unifi_alarms"
|
|
|
|
unifi_id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
alarm_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
alarm_key: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
message: Mapped[str] = mapped_column(Text, nullable=False)
|
|
archived: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
scraped_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|
|
|
|
|
|
class UnifiSpeedtestResult(Base):
|
|
"""WAN speed test results — upserted by run timestamp."""
|
|
__tablename__ = "unifi_speedtest_results"
|
|
|
|
run_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), primary_key=True)
|
|
download_mbps: Mapped[float | None] = mapped_column(Float, nullable=True)
|
|
upload_mbps: Mapped[float | None] = mapped_column(Float, nullable=True)
|
|
latency_ms: Mapped[float | None] = mapped_column(Float, nullable=True)
|
|
server_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
|
|
|
|
# ── Config inventory (upserted by UniFi ID) ───────────────────────────────────
|
|
|
|
class UnifiNetwork(Base):
|
|
"""VLAN/network configurations — upserted by UniFi ID."""
|
|
__tablename__ = "unifi_networks"
|
|
|
|
unifi_id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
purpose: Mapped[str | None] = mapped_column(String(32), nullable=True) # corporate, guest, vlan-only
|
|
vlan: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
ip_subnet: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
dhcp_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
is_guest: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
scraped_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|
|
|
|
|
|
class UnifiPortForward(Base):
|
|
"""Port forwarding rules — upserted by UniFi ID."""
|
|
__tablename__ = "unifi_port_forwards"
|
|
|
|
unifi_id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
name: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
proto: Mapped[str] = mapped_column(String(8), nullable=False, default="tcp")
|
|
dst_port: Mapped[str] = mapped_column(String(32), nullable=False)
|
|
fwd_ip: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
fwd_port: Mapped[str] = mapped_column(String(32), nullable=False)
|
|
src_filter: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
|
scraped_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|
|
|
|
|
|
class UnifiFwRule(Base):
|
|
"""Firewall rules — upserted by UniFi ID."""
|
|
__tablename__ = "unifi_fw_rules"
|
|
|
|
unifi_id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
ruleset: Mapped[str] = mapped_column(String(16), nullable=False) # WAN_IN, LAN_IN, etc.
|
|
rule_index: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
action: Mapped[str] = mapped_column(String(16), nullable=False) # accept, drop, reject
|
|
protocol: Mapped[str | None] = mapped_column(String(16), nullable=True)
|
|
enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
|
src_address: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
dst_address: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
scraped_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|