feat: initial commit — UniFi snap-in package

Full UniFi dashboard snap-in including:
- WiFi/client/device stats with time-series snapshots
- Client Dashboard with traffic, satisfaction, signal, download charts
- Webhook alerting with debounced offline/online detection
- AP snapshot collection, client snapshot collection
- Device classification (type and OS) from OUI/hostname heuristics
- Webhook cooldown, templates, and multi-platform delivery

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Joel Wedemire
2026-04-12 23:00:05 -07:00
commit ce3217d8f4
29 changed files with 2972 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
<?php
namespace Dashboard\Unifi\Console;
use Dashboard\Unifi\Models\ApSnapshot;
use Dashboard\Unifi\Models\ClientSnapshot;
use Dashboard\Unifi\Services\UnifiApiClient;
use Dashboard\Unifi\Services\WebhookCheckService;
use Illuminate\Console\Command;
class CaptureSnapshots extends Command
{
protected $signature = 'unifi:capture-snapshots';
protected $description = 'Capture AP stats snapshot and evaluate webhook alerts using the same data';
public function handle(UnifiApiClient $unifi, WebhookCheckService $webhooks): int
{
try {
$aps = $unifi->getAccessPoints();
} catch (\Throwable $e) {
$this->error('Failed to fetch APs: ' . $e->getMessage());
return self::FAILURE;
}
// ── 1. Store snapshot ─────────────────────────────────────────────
$now = now();
$rows = [];
foreach ($aps as $ap) {
$rows[] = [
'ap_mac' => $ap['mac'],
'ap_name' => $ap['name'] ?? $ap['model'] ?? null,
'num_sta' => $ap['num_sta'] ?? 0,
'rx_bytes' => $ap['rx_bytes'] ?? 0,
'tx_bytes' => $ap['tx_bytes'] ?? 0,
'tx_retries' => $ap['stat']['ap']['tx_retries'] ?? 0,
'tx_dropped' => $ap['stat']['ap']['tx_dropped'] ?? 0,
'rx_dropped' => $ap['stat']['ap']['rx_dropped'] ?? 0,
'tx_errors' => $ap['stat']['ap']['tx_errors'] ?? 0,
'rx_errors' => $ap['stat']['ap']['rx_errors'] ?? 0,
'satisfaction' => ($ap['satisfaction'] ?? -1) >= 0 ? $ap['satisfaction'] : null,
'cu_2g' => $this->getRadioStat($ap, 'ng'),
'cu_5g' => $this->getRadioStat($ap, 'na'),
'cu_6g' => $this->getRadioStat($ap, 'a6'),
'captured_at' => $now,
];
}
if (! empty($rows)) {
ApSnapshot::insert($rows);
}
// ── 1b. Store per-client snapshot ─────────────────────────────────
try {
$clients = $unifi->getActiveClients();
$clientRows = [];
foreach ($clients as $c) {
if (empty($c['mac'])) continue;
$clientRows[] = [
'mac' => strtolower($c['mac']),
'name' => $c['hostname'] ?? $c['name'] ?? null,
'dev_cat' => $c['dev_cat'] ?? null,
'os_name' => $c['os_name'] ?? null,
'is_wired' => (bool) ($c['is_wired'] ?? false),
'rx_bytes' => $c['rx_bytes'] ?? 0,
'tx_bytes' => $c['tx_bytes'] ?? 0,
'satisfaction' => ($c['satisfaction'] ?? -1) >= 0 ? $c['satisfaction'] : null,
'signal' => isset($c['signal']) ? (int) $c['signal'] : null,
'captured_at' => $now,
];
}
if (! empty($clientRows)) {
ClientSnapshot::insert($clientRows);
}
} catch (\Throwable $e) {
$this->warn('Client snapshot failed: ' . $e->getMessage());
}
// ── 2. Check webhook alerts with the same data ────────────────────
$fired = $webhooks->checkAll($unifi);
if ($fired > 0) {
$this->info("Fired {$fired} webhook(s).");
}
return self::SUCCESS;
}
private function getRadioStat(array $ap, string $radio): ?int
{
foreach ($ap['radio_table_stats'] ?? [] as $stat) {
if (($stat['radio'] ?? '') === $radio && isset($stat['cu_total'])) {
return (int) $stat['cu_total'];
}
}
return null;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Dashboard\Unifi\Console;
use Dashboard\Unifi\Services\UnifiApiClient;
use Dashboard\Unifi\Services\WebhookCheckService;
use Illuminate\Console\Command;
class CheckWebhooks extends Command
{
protected $signature = 'unifi:check-webhooks';
protected $description = 'Evaluate UniFi webhook conditions and fire alerts';
public function handle(WebhookCheckService $service, UnifiApiClient $unifi): int
{
$fired = $service->checkAll($unifi);
if ($fired > 0) {
$this->info("Fired {$fired} webhook(s).");
}
return self::SUCCESS;
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Dashboard\Unifi\Console;
use App\Models\Setting;
use Dashboard\Unifi\Models\ApSnapshot;
use Dashboard\Unifi\Models\ClientSnapshot;
use Dashboard\Unifi\Models\WebhookLog;
use Illuminate\Console\Command;
class CleanupSnapshots extends Command
{
protected $signature = 'unifi:cleanup';
protected $description = 'Remove old AP snapshots and webhook logs based on retention setting';
public function handle(): int
{
$days = (int) Setting::get('unifi.retention_days', 30);
$cutoff = now()->subDays($days);
$snapshots = ApSnapshot::where('captured_at', '<', $cutoff)->delete();
$clientSnapshots = ClientSnapshot::where('captured_at', '<', $cutoff)->delete();
$webhookLogs = WebhookLog::where('fired_at', '<', $cutoff)->delete();
$this->info("Cleaned up: {$snapshots} AP snapshots, {$clientSnapshots} client snapshots, {$webhookLogs} webhook logs older than {$days} days.");
return self::SUCCESS;
}
}