diff --git a/README.md b/README.md index 8373c1b..f0ad2e3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # anker-shelly-bridge

- Live monitor dashboard showing battery level, solar input, grid input and load for an Anker SOLIX F3000, with Shelly switch states and a rolling history chart + Live monitor dashboard showing battery level, solar input, grid input and load for an Anker SOLIX F3000, with Shelly switch states and a rolling history chart

Automate a Shelly smart plug from live Anker SOLIX telemetry. @@ -35,8 +35,9 @@ cd anker-shelly-bridge On Windows, double-click `start.bat`. -That is the whole install. `start.sh` creates its own Python environment, -installs everything, and hands over to a guided setup with eight steps: +That is the whole install. `start.sh` builds a Python environment for this +project at `~/solix-automation/venv`, installs everything into it, and hands +over to a guided setup with eight steps: 1. **Dependencies**: installs whatever is missing 2. **Anker account**: your login, stored locally with owner-only permissions @@ -54,6 +55,10 @@ Nothing touches your hardware until step 8 asks. Ctrl-C is safe at any point. Needs Python 3.12+ and git. If they are missing, `start.sh` tells you how to install them for your platform. +Everything the project needs lives in its own environment, so installing or +removing packages elsewhere on the machine cannot break your automation. The +background service is pinned to that same interpreter. + --- ## What it does diff --git a/solixauto/service.py b/solixauto/service.py index d292823..bcde3c7 100644 --- a/solixauto/service.py +++ b/solixauto/service.py @@ -23,6 +23,22 @@ def service_label(profile_name): return f"com.solixauto.{profile_name}" +def preferred_python(): + candidates = [ + paths.BASE_DIR / "venv" / "bin" / "python", + paths.BASE_DIR / "venv" / "Scripts" / "python.exe", + ] + for candidate in candidates: + if candidate.exists(): + probe = subprocess.run( + [str(candidate), "-c", "import anker_solix_api.api"], + capture_output=True, + ) + if probe.returncode == 0: + return str(candidate) + return sys.executable + + def script_path(): if sys.argv and sys.argv[0]: return str(Path(sys.argv[0]).resolve()) @@ -84,7 +100,7 @@ def launchd_plist(profile_name): ProgramArguments - {sys.executable} + {preferred_python()} {script} run {profile_name} @@ -125,7 +141,7 @@ After=network-online.target [Service] Type=simple -ExecStart={sys.executable} {script} run {profile_name} --quiet +ExecStart={preferred_python()} {script} run {profile_name} --quiet WorkingDirectory={Path(script).parent} {env_lines}Restart=always RestartSec=60 @@ -142,7 +158,7 @@ def windows_instructions(profile_name): others. Use Task Scheduler: schtasks /create /tn "{label}" /sc onlogon /rl highest ^ - /tr "\\"{sys.executable}\\" \\"{script}\\" run {profile_name} --quiet" + /tr "\\"{preferred_python()}\\" \\"{script}\\" run {profile_name} --quiet" To remove it: diff --git a/start.bat b/start.bat index f652401..9d45ca9 100644 --- a/start.bat +++ b/start.bat @@ -1,35 +1,106 @@ @echo off -setlocal +setlocal enabledelayedexpansion cd /d "%~dp0" +if "%SOLIXAUTO_HOME%"=="" ( + set "DATA_DIR=%USERPROFILE%\solix-automation" +) else ( + set "DATA_DIR=%SOLIXAUTO_HOME%" +) +set "VENV_DIR=%DATA_DIR%\venv" +set "VENV_PYTHON=%VENV_DIR%\Scripts\python.exe" + echo. echo ============================================================== -echo Anker SOLIX to Shelly automation - setup +echo Anker SOLIX to Shelly automation echo ============================================================== echo. -set PYTHON= - -for %%P in (py python) do ( - if not defined PYTHON ( - where %%P >nul 2>&1 && set PYTHON=%%P +if exist "%VENV_PYTHON%" ( + "%VENV_PYTHON%" -c "import anker_solix_api.api, yaml, aiohttp" >nul 2>&1 + if not errorlevel 1 ( + echo Using: %VENV_PYTHON% + echo. + "%VENV_PYTHON%" "%~dp0solixauto.py" setup + goto :end ) ) -if not defined PYTHON ( - echo No Python found on PATH. - echo Install Python 3.12 or newer from https://www.python.org/downloads/ +set "BASE=" +for %%P in (py python) do ( + if not defined BASE ( + where %%P >nul 2>&1 && set "BASE=%%P" + ) +) + +if not defined BASE ( + echo Python 3.12 or newer is required and was not found on PATH. + echo Install it from https://www.python.org/downloads/ echo Tick "Add python.exe to PATH" during installation, then run this again. echo. pause exit /b 1 ) -echo Using: %PYTHON% +where git >nul 2>&1 +if errorlevel 1 ( + echo Git is required to fetch the Anker library, and was not found. + echo Install it from https://git-scm.com/download/win + echo. + pause + exit /b 1 +) + +echo This project runs in its own Python environment, so that installing or +echo removing packages elsewhere on this machine cannot break your automation. echo. +echo It will be created at: +echo %VENV_DIR% +echo. +set /p REPLY="Set it up now? [Y/n]: " +if /i "%REPLY%"=="n" ( + echo Nothing was changed. + goto :end +) -%PYTHON% "%~dp0solixauto.py" setup +echo. +echo Creating the environment... +if not exist "%DATA_DIR%" mkdir "%DATA_DIR%" +if exist "%VENV_DIR%" rmdir /s /q "%VENV_DIR%" +%BASE% -m venv "%VENV_DIR%" +if not exist "%VENV_PYTHON%" ( + echo Could not create a virtual environment at %VENV_DIR% + pause + exit /b 1 +) + +echo Installing packages. This takes a minute. +echo. +"%VENV_PYTHON%" -m pip install --upgrade pip --quiet +"%VENV_PYTHON%" -m pip install -r "%~dp0requirements.txt" --quiet +"%VENV_PYTHON%" -m pip install "git+https://github.com/thomluther/anker-solix-api.git" --quiet +"%VENV_PYTHON%" -m pip install aiofiles cryptography paho-mqtt --quiet + +"%VENV_PYTHON%" -c "import anker_solix_api.api" >nul 2>&1 +if errorlevel 1 ( + echo. + echo The install finished but the Anker library still will not import. + echo Run this to see the error: + echo "%VENV_PYTHON%" -c "import anker_solix_api.api" + pause + exit /b 1 +) + +echo. +echo Environment ready. +echo. +echo From now on, run commands with: +echo "%VENV_PYTHON%" solixauto.py ^ +echo. +"%VENV_PYTHON%" "%~dp0solixauto.py" setup + +:end echo. pause diff --git a/start.sh b/start.sh index 5c1c09e..fc9b7d4 100755 --- a/start.sh +++ b/start.sh @@ -4,51 +4,35 @@ set -euo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$HERE" -VENV_DIR="${SOLIXAUTO_VENV:-$HOME/solix-automation/venv}" +DATA_DIR="${SOLIXAUTO_HOME:-$HOME/solix-automation}" +VENV_DIR="${SOLIXAUTO_VENV:-$DATA_DIR/venv}" +VENV_PYTHON="$VENV_DIR/bin/python" REPO_URL="https://github.com/thomluther/anker-solix-api.git" echo echo "==============================================================" -echo " Anker SOLIX to Shelly automation - setup" +echo " Anker SOLIX to Shelly automation" echo "==============================================================" echo works() { - [ -x "$1" ] && "$1" -c "import anker_solix_api.api" >/dev/null 2>&1 + [ -x "$1" ] && "$1" -c "import anker_solix_api.api, yaml, aiohttp" >/dev/null 2>&1 } -usable_python() { - local candidates=( - "$VENV_DIR/bin/python" - "$HOME/anker-solix-mqtt/venv/bin/python" - "$HERE/venv/bin/python" - ) - for candidate in "${candidates[@]}"; do - if works "$candidate"; then - echo "$candidate" - return 0 - fi - done - for name in python3.14 python3.13 python3.12 python3; do - if command -v "$name" >/dev/null 2>&1 && works "$(command -v "$name")"; then - command -v "$name" - return 0 - fi - done - return 1 -} +if works "$VENV_PYTHON"; then + echo "Using: $VENV_PYTHON" + echo + exec "$VENV_PYTHON" "$HERE/solixauto.py" setup +fi -base_python() { - if [ -x "$VENV_DIR/bin/python" ]; then - echo "$VENV_DIR/bin/python" - return 0 - fi +find_base_python() { for name in python3.14 python3.13 python3.12 python3; do if command -v "$name" >/dev/null 2>&1; then - local version - version="$("$name" -c 'import sys; print(sys.version_info[0]*100+sys.version_info[1])' 2>/dev/null || echo 0)" + local path version + path="$(command -v "$name")" + version="$("$path" -c 'import sys; print(sys.version_info[0]*100+sys.version_info[1])' 2>/dev/null || echo 0)" if [ "$version" -ge 312 ]; then - command -v "$name" + echo "$path" return 0 fi fi @@ -56,18 +40,7 @@ base_python() { return 1 } -PYTHON="$(usable_python || true)" - -if [ -n "${PYTHON:-}" ]; then - echo "Using Python: $PYTHON" - echo - exec "$PYTHON" "$HERE/solixauto.py" setup -fi - -echo "Nothing on this machine can talk to Anker yet." -echo - -BASE="$(base_python || true)" +BASE="$(find_base_python || true)" if [ -z "${BASE:-}" ]; then echo "Python 3.12 or newer is required and was not found." @@ -76,8 +49,10 @@ if [ -z "${BASE:-}" ]; then echo "Install it with: brew install python@3.13" elif command -v apt >/dev/null 2>&1; then echo "Install it with: sudo apt install python3 python3-venv python3-pip git" + elif command -v dnf >/dev/null 2>&1; then + echo "Install it with: sudo dnf install python3 python3-pip git" else - echo "Install Python 3.12+ from https://www.python.org/downloads/" + echo "Install Python 3.12 or newer from https://www.python.org/downloads/" fi echo echo "Then run this script again." @@ -90,18 +65,28 @@ if ! command -v git >/dev/null 2>&1; then if [ "$(uname)" = "Darwin" ]; then echo "Install the Xcode command line tools: xcode-select --install" else - echo "Install git with your package manager, for example: sudo apt install git" + echo "Install git with your package manager, for example:" + echo " sudo apt install git" fi echo echo "Then run this script again." exit 1 fi -echo "This will create a self-contained Python environment and install" -echo "everything needed. Nothing outside these two locations is touched:" +if [ -d "$VENV_DIR" ]; then + echo "The environment at $VENV_DIR is incomplete." + echo "It will be rebuilt. Nothing else is touched." +else + echo "This project runs in its own Python environment, so that installing or" + echo "removing packages elsewhere on this machine cannot break your" + echo "automation." + echo + echo "It will be created at:" + echo " $VENV_DIR" +fi + echo -echo " $VENV_DIR" -echo " $HOME/solix-automation" +echo "Base interpreter: $BASE" echo printf "Set it up now? [Y/n]: " read -r reply @@ -110,38 +95,48 @@ case "${reply:-y}" in esac echo -echo "Creating the environment with $BASE ..." -mkdir -p "$(dirname "$VENV_DIR")" -"$BASE" -m venv "$VENV_DIR" +echo "Creating the environment..." +mkdir -p "$DATA_DIR" +rm -rf "$VENV_DIR" -VENV_PYTHON="$VENV_DIR/bin/python" -[ -x "$VENV_PYTHON" ] || VENV_PYTHON="$VENV_DIR/Scripts/python.exe" +if ! "$BASE" -m venv "$VENV_DIR"; then + echo + echo "Could not create a virtual environment." + if command -v apt >/dev/null 2>&1; then + echo "On Debian and Ubuntu this usually means python3-venv is missing:" + echo " sudo apt install python3-venv" + fi + exit 1 +fi if [ ! -x "$VENV_PYTHON" ]; then - echo "Could not create the environment at $VENV_DIR" + echo "The environment was created but has no interpreter at $VENV_PYTHON" exit 1 fi echo "Installing packages. This takes a minute." echo + "$VENV_PYTHON" -m pip install --upgrade pip --quiet "$VENV_PYTHON" -m pip install -r "$HERE/requirements.txt" --quiet "$VENV_PYTHON" -m pip install "git+$REPO_URL" --quiet -# The upstream package does not declare its runtime dependencies, so a plain -# install of it imports the package but fails on first real use. +# The upstream Anker library does not declare its runtime dependencies, so a +# plain install imports the package but fails on first real use. "$VENV_PYTHON" -m pip install aiofiles cryptography paho-mqtt --quiet if ! works "$VENV_PYTHON"; then echo - echo "Some dependencies are still missing. The setup wizard will resolve" - echo "the rest as it goes." + echo "The install finished but the Anker library still will not import." + echo "Run this to see the error:" + echo " $VENV_PYTHON -c 'import anker_solix_api.api'" + exit 1 fi echo -echo "Environment ready: $VENV_PYTHON" +echo "Environment ready." echo -echo "Tip: from now on you can run commands as" +echo "From now on, run commands with:" echo " $VENV_PYTHON solixauto.py " echo exec "$VENV_PYTHON" "$HERE/solixauto.py" setup