Commit Graph
70 Commits
Author SHA1 Message Date
andreasandClaude Haiku 4.5 bb2e9c4618 Disable system dnsmasq.service to avoid port 53 conflicts
The system dnsmasq.service from the Debian dnsmasq package conflicts with
van-ap-dnsmasq.service on port 53 (DNS/DHCP). Explicitly disable the system
service during deploy so only van-ap-dnsmasq (bound to br0 for AP DHCP) runs.

Fixes DHCP failures where clients connect to the AP but don't get IP addresses
because port 53 is already in use.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-18 18:00:33 -04:00
andreasandClaude Haiku 4.5 af241339a3 Add package dependency checks to deploy.sh
Check for required packages (hostapd, iw, nftables, dnsmasq, systemd-resolved)
at deploy start and fail if any are missing. Also warn about optional packages
used by specific components (rsync, mosquitto, gpsd, python3-paho-mqtt, etc.).

Missing required packages halt deployment with clear error message. Optional
packages generate warnings that are captured in deploy-warnings.json for the
router-status MQTT dashboard.

This prevents silent failures when the R5C or other hardware doesn't have
expected dependencies installed.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-18 17:51:22 -04:00
andreasandClaude Haiku 4.5 7d68991c37 Update deploy.conf comments for R5C hardware (RTL8822CE 5GHz AP)
Clarify that WIFI_5G_IFACE=wlan1 uses the built-in RTL8822CE chip on
FriendlyElec R5C with rtl88x2ce driver, not the RTL8852BU from the Pi 5.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-18 17:42:35 -04:00
andreasandClaude Haiku 4.5 ad199fb0b0 Make router IP address configurable via deploy.conf
Add GATEWAY_IP, GATEWAY_PREFIX, GATEWAY_NETWORK, DHCP_RANGE_START, and
DHCP_RANGE_END variables to deploy.conf. Update ap/21-van-br0.network,
ap/van-ap-dnsmasq.conf, and ap/nftables.conf to use @TOKEN@ placeholders
for IP configuration instead of hardcoding 10.42.x addresses.

This allows deploying the same configuration to multiple routers on
different networks (e.g., Pi 5 on 10.42.0.x and R5C on 10.42.1.x) by
simply editing deploy.conf before running ./deploy.sh.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-18 17:31:18 -04:00
Andreas WredeandClaude Sonnet 5 ec5130ea7d router-status: replace Cockpit's Van Router plugin with MQTT -> Home Assistant
Cockpit's login was unreliable (Firefox rejected sometimes, Android failed
outright with a TLS handshake error) and it was a resource hog on this Pi.
van-router-mqtt publishes the same AP/client/WAN/Starlink/modem status and
accepts the same control actions (AP/modem restart, WAN connect/disconnect,
prefer) via MQTT with HA discovery, using the local Mosquitto broker already
running here. It republishes van-failover/van-thermal's own state.json files
verbatim rather than modifying either daemon (van-thermal is deliberately
stdlib-only; van-failover's control loop wasn't worth an added dependency).

Adds a Lovelace dashboard (ha/dashboards/van-router.yaml) consuming those
entities. Cockpit's plugin is left deployed for now, not yet retired.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S2tpeaiHUriQbbnQ4SCbkf
2026-09-14 16:28:01 -04:00
Andreas WredeandClaude Sonnet 5 918003d2e3 ha: add carefree_bt12 integration for the Carefree Connects awning
Commands reverse-engineered from Bluetooth HCI snoop captures (adb bugreport)
of the official Carefree Connects (BT12) Android app, confirmed across three
independent captures including two isolated single-action live tests:
extend/retract (feature 0x05, values 0x01/0x02 -- the device toggles motor
state internally, there's no separate stop byte) and light on/off (feature
0x1a, values 0x19/0x01). Both live under GATT characteristic 02060002 on
service 02060001-50e1-405f-bab0-6bb582b4d96e.

Connects on demand per command (mirrors the app's own connect/act/disconnect
pattern) rather than holding a persistent connection like li3_battery, since
this device doesn't stream telemetry. Cover/light state is assumed/optimistic
-- the notify channel (02060003) isn't decoded yet, so a diagnostic sensor
just surfaces raw undecoded replies to build up data for that follow-on work.

