Skip to content

Commit f9a76ea

Browse files
committed
2.04: standalone Docker image, ble_stack works outside HA (#120)
Make addon_main.sh bashio-optional so the same Dockerfile builds both the HA add-on and a standalone image (BUILD_FROM=alpine). A new entrypoint.sh dispatches on /usr/bin/with-contenv. Publish ghcr.io/fl4p/batmon-ha (amd64/arm64/armv7) and add a Helm chart. Community images that ran addon_main.sh by rewriting its shebang to /bin/sh silently disabled bumble/bluek/esphome and always used bleak. Fixes: - MQTT_HOST/PORT/USER/PASSWORD from `docker run -e` were overwritten with empty strings when no Supervisor MQTT service was present - options.json without ble_stack skipped the pairing pre-step instead of defaulting to bleak - a failing pair-only step was swallowed on the non-bashio path - docker stop now terminates promptly (entrypoint execs python) - add .dockerignore so a local build no longer bakes options.json credentials in
1 parent 7519cf0 commit f9a76ea

18 files changed

Lines changed: 989 additions & 64 deletions

.dockerignore

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Keep local dev state out of the image. `options.json` in particular holds real
2+
# MQTT/InfluxDB credentials on a developer machine (it is gitignored, so CI never
3+
# sees it, but `docker build .` would happily bake it in). Excluding it also makes
4+
# the standalone contract deterministic: config always resolves to the mounted
5+
# /data/options.json, never to a stale copy at /app/options.json.
6+
options.json
7+
bms_meter_states.json
8+
bat_*.json
9+
user_id
10+
.env
11+
12+
venv
13+
venv_*
14+
__pycache__
15+
**/__pycache__
16+
*.pyc
17+
*.pickle
18+
19+
.git
20+
.github
21+
.idea
22+
.pytest_cache
23+
.explyt
24+
.DS_Store
25+
plans

.github/workflows/docker.yml

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
name: Docker
2+
3+
# Publishes the standalone image (doc/Docker.md) to ghcr.io/fl4p/batmon-ha.
4+
# The Home Assistant add-on is NOT built here — the supervisor builds that on the
5+
# user's machine from the same Dockerfile, with BUILD_FROM taken from build.yaml.
6+
# The `validate-addon` job below only guards that path from Dockerfile regressions.
7+
8+
on:
9+
push:
10+
branches: [master]
11+
paths:
12+
- 'Dockerfile'
13+
- '.dockerignore'
14+
- 'entrypoint.sh'
15+
- 'addon_main.sh'
16+
- 'requirements.txt'
17+
- 'config.yaml'
18+
- 'main.py'
19+
- 'bmslib/**'
20+
- '.github/workflows/docker.yml'
21+
# Keep in sync with push.paths. config.yaml belongs here too: it carries the
22+
# release version, and a PR that only bumps it must still run validate-addon.
23+
pull_request:
24+
paths:
25+
- 'Dockerfile'
26+
- '.dockerignore'
27+
- 'entrypoint.sh'
28+
- 'addon_main.sh'
29+
- 'requirements.txt'
30+
- 'config.yaml'
31+
- 'main.py'
32+
- 'bmslib/**'
33+
- '.github/workflows/docker.yml'
34+
workflow_dispatch:
35+
36+
concurrency:
37+
group: docker-${{ github.ref }}
38+
cancel-in-progress: true
39+
40+
env:
41+
REGISTRY: ghcr.io
42+
IMAGE_NAME: ${{ github.repository }}
43+
# Pin the base. The Dockerfile does `apk add python3~3.13 || ~3.12 || python3`,
44+
# so `alpine:latest` would silently move the python minor version (and musl
45+
# wheel availability) under us. Bump this deliberately.
46+
BASE_IMAGE: alpine:3.21
47+
48+
jobs:
49+
# Cheap regression guard: a Dockerfile change must not break the add-on build.
50+
# Native amd64, never pushed.
51+
validate-addon:
52+
runs-on: ubuntu-latest
53+
steps:
54+
- uses: actions/checkout@v4
55+
- uses: docker/setup-buildx-action@v3
56+
- name: Build add-on image (HA base)
57+
uses: docker/build-push-action@v6
58+
with:
59+
context: .
60+
push: false
61+
platforms: linux/amd64
62+
build-args: |
63+
BUILD_FROM=ghcr.io/home-assistant/amd64-base:latest
64+
cache-from: type=gha,scope=addon-amd64
65+
cache-to: type=gha,mode=max,scope=addon-amd64
66+
67+
# One job per platform so arm64/armv7 (QEMU-emulated, and this Dockerfile
68+
# builds four venvs with several git+https installs) run in parallel rather
69+
# than serially inside a single buildx invocation.
70+
build:
71+
runs-on: ubuntu-latest
72+
permissions:
73+
contents: read
74+
packages: write
75+
strategy:
76+
fail-fast: false
77+
matrix:
78+
include:
79+
- platform: linux/amd64
80+
slug: amd64
81+
- platform: linux/arm64
82+
slug: arm64
83+
# armv7 ships WITHOUT the esphome stack: there are no musl armv7 wheels
84+
# for cryptography/dbus-fast/bleak-esphome, and the Dockerfile declines
85+
# to pull in a ~400MB Rust toolchain to compile them. venv_esphome fails,
86+
# `|| true` swallows it, and addon_main.sh falls back to bleak at runtime.
87+
- platform: linux/arm/v7
88+
slug: armv7
89+
steps:
90+
- uses: actions/checkout@v4
91+
92+
- uses: docker/setup-qemu-action@v3
93+
if: matrix.platform != 'linux/amd64'
94+
95+
- uses: docker/setup-buildx-action@v3
96+
97+
- name: Log in to ${{ env.REGISTRY }}
98+
if: github.event_name != 'pull_request'
99+
uses: docker/login-action@v3
100+
with:
101+
registry: ${{ env.REGISTRY }}
102+
username: ${{ github.actor }}
103+
password: ${{ secrets.GITHUB_TOKEN }}
104+
105+
# NOTE on caching: the unpinned `git+https` installs (aiobmsble,
106+
# bumble-bleak) key their layer on the RUN string, not on upstream HEAD, so
107+
# a warm cache keeps serving a stale revision. Bump config.yaml `version:`
108+
# (or clear the cache) when you want to pull new upstream commits.
109+
- name: Build and push by digest
110+
id: build
111+
uses: docker/build-push-action@v6
112+
with:
113+
context: .
114+
platforms: ${{ matrix.platform }}
115+
build-args: |
116+
BUILD_FROM=${{ env.BASE_IMAGE }}
117+
cache-from: type=gha,scope=standalone-${{ matrix.slug }}
118+
cache-to: type=gha,mode=max,scope=standalone-${{ matrix.slug }}
119+
outputs: >-
120+
type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
121+
122+
- name: Export digest
123+
if: github.event_name != 'pull_request'
124+
env:
125+
DIGEST: ${{ steps.build.outputs.digest }}
126+
run: |
127+
mkdir -p /tmp/digests
128+
touch "/tmp/digests/${DIGEST#sha256:}"
129+
130+
- uses: actions/upload-artifact@v4
131+
if: github.event_name != 'pull_request'
132+
with:
133+
name: digests-${{ matrix.slug }}
134+
path: /tmp/digests/*
135+
if-no-files-found: error
136+
retention-days: 1
137+
138+
# Join the per-platform digests into one multi-arch manifest under the real tags.
139+
merge:
140+
runs-on: ubuntu-latest
141+
needs: [build, validate-addon]
142+
# Only ever push from master. `push` is already branch-scoped, but
143+
# workflow_dispatch is not — without the ref check, dispatching from a
144+
# feature branch would publish a `sha-` tag to the public registry (the
145+
# `latest`/version tags are already guarded by is_default_branch).
146+
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master'
147+
permissions:
148+
contents: read
149+
packages: write
150+
steps:
151+
- uses: actions/checkout@v4
152+
153+
- uses: actions/download-artifact@v4
154+
with:
155+
path: /tmp/digests
156+
pattern: digests-*
157+
merge-multiple: true
158+
159+
- uses: docker/setup-buildx-action@v3
160+
161+
- uses: docker/login-action@v3
162+
with:
163+
registry: ${{ env.REGISTRY }}
164+
username: ${{ github.actor }}
165+
password: ${{ secrets.GITHUB_TOKEN }}
166+
167+
# Single source of truth for the release tag. Upstream bumps config.yaml
168+
# `version:` per release and does not cut git tags, so `type=semver` would
169+
# never fire.
170+
- name: Read version from config.yaml
171+
id: ver
172+
run: |
173+
v="$(sed -nE 's/^version:[[:space:]]*"?([^"]+)"?[[:space:]]*$/\1/p' config.yaml)"
174+
[ -n "$v" ] || { echo "could not parse version from config.yaml" >&2; exit 1; }
175+
echo "version=$v" >> "$GITHUB_OUTPUT"
176+
177+
- uses: docker/metadata-action@v5
178+
id: meta
179+
with:
180+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
181+
tags: |
182+
type=raw,value=latest,enable={{is_default_branch}}
183+
type=raw,value=${{ steps.ver.outputs.version }},enable={{is_default_branch}}
184+
type=sha,format=short
185+
186+
- name: Create manifest list and push
187+
working-directory: /tmp/digests
188+
run: |
189+
# shellcheck disable=SC2046
190+
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
191+
$(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
192+
193+
- name: Inspect
194+
run: docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.ver.outputs.version }}

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
# Changelog
22

33

4+
## [2.04]
5+
6+
* Standalone Docker: prebuilt multi-arch images at `ghcr.io/fl4p/batmon-ha` (amd64, arm64, armv7), plus a Helm chart in `charts/batmon-ha`. See [doc/Docker.md](doc/Docker.md) (#120)
7+
* `addon_main.sh` no longer requires bashio, so `ble_stack` (`bumble`/`bluek`/`esphome`) now works outside Home Assistant. Community images that ran it by rewriting the shebang to `#!/bin/sh` silently ignored the setting and always used `bleak`
8+
* Fix: `MQTT_HOST`/`MQTT_PORT`/`MQTT_USER`/`MQTT_PASSWORD` from the environment were overwritten with empty strings when no Supervisor MQTT service was present, so `docker run -e MQTT_HOST=...` never took effect
9+
* Fix: an `options.json` without `ble_stack` skipped the BLE pairing pre-step instead of defaulting to `bleak`
10+
* `docker stop` now terminates batmon promptly (the entrypoint `exec`s python, so SIGTERM is delivered)
11+
* Add `.dockerignore`: a local `docker build` used to bake the developer's `options.json` (MQTT/InfluxDB credentials) into the image
12+
13+
414
## [2.03]
515

616
* JK BLE: fix notify framing — resync on the frame header instead of clearing the buffer, which dropped a frame whenever one packet carried two. Fixes endless `timeout waiting for 2/3` and `crc check failed` after a reconnect (#377), and tolerates junk spliced between frames (#370)

Dockerfile

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ RUN python3 -m venv venv_esphome \
7070
# typically won't build; addon_main.sh falls back to bleak.
7171
RUN . venv/bin/activate
7272

73-
RUN chmod a+x addon_main.sh
73+
RUN chmod a+x addon_main.sh entrypoint.sh
7474

75-
CMD ["./addon_main.sh" ]
75+
# entrypoint.sh dispatches: HA add-on base -> addon_main.sh via its
76+
# `with-contenv bashio` shebang; plain alpine (standalone, doc/Docker.md) ->
77+
# `/bin/sh addon_main.sh`, which then falls back to reading options.json.
78+
CMD ["/app/entrypoint.sh"]

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,9 @@ You can run the add-on outside of Home Assistant (e.g. on a remote RPI sending M
324324
All you need is an operating system supported by [bleak](https://pypi.org/project/bleak/).
325325
See [doc/Standalone.md](doc/Standalone.md)
326326

327+
To run it in a container instead, see [doc/Docker.md](doc/Docker.md): prebuilt
328+
multi-arch images (`ghcr.io/fl4p/batmon-ha`), compose examples, and a Helm chart.
329+
327330
# Contribute / Donate
328331

329332
* [PayPal](https://www.paypal.com/donate/?hosted_button_id=6LACACFHQMR3C)

0 commit comments

Comments
 (0)