Development guide
How Epilykos is built, how to run it locally, and where to add cards and data sources.
Troubleshooting
Fixes for the problems people run into most.
Contributing
Report problems, suggest changes to Epilykos, and help improve this wiki.
This page is for anyone who wants to work on Epilykos itself: how it's put together, how to run it on your own machine, and where to start when adding a card or a data source.
What it's built with
- Backend: Node.js 22 and Express, spread across
server.js,modules/,routes/andservices/. - Database: SQLite (better-sqlite3, in WAL mode), stored in
data/energy.db. - Frontend: plain JavaScript ES modules with no framework, Chart.js for the charts, and GridStack for the layout editor.
- Live updates: a WebSocket at
/ws. - Python helpers: one for Bluetooth (built on
aiobmsble) and the Tuya bridges (built ontinytuya).
Where things live
| Path | What you'll find there |
|---|---|
server.js | The entry point: Express, routes, the WebSocket, start-up and shutdown |
modules/ | Data sources, the database, metrics, notifications, backups, updates, remote access |
modules/dongle/, modules/bms-decoders/, modules/rs232-decoders/, modules/pvoutput/ | The protocol code for each family of devices |
services/PollingManager.js | The 30-second polling cycle |
routes/ | API routes that have been split out of server.js |
public/ | The dashboard, Settings, the layout editor and the setup wizard |
public/js/components/ | One file per dashboard card |
private/ | Pages that need sign-in: the card showcase and the Controls page |
profiles/ | Device profiles: the Modbus maps, plus dongles/ and rs232/ |
docker/ | The container's entrypoint |
test/ | The test suite, which needs no hardware |
Run it locally
npm ci
npm test # every test/*.test.js, no hardware needed
python3 modules/ble/ble_helper.py --self-test # the Bluetooth helper's own checks
PORT=3002 SETTINGS_PASSWORD=dev node server.js # a local copy, with data/ in the working directoryUse Node.js 22. The Docker image is built on node:22, and the native modules (serialport and
better-sqlite3) are compiled for whichever Node version installed them, so mixing versions causes
errors. To use Bluetooth outside Docker, you also need Python 3.12 or newer, pip install -r modules/ble/requirements.txt, and BlueZ running.
How data flows
Sources ──► metric queue ──► one SQLite transaction ──► dashboard state ──► WebSocket ──► cards- The polling cycle (
services/PollingManager.js) runs every 30 seconds and visits Home Assistant, Modbus, Tuya, RS232, the battery estimate, combined metrics, history and grid status, in that order. Each one runs inside its own circuit breaker (modules/circuitBreaker.js): after five failures in a row the breaker opens, and it tries again 60 seconds later. - Other sources keep their own time. Dongles and Bluetooth devices are read at their own intervals, REST sources on their shared interval, and MQTT messages arrive whenever they're published.
- Writes are batched. Sources queue their values (
queueMetricWriteandqueueMetricValueinmodules/database.js), andflushMetrics()writes the whole batch in one transaction after each cycle and every few seconds in between. Please don't write to SQLite for every single reading. - Dashboards receive the state over the WebSocket, first in full and then as smaller updates, and keep a copy in IndexedDB so a reopened app has something to show straight away.
Adding a card
- Write the card in
public/js/components/myCard.js: a builder that creates its element, and an updater that refreshes it from the dashboard state. - Register the builder in
public/js/components/index.js, undercomponentBuilders(orcontrolBuildersfor a control card). - Call its updater from
public/js/cards-update.js, which both the dashboard and the editor's live previews use. - Add it to the block library in
public/js/editor-catalog.js(BLOCKS), with its group, name, a one-line description, an icon and its default size in grid columns × rows. - Add its settings to the editor's inspector in
public/js/editor.js(the Data, Style and Layout tabs), and its styles topublic/cards.css. - Have the card read its settings through
components/blockRegistry.js, so several copies of it can sit on one dashboard with different metrics.
Every card also appears in the card showcase at /showcase
(private/js/showcase.js) with made-up readings, so add yours there too.
Adding a data source
- Write the module in
modules/, queueing values rather than writing them directly. - If it fits the 30-second cycle, add
{ id, pollFn }todefaultRegistry()inservices/PollingManager.js. If not, give it its own timer, and make sure it stops cleanly on shutdown. - Add its API routes (listing profiles, testing the connection), and restart it whenever its settings are saved.
- Add its card in Settings, in
public/settings.htmlandpublic/settings.js. - Add its config key to
essentialKeysinmodules/database.js, so the setting gets created. - Register any password or token field in
SECRET_FIELDSinmodules/encryption.js, so it's encrypted when stored. - Add tests in
test/.
Sources that use profiles (Modbus, RS232 and dongles) read their register maps from profiles/. This
wiki's Supported hardware page is generated from those files.
Secrets at rest
Passwords and tokens saved in Settings are stored as $enc1$ envelopes (AES-256-GCM), with the key kept
in data/encryption-key. SECRET_FIELDS lists which settings count as secret. Backups can lock them
with a passphrase instead; see Backup and restore.
Builds and releases
| Workflow | When it runs | What it does |
|---|---|---|
test.yml | Pushes to dev, and pull requests to main and dev | Runs the test suite |
docker-build-dev.yml | Pushes to dev | Builds irunmole/epilykos:dev |
docker-build.yml | Pushes to main (except documentation-only changes), and weekly | Builds irunmole/epilykos:latest |
The weekly rebuild brings the base image's security fixes and Tailscale's latest stable release to
everyone, even without a new Epilykos release. After each build, the image's digest is sent to
epilykos-os, and merging the pull request there releases it
to Raspberry Pi boxes. Installed copies find out about new builds from the repository's update-feed
branch.
Last updated on