Docker PoC for batteries
This commit is contained in:
38
eg4battery/Dockerfile
Normal file
38
eg4battery/Dockerfile
Normal file
@@ -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"]
|
||||||
@@ -100,9 +100,25 @@ class AppConfig:
|
|||||||
|
|
||||||
def load_config(path: Path) -> AppConfig:
|
def load_config(path: Path) -> AppConfig:
|
||||||
raw = yaml.safe_load(path.read_text())
|
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(
|
return AppConfig(
|
||||||
bus=BusConfig(**raw["bus"]),
|
bus=BusConfig(**raw["bus"]),
|
||||||
mqtt=MQTTConfig(**raw["mqtt"]),
|
mqtt=MQTTConfig(**mqtt_raw),
|
||||||
packs=[PackConfig(**p) for p in raw["packs"]],
|
packs=[PackConfig(**p) for p in raw["packs"]],
|
||||||
cell_count=raw.get("cell_count", 16),
|
cell_count=raw.get("cell_count", 16),
|
||||||
)
|
)
|
||||||
|
|||||||
50
eg4battery/docker-compose.yml
Normal file
50
eg4battery/docker-compose.yml
Normal file
@@ -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"
|
||||||
3
eg4battery/requirements.txt
Normal file
3
eg4battery/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pyserial>=3.5
|
||||||
|
paho-mqtt>=2.0
|
||||||
|
pyyaml>=6.0
|
||||||
Reference in New Issue
Block a user