Verified end-to-end against the real device: light on/off and awning
extend both worked through Home Assistant.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 15:52:06 -04:00
Andreas Wrede 7911339743 changed devices for Starlink and 2.4GHz AP 2026-08-24 14:37:07 -04:00
Andreas WredeandClaude Sonnet 5 d25f48727f ha: add li3_battery HA integration, replacing van-li3-battery
The li3 BMS has a persistently marginal BLE link from any single fixed
vantage point on this Pi (onboard adapter, a USB dongle that turned out to
be BR/EDR-only, and the Athom ESPHome BT proxy even after moving it closer).
Move capture into HA proper so bluetooth.async_ble_device_from_address can
pick whichever known source currently has the device, instead of hardcoding
one. Protocol parsing ported verbatim from li3/van-li3-battery.

van-li3-battery is disabled on the Pi; its MQTT discovery entities were
cleared.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 11:49:44 -04:00
Andreas WredeandClaude Sonnet 5 94e6a2d811 failover: tolerate rsync's benign vanished-file exit code in backup syncs
Both sync-sd-backup.sh and sync-usb-backup.sh used set -e, so rsync
returning exit 24 (partial transfer due to vanished source files --
expected on a live root, e.g. a container's shm socket disappearing
mid-sync) aborted the script before it printed its own success message,
even though the actual file transfer completed fully. Only exit 24 is
now tolerated; any other rsync failure still aborts as before.

Found running sync-sd-backup.sh today: it transferred all 919k files
correctly but exited non-zero and skipped its completion line.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJfEELeh3ercpRBp8yYrYS
2026-08-23 15:55:38 -04:00
Andreas WredeandClaude Sonnet 5 5c793186ef ap: switch 5GHz driver to out-of-tree morrownr/rtw89 for USB3 support
The in-kernel rtw89_8852bu on this Ubuntu kernel base predates mainline's
USB2->3 auto-switch for this chip, permanently capping the AP dongle at
USB2/480M. morrownr/rtw89 (dkms) has that switch. ap/rtw89.conf now
blacklists the in-kernel rtw89 modules and tunes the replacement
(disable_ps_mode + switch_usb_mode); ap/install-rtw89-driver.sh builds
and installs it, pinned to a specific upstream commit, kept separate
from deploy.sh since a dkms rebuild is too slow to run on every deploy.

