Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .changeset/ha-addon-first-party.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
"ftw": minor
---

Add a first-party, all-in-one Home Assistant OS / Supervised add-on under
`deploy/homeassistant/`, versioned in lockstep with releases, replacing the
stale community add-on (`erikarenhill/ha-addon-forty-two-watts`).

FTW now ships as several containers (core, Python/CVXPY optimizer, updater,
Mosquitto). A HA add-on is single-container, so the add-on **bundles core +
optimizer into one image**: since the two already talk over a Unix socket
(`FTW_OPTIMIZER_TRANSPORT=auto`), the Dockerfile bases on the optimizer image
and copies in the fully static Go core, with a `tini`-supervised `run.sh`
running both. This gives the full CVXPY optimizer with no extra setup — a real
"install one add-on and it works" trial. The updater is dropped (Supervisor
owns updates) and Mosquitto is left to the HA Mosquitto add-on.

Targets canonical `ghcr.io/srcfl/ftw:v1.4.0` + `ghcr.io/srcfl/ftw-optimizer:v1.3.1`
(they version independently), rebases onto Supervisor's `/data`, restores the
`-user-drivers` overlay, and uses host networking (Modbus TCP / LAN MQTT /
mDNS). The full analysis, storage-growth guidance, and remaining items
(CI-validated tag pairing, optional slim variant, options schema) are in
`docs/ha-addon.md`.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
/release
*.redb
config.yaml
# The Home Assistant add-on manifest is literally named config.yaml but is
# source, not user state — keep it tracked.
!deploy/homeassistant/ftw/config.yaml
data/

# Go build artifacts
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ The dashboard is intentionally local. Use a VPN or another operator-managed
private network when access is needed away from home; FTW does not ship a
public relay.

## Install as a Home Assistant add-on

For Home Assistant OS / Supervised, a first-party add-on lives in
[`deploy/homeassistant/`](deploy/homeassistant/). It wraps the released
`ghcr.io/srcfl/ftw` image onto Supervisor's `/data` volume and is versioned in
lockstep with releases. Setup, the optimizer/storage caveats, and the
publishing plan are in [`docs/ha-addon.md`](docs/ha-addon.md).

## Local development

Requirements are Go, Python 3 and Node.js. The optimizer environment is cached
Expand Down
50 changes: 50 additions & 0 deletions deploy/homeassistant/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# FTW — Home Assistant add-on repository

