Homeassistant core on android

Great work, @ondyn - though, after reviewing your code changes, I came to an “ugly” but interesting alternitve: what if we just replace uv with a wrapper script - hehe? So I asked ChatGPT to build one - well, it took longer than expected - stupid A.I., but here it is and it works great. I also had IPv6 issues on zeroconf with failing weather service and others, too…

precedence ::ffff:0:0/96  100

in /etc/gai.conf didn’t work either, so I patched lib/python3.13/site-packages/zeroconf/_utils/net.py in ip6_addresses_to_indexes to swallow wired, failing adapters from

    for iface in interfaces:
        if isinstance(iface, int):
            result.append((interface_index_to_ip6_address(adapters, iface), iface))  # type: ignore[arg-type]
        elif isinstance(iface, str) and ipaddress.ip_address(iface).version == 6:
            result.append(ip6_to_address_and_index(adapters, iface))  # type: ignore[arg-type]

to

    for iface in interfaces:
        try:
            if isinstance(iface, int):
                result.append((interface_index_to_ip6_address(adapters, iface), iface))  # type: ignore[arg-type]
            elif isinstance(iface, str) and ipaddress.ip_address(iface).version == 6:
                result.append(ip6_to_address_and_index(adapters, iface))  # type: ignore[arg-type]
        except:
            pass

Anyway, latest, untouched HA-Core is now running fine, when I install homeassistant and before first launch call this vu_wrapper.sh to direct all uv pip things to pip when HA is trying to install things on first launch :slight_smile:

uv_wrapper.sh:

#!/bin/sh
#
# A single POSIX-compatible script that:
#  1) On "install": replaces the system uv with this wrapper
#  2) On "uninstall": restores the original uv
#  3) Otherwise acts as the uv wrapper itself

# Current script path
MANAGER_SCRIPT="$0"

# Try to find the "real" uv in PATH
SYSTEM_UV="$(command -v uv 2>/dev/null || true)"

show_usage() {
  echo "Usage:"
  echo "  uv install       → replace the system uv with this wrapper"
  echo "  uv uninstall     → restore the original uv"
  echo "  uv pip [...]     → call pip"
  echo "  uv venv [...]    → call python3 -m venv"
  echo "  uv install ...   → same as pip install ..."
  echo "  uv uninstall ... → same as pip uninstall ..."
  echo "  uv [other]       → forward to original uv_disabled"
  exit 0
}

# ----------------------------
# 1) Handle "install"
# ----------------------------
if [ "$1" = "install" ]; then
  # Check if the wrapper is already installed (presence of uv_disabled indicates an active wrapper)
  if [ -n "$SYSTEM_UV" ] && [ -f "${SYSTEM_UV}_disabled" ]; then
    echo "⚠️  It appears the wrapper is already installed at $SYSTEM_UV"
    echo "    Original binary: ${SYSTEM_UV}_disabled"
    exit 1
  fi

  if [ -z "$SYSTEM_UV" ]; then
    echo "❌ Error: No 'uv' found in PATH. Cannot install wrapper."
    exit 1
  fi

  # If the discovered uv equals this script, we're already running as uv.
  # Possibly the user manually placed this script at /usr/local/bin/uv, etc.
  # => If so, either it's already installed or we can't reinstall
  if [ "$SYSTEM_UV" = "$MANAGER_SCRIPT" ]; then
    echo "⚠️  This script is already located at $SYSTEM_UV."
    echo "    If you intended to install, place it elsewhere and run './uv_manager.sh install'."
    exit 1
  fi

  # Optional check: if uv is a symlink
  if [ -L "$SYSTEM_UV" ]; then
    echo "⚠️  Refusing to overwrite a symlink: $SYSTEM_UV."
    echo "    Please replace the real binary manually if needed."
    exit 1
  fi

  # Rename the discovered uv to uv_disabled
  echo "🔧 Installing wrapper: renaming $SYSTEM_UV → ${SYSTEM_UV}_disabled"
  mv "$SYSTEM_UV" "${SYSTEM_UV}_disabled" 2>/dev/null || {
    echo "❌ Could not rename $SYSTEM_UV. Try running with sudo?"
    exit 1
  }

  echo "🔧 Copying this script to $SYSTEM_UV..."
  cp "$MANAGER_SCRIPT" "$SYSTEM_UV" || {
    echo "❌ Could not copy wrapper to $SYSTEM_UV. Check permissions."
    mv "${SYSTEM_UV}_disabled" "$SYSTEM_UV" 2>/dev/null || true
    exit 1
  }
  chmod +x "$SYSTEM_UV"
  echo "✅ Wrapper installed successfully!"
  echo "   Original uv binary saved as: ${SYSTEM_UV}_disabled"
  exit 0
fi

# ----------------------------
# 2) Handle "uninstall"
# ----------------------------
if [ "$1" = "uninstall" ]; then
  if [ -z "$SYSTEM_UV" ] || [ ! -f "${SYSTEM_UV}_disabled" ]; then
    echo "⚠️  The wrapper does not seem to be installed (no uv_disabled found)."
    exit 1
  fi

  # Check if we really are the wrapper script
  if [ "$SYSTEM_UV" != "$MANAGER_SCRIPT" ]; then
    echo "⚠️  The uv in PATH ($SYSTEM_UV) is not the same as this script ($MANAGER_SCRIPT)."
    echo "    Possibly the wrapper is installed elsewhere, or the system is inconsistent."
    exit 1
  fi

  echo "🔁 Restoring original uv from ${SYSTEM_UV}_disabled → $SYSTEM_UV"
  rm -f "$SYSTEM_UV" || {
    echo "❌ Could not remove wrapper at $SYSTEM_UV."
    exit 1
  }
  mv "${SYSTEM_UV}_disabled" "$SYSTEM_UV" || {
    echo "❌ Could not restore original uv."
    exit 1
  }
  chmod +x "$SYSTEM_UV"
  echo "✅ Original uv restored."
  exit 0
