diff --git a/eg4battery/Dockerfile b/eg4battery/Dockerfile new file mode 100644 index 0000000..ec73644 --- /dev/null +++ b/eg4battery/Dockerfile @@ -0,0 +1,38 @@ +# eg4-battery daemon — containerized. +# +# Build: docker build -t eg4-battery . +# Run: see docker-compose.yml for device passthrough + config mount +# +# Design notes: +# - Same Python source (bin/eg4-battery) used in both systemd and Docker +# deployments. PEP-723 inline deps are ignored when run with `python` directly, +# so they're harmless. Container deps come from requirements.txt. +# - Config is mounted at /config/eg4-battery.yaml (read-only). MQTT +# credentials can be overridden via env vars (see env-override block in +# bin/eg4-battery's main()), making them suitable for Docker secrets or +# HA-addon options translation. +# - Logs go to stdout (Python logging defaults work — `docker logs` captures). + +FROM python:3.11-slim AS base + +# pyserial wants the native serial-port permissions to "just work" inside +# the container; we hand the device through via docker-compose `devices:`. +# No extra system packages needed. + +WORKDIR /app + +# --- Python deps -------------------------------------------------------- +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# --- daemon source ------------------------------------------------------ +COPY bin/eg4-battery /app/eg4-battery +RUN chmod +x /app/eg4-battery + +# --- runtime ------------------------------------------------------------ +# Config volume mount expected at /config (read-only); see compose file. +VOLUME ["/config"] + +# Run as the daemon's main entrypoint. We exec via `python` so the PEP-723 +# shebang isn't invoked (uv isn't in this image). +ENTRYPOINT ["python", "/app/eg4-battery", "-C", "/config/eg4-battery.yaml"] diff --git a/eg4battery/bin/eg4-battery b/eg4battery/bin/eg4-battery index 74d9ec2..aa88989 100755 --- a/eg4battery/bin/eg4-battery +++ b/eg4battery/bin/eg4-battery @@ -100,9 +100,25 @@ class AppConfig: def load_config(path: Path) -> AppConfig: raw = yaml.safe_load(path.read_text()) + + # Allow env-var overrides for MQTT credentials. Useful for Docker + # deployments where secrets shouldn't live in the YAML, and for HA + # addons translating addon-options into env at runtime. + import os + mqtt_raw = dict(raw["mqtt"]) + for key, env_var in ( + ("host", "MQTT_HOST"), + ("port", "MQTT_PORT"), + ("username", "MQTT_USERNAME"), + ("password", "MQTT_PASSWORD"), + ): + v = os.environ.get(env_var) + if v is not None: + mqtt_raw[key] = int(v) if key == "port" else v + return AppConfig( bus=BusConfig(**raw["bus"]), - mqtt=MQTTConfig(**raw["mqtt"]), + mqtt=MQTTConfig(**mqtt_raw), packs=[PackConfig(**p) for p in raw["packs"]], cell_count=raw.get("cell_count", 16), ) diff --git a/eg4battery/docker-compose.yml b/eg4battery/docker-compose.yml new file mode 100644 index 0000000..3876001 --- /dev/null +++ b/eg4battery/docker-compose.yml @@ -0,0 +1,50 @@ +# eg4-battery — local Docker deployment. +# +# Usage: +# docker compose build # one-time, after changes to Dockerfile / requirements / source +# docker compose up -d # start daemon in background +# docker compose logs -f # tail the daemon log +# docker compose down # stop + remove +# +# This deployment runs side-by-side with the systemd `eg4-battery.service`. +# Stop systemd before bringing this up, or each pack will be polled twice +# per cycle (and HA will see duplicate publishes from two MQTT clients): +# sudo systemctl stop eg4-battery.service +# +# Side-by-side cutover, once verified: +# sudo systemctl disable --now eg4-battery.service + +services: + eg4-battery: + build: . + image: eg4-battery:local + container_name: eg4-battery + restart: unless-stopped + + # USB-RS-485 adapters → daemon. The /dev/ttyUSBN device numbers can shift + # on USB reshuffles; the by-id symlinks (mounted via the volume below) + # are the stable identifiers the YAML config references. + devices: + - /dev/ttyUSB0:/dev/ttyUSB0 + - /dev/ttyUSB1:/dev/ttyUSB1 + - /dev/ttyUSB3:/dev/ttyUSB3 + + # Read-only bind: lets the container resolve /dev/serial/by-id/usb-FTDI_* + # symlinks (Docker doesn't propagate symlinks via `devices:`). + volumes: + - /dev/serial:/dev/serial:ro + - ${HOME}/.config/eg4-battery:/config:ro + + # MQTT credentials override the YAML's mqtt: block when set. Comment out + # to use the values from eg4-battery.yaml. + environment: + - MQTT_HOST=10.0.0.41 + - MQTT_USERNAME=mqtt + - MQTT_PASSWORD=nutterino + + # Standard log driver — `docker logs` reads from stdout + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" diff --git a/eg4battery/requirements.txt b/eg4battery/requirements.txt new file mode 100644 index 0000000..1377b2b --- /dev/null +++ b/eg4battery/requirements.txt @@ -0,0 +1,3 @@ +pyserial>=3.5 +paho-mqtt>=2.0 +pyyaml>=6.0