This directory is a self-contained [Home Assistant add-on
repository](https://developers.home-assistant.io/docs/add-ons/repository) for
**FTW**, kept inside the main repo so it stays in lockstep with the code it
wraps.

```
deploy/homeassistant/
├── repository.yaml # add-on repository descriptor
└── ftw/
├── config.yaml # add-on manifest (arch, ports, host_network, map)
├── build.yaml # pinned base images (optimizer + core), per arch
├── Dockerfile # all-in-one: core + optimizer in one container
├── run.sh # tini-supervised two-process entrypoint
├── DOCS.md # user-facing docs shown in the HA UI
└── README.md # this add-on's short description
```

This is an **all-in-one** image: HA add-ons are single-container, so the Go
core and the Python/CVXPY optimizer (normally separate containers) are bundled
and supervised together. See [`docs/ha-addon.md`](../../docs/ha-addon.md).

## Why it lives here

The previous community add-on
([`erikarenhill/ha-addon-forty-two-watts`](https://github.com/erikarenhill/ha-addon-forty-two-watts))
pinned `ghcr.io/frahlg/forty-two-watts:v1.3.0` and stopped tracking releases.
The project was restructured under Sourceful: the canonical image is now
`ghcr.io/srcfl/ftw` (the old path survives only as a byte-identical mirror),
the binary was renamed `/app/ftw`, and the optimizer runs as a separate
`ftw-optimizer` sidecar container. A hand-maintained external wrapper drifts
against those changes.

Full analysis — what still works, the optimizer single-container limitation,
storage growth, and the distribution plan — is in
[`docs/ha-addon.md`](../../docs/ha-addon.md). Versioning the add-on next to the
code lets each release bump the pinned image tag and the add-on `version:` in
the same commit.

## Consuming it

A HA add-on repository must have `repository.yaml` at its **git root**, so the
Supervisor cannot add the monorepo root directly. Either:

- **Publish/mirror** this `deploy/homeassistant/` subtree to a dedicated
add-on repo (e.g. `srcfl/ha-addons`) — ideally from CI on each release — and
point users at that repo URL; or
- **Local test:** copy `deploy/homeassistant/` onto the HA host under
`/addons` (Supervised) or `/root/addons`, then reload local add-ons.
81 changes: 81 additions & 0 deletions deploy/homeassistant/ftw/DOCS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# FTW

Local-first home energy coordination — model-predictive battery dispatch,
PV / load / price planning, and hot-reloadable Lua device drivers — running
as a Home Assistant OS / Supervised add-on.

## Installation

1. Add this add-on repository to Home Assistant (**Settings → Add-ons →
Add-on Store → ⋮ → Repositories**), then install **FTW**.
2. Start the add-on. First boot has no configuration yet, so the app comes
up in its setup wizard.
3. Open the web UI (**Open Web UI**, or `http://<your-ha-host>:8080/setup`)
and complete onboarding: pick your drivers, scan the LAN for devices, and
save. The wizard writes `/data/config.yaml`.

## Configuration

Almost all configuration happens **in the web UI**, not in the add-on
Options tab. The setup wizard and the running dashboard read and write
`/data/config.yaml`, which persists across add-on updates and restarts.
There are no add-on options to set today.

### Networking

The add-on uses **host networking**, required for its core function:

- **Modbus TCP** to inverters and meters on your LAN
- **MQTT** brokers on your LAN, including the Home Assistant **Mosquitto
broker** add-on (reachable at `core-mosquitto:1883`)
- **mDNS / broadcast discovery** (e.g. Sourceful Zap at `zap.local`)

Because of host networking, the UI is served directly on **port 8080** of the
Home Assistant host (not via Ingress). If something else already uses port
8080 on that host, the add-on will fail to bind.

### Optimizer (included)

Upstream runs FTW as several containers — the Go core, the Python/CVXPY
optimizer, an updater sidecar and (optionally) Mosquitto. A Home Assistant
add-on is single-container, so this is an **all-in-one image**: it bundles the
**core and the optimizer together** and supervises both, so you get the full
CVXPY optimizer with no extra setup. (The updater is dropped — Supervisor
handles updates — and Mosquitto is left to the HA Mosquitto add-on.)

Because the CVXPY stack (numpy/scipy) is bundled, the image is a few hundred MB
— a one-time download. If either process stops the add-on restarts as a whole.

### Adding your own Lua drivers

Drop hot-reloadable `*.lua` files into **`/data/drivers`** (via the Samba/SSH
add-ons or a file editor). They overlay the bundled drivers and survive add-on
updates. With the `share` mapping you can also keep them under `/share`.

## Data, persistence & sizing

Everything the app keeps — `config.yaml`, `state.db`, trained battery models,
and the `cold/` Parquet history — lives in the add-on's `/data` volume and is
preserved across updates.

**Please read on SD-card installs.** FTW is a time-series system; its cold
Parquet history grows roughly **a few GB per year** (50 metrics), on top of
`state.db`. That data is on the **shared Home Assistant data partition**, and
Home Assistant **includes add-on `/data` in full backups** — so an unattended
install can bloat both the partition and every backup. Recommendations:

- Prefer an **SSD / large eMMC** over a small SD card.
- Point the app's `state.cold_dir` at `/share` (or a mounted data disk) so
history isn't on the OS partition.
- **Exclude this add-on's data** from routine Home Assistant backups unless
you want the full history in them.

## Updates

Updates are handled by Home Assistant's Supervisor. The app's own in-app
self-update feature (the standalone Docker deploy's Update/Restart buttons) is
not part of this add-on — Supervisor owns the lifecycle here.

## Support

Issues and questions: <https://github.com/srcfl/ftw/issues>.
57 changes: 57 additions & 0 deletions deploy/homeassistant/ftw/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# All-in-one Home Assistant add-on image for FTW.
#
# FTW normally runs as several containers (docker-compose.yml): the Go core,
# the Python/CVXPY optimizer, an updater sidecar and (optionally) Mosquitto.
# A Home Assistant add-on is single-container, so this image bundles the two
# that matter for the app itself — CORE + OPTIMIZER — into one. They already
# communicate over a Unix socket (/run/ftw-optimizer/optimizer.sock), so
# inside one container they simply share that path; there is no networking
# between them and nothing to coordinate.
#
# Dropped on purpose:
# - ftw-updater — Supervisor owns add-on updates.
# - mosquitto — use the HA Mosquitto add-on, or any LAN broker.
#
# Base = the optimizer image (Debian `python:3.12-slim` + the CVXPY venv at
# /opt/venv). The core binary is a fully static, CGO-free Go build, so it runs
# on this Debian base unchanged. build.yaml pins BOTH images per release; they
# version independently (e.g. core v1.4.0 + optimizer v1.3.1).

ARG BUILD_FROM # ghcr.io/srcfl/ftw-optimizer:<tag>
ARG FTW_CORE_FROM=ghcr.io/srcfl/ftw:v1.4.0

FROM ${FTW_CORE_FROM} AS core

# hadolint ignore=DL3006
FROM ${BUILD_FROM}
USER root

# Core needs TLS roots (price/weather HTTPS) and tzdata (tz-aware windows).
# tini is PID 1 so the two long-running children get reaped cleanly.
# hadolint ignore=DL3008
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates tzdata tini \
&& rm -rf /var/lib/apt/lists/*

# Core artifacts from the Go image: the static binary + bundled web + drivers.
COPY --from=core /app/ftw /app/ftw
COPY --from=core /app/web /app/web
COPY --from=core /app/drivers /app/drivers

# Supervisor's persistent volume + the shared optimizer socket directory.
RUN mkdir -p /data/drivers /run/ftw-optimizer
WORKDIR /data

# transport=auto: core dials the bundled optimizer over the socket below, and
# falls back to the in-Go DP planner if it is ever unavailable.
ENV FTW_OPTIMIZER_TRANSPORT=auto \
FTW_OPTIMIZER_SOCKET=/run/ftw-optimizer/optimizer.sock

COPY run.sh /run.sh
RUN chmod +x /run.sh

# Health = the user-facing service (core API on :8080), not the optimizer.
HEALTHCHECK --interval=15s --timeout=5s --start-period=30s --retries=6 \
CMD python3 -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/api/health', timeout=4)" || exit 1

ENTRYPOINT ["/usr/bin/tini", "--", "/run.sh"]
10 changes: 10 additions & 0 deletions deploy/homeassistant/ftw/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# FTW

Local-first home energy coordination — MPC battery dispatch, PV / load /
price planning, and hot-reloadable Lua device drivers — as a Home Assistant
add-on.

Configuration happens in the built-in web setup wizard on first boot. See
[DOCS.md](DOCS.md) for installation, networking, and the storage-sizing
caveat, and the [project repository](https://github.com/srcfl/ftw) for full
documentation.
21 changes: 21 additions & 0 deletions deploy/homeassistant/ftw/build.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Base images for the all-in-one add-on build (see Dockerfile).
#
# build_from = the OPTIMIZER image (Debian python:3.12-slim + the CVXPY venv);
# the static Go core is copied in from FTW_CORE_FROM. Both are multi-arch
# manifests, so the same tags serve amd64 + aarch64.
#
# The two images VERSION INDEPENDENTLY — core follows the app release line
# (v1.4.0) while the optimizer has its own (v1.3.1 stable). Pin both to a
# validated pair (the tags the docker-compose `latest` stack resolves to) and
# bump them together with config.yaml `version:` each release. Never :latest —
# the build must be reproducible.
build_from:
amd64: ghcr.io/srcfl/ftw-optimizer:v1.3.1
aarch64: ghcr.io/srcfl/ftw-optimizer:v1.3.1

args:
FTW_CORE_FROM: ghcr.io/srcfl/ftw:v1.4.0

labels:
org.opencontainers.image.title: "FTW (Home Assistant add-on, all-in-one)"
org.opencontainers.image.source: "https://github.com/srcfl/ftw"
59 changes: 59 additions & 0 deletions deploy/homeassistant/ftw/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# FTW — Home Assistant add-on manifest.
#
# Keep `version` in sync with the pinned base image tag in build.yaml and
# with the repo's package.json version (both track the release line, e.g.
# 1.4.0). Home Assistant Supervisor orders add-on updates by semver on this
# `version`, so it must follow the same line as the published image tags.
name: FTW
version: "1.4.0"
slug: ftw
description: >-
Local-first home energy coordination — MPC battery dispatch, PV / load /
price planning, and hot-reloadable Lua device drivers.
url: https://github.com/srcfl/ftw/tree/master/deploy/homeassistant/ftw
arch:
- amd64
- aarch64
init: false
startup: application
boot: auto

# Host networking is required, not optional, for the core value of the app:
# - Modbus TCP to inverters / meters on the LAN
# - MQTT brokers on the LAN (incl. the HA Mosquitto add-on at core-mosquitto)
# - mDNS / broadcast driver discovery (e.g. Sourceful Zap at zap.local)
# Ingress is intentionally NOT enabled: the app is not X-Ingress-Path aware,
# and host networking + ingress do not compose cleanly. Access is via the
# direct port below.
host_network: true

ports:
"8080/tcp": 8080
ports_description:
"8080/tcp": Web UI / HTTP API
webui: "http://[HOST]:[PORT:8080]/"

# Persistent state — config.yaml, state.db, battery models, cold/ Parquet —
# lives in the add-on's /data volume and survives add-on updates. See the
# wrapper Dockerfile for how /data is wired to the app, and DOCS.md for the
# storage-growth caveat (the cold Parquet history grows ~a few GB/year and
# is included in HA backups).
#
# share:rw lets operators keep hot-reloadable Lua drivers under /share, or
# point the app's state.cold_dir there to keep history off the OS partition.
map:
- share:rw

# Configuration is done in the built-in web setup wizard
# (http://<host>:8080/setup on first boot), which writes /data/config.yaml.
# No options schema yet — wiring HA options.json through to the app's YAML
# config is tracked as follow-up in docs/ha-addon.md.
#
# This is an ALL-IN-ONE image: upstream runs FTW as several containers (core,
# Python/CVXPY optimizer, updater, Mosquitto), but a HA add-on is single-
# container, so the Dockerfile bundles core + optimizer into one and supervises
# both. The optimizer is included — no separate sidecar needed. See
# docs/ha-addon.md. (Trade-off: the CVXPY stack makes the image a few hundred
# MB; on very small SD-card hosts a slim optimizer-less build is possible.)
options: {}
schema: {}
Loading