Files
jhodgkin 1caad69448
CI / web (push) Successful in 20s
CI / api (push) Successful in 29s
Identify unknown devices: OUI vendor lookup, mDNS, manual labels
- OUI: mac-oui-lookup package resolves vendor from the MAC prefix
  (computed on read, no storage needed). Already correctly identifies
  the LXC host prefix as "Proxmox Server Solutions GmbH" and several
  "unknown" devices as "Amazon Technologies Inc." -- likely the Echo
  Dots / Ring gear.
- mDNS: discover-devices.sh now runs avahi-resolve per discovered IP
  (parallel, bounded 2s timeout per host so one non-mDNS device can't
  stall the run), stored in a new devices.mdns_hostname column.
- Manual labels: new device_labels table keyed by MAC (survives DHCP
  IP changes), PUT/DELETE /api/devices/:mac/label, inline-editable
  Name cell in the dashboard. Deliberately separate from vendor/mDNS
  info -- those are shown as an italic *hint* for unlabeled devices,
  not treated as "known" until the admin actually confirms one.
- Fixed the Name column's sort comparator to match what's rendered
  (name, else vendor/mDNS hint) instead of just the raw name field --
  caught while reasoning through what the existing sort test would
  actually need to assert once hints appear in the column.

Part of #15 (OUI/mDNS/manual labels done; on-demand deep-check next).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 23:03:06 -06:00

82 lines
2.5 KiB
Bash
Executable File

#!/bin/bash
# Runs on the CT122 host (not in a container) via the homelab-monitor-discover
# systemd timer. Real ARP entries live in the host's network namespace, not an
# isolated Docker bridge network, so this has to run here rather than inside
# the api container — see docs/device-discovery.md.
#
# Pings every address in the subnet (ICMP only, no port scanning) to populate
# the ARP cache, then mDNS-resolves each responder (avahi-resolve, bounded
# per-IP timeout so one non-mDNS device can't stall the whole run), and dumps
# ip+mac+state+mdnsHostname as JSON. The API service reads this file and does
# known/unknown labeling using config/hosts.yaml.
set -euo pipefail
SUBNET_PREFIX="${SUBNET_PREFIX:-192.168.1}"
OUTPUT_FILE="${OUTPUT_FILE:-/opt/homelab-monitor/data/devices-raw.json}"
IFACE="${IFACE:-eth0}"
mkdir -p "$(dirname "$OUTPUT_FILE")"
for i in $(seq 1 254); do
ping -c 1 -W 1 "${SUBNET_PREFIX}.${i}" >/dev/null 2>&1 &
# cap concurrency so we don't fork 254 pings at once
if (( i % 32 == 0 )); then wait; fi
done
wait
MDNS_DIR="$(mktemp -d)"
trap 'rm -rf "$MDNS_DIR"' EXIT
# `ip neigh show dev <iface>` output: "<ip> lladdr <mac> <state> [router]"
mapfile -t neighbors < <(ip neigh show dev "$IFACE")
i=0
for line in "${neighbors[@]}"; do
read -r ip _ mac state _ <<< "$line"
[[ "$ip" == *:* ]] && continue
[[ -z "$mac" ]] && continue
case "$state" in
REACHABLE|STALE|DELAY|PERMANENT) ;;
*) continue ;;
esac
(
hostname=$(timeout 2 avahi-resolve -a "$ip" 2>/dev/null | awk '{print $2}')
echo "${hostname:-}" > "$MDNS_DIR/$ip"
) &
i=$((i + 1))
if (( i % 32 == 0 )); then wait; fi
done
wait
TMP_FILE="$(mktemp)"
{
echo "["
first=1
for line in "${neighbors[@]}"; do
read -r ip _ mac state _ <<< "$line"
[[ "$ip" == *:* ]] && continue
[[ -z "$mac" ]] && continue
case "$state" in
REACHABLE|STALE|DELAY|PERMANENT) ;;
*) continue ;;
esac
mdns=""
[[ -f "$MDNS_DIR/$ip" ]] && mdns=$(cat "$MDNS_DIR/$ip")
# Minimal JSON string escaping -- mDNS hostnames come from the network,
# not a source we control.
mdns="${mdns//\\/\\\\}"
mdns="${mdns//\"/\\\"}"
[[ $first -eq 0 ]] && echo ","
first=0
if [[ -n "$mdns" ]]; then
printf '{"ip":"%s","mac":"%s","state":"%s","mdnsHostname":"%s"}' "$ip" "$mac" "$state" "$mdns"
else
printf '{"ip":"%s","mac":"%s","state":"%s"}' "$ip" "$mac" "$state"
fi
done
echo
echo "]"
} > "$TMP_FILE"
mv "$TMP_FILE" "$OUTPUT_FILE"