Always run in a project virtual environment

This commit is contained in:
Sterling Archer
2026-08-12 10:06:30 -07:00
parent 2a9e9cda4c
commit 89f841b31f
4 changed files with 166 additions and 79 deletions
+8 -3
View File
@@ -1,7 +1,7 @@
# anker-shelly-bridge
<p align="center">
<img src="docs\screenshot.jpg" alt="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" width="900">
<img src="docs/screenshot.jpg" alt="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" width="900">
</p>
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
+19 -3
View File
@@ -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):
<key>ProgramArguments</key>
<array>
<string>{sys.executable}</string>
<string>{preferred_python()}</string>
<string>{script}</string>
<string>run</string>
<string>{profile_name}</string>
@@ -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:
+83 -12
View File
@@ -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 ^<command^>
echo.
"%VENV_PYTHON%" "%~dp0solixauto.py" setup
:end
echo.
pause
+56 -61
View File
@@ -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 <command>"
echo
exec "$VENV_PYTHON" "$HERE/solixauto.py" setup