fi

# ----------------------------
# 3) On any other call with arguments,
#    we assume we're running in "wrapper mode"
# ----------------------------

# If user calls "uv" with no arguments → show usage
if [ $# -eq 0 ]; then
  show_usage
fi

UV_DISABLED="${MANAGER_SCRIPT}_disabled"

# (a) uv pip ...
if [ "$1" = "pip" ]; then
  shift
  ARGS=""
  SKIP_NEXT=0
  for arg in "$@"; do
    if [ "$SKIP_NEXT" -eq 1 ]; then
      SKIP_NEXT=0
      continue
    fi
    if [ "$arg" = "--index-strategy" ]; then
      SKIP_NEXT=1
      continue
    fi
    ARGS="$ARGS \"$arg\""
  done
  # shellcheck disable=SC2086
  eval exec pip $ARGS

# (b) uv venv ...
elif [ "$1" = "venv" ]; then
  shift
  if [ -z "$1" ]; then
    exec python3 -m venv .venv
  else
    exec python3 -m venv "$@"
  fi

# (c) uv install => pip install ...
elif [ "$1" = "install" ]; then
  shift
  exec pip install "$@"

# (d) uv uninstall => pip uninstall ...
elif [ "$1" = "uninstall" ]; then
  shift
  exec pip uninstall "$@"

# (e) All other commands => forward to uv_disabled
else
  if [ -x "$UV_DISABLED" ]; then
    exec "$UV_DISABLED" "$@"
  else
    echo "❌ Original uv not found at: $UV_DISABLED"
    echo "   Possibly it's already uninstalled or the system is inconsistent."
    exit 1
  fi
fi

My final installation steps on an old Asus ZenFone 4 (ZE554KL) with stock un-rooted Android 8 is:

  1. Install F-Droid from homepage
  2. In F-Droid install Termux and Termux:Boot
  3. Launch Termux:Boot and then Termux and enter commands:
pkg update
pkg upgrade -y
pkg install openssh -y
passwd
sshd
# continue in ssh

pkg install proot-distro -y
proot-distro install debian
proot-distro login debian
apt update && apt upgrade -y

# follow https://github.com/pascallj/python3.13-backport to install python 3.13
apt install wget -y && wget -qO- https://pascalroeleven.nl/deb-pascalroeleven.gpg > /etc/apt/keyrings/deb-pascalroeleven.gpg

cat <<EOF | tee /etc/apt/sources.list.d/pascalroeleven.sources
Types: deb
URIs: http://deb.pascalroeleven.nl/python3.13
Suites: bookworm-backports
Components: main
Signed-By: /etc/apt/keyrings/deb-pascalroeleven.gpg
EOF

apt update && apt install python3.13 python3.13-dev python3.13-venv build-essential ffmpeg libisal-dev libturbojpeg0 libpcap0.8 -y

# install HA-Core
python3.13 -m venv hass
source hass/bin/activate
pip install --upgrade pip wheel
pip install homeassistant home-assistant-frontend

# patch lib/python3.13/site-packages/zeroconf/_utils/net.py in ip6_addresses_to_indexes to fix USB, DHPC and zeroconf errors
python -c "import zeroconf._utils.net as m; p=m.__file__; o=open(p).read(); old=('    for iface in interfaces:\\n        if isinstance(iface, int):\\n            result.append((interface_index_to_ip6_address(adapters, iface), iface))  # type: ignore[arg-type]\\n        elif isinstance(iface, str) and ipaddress.ip_address(iface).version == 6:\\n            result.append(ip6_to_address_and_index(adapters, iface))  # type: ignore[arg-type]'); new=('    for iface in interfaces:\\n        try:\\n            if isinstance(iface, int):\\n                result.append((interface_index_to_ip6_address(adapters, iface), iface))  # type: ignore[arg-type]\\n            elif isinstance(iface, str) and ipaddress.ip_address(iface).version == 6:\\n                result.append(ip6_to_address_and_index(adapters, iface))  # type: ignore[arg-type]\\n        except:\\n            pass'); patched=o.replace(old,new); import sys; open(p,'w').write(patched); print('No changes made. Possibly already patched or code differs:', p) if patched==o else print('Successfully patched:', p)"

# run HA
hass -v

# wired thing was, now I didn't need the vu_wrapper.sh script anymore, updates to packages or the installation order might have fixed it - haha :-)

# Finally, create boot scripts
apt install daemonize -y
mkdir -p ~/.termux/boot
echo '#!/data/data/com.termux/files/usr/bin/bash
  termux-wake-lock
  sshd
' > ~/.termux/boot/01-sshd.sh && chmod +x ~/.termux/boot/01-sshd.sh
echo '#!/data/data/com.termux/files/usr/bin/bash
daemonize $(which proot-distro) login debian -- bash -c "
  cd ~
  source hass/bin/activate
  while true; do  
    hass
  done
"' > ~/.termux/boot/02-hass.sh && chmod +x ~/.termux/boot/02-hass.sh