Skip to content

Commit 510ad08

Browse files
robeertmclaude
andcommitted
17.7.0 — one-command Docker install, arm64 image, three update paths
- deploy/install.sh + docker-compose.yml + .env.example: one command, image plus a Watchtower that watches only the analyzer; SEA_DIR, SEA_PORT, SEA_HOST_NETWORK (mDNS discovery), TS_AUTHKEY (.env, 0600) - image built natively for amd64 and arm64, published as one manifest; the build checks that the mark carries both - SEA_CONTAINER=1 in the image: update_channel() = addon | docker | app; in Docker the in-app updater, checker and auto-heal stay off, endpoints answer channel=docker without contacting GitHub, install -> 409, Settings shows the command. Add-on wins; ZIP installs unchanged. - texts in all nine languages (add-on text was missing in six) - tests/test_update_channel.py pins all three paths Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 1a4bd7a commit 510ad08

17 files changed

Lines changed: 695 additions & 66 deletions

File tree

‎.env.example‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Copy to .env next to docker-compose.yml and adjust. Everything is optional.
2+
3+
# Time zone for timestamps, reports and schedules.
4+
TZ=Europe/Berlin
5+
6+
# Reach the dashboard over Tailscale (HTTPS through `tailscale serve`, no port
7+
# opened to the internet). Create a key at https://login.tailscale.com/admin/settings/keys
8+
# Needs MagicDNS and HTTPS certificates enabled in your tailnet.
9+
# TS_AUTHKEY=tskey-auth-...
10+
11+
# When Watchtower looks for a new image (seconds minutes hours day month weekday).
12+
# WATCHTOWER_SCHEDULE=0 0 4 * * *

‎.github/workflows/docker.yml‎

Lines changed: 129 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,16 @@ concurrency:
2424
cancel-in-progress: true
2525

2626
jobs:
27-
build:
27+
# ── 1. Which marks this build may set — decided ONCE, before any build ──
28+
prepare:
2829
runs-on: ubuntu-latest
2930
permissions:
3031
contents: read
31-
packages: write
32+
packages: read
33+
outputs:
34+
slug: ${{ steps.repo.outputs.slug }}
35+
version: ${{ steps.ver.outputs.version }}
36+
tags: ${{ steps.marken.outputs.tags }}
3237
steps:
3338
- uses: actions/checkout@v4
3439

