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:
97
src/Console/CaptureSnapshots.php
Normal file
97
src/Console/CaptureSnapshots.php
Normal 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;
|
||||
}
|
||||
}
|
||||
22
src/Console/CheckWebhooks.php
Normal file
22
src/Console/CheckWebhooks.php
Normal 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;
|
||||
}
|
||||
}
|
||||
30
src/Console/CleanupSnapshots.php
Normal file
30
src/Console/CleanupSnapshots.php
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user