Always run in a project virtual environment
This commit is contained in:
1 parent
2a9e9cda4c
commit
89f841b31f
4 files changed
+162
-75
No files matched your search
@@ -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
@@ -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:
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
Reference in new issue
Block a user