This repository has been archived on 2026-06-01. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
bvandeusen ad4fcd1cfd feat: initial release — traefik, unifi, ups plugins
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>
2026-03-22 18:28:43 -04:00

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))