mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 19:07:04 +01:00
Home Assistant setup + allow-homeassistant firewall option (Viomi V3 LAN control)
setup_homeassistant.sh runs a digest-pinned Home Assistant container with host networking and installs hass-xiaomi-miot v1.1.5, so the Viomi V3 (viomi.vacuum.v13) is controllable over the LAN from the HA companion app. setup_wireguard_ssh.sh gains a persisted ALLOW_HOMEASSISTANT flag that opens tcp/8123 to the LAN subnet and wg0 only. Prunes the oldest evidence file to stay under the 60-file cap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CGnk48tUeJteJWEQ7UhKDd
This commit is contained in:
parent
6fec606368
commit
07810fb270
@ -0,0 +1,16 @@
|
||||
{
|
||||
"title": "Home Assistant for LAN control of the Viomi V3 robot vacuum",
|
||||
"objective": "Deploy Home Assistant on the PC (Docker, host networking, pinned image) with the hass-xiaomi-miot integration so viomi.vacuum.v13 can be started and docked over the LAN from a FOSS UI, and open tcp/8123 to the LAN and wg0 only through the existing single-source-of-truth firewall script. Success: start and dock work from the Home Assistant Android app on the Pixel 6a, and setup_wireguard_ssh.sh verify passes.",
|
||||
"acceptance_criteria": [
|
||||
"setup_homeassistant.sh setup is idempotent and ends with the HA API answering on 127.0.0.1:8123",
|
||||
"setup_homeassistant.sh status reports container, API, xiaomi_miot version and the 8123 firewall rule",
|
||||
"setup_wireguard_ssh.sh allow-homeassistant persists ALLOW_HOMEASSISTANT and renders exactly two 8123 accept rules (LAN subnet, wg0)",
|
||||
"setup_wireguard_ssh.sh verify passes on the host after the change"
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Exposing Home Assistant to the internet",
|
||||
"Rooting or reflashing the robot",
|
||||
"Automations, dashboards or other devices"
|
||||
],
|
||||
"verifier": "vmbox run of allow-homeassistant + verify; host setup_wireguard_ssh.sh verify; setup_homeassistant.sh setup/status; on-device start/dock from the HA companion app"
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
{
|
||||
"intent": "Run Home Assistant on the PC so the Viomi V3 robot vacuum (viomi.vacuum.v13) can be controlled over the LAN from a FOSS UI, including the Home Assistant Android app, with tcp/8123 reachable from the LAN and WireGuard only.",
|
||||
"scope": [
|
||||
"linux_configuration/features/setup_homeassistant.sh (new)",
|
||||
"linux_configuration/features/setup_wireguard_ssh.sh, lib/wg_sshd.sh, lib/wg_firewall.sh: persisted ALLOW_HOMEASSISTANT flag + allow-homeassistant subcommand",
|
||||
"Non-goal: exposing Home Assistant to the internet; rooting the robot"
|
||||
],
|
||||
"changes": [
|
||||
"setup_homeassistant.sh writes ~/services/homeassistant/docker-compose.yml (host networking, image pinned by digest to 2026.9.4), installs al-one/hass-xiaomi-miot v1.1.5 into config/custom_components, starts the container and waits for the API; `status` self-diagnoses container, API, component version and firewall rule",
|
||||
"render_nftables_ruleset emits `ip saddr <LAN> tcp dport 8123 accept` and `iifname wg0 tcp dport 8123 accept` when ALLOW_HOMEASSISTANT=true; allow_homeassistant() persists the flag and applies it like allow_dns"
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "vm new hafw; sudo bash ~/lc/features/setup_wireguard_ssh.sh allow-homeassistant; nft list chain inet filter input; setup_wireguard_ssh.sh verify",
|
||||
"result": "pass",
|
||||
"evidence": "vmbox guest: both 8123 rules present in live input chain, ALLOW_HOMEASSISTANT=\"true\" persisted, verify: /etc/nftables.conf matches generator, forward chain lets Docker through"
|
||||
},
|
||||
{
|
||||
"command": "render_nftables_ruleset into tmp; diff vs /etc/nftables.conf; nft -c -f; then host apply + sudo setup_wireguard_ssh.sh verify",
|
||||
"result": "pass",
|
||||
"evidence": "host diff was exactly the two 8123 lines; nft -c OK; verify: file matches generator and live chain. Applied via nft add rule (no flush) to avoid restarting dockerd under running containers"
|
||||
},
|
||||
{
|
||||
"command": "bash linux_configuration/features/setup_homeassistant.sh setup",
|
||||
"result": "pass",
|
||||
"evidence": "Installed xiaomi_miot v1.1.5; container started; HA answers on http://127.0.0.1:8123 (HTTP 401)"
|
||||
},
|
||||
{
|
||||
"command": "HA REST: POST /api/services/vacuum/start then vacuum/return_to_base on vacuum.viomi_v13_9f32_robot_cleaner",
|
||||
"result": "pass",
|
||||
"evidence": "state cleaning (miio status 5 Sweeping) then returning -> docked"
|
||||
},
|
||||
{
|
||||
"command": "Pixel 6a, HA companion minimal 2026.6.5: login to http://192.168.1.43:8123, tap play then dock on the robot card",
|
||||
"result": "pass",
|
||||
"evidence": "state cleaning after play; returning -> docked 56 s after dock tap"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"hass-xiaomi-miot issue #2677 (Developer-tools send_command broken since v1.1.0) is still open; vacuum start/dock verified working through entity services",
|
||||
"Host networking: HA listens on 0.0.0.0:8123; only the nftables input rules keep it LAN/wg0-only"
|
||||
],
|
||||
"rollback": [
|
||||
"docker compose -f ~/services/homeassistant/docker-compose.yml down; set ALLOW_HOMEASSISTANT=\"false\" in features/.wireguard_ssh.conf and re-run sudo setup_wireguard_ssh.sh allow-web (regenerates the ruleset)",
|
||||
"Validate: sudo setup_wireguard_ssh.sh verify passes and no dport 8123 rule remains"
|
||||
]
|
||||
}
|
||||
@ -1,51 +0,0 @@
|
||||
{
|
||||
"intent": "Cut the fixes/lib test suite's runtime from 110s to 37s so the commit/push cycle stops being dominated by it, without weakening a single assertion.",
|
||||
"scope": [
|
||||
"linux_configuration/scripts/single_use/fixes/lib/tests/lib_test_core.sh and the new lib_test_path.sh",
|
||||
"linux_configuration/scripts/single_use/fixes/lib/tests/run_all.sh",
|
||||
"linux_configuration/scripts/single_use/fixes/lib/thorium_repairs.sh (one timing seam)",
|
||||
"linux_configuration/scripts/single_use/fixes/lib/tests/thorium_harness.sh"
|
||||
],
|
||||
"changes": [
|
||||
"thorium_repairs.sh's hardcoded `sleep 4` is now `sleep ${THORIUM_STARTUP_WAIT:-4}`; the harness sets 0.2s. Three cases paid four real seconds each.",
|
||||
"_t_hide now builds ONE farm of every executable on PATH and hard-links it per tool set with `cp -al`, instead of walking PATH and creating ~13.5k symlinks for each distinct set",
|
||||
"The farm cache moved out of $TEST_TMPDIR into a run-scoped dir exported by run_all.sh, because each test file gets a fresh tmpdir and the cache never survived between files",
|
||||
"Split _t_hide and the farm into lib_test_path.sh; lib_test_core.sh had reached exactly 250 lines"
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "time bash linux_configuration/scripts/single_use/fixes/lib/tests/run_all.sh",
|
||||
"result": "pass",
|
||||
"evidence": "110.0s at session start -> 98.5s after the sleep seam -> 84.6s after the run-scoped cache -> 36.7s after the hard-linked farm. Exit 0 throughout."
|
||||
},
|
||||
{
|
||||
"command": "Per-file timing of the slowest files",
|
||||
"result": "pass",
|
||||
"evidence": "test_thorium_repairs_deep.sh 17041ms -> 5563ms. A single _t_hide farm build measured 4645ms, with ~16 distinct tool sets per run."
|
||||
},
|
||||
{
|
||||
"command": "bash run_all.sh | grep -c '^ OK' and '^ FAIL'",
|
||||
"result": "pass",
|
||||
"evidence": "645 OK, 0 FAIL. No assertion was deleted, skipped or weakened."
|
||||
},
|
||||
{
|
||||
"command": "Targeted re-check that the 'not installed' branches still hide",
|
||||
"result": "pass",
|
||||
"evidence": "install_pyroveil's missing-dependency cases and fix_aggressive's sqlite3-absent case all still pass; they assert on behaviour only reachable when the tool is genuinely unfindable, so a broken farm would fail them loudly."
|
||||
},
|
||||
{
|
||||
"command": "shellcheck + wc -l on both split files",
|
||||
"result": "pass",
|
||||
"evidence": "lib_test_core.sh 194 lines, lib_test_path.sh 67 lines, both clean with zero suppressions."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"`cp -al` hard-links symlinks; if a future _t_hide caller mutated the farm in place it would corrupt the shared copy. Callers only ever read it and the per-set copy is what gets the deletions.",
|
||||
"The shared farm assumes PATH does not change mid-run. _t_full_path restores the same original PATH, so the assumption holds for this suite.",
|
||||
"THORIUM_STARTUP_WAIT shortens a real timing check to 0.2s in tests; the stubbed browser sleeps 30s, so the process is still comfortably alive when kill -0 runs."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit; the suite returns to ~110s with identical assertions",
|
||||
"Re-run run_all.sh and confirm 645 OK / 0 FAIL is unchanged"
|
||||
]
|
||||
}
|
||||
@ -28,6 +28,13 @@ render_nftables_ruleset() {
|
||||
# saddr. Link-local only: :67 broadcasts never route in from the internet.
|
||||
dhcp_rule=$'\n\t\tudp sport 68 udp dport 67 accept'
|
||||
fi
|
||||
# Home Assistant (setup_homeassistant.sh): its UI/API on 8123 for the phone
|
||||
# app, from the LAN and over WireGuard only -- never the internet.
|
||||
local ha_rule=""
|
||||
if [[ ${ALLOW_HOMEASSISTANT:-false} == "true" ]]; then
|
||||
ha_rule=$'\n\t\tip saddr '"${LAN_SUBNET}"$' tcp dport 8123 accept'
|
||||
ha_rule+=$'\n\t\tiifname "'"${WG_IFACE}"$'" tcp dport 8123 accept'
|
||||
fi
|
||||
cat >"$target" <<EOF
|
||||
#!/usr/sbin/nft -f
|
||||
flush ruleset
|
||||
@ -46,7 +53,7 @@ table inet filter {
|
||||
udp dport ${WG_PORT} accept
|
||||
|
||||
iifname "${WG_IFACE}" tcp dport 22 accept
|
||||
ip saddr ${LAN_SUBNET} tcp dport 22 accept${web_rule}${dns_rule}
|
||||
ip saddr ${LAN_SUBNET} tcp dport 22 accept${web_rule}${dns_rule}${ha_rule}
|
||||
}
|
||||
chain forward {
|
||||
type filter hook forward priority 0; policy drop;
|
||||
|
||||
@ -204,6 +204,14 @@ allow_dns() {
|
||||
log_ok "Opened udp/tcp 53 from ${LAN_SUBNET:-the LAN} in the input chain (persisted -- future 'setup' re-runs keep this rule)."
|
||||
}
|
||||
|
||||
allow_homeassistant() {
|
||||
export ALLOW_HOMEASSISTANT=true
|
||||
save_config
|
||||
write_nftables_ruleset
|
||||
verify_nftables_then_apply
|
||||
log_ok "Opened tcp/8123 from ${LAN_SUBNET:-the LAN} and ${WG_IFACE} (persisted)."
|
||||
}
|
||||
|
||||
status_cmd() {
|
||||
echo "=== WireGuard ==="
|
||||
wg show 2>/dev/null || echo "(interface not up)"
|
||||
|
||||
177
linux_configuration/features/setup_homeassistant.sh
Executable file
177
linux_configuration/features/setup_homeassistant.sh
Executable file
@ -0,0 +1,177 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# setup_homeassistant.sh — Home Assistant on this PC, for LAN-local control of
|
||||
# the Viomi V3 robot vacuum (viomi.vacuum.v13) and anything added later.
|
||||
#
|
||||
# What it does (idempotent — safe to re-run):
|
||||
# - Writes ~/services/homeassistant/docker-compose.yml and runs HA with
|
||||
# network_mode: host (bridge containers have no egress on this host; see
|
||||
# setup_searxng.sh for the nftables reason), listening on :8123.
|
||||
# - Installs the al-one/hass-xiaomi-miot custom integration at a pinned tag
|
||||
# into config/custom_components/xiaomi_miot (MIoT over LAN; HA core's own
|
||||
# xiaomi_miio does not know viomi.vacuum.v13).
|
||||
# - Waits until the HTTP API answers.
|
||||
#
|
||||
# Reachability: 8123 is opened to the LAN and wg0 only, by
|
||||
# sudo setup_wireguard_ssh.sh allow-homeassistant
|
||||
# (the firewall's single source of truth). `status` checks that rule.
|
||||
#
|
||||
# Usage:
|
||||
# setup_homeassistant.sh [setup] Deploy (default).
|
||||
# setup_homeassistant.sh status Self-diagnose the deployment.
|
||||
# setup_homeassistant.sh help Show this help.
|
||||
#
|
||||
# Run as your normal user, not root.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||
readonly SCRIPT_DIR
|
||||
# shellcheck source=../lib/common.sh
|
||||
source "${SCRIPT_DIR}/../lib/common.sh"
|
||||
|
||||
# --- Configuration ----------------------------------------------------------
|
||||
readonly HA_PORT="8123"
|
||||
# home-assistant 2026.9.4 (newest stable on 2026-09-27), pinned by digest so a
|
||||
# re-run never silently upgrades. To upgrade: change digest + comment, re-run.
|
||||
readonly HA_IMAGE="ghcr.io/home-assistant/home-assistant@sha256:3e6710a7ab2a61311d9d899b719f6c3657791c63e8f4942cec4ebc42401d6b76"
|
||||
readonly HA_CONTAINER="homeassistant"
|
||||
# hass-xiaomi-miot newest stable on 2026-09-27. Issue #2677 (send_command via
|
||||
# Developer tools broken since v1.1.0) is still open; vacuum start/dock go
|
||||
# through entity services, not that path.
|
||||
readonly MIOT_VERSION="v1.1.5"
|
||||
readonly MIOT_REPO="al-one/hass-xiaomi-miot"
|
||||
readonly HA_DIR="${HOME}/services/homeassistant"
|
||||
readonly HA_COMPOSE="${HA_DIR}/docker-compose.yml"
|
||||
readonly HA_CONFIG="${HA_DIR}/config"
|
||||
readonly MIOT_DIR="${HA_CONFIG}/custom_components/xiaomi_miot"
|
||||
readonly MIOT_STAMP="${MIOT_DIR}/.installed_version"
|
||||
|
||||
TMP_DIR=""
|
||||
cleanup() {
|
||||
if [[ -n ${TMP_DIR} && -d ${TMP_DIR} ]]; then
|
||||
rm -rf "${TMP_DIR}"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
die() {
|
||||
log_error "$1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
usage() {
|
||||
grep -E '^#( |$)' "$0" | sed -E 's/^# ?//'
|
||||
exit "${1:-0}"
|
||||
}
|
||||
|
||||
preflight() {
|
||||
[[ ${EUID} -ne 0 ]] || die "Run as your normal user, not root."
|
||||
install_missing_pacman_packages docker docker-compose curl tar
|
||||
is_service_active docker || die "docker.service is not running."
|
||||
# A foreign listener on 8123 is fatal; our own container is a re-run.
|
||||
if ss -ltn 2>/dev/null | grep -qE "[:.]${HA_PORT}[[:space:]]" &&
|
||||
! docker ps --format '{{.Names}}' | grep -qx "${HA_CONTAINER}"; then
|
||||
die "Port ${HA_PORT} is taken by something other than ${HA_CONTAINER}."
|
||||
fi
|
||||
}
|
||||
|
||||
write_compose() {
|
||||
mkdir -p "${HA_CONFIG}"
|
||||
cat >"${HA_COMPOSE}" <<EOF
|
||||
# Managed by setup_homeassistant.sh — do not edit by hand.
|
||||
services:
|
||||
homeassistant:
|
||||
container_name: ${HA_CONTAINER}
|
||||
image: ${HA_IMAGE}
|
||||
network_mode: host
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- TZ=Europe/Warsaw
|
||||
volumes:
|
||||
- ${HA_CONFIG}:/config
|
||||
EOF
|
||||
log_ok "Wrote ${HA_COMPOSE}"
|
||||
}
|
||||
|
||||
install_miot() {
|
||||
if [[ -f ${MIOT_STAMP} && "$(<"${MIOT_STAMP}")" == "${MIOT_VERSION}" ]]; then
|
||||
log_ok "xiaomi_miot ${MIOT_VERSION} already installed."
|
||||
return 0
|
||||
fi
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
curl -fsSL "https://github.com/${MIOT_REPO}/archive/refs/tags/${MIOT_VERSION}.tar.gz" |
|
||||
tar -xz -C "${TMP_DIR}"
|
||||
local src
|
||||
src="$(find "${TMP_DIR}" -maxdepth 3 -type d -path '*/custom_components/xiaomi_miot' | head -1)"
|
||||
[[ -n ${src} ]] || die "custom_components/xiaomi_miot not found in ${MIOT_VERSION} tarball."
|
||||
rm -rf "${MIOT_DIR}"
|
||||
mkdir -p "$(dirname "${MIOT_DIR}")"
|
||||
cp -r "${src}" "${MIOT_DIR}"
|
||||
printf '%s' "${MIOT_VERSION}" >"${MIOT_STAMP}"
|
||||
log_ok "Installed xiaomi_miot ${MIOT_VERSION} into ${MIOT_DIR}"
|
||||
}
|
||||
|
||||
wait_for_api() {
|
||||
local code="" i
|
||||
for ((i = 0; i < 60; i++)); do
|
||||
code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${HA_PORT}/api/" || true)"
|
||||
# 401 = API up and demanding auth; 200 = onboarding not done yet.
|
||||
[[ ${code} == 401 || ${code} == 200 ]] && break
|
||||
sleep 2
|
||||
done
|
||||
[[ ${code} == 401 || ${code} == 200 ]] || die "HA API did not come up (last HTTP ${code:-none})."
|
||||
log_ok "HA answers on http://127.0.0.1:${HA_PORT} (HTTP ${code})."
|
||||
}
|
||||
|
||||
setup_cmd() {
|
||||
preflight
|
||||
write_compose
|
||||
install_miot
|
||||
docker compose -f "${HA_COMPOSE}" up -d
|
||||
# A freshly copied custom component is only loaded at start.
|
||||
docker restart "${HA_CONTAINER}" >/dev/null
|
||||
wait_for_api
|
||||
log_info "Open http://<this-PC-LAN-IP>:${HA_PORT} from the phone app."
|
||||
}
|
||||
|
||||
status_cmd() {
|
||||
local rc=0 state code
|
||||
state="$(docker inspect -f '{{.State.Status}}' "${HA_CONTAINER}" 2>/dev/null || echo absent)"
|
||||
if [[ ${state} == running ]]; then
|
||||
log_ok "container: ${state}"
|
||||
else
|
||||
log_error "container: ${state}"
|
||||
rc=1
|
||||
fi
|
||||
code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${HA_PORT}/api/" || true)"
|
||||
if [[ ${code} == 401 || ${code} == 200 ]]; then
|
||||
log_ok "API: HTTP ${code}"
|
||||
else
|
||||
log_error "API: HTTP ${code:-none}"
|
||||
rc=1
|
||||
fi
|
||||
if [[ -f ${MIOT_STAMP} ]]; then
|
||||
log_ok "xiaomi_miot: $(<"${MIOT_STAMP}") (want ${MIOT_VERSION})"
|
||||
else
|
||||
log_error "xiaomi_miot: not installed"
|
||||
rc=1
|
||||
fi
|
||||
if sudo -n nft list chain inet filter input 2>/dev/null | grep -q "dport ${HA_PORT} accept"; then
|
||||
log_ok "firewall: tcp/${HA_PORT} open to LAN/wg0"
|
||||
else
|
||||
log_warn "firewall: no tcp/${HA_PORT} rule (or no sudo) — sudo setup_wireguard_ssh.sh allow-homeassistant"
|
||||
fi
|
||||
return "${rc}"
|
||||
}
|
||||
|
||||
main() {
|
||||
case "${1:-setup}" in
|
||||
setup) setup_cmd ;;
|
||||
status) status_cmd ;;
|
||||
help | -h | --help) usage 0 ;;
|
||||
*) usage 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@ -39,6 +39,7 @@ DUCKDNS_TOKEN="${DUCKDNS_TOKEN:-}"
|
||||
LAN_SUBNET="${LAN_SUBNET:-}"
|
||||
export ALLOW_WEB="${ALLOW_WEB:-false}"
|
||||
export ALLOW_DNS="${ALLOW_DNS:-false}"
|
||||
export ALLOW_HOMEASSISTANT="${ALLOW_HOMEASSISTANT:-false}"
|
||||
|
||||
die() {
|
||||
log_error "$1"
|
||||
@ -52,6 +53,7 @@ DUCKDNS_TOKEN="${DUCKDNS_TOKEN}"
|
||||
LAN_SUBNET="${LAN_SUBNET}"
|
||||
ALLOW_WEB="${ALLOW_WEB}"
|
||||
ALLOW_DNS="${ALLOW_DNS}"
|
||||
ALLOW_HOMEASSISTANT="${ALLOW_HOMEASSISTANT}"
|
||||
EOF
|
||||
chmod 600 "$CONFIG_FILE"
|
||||
}
|
||||
@ -91,7 +93,7 @@ main() {
|
||||
# off -- exec sudo "$0" "$@" inside require_root must re-launch with the
|
||||
# subcommand still present, or sudo would silently run with no args.
|
||||
case "$cmd" in
|
||||
setup | add-peer | revoke | allow-web | allow-dns | verify | install-verify)
|
||||
setup | add-peer | revoke | allow-web | allow-dns | allow-homeassistant | verify | install-verify)
|
||||
require_root "$@"
|
||||
;;
|
||||
esac
|
||||
@ -128,6 +130,10 @@ main() {
|
||||
allow_dns
|
||||
install_verify_units
|
||||
;;
|
||||
allow-homeassistant)
|
||||
allow_homeassistant
|
||||
install_verify_units
|
||||
;;
|
||||
verify)
|
||||
verify_nft
|
||||
;;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user