Verified live: negotiates USB3/5000M on a dedicated USB3 controller
(480M on a USB2-only one, as expected), AP recovered via hostapd's
Restart=always + van-ap-watchdog with no manual intervention. Hit and
documented one real gotcha along the way: in-kernel rtw89_core refused
to unload while its own dependents (rtw89_8852b, rtw89_8852b_common)
were still loaded, which blocked the new module with a duplicate-symbol
error until all in-kernel modules were removed first.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJfEELeh3ercpRBp8yYrYS
2026-08-23 15:42:23 -04:00
Andreas WredeandClaude Sonnet 5 6999b2d12b failover: add sync-usb-backup.sh, the reverse of sync-sd-backup.sh
Refreshes the USB disk (sda2) as a live boot-fallback clone of the
running root, same self-sync guard/excludes/rsync flags as the SD
script. Fixed copy-paste leftovers from sync-sd-backup.sh (SD_ROOT_PART,
mount point, and log message all still said "SD").

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJfEELeh3ercpRBp8yYrYS
2026-08-23 14:53:31 -04:00
Andreas WredeandClaude Sonnet 5 a99a39a859 ap: fix driver_name() silently caching wrong value on startup race
Path.resolve() doesn't raise on a nonexistent path — it just returns the
syntactic path unchanged — so when driver_name() ran before the radio's
netdev had enumerated (startup race with USB re-enum), it silently
returned the literal string "driver" instead of None. The loop's retry
guard (`if driver is None: retry`) never fired since "driver" is truthy,
so the bogus value was cached for the service's entire uptime and the
queue-flush wedge regex could never match real driver names — the 3rd
check added 2026-08-19 was silently inert. Fixes 2026-08-23 recurrence
where the 5GHz SSID went invisible and hostapd never got restarted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJfEELeh3ercpRBp8yYrYS
2026-08-23 14:49:27 -04:00
Andreas WredeandClaude Sonnet 5 db22cb50d6 modem: add van-sms-send CLI for sending SMS ad hoc
mmcli's inline "number=...,text=..." properties string breaks on plain
spaces/quotes/colons in the text — found while testing van-sms-watch.
Sidesteps that entirely by passing the message via
--messaging-create-sms-with-text=<tempfile> instead. Tested live
(multi-word text, punctuation, multi-line stdin input all delivered).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 15:40:22 -04:00
Andreas WredeandClaude Sonnet 5 a74257e44b modem: fix executable bit on van-sms-watch
Was checked in 100644, inconsistent with the other repo scripts
(deploy.sh's install -D -m0755 masked this in practice).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 15:13:06 -04:00
Andreas WredeandClaude Sonnet 5 d002058701 modem: add van-sms-watch to archive inbound SMS + Pushover alert
Polls ModemManager for SMS on the EC25's own flash storage, appends each
inbound message to /var/log/van-sms.jsonl, pages via Pushover (shared
van-battery creds), then deletes it from modem storage so it doesn't
silently fill up. MMS can't be decoded (WAP-push notification needs the
carrier's separate MMS APN) — detected and paged as notification-only.

Wired into deploy.sh; installed/verified live on the Pi (backlog SMS
archived + paged successfully) ahead of committing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 15:12:54 -04:00
Andreas Wrede e5e776405a li3: decode status flags, fix info-sensor expire_after, restart bluetooth after first failed scan
- status_text: decode the 24-bit "status" field into human-readable flag
  names (e.g. "Cell Temp Low, Low Voltage") instead of raw hex, using the
  bit table pulled from com.lithionics.bms's array/advanced resource (dumped
  with aapt -- the app's own StatusCodeTable class references stale/wrong
  resource IDs and can't be trusted for this). Applied to "status" only;
  last_fault_code is a lifetime latch that accumulates many bits over time
  and isn't meaningfully summarized the same way.

- Fixed the info sensors (total_consumed, firmware_version, serial_number,
  etc.) showing "Unavailable" in HA a minute after connecting: they were
  getting the same expire_after as the periodic telemetry sensors, but the
  $info line the come from is only sent once per BLE connection, so HA's
  expire timer always fired. These are retained-topic diagnostics meant to
  hold their last value indefinitely; expire_after now only applies to the
  periodic SENSORS group.

- STUCK_DISCOVERY_THRESHOLD 3 -> 1: restart bluetooth.service after the
  first failed scan instead of waiting for three, cutting reconnect time
  roughly in third. Still rate-limited via BLUETOOTH_RESTART_COOLDOWN_S.

Also removed the per-line "RAW ..." debug print (was flooding the journal).

All three changes verified live on host wan via journalctl/mosquitto_sub.
2026-08-18 10:18:56 -04:00
Andreas Wrede 382f99b508 li3: publish CAN-trace fields with telemetry, info fields as separate retained topic
Decoded the "&" and "$" line formats from log1 by matching them against the
decompiled com.lithionics.bms app: TraceFormat/InfoFormat gate on the raw
line's first byte ('&'=trace, '$'=info) before BmsSeries.create() ever sees
the row, which is why the earlier decode (MainBmsCsParameters) never touched
them.

"&" trace lines stream continuously (enabled by our own $traceon) and carry
CAN-charger-bus fields: remaining_capacity, remaining_time, can_charger_
voltage/current, can_charger_status, can_status. These merge into the
existing periodic state message alongside the Cs telemetry.

"$" info is sent once per connection (response to $info) and never repeats:
total_consumed, last_fault_code, highest/lowest_recorded_temp, firmware_
version, aging_factor_temp/soc, serial_number. Published to its own retained
topic (van/<device_id>/info) instead of the periodic one, with its own
discovery config (entity_category: diagnostic) — so HA keeps the last known
value across BMS disconnects rather than expiring it.

Deployed and verified live on host wan: info message publishes once on
connect, state messages carry the merged fields, and 14 new HA entities
registered with correct precision.
2026-08-18 09:56:32 -04:00
Andreas Wrede 7eab5ed422 fix: adjust precision of temperatures and current 2026-08-18 07:22:45 -04:00
Andreas Wrede 7a1c47d34c li3: add Lithionics Li3 house battery BMS -> Home Assistant MQTT
Publishes the RV's 12V LiFePO4 house battery to HA via MQTT discovery
(pack voltage, 4 cell voltages, current, SOC, BMS/battery temp, status).
Connects over the battery's BLE HM-10 UART module (service ffe0/char
ffe1, no pairing) using bleak; protocol reverse-engineered from the
com.lithionics.bms Android app's own BLE/parsing code.

