Manual installation
This page sets up an Energy Node on a Raspberry Pi and connects it to the main site. It is written for the hardest case, a Raspberry Pi 1 Model B (ARMv6), so everything here also works on a Pi 2, 3, 4, 5 or Zero.
On ARMv6 some software has to be installed in an old version first and updated afterwards, because the current installers no longer support the architecture. Section 3 covers this. Read it before you start.
The desktop installer runs sections 2 to 8 from your own computer: package bootstrap, broker, firewall, Tailscale, the dashboard, HTTPS and the device services. This page is the manual route and explains what the installer automates and why.
0. What ends up where
| Where | What |
|---|---|
| Development machine | Go toolchain, Node.js, Python venv, this repository, the installer’s developer CLI |
Node, /home/energynode/ |
Python services, devices/*.json, the dashboard binary and its data dir |
Node, /etc/energy-node/ |
config.json (the single source of truth) and mqtt.pw |
Node, /etc/energy-node-dashboard/ |
auth.pw (dashboard admin password) |
Node, /etc/systemd/system/ |
one unit per service |
Node, /etc/mosquitto/conf.d/ |
broker config and the bridge to the main site |
Nothing is built on the node. The development machine builds an installation package with the cross-compiled Go dashboard, the Python services as source and their dependencies as wheels.
1. Prerequisites
At the remote site you need a Raspberry Pi (Pi 1 Model B or newer) with a
reliable power supply, an SD card, a network connection (Ethernet or WiFi) and
SSH access from the development machine. A Pi 1 with too little voltage
throttles without telling you, which is why the node’s diagnostics report
vcgencmd get_throttled.
The main site needs Home Assistant with an MQTT broker (for example the Mosquitto add-on) and the MQTT integration enabled. You also need a Tailscale account with both machines in the same tailnet. The free tier is enough.
The development machine needs Go 1.22 or newer (dashboard/go.mod), Node.js
for the dashboard’s JS tests, Python 3.9 or newer with a project venv, git
and bash. The installer module pins a newer Go than the dashboard.
scripts/dev/run-installer.sh lets Go fetch that toolchain itself.
git clone <your-fork> energy-node && cd energy-node
python3 -m venv .venv && .venv/bin/pip install -e libs/energy_node_common -e libs/battery_soc_core pytest
2. Operating system
Use a 32-bit (armhf) Raspberry Pi OS Lite image. The 64-bit images do not run
on ARMv6. If the current 32-bit image does not boot on a Pi 1, use Raspberry Pi
OS (Legacy, 32-bit) Lite instead. The reference node runs Python 3.11.2, which
is enough for every service in this repository (requires-python >= 3.9).
Flash the card with the Raspberry Pi Imager and set hostname, user, SSH and WiFi there. On a headless remote site you do not want to find out afterwards that SSH is off. The root partition resizes itself on first boot, so leave it alone.
ssh <user>@<node>.local
sudo apt update && sudo apt full-upgrade -y
sudo raspi-config # timezone
A full apt upgrade on a Pi 1 takes a long time. Let it finish before you go
on, because a half upgraded system makes every later error harder to read.
The installer also updates the node’s system packages (apt-get update, then
apt-get upgrade) once per installer version. You can switch this off on its
configuration screen (Update system packages). Switched off, it still
checks and writes the number of available updates to the log. It never removes
packages and never reboots. When a kernel or firmware update needs a reboot,
the result screen and the diagnose say so. Reboot the node yourself with
sudo reboot.
3. Packages you must install outdated first
Two ecosystems have dropped or restricted ARMv6 support. In both cases the program still runs and only the installer refuses. The fix is the same each time: install the last version that still supports the architecture, then let the software update itself in place.
| Software | Why the normal path fails | What to do instead |
|---|---|---|
| Tailscale | The official install script serves no ARMv6 build past 1.62.0 | Unpack the 1.62.0 tarball by hand, then tailscale update |
| Python packages | Recent Raspberry Pi OS marks the system Python as externally managed (PEP 668) | Install into the system interpreter with --break-system-packages |
3.1 Tailscale — install 1.62.0, then update
cd ~
wget https://dl.tailscale.com/stable/tailscale_1.62.0_arm.tgz
tar -xzf tailscale_1.62.0_arm.tgz
cd tailscale_1.62.0_arm
sudo cp tailscale tailscaled /usr/sbin/
sudo cp systemd/tailscaled.service /etc/systemd/system/
sudo cp systemd/tailscaled.defaults /etc/default/tailscaled
Leave /etc/default/tailscaled at its default (FLAGS=""). Do not set
--tun=userspace-networking. With it, tailscaled creates no tailscale0
interface and installs no kernel routes, so the node cannot route IP traffic to
other tailnet peers at all. The ARMv6 kernel does not need it: wireguard-go
runs in userspace on every architecture and only needs /dev/net/tun, which
Raspberry Pi OS already provides.
sudo systemctl daemon-reload
sudo systemctl enable --now tailscaled
sudo tailscale up
Open the printed login URL in any browser and approve the machine. Then update in place to get off the old version:
sudo tailscale update
tailscale status
tailscale ip -4 # the node's 100.x.x.x address
Disable key expiry for this machine in the Tailscale admin console. Otherwise the node drops off the tailnet after about 180 days and needs an interactive login on site.
Reach the main site over its Tailscale IP (100.x.x.x) and not over a subnet
route.
Do not pass --accept-routes on the node and do not advertise routes from it.
On one node, an old --advertise-routes setting took effect once IP forwarding
was enabled and turned the node into a router for a whole /24 without anyone
noticing. Clients with --accept-routes sent their LAN traffic for that subnet
through the tunnel, so ping and curl to those hosts failed while existing
SSH sessions stayed up. Clear it with
sudo tailscale up --reset --advertise-routes= and check in the admin console
that the route is gone.
On the Home Assistant side, run the Tailscale add-on with
userspace_networking: false so the HA host gets a 100.x address. The
Mosquitto add-on’s 1883:1883 port mapping binds every interface, so the node
reaches the broker at <HA-Tailscale-IP>:1883. You need neither
advertise_routes nor a route approval in the admin console.
Never point the bridge at a LAN IP that could also exist in the node site’s own
network. Tailscale prefers a local network over an advertised route, so the
bridge would connect to the wrong local host. A 100.x address cannot be
confused like that.
3.2 Python packages on an externally managed system
Recent Raspberry Pi OS releases mark the system Python as externally managed,
so pip install refuses to touch it. The services run on the system
interpreter on purpose. Every unit starts /usr/bin/python3 and there is no
venv on the node, so you need the flag:
sudo python3 -m pip install --break-system-packages \
"paho-mqtt>=2.0" apsystems-ez1 tinytuya requests
Install only what you need. Every service needs paho-mqtt. apsystems-ez1
is only for the EZ1 bridge, tinytuya only for Tuya devices and requests for
the Shelly and Trucki bridges.
On an older Legacy image the --break-system-packages flag does not exist yet.
Leave it out there and use plain sudo python3 -m pip install ….
The installer’s bootstrap step 50 does this for you. It installs every wheel
from the bundle with --no-index, so the node never fetches packages from PyPI
over a possibly unreliable link. The build machine downloaded them from
piwheels beforehand.
3.3 Go and Node.js — do not install them on the node
Compiling Go on a Pi 1 takes forever and is not needed. The dashboard is cross-compiled on the development machine into one statically linked ARMv6 binary with CGO disabled:
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -o energy-node-dashboard ./cmd/dashboard
scripts/build/make_bundle.sh runs this when it builds the installation
package. Node.js is only used for the dashboard’s browser side tests on the
development machine.
4. Broker and firewall on the node
sudo apt install mosquitto mosquitto-clients -y
sudo systemctl enable --now mosquitto
sudo mosquitto_passwd -c /etc/mosquitto/passwd <mqtt-user>
/etc/mosquitto/conf.d/default.conf:
listener 1883
allow_anonymous false
password_file /etc/mosquitto/passwd
sudo systemctl restart mosquitto
mosquitto_sub -h localhost -u <mqtt-user> -P <password> -t 'test/#' -v # in one shell
mosquitto_pub -h localhost -u <mqtt-user> -P <password> -t test/x -m ok # in another
Firewall:
sudo ufw allow ssh
sudo ufw allow 1883/tcp # MQTT
sudo ufw allow 8080/tcp # dashboard (HTTP)
sudo ufw allow 443/tcp # dashboard (HTTPS via Caddy, see section 8)
sudo ufw enable
If you use the Shelly wake webhook (opt-in, see docs/services/index.md),
open its port as well. The installer only does this when you tick
Shelly wake webhook in the firewall on its configuration screen (step 35,
off by default, only offered while the Shelly service is selected):
sudo ufw allow 8082/tcp # Shelly wake webhook, default webhook_port
5. Bridge to the main site
You configure the bridge to the main site on the dashboard’s MQTT settings
page. The node opens an outgoing connection to the main site’s broker and
mirrors outstation/# in both directions. Enter the main site’s Tailscale IP
(100.x, see section 3.1) and the broker credentials.
Check on the main site that outstation/# messages arrive, with MQTT Explorer
or mosquitto_sub -t 'outstation/#' -v. Tailscale already encrypts the link,
so you need no extra TLS.
Next: Deploying and verifying