@@ -101,31 +106,129 @@ jobs:
101106
echo "ENDE_DER_MARKEN"
102107
} >> "$GITHUB_OUTPUT"
103108
104-
# 🔴 amd64 only — on purpose. The image carries numpy/pandas/pillow, and
105-
# an emulated arm64 build of those takes the better part of an hour on a
106-
# hosted runner. The machine this runs on is an x86 NAS. Add arm64 the
107-
# day something actually needs it, not before.
108-
- name: Build and push
109+
# ── 2. One build per architecture, each on its own NATIVE runner ────────
110+
# 🔑 amd64 AND arm64 since 17.7.0 — the one-line Docker install is for
111+
# everybody, and a Raspberry Pi or an Apple-silicon Mac is arm64. Until then
112+
# this said "amd64 only — on purpose": an EMULATED arm64 build of
113+
# numpy/pandas/pillow took the better part of an hour. GitHub's arm64
114+
# runners build natively, in parallel with amd64, so that cost is gone.
115+
# Each build pushes by DIGEST only — no mark yet; step 3 sets the marks on
116+
# one manifest that carries both.
117+
build:
118+
needs: prepare
119+
strategy:
120+
fail-fast: true
121+
matrix:
122+
include:
123+
- platform: linux/amd64
124+
arch: amd64
125+
runner: ubuntu-latest
126+
- platform: linux/arm64
127+
arch: arm64
128+
runner: ubuntu-24.04-arm
129+
runs-on: ${{ matrix.runner }}
130+
permissions:
131+
contents: read
132+
packages: write
133+
steps:
134+
- uses: actions/checkout@v4
135+
136+
- name: Log in to GHCR
137+
uses: docker/login-action@v3
138+
with:
139+
registry: ghcr.io
140+
username: ${{ github.actor }}
141+
password: ${{ secrets.GITHUB_TOKEN }}
142+
143+
- name: Set up Buildx
144+
uses: docker/setup-buildx-action@v3
145+
146+
- name: Build and push by digest
147+
id: build
109148
uses: docker/build-push-action@v6
110149
with:
111150
context: .
112-
platforms: linux/amd64
113-
push: true
114-
# 🔴 HIER STEHT KEIN `:latest` MEHR (04.10.2026). Es stand hier — und
115-
# damit wanderte das Etikett, dem jede Installation ueber Watchtower
116-
# folgt, in dem Moment, in dem ein Commit auf main landete. Also
117-
# bevor irgendetwas das gebaute Abbild angesehen hatte.
118-
#
119-
# Am 04.10.2026 ging bei DocuSort auf genau diesem Weg eine Fassung
120-
# hinaus, die auf bestehenden Datenbanken nicht startete. Watchtower
121-
# verteilte sie binnen einer Stunde an Roberts Kunden; die
122-
# Installationen standen still — und waeren NICHT von selbst
123-
# zurueckgekommen, weil Watchtower abstuerzende Container
124-
# ueberspringt (`scanned=0`, am Pi nachgestellt).
125-
#
126-
# Jetzt setzt `promote.yml` das Etikett — von Hand, und erst nachdem
127-
# es das VEROEFFENTLICHTE Abbild selbst angefahren hat.
128-
tags: ${{ steps.marken.outputs.tags }}
129-
cache-from: type=gha
130-
cache-to: type=gha,mode=max
151+
platforms: ${{ matrix.platform }}
152+
outputs: type=image,name=ghcr.io/${{ needs.prepare.outputs.slug }},push-by-digest=true,name-canonical=true,push=true
153+
cache-from: type=gha,scope=${{ matrix.arch }}
154+
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
131155
provenance: false
156+
157+
- name: Keep the digest for the manifest
158+
run: |
159+
mkdir -p /tmp/digests
160+
d="${{ steps.build.outputs.digest }}"
161+
touch "/tmp/digests/${d#sha256:}"
162+
163+
- uses: actions/upload-artifact@v4
164+
with:
165+
name: digest-${{ matrix.arch }}
166+
path: /tmp/digests/*
167+
if-no-files-found: error
168+
retention-days: 1
169+
170+
# ── 3. The marks, on ONE manifest for both architectures ────────────────
171+
merge:
172+
needs: [prepare, build]
173+
runs-on: ubuntu-latest
174+
permissions:
175+
contents: read
176+
packages: write
177+
steps:
178+
- uses: actions/download-artifact@v4
179+
with:
180+
path: /tmp/digests
181+
pattern: digest-*
182+
merge-multiple: true
183+
184+
- name: Log in to GHCR
185+
uses: docker/login-action@v3
186+
with:
187+
registry: ghcr.io
188+
username: ${{ github.actor }}
189+
password: ${{ secrets.GITHUB_TOKEN }}
190+
191+
- name: Set up Buildx
192+
uses: docker/setup-buildx-action@v3
193+
194+
# 🔴 HIER STEHT KEIN `:latest` MEHR (04.10.2026). Es stand hier — und
195+
# damit wanderte das Etikett, dem jede Installation ueber Watchtower
196+
# folgt, in dem Moment, in dem ein Commit auf main landete. Also
197+
# bevor irgendetwas das gebaute Abbild angesehen hatte.
198+
#
199+
# Am 04.10.2026 ging bei DocuSort auf genau diesem Weg eine Fassung
200+
# hinaus, die auf bestehenden Datenbanken nicht startete. Watchtower
201+
# verteilte sie binnen einer Stunde an alle Installationen; die
202+
# Installationen standen still — und waeren NICHT von selbst
203+
# zurueckgekommen, weil Watchtower abstuerzende Container
204+
# ueberspringt (`scanned=0`, am Pi nachgestellt).
205+
#
206+
# Jetzt setzt `promote.yml` das Etikett — von Hand, und erst nachdem
207+
# es das VEROEFFENTLICHTE Abbild selbst angefahren hat.
208+
- name: Publish the marks on one manifest
209+
working-directory: /tmp/digests
210+
env:
211+
SLUG: ghcr.io/${{ needs.prepare.outputs.slug }}
212+
MARKEN: ${{ needs.prepare.outputs.tags }}
213+
run: |
214+
set -euo pipefail
215+
args=()
216+
while IFS= read -r m; do
217+
[ -n "$m" ] && args+=(-t "$m")
218+
done <<< "$MARKEN"
219+
[ "${#args[@]}" -gt 0 ] || { echo "::error::no marks to set"; exit 1; }
220+
quellen=()
221+
for d in *; do quellen+=("$SLUG@sha256:$d"); done
222+
[ "${#quellen[@]}" -eq 2 ] || { echo "::error::expected 2 digests, got ${#quellen[@]}"; exit 1; }
223+
docker buildx imagetools create "${args[@]}" "${quellen[@]}"
224+
225+
# „Created" is not „carries both". Ask the registry what the mark holds.
226+
- name: The manifest carries amd64 AND arm64
227+
env:
228+
SLUG: ghcr.io/${{ needs.prepare.outputs.slug }}
229+
run: |
230+
set -euo pipefail
231+
out="$(docker buildx imagetools inspect "$SLUG:edge")"
232+
printf '%s\n' "$out"
233+
printf '%s' "$out" | grep -q 'linux/amd64' || { echo "::error::amd64 missing"; exit 1; }
234+
printf '%s' "$out" | grep -q 'linux/arm64' || { echo "::error::arm64 missing"; exit 1; }

‎CHANGELOG.md‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,50 @@
11
# Changelog
22

3+
## 17.7.0
4+
5+
### Install with Docker in one command — and every install updates the way it should
6+
7+
```bash
8+
curl -fsSL https://raw.githubusercontent.com/robeertm/shelly-energy-analyzer/main/deploy/install.sh | sh
9+
```
10+
11+
`deploy/install.sh` writes a `docker-compose.yml`, pulls the published image
12+
and starts it together with a Watchtower that looks for a new version every
13+
night at 04:00 and watches only the analyzer. Re-running it is safe — an
14+
existing `docker-compose.yml` is kept. Options as environment variables:
15+
`SEA_DIR`, `SEA_PORT`, `SEA_HOST_NETWORK=1` (host network, so mDNS discovery
16+
of Shellys works on Linux) and `TS_AUTHKEY` (join your tailnet; written to a
17+
`.env` that only you can read). The repository now also carries a
18+
`docker-compose.yml` and a `.env.example` for doing it by hand.
19+
20+
**The image now runs on arm64 as well** — a Raspberry Pi, an Apple-silicon
21+
Mac. It used to be published for amd64 only, because an emulated arm64 build
22+
of numpy/pandas took most of an hour; both architectures are now built
23+
natively, in parallel, and published as one manifest. The build checks that
24+
the published mark really carries both.
25+
26+
**Three installs, three update paths — each one keeps its own:**
27+
28+
| Install | How a new version arrives |
29+
|---|---|
30+
| ZIP / `start.command` / `start.sh` / `start.bat` | the in-app updater, unchanged |
31+
| Home Assistant add-on | the add-on is rebuilt, unchanged |
32+
| Docker image | a new image — Watchtower or `docker compose pull && docker compose up -d` |
33+
34+
Until now nothing told the app it was running inside the image, so the in-app
35+
updater ran there too: it swapped files that the next `docker compose up`
36+
threw away again, and it competed with Watchtower. The image now sets
37+
`SEA_CONTAINER=1`; with it the update checker does not start, the update
38+
endpoints answer with `"channel": "docker"` and never contact GitHub, an
39+
install request is refused with 409, and Settings → Updates shows the command
40+
instead of the buttons. The add-on wins when both apply — an add-on is a
41+
container too. The message about add-on updates is now translated into all
42+
nine languages (it existed in three).
43+
44+
`tests/test_update_channel.py` pins all three paths, including the one that
45+
must not change: a normal install still reaches GitHub through the old
46+
updater.
47+
348
## 17.6.4
449

550
### A preset range now means the same period for every device on the chart

‎Dockerfile‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,15 @@ ENV PYTHONUNBUFFERED=1 \
1616
TZ=Europe/Berlin \
1717
SEA_PORT=8765 \
1818
TS_STATE_DIR=/data/tailscale \
19-
TS_HOSTNAME=shelly-energy-analyzer
19+
TS_HOSTNAME=shelly-energy-analyzer \
20+
SEA_CONTAINER=1
21+
22+
# 🔑 SEA_CONTAINER=1 IS HOW THE APP KNOWS IT IS THIS IMAGE (since 17.7.0).
23+
# The code lives in site-packages and the container is replaced on every
24+
# update, so the in-app updater is switched off here — a file swap inside the
25+
# container would be gone with the next `docker compose up`. Updates come with
26+
# a new image (`docker compose pull && docker compose up -d`, or Watchtower).
27+
# A Home-Assistant add-on keeps its own path; see `io/config.update_channel()`.
2028

2129
# Build tools for wheels that may need compiling (numpy/pandas ship wheels, but
2230
# pymodbus/reportlab/pillow occasionally need a compiler on slim images).

‎README.md‎

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ No cloud. No subscription. No data lock-in.
1111
[![Release](https://img.shields.io/github/v/release/robeertm/shelly-energy-analyzer?logo=github)](https://github.com/robeertm/shelly-energy-analyzer/releases/latest)
1212
[![Last commit](https://img.shields.io/github/last-commit/robeertm/shelly-energy-analyzer?logo=git&logoColor=white)](https://github.com/robeertm/shelly-energy-analyzer/commits/main)
1313
[![i18n](https://img.shields.io/badge/i18n-9%20languages-brightgreen)](#languages)
14-
[![Platforms](https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-lightgrey)](#quick-start-end-users)
14+
[![Platforms](https://img.shields.io/badge/platform-Docker%20%7C%20macOS%20%7C%20Linux%20%7C%20Windows-lightgrey)](#quick-start-end-users)
1515

1616
![Hero](docs/screenshots/aurora_desktop_live.png)
1717

@@ -82,7 +82,7 @@ Commercial energy dashboards lock you into subscriptions, truncate history after
8282
- 🌐 **Full REST API v1** + **InfluxDB line-protocol push** + **Prometheus `/metrics`** for your own stack
8383
- 🔒 **100 % self-hosted** — your energy data never leaves your LAN
8484
- 🆓 **Zero subscription** — no cloud, no accounts, no analytics
85-
- ⬆ **Self-updating** — background thread polls GitHub every hour for new releases; the Live tab shows a one-click banner to install or roll back to any of the last 10 versions, entirely from the browser
85+
- ⬆ **Self-updating** — background thread polls GitHub every hour for new releases; the Live tab shows a one-click banner to install or roll back to any of the last 10 versions, entirely from the browser. The Docker image and the Home Assistant add-on update through their own channel instead — see [Updates](#updates)
8686

8787
### Who it's for
8888

@@ -97,6 +97,31 @@ Commercial energy dashboards lock you into subscriptions, truncate history after
9797

9898
## ⚡ Quickstart
9999

100+
### 🐳 Docker — one command (recommended)
101+
102+
```bash
103+
curl -fsSL https://raw.githubusercontent.com/robeertm/shelly-energy-analyzer/main/deploy/install.sh | sh
104+
```
105+
106+
That writes a `docker-compose.yml` into `./shelly-energy-analyzer`, pulls the
107+
image (amd64 and arm64 — a NAS, a Raspberry Pi, a Mac) and starts it, together
108+
with a Watchtower that installs new versions every night. Open
109+
`http://<this machine>:8765` and follow the setup wizard. Everything the app
110+
keeps lives in `./shelly-energy-analyzer/data`.
111+
112+
| Option (in front of `sh`) | Effect |
113+
|---|---|
114+
| `SEA_DIR=/volume1/docker/shelly` | install somewhere else (e.g. on a Synology) |
115+
| `SEA_PORT=8080` | another port for the dashboard |
116+
| `SEA_HOST_NETWORK=1` | host network — mDNS discovery of Shellys works (Linux). On the default bridge network, add Shellys by IP address |
117+
| `TS_AUTHKEY=tskey-auth-…` | also reach it over Tailscale (HTTPS); stored in `.env`, readable only by you |
118+
119+
Running it again is safe — an existing `docker-compose.yml` is kept. Prefer
120+
doing it by hand? `docker-compose.yml` and `.env.example` are in the
121+
repository root: `docker compose up -d`.
122+
123+
### Without Docker
124+
100125
```bash
101126
git clone https://github.com/robeertm/shelly-energy-analyzer.git
102127
cd shelly-energy-analyzer
@@ -111,7 +136,7 @@ chmod +x start.sh && ./start.sh
111136
start.bat
112137
```
113138

114-
Open <https://localhost:8765> and follow the 12-step setup wizard — pick demo mode for a zero-hardware tour, or connect real Shellys via mDNS, set your tariff, and you're live. Every optional step is skippable.
139+
Open <https://localhost:8765> (Docker: `http://…:8765`) and follow the 12-step setup wizard — pick demo mode for a zero-hardware tour, or connect real Shellys via mDNS, set your tariff, and you're live. Every optional step is skippable.
115140

116141
> **No Shellys yet?** Enable **Demo Mode** in Settings → Advanced to explore the full UI with realistic generated data. Great for seeing what the tool can do before buying hardware.
117142
@@ -647,6 +672,9 @@ Browser-based 12-step wizard at `/setup` (auto-redirected on first launch when n
647672

648673
## Quick Start (End Users)
649674

675+
### Docker
676+
See [Quickstart](#-quickstart) — one command, any machine with Docker.
677+
650678
### macOS
651679
```
652680
Double-click start.command
@@ -723,7 +751,18 @@ python -m shelly_analyzer
723751

724752
---
725753

726-
## Updates (GitHub Releases)
754+
## Updates
755+
756+
How a new version arrives depends on how you installed it — each install keeps
757+
its own path:
758+
759+
| Install | Updates |
760+
|---|---|
761+
| **Docker** (`deploy/install.sh`, `docker-compose.yml`) | a new image. The bundled Watchtower installs it every night at 04:00; by hand: `docker compose pull && docker compose up -d`. The in-app updater is switched off inside the container — a file swap there would be gone with the next restart. |
762+
| **Home Assistant add-on** | the add-on is rebuilt from the add-on store. The in-app updater is switched off there as well. |
763+
| **ZIP / start script** (macOS, Linux, Windows) | the built-in updater below. |
764+
765+
### Built-in updater (installs without Docker)
727766

728767
The built-in updater checks the latest release on GitHub automatically.
729768

0 commit comments

Comments
 (0)