Deliberately named li3/ and van-li3-battery, not battery/van-battery —
that name is reserved for the host's own AC/UPS power-supply monitor
(different hardware, unrelated concern).

Self-heals a bluetoothd discovery-state wedge (Discovering stuck "yes",
connects failing with le-connection-abort-by-local) that shows up after
repeated failed connects to this device on the Pi's onboard adapter: the
daemon retries scan/connect internally (MQTT session and HA entities
stay up across retries) and restarts bluetooth.service itself after 3
consecutive scan failures, rate-limited to once per 5 min.
2026-08-17 17:26:14 -04:00
Andreas WredeandClaude Sonnet 5 b7758c9cd5 ha: add Frigate NVR as a Podman Quadlet, wire into deploy.sh
Same sibling-container pattern as HA/ESPHome. The hand-written unit used
Privileged=true, which isn't a real Quadlet key — the generator silently
drops the whole file on an unsupported key, so frigate.service was never
generated (hence `systemctl enable frigate` saying the unit doesn't exist).
Fixed with PodmanArgs=--privileged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 21:12:45 -04:00
Andreas Wrede a4c03496c0 add: don't be so quiet 2026-08-12 07:04:51 -04:00
Andreas Wrede 584f2e0b82 match font to rest of cockpit 2026-08-07 09:13:12 -04:00
Andreas WredeandClaude Sonnet 5 b0283f6083 failover: fix enforce_route() to handle gateway-less WANs (cellular)
The EC25/Koodo GSM connection is QMI raw-ip with an on-link /29 and no
gateway at all (nh 0.0.0.0), not a normal DHCP WAN with a temporarily
unknown gateway. enforce_route() required a gw and silently gave up
when none was found, so cellular could be reported up/healthy/preferred
in state.json while never actually getting a default route (Prefer
button had no effect since it only touches metric, not this bailout).
Now falls back to installing a gateway-less `ip route ... dev DEV`
default when the device genuinely has an IPv4 address (carrier truly
up), mirroring what NM itself would install without never-default.
Also fixes stale-route pruning to handle gateway-less old routes,
which the previous gw-only check skipped.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 15:10:21 -04:00
Andreas WredeandClaude Sonnet 5 6d5184e2e7 failover: ipv4.never-default + per-WAN probe scheduling; tune probe cadence
van-failover is now the sole owner of default-route selection: netplan/NM
profiles get ipv4.never-default so NM's own DHCP/lease renewals stop
reinstalling competing default routes (was racing van-failover's enforce_route
on Starlink's 16s lease). set_profile_metric() also pins never-default on
NM-managed connections it doesn't own the netplan source for (e.g. cellular),
and nm_gateway() falls back to the raw DHCP4 lease's `routers` option since NM
stops populating IP4.GATEWAY once never-default is set.

Probing moved from one shared round to independent per-WAN retry scheduling,
so a flaky WAN retries on its own clock instead of dragging healthy WANs into
extra probes or throttling a failing one to the slow steady-state cadence.

config.json: probe_interval 4->60, fail/ok_threshold 3/2->2/1, single probe
URL — verified live across this reboot (cellular took ~2min after NM reported
"activated" to actually pass traffic; van-failover correctly withheld/
deprioritized the default route until then, then installed it automatically).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 14:22:46 -04:00
Andreas WredeandClaude Sonnet 5 21c8870c0d failover: add sync-sd-backup.sh — script the post-push SD fallback refresh
The SD card (mmcblk0p2) is refreshed as a live boot-fallback clone of
NVMe root after pushes, but that rsync was being hand-typed each time
and its exclude list didn't cover /etc/fstab. That let it clobber the
SD's own fstab with the NVMe's PARTUUIDs, breaking SD-only boot (only
mounts correctly if the NVMe happens to also be present). Also excludes
/etc/machine-id and /var/log so the clone doesn't inherit the NVMe's
identity/journal history.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 20:28:51 -04:00
Andreas WredeandClaude Sonnet 5 af18dc0755 failover: wire up van-modem-watch in deploy.sh
Completes the van-modem-usb-kick -> van-modem-watch swap from the
previous commit: deploy.sh was still installing/enabling the old unit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 20:28:46 -04:00
Andreas WredeandClaude Sonnet 5 3169297148 failover: replace van-modem-usb-kick with van-modem-watch (detect-only, no recovery)
Hub power-cycle recovery was proven not to work — only a physical
unplug/replug clears a cold-boot enumeration failure. Modem moved to the
Pi's native USB port as the actual fix; this just pages via Pushover if
it recurs instead of attempting a recovery that doesn't work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 20:28:21 -04:00
Andreas WredeandClaude Sonnet 5 a1bad186b1 failover: add van-modem-usb-kick — recover EC25 modem when it never enumerates at boot
Seen after moving the modem onto the new powered UGreen hub: the port
sometimes never signals a connect during boot (no descriptor-read attempt at
all in dmesg), so the hub never rescans it. Unbind/rebinding the inner hub
chip reproduces the same connect edge a physical unplug/replug does.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 10:17:36 -04:00
Andreas Wrede e7a82126f2 power: add van-nvme-watch — pages on NVMe I/O-timeout/reset recurrence
Tails journalctl -kf for the "nvme nvmeN: I/O tag ... timeout, reset
controller" signature that crashed and corrupted the root fs on 2026-08-02
(and recurred 2026-08-04, that time self-healing). Watches a short grace
window to tell a clean self-heal from an escalation (repeated timeout or a
following ext4 error) before paging via the existing Pushover credentials,
with a live SMART/superblock snapshot in the alert body.
2026-08-04 08:44:53 -04:00
Andreas Wrede 86fa9509df failover: fix Starlink interface + template config.json from deploy.conf
STARLINK_IFACE pointed at the wrong device after the USB hub swap (was
accidentally set to the 5GHz AP dongle's interface). failover/config.json
also hardcoded the old interface name instead of being templated like the
other configs, so deploy.conf changes never reached it — switched it to
@STARLINK_IFACE@ and deploy.sh now renders it.
2026-08-04 08:35:18 -04:00
Andreas Wrede 1d3a053f5d set mqtt msgs to retain 2026-08-02 17:09:51 -04:00
Andreas WredeandClaude Sonnet 5 8efb8dba5f dns: fixed 1.1.1.1/8.8.8.8 lockdown + scoped mDNS, esphome sibling container
- deploy.conf: DNS_RESOLVERS, always 1.1.1.1/8.8.8.8, never a WAN's own
  DHCP/RA-provided servers (previously whatever Wapana handed out).
- ap/99-van-router-dns.conf: global resolved config (fixed DNS, Domains=~.,
  global MulticastDNS=yes — a prerequisite for any per-link mDNS to work at
  all, not just an on/off toggle).
- failover/60-van-wan-dns: NM dispatcher that strips each WAN's DNS/search-
  domain and disables its mDNS via resolvectl on every connect/lease event
  (NM's own ipv4/ipv6.ignore-auto-dns can't be set as a config-file default —
  confirmed rejected as an unknown key — so this enforces it directly
  instead), retried over ~5s to beat NM's own async DNS commit. Also logs
  what each WAN advertised, never used, to /run/van-wan-dns/.
- ap/21-van-br0.network: MulticastDNS=yes, scoped to the van's own LAN only
  — .local/mDNS now resolves for ESPHome and other LAN devices without
  leaking mDNS onto Wapana/Starlink/cellular.
- dns/: ZeroTier-managed DNS (zt.wrede.pvt) made reproducible — installed
  the official zerotier-systemd-manager package (verified against upstream
  checksums), additive to the above so *.zt.wrede.pvt keeps resolving over
  the overlay independent of WAN.
- ha/esphome.container: ESPHome dashboard as a sibling Podman Quadlet to
  Home Assistant, same host-network/config-volume pattern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-01 16:09:01 -04:00
Andreas WredeandClaude Sonnet 5 5d88e1b30c deploy: hardware-instance templating, wlan0 boot watchdog, deploy-time warnings in Cockpit
- deploy.conf templates interface names/USB IDs (@TOKEN@ substitution) across
  ap/* configs so a dongle swap only needs deploy.conf edited, not the repo
  configs themselves; drops ap/rtw88.conf (old 2.4GHz dongle retired for the
  DWA-171, which needs no such power-save override).
- failover/van-wlan-watchdog: recovers wlan0 from NM's post-boot no-secrets
  wedge (a boot-time supplicant race, not a real credential failure).
- deploy.sh: warn() collects dependency/config warnings (missing python3-gps,
  python3-paho-mqtt, mobile-broadband-provider-info, grpcurl, gpsd; netplan
  drift; unedited example configs) into /var/lib/vanlink/deploy-warnings.json,
  rendered as an amber Cockpit card so they're visible without reading deploy
  output.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-01 11:25:11 -04:00
Andreas WredeandClaude Fable 5 8d5415f326 gps: modem GNSS -> gpsd -> OwnTracks/MQTT publisher
EC25-AF GNSS wiring (udev hotplug into gpsd) plus van-gps-owntracks, a
port of the wayback-era gps_to_owntracks.py: apt-only deps (python3-gps
instead of pip-only gpsdclient, paho 2.x callback API), broker secrets
moved out of the code into /etc/van-gps/config.json (0600, seeded from
a sanitized example), gpsd host now localhost. client_id is vanq-wan —
a legacy client still holds vanq on the broker and shared IDs get
kicked in a connect/disconnect loop. Exit-on-disconnect + systemd
Restart=always is the reconnect logic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 22:03:40 -04:00
Andreas WredeandClaude Fable 5 d2a2b19b85 cockpit: cellular WAN row with signal + Restart button
Include the gsm device in the WAN/Uplinks table, shown as its routed
netdev with IP and metric (nmcli actions still target cdc-wdm0). New
Signal column fed by mmcli -K in the single status spawn (percent, tech,
operator). Restart = mmcli disable/enable — the EC25 MBIM plugin doesn't
support --reset — with a USB unbind/bind fallback (vendor 2c7c) when
ModemManager can't reach the modem. README: modem setup steps (Koodo
profile creation, MBIM netdev gotcha, Cockpit row).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 17:28:22 -04:00
Andreas WredeandClaude Fable 5 fe9e6d3b30 failover: resolve MBIM/QMI modems to their wwan netdev
NM reports a gsm connection's device as the control port (cdc-wdm0), but
IP + routes live on the wwan netdev (wwu1u2i4 on the Quectel EC25-AF), so
probes and route enforcement silently saw nothing (cellular stuck "down",
no metric). Map connection-keyed WANs through GENERAL.IP-IFACE, and let
the gateway fallback query the connection since the netdev isn't an NM
device.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 17:28:12 -04:00
Andreas WredeandClaude Fable 5 7c36757d35 sysctl: correct the use_tempaddr comment with the real root cause
The rotating privacy address was not itself the packet-loss mechanism:
the 2026-07-13 discrimination experiment (4 flows, dual-ended capture)
showed the loss keyed to hbc's exact 5-tuple — upstream per-flow state
poisoned when the flow was created during boot/apply address churn, kept
alive forever by the 10s heartbeat cadence. Fresh flows to the same
host/port were clean; restarting hbc fixed it instantly. use_tempaddr=0
stays: it removes one source of the churn that poisons newborn flows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuPg8vz6FmDUj4SgEJ525C
2026-07-13 13:13:08 -04:00
Andreas WredeandClaude Sonnet 5 09259eb6cc sysctl: force use_tempaddr=0 to beat Ubuntu's 55-ipv6-privacy.conf default
Root cause of the flow-selective IPv6 heartbeat loss investigated
2026-07-12/13: Ubuntu ships /usr/lib/sysctl.d/55-ipv6-privacy.conf with
use_tempaddr=2 system-wide. Nothing on wlan0 overrode it before this
morning's netplan fix, so the interface carried a rotating privacy
address alongside the stable one. hbc's long-lived flow eventually
straddled a temp-address deprecation event mid-flight, which looked
exactly like random upstream packet loss (in-transit, IPv6-only,
flow-selective) and cost a day of packet captures before the cause
turned out to be this default fighting the WAN config instead.

The netplan/NM ipv6.ip6-privacy=false fix already forces this per
connection, but that's a timing-dependent override racing a package
default. This makes it explicit and permanent: 99- loads after (and
wins over) 55- the next time sysctl --system runs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 12:06:33 -04:00
Andreas WredeandClaude Sonnet 5 daa545f15c deploy.sh: warn on netplan drift between repo and /etc/netplan
The 07-12 IPv6 fix sat in ap/50-van-wan.yaml for a day without ever being
copied to /etc/netplan/ or applied — deploy.sh deliberately skips netplan
(applying it flaps the uplinks) so nothing caught the gap. Now it diffs
the two and prints the manual deploy commands when they disagree.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 11:04:19 -04:00
andreas 4aae62372e deument last change 2026-07-12 12:22:45 -04:00
Andreas WredeandClaude Fable 5 776f020af3 cockpit: unified Clients card (Wi-Fi + wired via bridge FDB)
New "Clients" card lists everything on the van LAN in one table — hostname,
IP, MAC, connection (5GHz / 2.4GHz / LAN eth0 / LAN USB), signal, TX rate.
Wi-Fi rows come from the hostapd station dumps as before; wired rows from
learned bridge-FDB entries on the LAN ports (the port's own MAC is
"permanent", and FDB duplicates entries per vlan — both filtered), enriched
with IP/hostname from dnsmasq leases + neighbor table.

The per-band station tables move out of the Access Points card (which keeps
its status line + client count), and the WAN/Uplinks table now hides
unmanaged devices so the networkd-owned LAN ports don't show up as ghost
WAN rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 07:44:57 -04:00
Andreas WredeandClaude Fable 5 a36b64c7c4 lan: repurpose onboard eth0 as an internal LAN port on br0
With wifi + Starlink + cellular as uplinks, the onboard GbE earns its keep
as a wired LAN port instead of the metric-150 ethernet WAN: eth0 leaves
netplan/NM (added to van-ap-unmanaged.conf) and joins br0 via networkd
(23-van-lan-eth0.network), so wired clients get 10.42.0.x DHCP/DNS and NAT
exactly like Wi-Fi clients. The eth WAN is gone from van-failover's config.

Cutover notes: delete the old netplan-eth0 / stray eth0 NM profiles, then
nmcli general reload + networkctl reload/reconfigure eth0. NEVER cable this
port (or the USB LAN dongle) back into an upstream LAN — dnsmasq on br0
would serve rogue DHCP there; STP stays off, so don't cable both wired LAN
ports to the same switch either.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 07:44:44 -04:00
Andreas WredeandClaude Fable 5 751facd511 starlink: dish route + status card on the Van Router page
The Starlink RTL8153 gets a declarative NM profile in 50-van-wan.yaml
(replaces the auto 'Wired connection 1') with a 192.168.100.1/32 link route,
so the dish's management address stays reachable from the router and — via
the existing !br0 masquerade — from the van LAN, regardless of which WAN
holds the default route.

New Cockpit "Starlink" card queries the dish's gRPC API (:9200, get_status
via grpcurl — not packaged in apt, deploy.sh warns when missing) inside the
existing single batched spawn: online/obstructed/outage pill, alerts,
uptime, sw version, PoP latency, down/up throughput, obstruction %.
Sentinels distinguish adapter-absent / no-grpcurl / dish-unreachable.

Gotcha captured while cutting over: a WAN profile without ipv4.route-metric
makes NM re-assert its DHCP default (metric ~101) against van-failover's
enforce_route pruning every cycle; the profile metric must match (failover's
set_profile_metric does this on health transitions).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 07:06:43 -04:00
Andreas WredeandClaude Fable 5 2da97d26f4 thermal: watch NVMe/RP1 temps, fan stall, and undervoltage on the Pi 5
van-thermal grows sensor kinds beyond plain temperatures: "fan" alerts when
pwmfan is commanded on (pwm > 0) but reads 0 RPM (warn on first sample, crit
if it persists — 0 RPM with pwm 0 is just the firmware idling a cool SoC),
and "undervolt" goes crit on the live rpi_volt alarm plus a sticky warn off
the firmware's latched since-boot bit, so dips shorter than the 10s sample
interval still surface once. All kinds share the existing hysteresis /
journal / Pushover machinery (fan 🌀, undervolt ⚡).

Config adds nvme Composite (65/70), rp1_adc (80/85 — the RP1 die drives all
USB/eth I/O), fan, and undervolt to the existing cpu sensor. The CSV logger
now derives per-kind columns and self-rotates when the header changes; the
Cockpit Temps card renders RPM + pwm duty and undervoltage state alongside
the temperatures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 06:41:17 -04:00
Andreas WredeandClaude Fable 5 6fe683000d thermal+ha: don't lose boot-time pages; stop HA gracefully
van-thermal fires its first sample seconds after start, and the Pi often
boots hot — order after network-online.target and retry network failures
in the Pushover send path (3 attempts, 15s apart) so that page survives
DNS not being up yet. Non-200 responses still don't retry.

The Quadlet gets StopTimeout=120: podman's default 10s window SIGKILLed
HA mid-flush and the recorder complained about an unclean sqlite shutdown
on every start. Verified clean after a full stop/start cycle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 16:06:16 -04:00
Andreas WredeandClaude Fable 5 3add42a0cb ha: replace the HAOS VM with a native Podman Quadlet container
The fixed 2 GiB ha_van allocation starved the 4 GB Pi. HA Container now
runs on the host network (http://10.42.0.1:8123): ha/homeassistant.container
installs to /etc/containers/systemd/, config in /srv/homeassistant, host
D-Bus mounted for onboard Bluetooth (needs apparmor=unconfined — Ubuntu's
dbus-daemon mediates per AppArmor label and denies AddMatch to BlueZ —
plus NET_ADMIN/NET_RAW for habluetooth adapter recovery).

Drop the VM-era plumbing: the 10.42.0.50 dhcp-host pin becomes a
host-record for 10.42.0.1, and the legacy-URL DNAT + hairpin masquerade
go away entirely. ha_van.xml retired to git history.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 15:46:30 -04:00
Andreas WredeandClaude Fable 5 8ab5a4cbd8 ha: fresh HAOS install + VNC console for the HAOS CLI
The backup-restore from the x86 instance left Core stopped with the
Supervisor idle and healthy-looking — no restart, download, or disk
activity, and it survived a guest reboot. The old config was minimal,
so: fresh 18.1 image, configure from scratch (old disk kept aside as
*.old-restored). Add VNC graphics + virtio-gpu while the domain was
down: the HAOS CLI runs only on the graphical console (no serial
getty), so without this there is no way into a broken guest — view it
via Cockpit's Virtual Machines page (cockpit-machines now installed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 20:16:17 -04:00
Andreas WredeandClaude Fable 5 c40662361f ha: move the Home Assistant VM onto the Pi as an aarch64 guest
The x86 ha_van stayed behind on wayback at the port (9bf1420 dropped
ha/ and its DNAT/lease); with the hub moved and the Pi owning
10.42.0.0/24, HA follows. New domain XML: machine=virt + AAVMF EFI,
virtio-scsi HAOS 18.1 aarch64 image, 2 GiB (data restored from an HA
full backup — the x86 qcow2 can't cross architectures). Same MAC, so
the dnsmasq pin (10.42.0.50 / "homeassistant") and the legacy
10.42.0.1:8123 DNAT + br0 hairpin port back verbatim.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 19:19:56 -04:00
Andreas WredeandClaude Fable 5 7a92548114 cockpit: fix bridge EMFILE — batch status reads into one spawn, raise fd limit
The Python cockpit-bridge frees spawn-pipe fds only at GC time; the Van
Router page's ~12 cockpit.spawn calls every 5s saw-toothed the bridge to
its 1024-fd soft limit, so the polkit admin-escalation spawn failed with
"Too many files open" whenever it landed near a peak.

Two-sided fix:
- vanrouter.js gathers all read-only status in ONE `sh -c` spawn per
  refresh, sections delimited by @@vr:<name>@@ marker lines (~12x less
  pipe churn). Mutating actions unchanged.
- cockpit-session@.service drop-in raises LimitNOFILE to 65535
  (hard limit is 524288), installed by deploy.sh.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 18:32:24 -04:00
Andreas WredeandClaude Fable 5 7066207333 deploy: tolerate absent AP radios (hub not yet plugged in)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 17:06:13 -04:00