Epilykos Wiki

Development guide

How Epilykos is built, how to run it locally, and where to add cards and data sources.

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/ and services/.
  • 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 on tinytuya).

Where things live

PathWhat you'll find there
server.jsThe 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.jsThe 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 directory

Use 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 (queueMetricWrite and queueMetricValue in modules/database.js), and flushMetrics() 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

  1. 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.
  2. Register the builder in public/js/components/index.js, under componentBuilders (or controlBuilders for a control card).
  3. Call its updater from public/js/cards-update.js, which both the dashboard and the editor's live previews use.
  4. 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.
  5. Add its settings to the editor's inspector in public/js/editor.js (the Data, Style and Layout tabs), and its styles to public/cards.css.
  6. 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

  1. Write the module in modules/, queueing values rather than writing them directly.
  2. If it fits the 30-second cycle, add { id, pollFn } to defaultRegistry() in services/PollingManager.js. If not, give it its own timer, and make sure it stops cleanly on shutdown.
  3. Add its API routes (listing profiles, testing the connection), and restart it whenever its settings are saved.
  4. Add its card in Settings, in public/settings.html and public/settings.js.
  5. Add its config key to essentialKeys in modules/database.js, so the setting gets created.
  6. Register any password or token field in SECRET_FIELDS in modules/encryption.js, so it's encrypted when stored.
  7. 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

WorkflowWhen it runsWhat it does
test.ymlPushes to dev, and pull requests to main and devRuns the test suite
docker-build-dev.ymlPushes to devBuilds irunmole/epilykos:dev
docker-build.ymlPushes to main (except documentation-only changes), and weeklyBuilds 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

On this page