config.json is re-rendered every deploy, so credentials go in
/etc/van-router-mqtt/credentials.json (seeded empty if absent, never
overwritten). The service logs MQTT connect/refuse/disconnect state changes,
and --post-check warns when the broker refuses the service.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqhjP7nud5gdBg25zCiRk9
Live R5C run: after the Armbian catch-all .network was removed, networkd kept its
in-memory claim on the uplink port (two addresses, extra proto dhcp default route)
until `networkctl reload`. The helper now reloads networkd after the forward apply
and after the rollback re-apply (not fatal, not gating; logs the port state and a
"networkd still manages" note on the forward path), the AN_* time budget includes
it, and --post-check warns when the netplan matches but networkd still lists the
uplink port configured/configuring.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqhjP7nud5gdBg25zCiRk9
- ap/50-van-wan.yaml: revert the comment to "see STARLINK_IFACE in deploy.conf" so the
rendered file is byte-identical to the Pi 5's installed netplan (it otherwise reports
"differs" forever); note in deploy.sh next to render_wan_netplan.
- check 14: baseline 3d117e7, keep byte-identity for wan.conf, assert no /etc/vanlink.conf;
the failure branch no longer aborts the suite under pipefail.
- helper restore(): restart van-br0-setup (unit present, timeout 30, non-fatal) after the
re-apply, before the connectivity re-check; asserted in the rollback sandbox scenario.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqhjP7nud5gdBg25zCiRk9
I-1 helper logs diagnostics (nmcli, DHCP routers, networkctl -n 0, ip addr, ip route; each
under timeout 5) before a stage 1/2 restore, and the port's networkctl list line on success
(information only, never gating).
I-2 ap/nm-uplink-pending.conf -> /etc/NetworkManager/conf.d/zz-van-wan-pending.conf keeps NM
off STARLINK_IFACE while the WAN netplan is missing; installed before network-manager,
removed once the netplan matches, left as is when it differs (port already handed to NM).
apply_network backs it up as nm-pending.conf; the helper removes it + reloads NM conf right
before netplan generate/apply, restore() puts it back and reloads after the re-apply.
--wan-plan prints "uplink NM pending conf: ...".
I-3 nm_wan_state_netplan: "unreadable" (0600 file, non-root); --wan-plan and post-check say so
without warning.
I-4 apply_preflight_problems (+ hidden --apply-preflight, VANLINK_HOSTNAME test knob ignored as
root): port absent / conf of another host -> refused without --force. Stage 2 also needs
van-failover's own "proto static" default route on the port.
M-1 helper restarts van-br0-setup (if the unit exists) after the forward netplan apply.
M-3 nm_pkg_installed (dpkg-query Status) in deploy, --wan-plan and --apply-network.
M-5 header documents VANLINK_CONF; stale test comment fixed.
M-6 cellular_profile_exists ignores VANLINK_NM_DIR as root.
M-7 tests: NM-off files installed before network-manager; read-only sub-commands never apt-get.
M-8 an existing, differing WAN netplan is shown (redact_secrets now masks YAML password:) and
refused without --force.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqhjP7nud5gdBg25zCiRk9
Li3, NVMe watchdog, Home Assistant/ESPHome, Frigate and Cockpit are
installed, enabled and restarted only when their ENABLE_* flag is 1.
--diff shows what would change in /etc. A unit that fails to restart is
recorded as a warning instead of aborting the deploy.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
nftables.conf hardcoded the R5C network, van-br0-setup.service the R5C
interfaces and several configs wlan0/eth0. All now come from the host
config; the onboard LAN port is skipped when ONBOARD_LAN_IFACE is empty.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
SKIP_COCKPIT only existed in the deleted deploy.conf; under set -u the
cockpit block would abort with an unbound variable.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
deploy.conf is replaced by one versioned file per host plus an example.
deploy.sh validates the config, renders every template with a generic
token loop, refuses to install output with leftover @TOKENs and gains
--check/--templates. LAN_USB_IFACE is renamed LAN_IFACE.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
van-ap-dnsmasq.conf and 21-van-br0.network carry @TOKEN@ placeholders but
were installed unrendered. Armbian's netplan-generated 10-netplan-* file
matched the LAN port before 22-van-lan.network, so it was never bridged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Added podman to REQUIRED_PACKAGES for running homeassistant and esphome
containers on R5C.
Created docker/start-containers.sh script to start both containers with
persistent /srv volumes for configuration.
Ready for migration of homeassistant + esphome from 10.42.0 to 10.42.1.
Configs can be copied from old vanrouter at /srv/homeassistant and
/srv/esphome, then containers started on R5C.
Ports:
- Home Assistant: 8123 (accessed via --network=host)
- ESPHome: 6052
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
systemd-resolved listening on port 53 (even localhost-only) blocks
dnsmasq from binding to port 53 globally, causing DHCP to fail on reboot.
Created drop-in systemd-resolved-disable.conf to prevent the service from
listening on DNS. dnsmasq becomes the sole DNS+DHCP server on bridge br0.
Critical fix: without this, clients don't receive DHCP after reboot.
Verified: 5+ active DHCP leases, clients receiving 10.42.1.x addresses.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Moved all packages from OPTIONAL_PACKAGES to REQUIRED_PACKAGES:
- rsync, mosquitto, python3-paho-mqtt
- mobile-broadband-provider-info, zerotier-one
- gpsd, gpsd-clients
Deploy will now fail if any package is missing, ensuring all
van-* services have their dependencies available.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
RTL8188GU WiFi adapter (0bda:1a2b) ships in CDROM mode on initial connection.
Install usb-modeswitch package and config to automatically switch to network
mode on boot.
Required for 2.4GHz AP to function when using D-Link DWA-171 or similar
RTL8188GU-based adapters.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Move python3-gps, python3-bleak to REQUIRED_PACKAGES (needed for van-li3-battery,
van-gps-owntracks). Add modemmanager to REQUIRED_PACKAGES (provides mmcli for
van-sms-* services).
Deploy will now fail if these packages are missing, preventing silent failures
when GPS or SMS services are needed.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
systemd-networkd Bridge= directive wasn't reliably adding interfaces to br0
on Armbian. Created persistent systemd service to ensure enP2p33s0 and wlp1s0
are added to the bridge after boot, maintaining 10.42.1.0/24 network.
This ensures the gateway bridge stays configured even after R5C reboots.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Problem: wlan1 regulatory domain not set on boot, defaults to 2.4GHz-only mode,
preventing channel 149 (5GHz) from being available. hostapd fails on startup.
Solution:
1. Modified regdomain.service to:
- Run BEFORE hostapd/hostapd-2g services (Before= directive)
- Run earlier in boot (After= sysinit.target)
- RemainAfterExit=yes to persist setting
2. Added 80-regdomain.rules udev rule:
- Triggers on regulatory device changes
- Executes 'iw reg set CA' automatically
3. Added hostapd drop-in config:
- Explicitly requires regdomain.service
- Adds Wants= to ensure it starts
This ensures the regulatory domain is set BEFORE hostapd attempts to use
channel 149, fixing the boot-time AP failure loop.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Problem: R5C spontaneously rebooted due to hardware watchdog timeout
Cause 1: van-gps-owntracks in infinite restart loop (MQTT connect failed)
Cause 2: mosquitto only listening on localhost, so clients couldn't connect
Solution:
- Add restart limits to van-gps-owntracks and van-li3-battery services
(max 3 restarts per 5 minutes prevents infinite restart loops)
- Configure mosquitto to listen on all interfaces (0.0.0.0:1883)
- Deploy mosquitto listener config via deploy.sh
- Restart mosquitto during deploy to apply config changes
This prevents the system from hitting the 11-second hardware watchdog timeout
when services crash and systemd auto-restarts them indefinitely.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
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>
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>
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>
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
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>
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>
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.
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>
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>
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>
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.
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.
- 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>