Always run in a project virtual environment

This commit is contained in:
Sterling Archer committed 2026-08-12 10:06:30 -07:00
1 parent 2a9e9cda4c
commit 89f841b31f
4 files changed
+162 -75

No files matched your search

+8 -3
View File
@@ -1,7 +1,7 @@
# anker-shelly-bridge # anker-shelly-bridge
<p align="center"> <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> </p>
Automate a Shelly smart plug from live Anker SOLIX telemetry. Automate a Shelly smart plug from live Anker SOLIX telemetry.
@@ -35,8 +35,9 @@ cd anker-shelly-bridge
On Windows, double-click `start.bat`. On Windows, double-click `start.bat`.
That is the whole install. `start.sh` creates its own Python environment, That is the whole install. `start.sh` builds a Python environment for this
installs everything, and hands over to a guided setup with eight steps: 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 1. **Dependencies**: installs whatever is missing
2. **Anker account**: your login, stored locally with owner-only permissions 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 Needs Python 3.12+ and git. If they are missing, `start.sh` tells you how to
install them for your platform. 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 ## What it does
+19 -3
View File
@@ -23,6 +23,22 @@ def service_label(profile_name):
return f"com.solixauto.{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(): def script_path():
if sys.argv and sys.argv[0]: if sys.argv and sys.argv[0]:
return str(Path(sys.argv[0]).resolve()) return str(Path(sys.argv[0]).resolve())
@@ -84,7 +100,7 @@ def launchd_plist(profile_name):
<key>ProgramArguments</key> <key>ProgramArguments</key>
<array> <array>
<string>{sys.executable}</string> <string>{preferred_python()}</string>
<string>{script}</string> <string>{script}</string>
<string>run</string> <string>run</string>
<string>{profile_name}</string> <string>{profile_name}</string>
@@ -125,7 +141,7 @@ After=network-online.target
[Service] [Service]
Type=simple Type=simple
ExecStart={sys.executable} {script} run {profile_name} --quiet ExecStart={preferred_python()} {script} run {profile_name} --quiet
WorkingDirectory={Path(script).parent} WorkingDirectory={Path(script).parent}
{env_lines}Restart=always {env_lines}Restart=always
RestartSec=60 RestartSec=60
@@ -142,7 +158,7 @@ def windows_instructions(profile_name):
others. Use Task Scheduler: others. Use Task Scheduler:
schtasks /create /tn "{label}" /sc onlogon /rl highest ^ 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: To remove it:
+81 -10
View File
@@ -1,35 +1,106 @@
@echo off @echo off
setlocal setlocal enabledelayedexpansion
cd /d "%~dp0" 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 ============================================================== echo ==============================================================
echo Anker SOLIX to Shelly automation - setup echo Anker SOLIX to Shelly automation
echo ============================================================== echo ==============================================================
echo. echo.
set PYTHON= 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
)
)
set "BASE="
for %%P in (py python) do ( for %%P in (py python) do (
if not defined PYTHON ( if not defined BASE (
where %%P >nul 2>&1 && set PYTHON=%%P where %%P >nul 2>&1 && set "BASE=%%P"
) )
) )
if not defined PYTHON ( if not defined BASE (
echo No Python found on PATH. echo Python 3.12 or newer is required and was not found on PATH.
echo Install Python 3.12 or newer from https://www.python.org/downloads/ echo Install it from https://www.python.org/downloads/
echo Tick "Add python.exe to PATH" during installation, then run this again. echo Tick "Add python.exe to PATH" during installation, then run this again.
echo. echo.
pause pause
exit /b 1 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. echo.
pause
exit /b 1
)
%PYTHON% "%~dp0solixauto.py" setup 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
)
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. echo.
pause pause
+54 -59
View File
@@ -4,51 +4,35 @@ set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$HERE" 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" REPO_URL="https://github.com/thomluther/anker-solix-api.git"
echo echo
echo "==============================================================" echo "=============================================================="
echo " Anker SOLIX to Shelly automation - setup" echo " Anker SOLIX to Shelly automation"
echo "==============================================================" echo "=============================================================="
echo echo
works() { 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() { if works "$VENV_PYTHON"; then
local candidates=( echo "Using: $VENV_PYTHON"
"$VENV_DIR/bin/python" echo
"$HOME/anker-solix-mqtt/venv/bin/python" exec "$VENV_PYTHON" "$HERE/solixauto.py" setup
"$HERE/venv/bin/python"
)
for candidate in "${candidates[@]}"; do
if works "$candidate"; then
echo "$candidate"
return 0
fi 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
}
base_python() { find_base_python() {
if [ -x "$VENV_DIR/bin/python" ]; then
echo "$VENV_DIR/bin/python"
return 0
fi
for name in python3.14 python3.13 python3.12 python3; do for name in python3.14 python3.13 python3.12 python3; do
if command -v "$name" >/dev/null 2>&1; then if command -v "$name" >/dev/null 2>&1; then
local version local path version
version="$("$name" -c 'import sys; print(sys.version_info[0]*100+sys.version_info[1])' 2>/dev/null || echo 0)" 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 if [ "$version" -ge 312 ]; then
command -v "$name" echo "$path"
return 0 return 0
fi fi
fi fi
@@ -56,18 +40,7 @@ base_python() {
return 1 return 1
} }
PYTHON="$(usable_python || true)" BASE="$(find_base_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)"
if [ -z "${BASE:-}" ]; then if [ -z "${BASE:-}" ]; then
echo "Python 3.12 or newer is required and was not found." 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" echo "Install it with: brew install python@3.13"
elif command -v apt >/dev/null 2>&1; then elif command -v apt >/dev/null 2>&1; then
echo "Install it with: sudo apt install python3 python3-venv python3-pip git" 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 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 fi
echo echo
echo "Then run this script again." echo "Then run this script again."
@@ -90,18 +65,28 @@ if ! command -v git >/dev/null 2>&1; then
if [ "$(uname)" = "Darwin" ]; then if [ "$(uname)" = "Darwin" ]; then
echo "Install the Xcode command line tools: xcode-select --install" echo "Install the Xcode command line tools: xcode-select --install"
else 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 fi
echo echo
echo "Then run this script again." echo "Then run this script again."
exit 1 exit 1
fi fi
echo "This will create a self-contained Python environment and install" if [ -d "$VENV_DIR" ]; then
echo "everything needed. Nothing outside these two locations is touched:" 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
echo "It will be created at:"
echo " $VENV_DIR" echo " $VENV_DIR"
echo " $HOME/solix-automation" fi
echo
echo "Base interpreter: $BASE"
echo echo
printf "Set it up now? [Y/n]: " printf "Set it up now? [Y/n]: "
read -r reply read -r reply
@@ -110,38 +95,48 @@ case "${reply:-y}" in
esac esac
echo echo
echo "Creating the environment with $BASE ..." echo "Creating the environment..."
mkdir -p "$(dirname "$VENV_DIR")" mkdir -p "$DATA_DIR"
"$BASE" -m venv "$VENV_DIR" rm -rf "$VENV_DIR"
VENV_PYTHON="$VENV_DIR/bin/python" if ! "$BASE" -m venv "$VENV_DIR"; then
[ -x "$VENV_PYTHON" ] || VENV_PYTHON="$VENV_DIR/Scripts/python.exe" 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 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 exit 1
fi fi
echo "Installing packages. This takes a minute." echo "Installing packages. This takes a minute."
echo echo
"$VENV_PYTHON" -m pip install --upgrade pip --quiet "$VENV_PYTHON" -m pip install --upgrade pip --quiet
"$VENV_PYTHON" -m pip install -r "$HERE/requirements.txt" --quiet "$VENV_PYTHON" -m pip install -r "$HERE/requirements.txt" --quiet
"$VENV_PYTHON" -m pip install "git+$REPO_URL" --quiet "$VENV_PYTHON" -m pip install "git+$REPO_URL" --quiet
# The upstream package does not declare its runtime dependencies, so a plain # The upstream Anker library does not declare its runtime dependencies, so a
# install of it imports the package but fails on first real use. # plain install imports the package but fails on first real use.
"$VENV_PYTHON" -m pip install aiofiles cryptography paho-mqtt --quiet "$VENV_PYTHON" -m pip install aiofiles cryptography paho-mqtt --quiet
if ! works "$VENV_PYTHON"; then if ! works "$VENV_PYTHON"; then
echo echo
echo "Some dependencies are still missing. The setup wizard will resolve" echo "The install finished but the Anker library still will not import."
echo "the rest as it goes." echo "Run this to see the error:"
echo " $VENV_PYTHON -c 'import anker_solix_api.api'"
exit 1
fi fi
echo echo
echo "Environment ready: $VENV_PYTHON" echo "Environment ready."
echo 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 " $VENV_PYTHON solixauto.py <command>"
echo echo
exec "$VENV_PYTHON" "$HERE/solixauto.py" setup exec "$VENV_PYTHON" "$HERE/solixauto.py" setup