diverter-device-emulator/README.md
2026-09-24 10:57:06 +01:00

6.2 KiB
Raw Blame History

DVgo device emulator

Node.js ThingsBoard device emulator with a SQLite-backed web portal. Each virtual device can connect over MQTT, publish live *_s telemetry, react to shared-attribute commands from the apps, and be taken offline without deleting the ThingsBoard record.

Requires Node 22+ (uses the built-in node:sqlite module).

Python historical data generation is not included.

What it does

  • Create / delete real ThingsBoard devices via the REST API (delete still removes the emulator row if the TB device is already gone)
  • Persist credentials, telemetry, published-key selection, and plant settings in SQLite
  • MQTT connect with the device access token as username (v1/devices/me/telemetry, plus attributes and RPC)
  • Web portal (http://localhost:3080) to:
    • add devices (or attach an existing MQTT access token)
    • select devices (select all / deselect all) and enable, disable, or delete
    • Connect / Disconnect MQTT (new devices stay idle until you connect)
    • start / stop the telemetry loop (Start telemetry is disabled until connected)
    • toggle which keys are included in each publish (per-card On, plus select all / deselect all under Publish now)
    • edit every live _s key from the device dashboard
    • run a virtual plant that follows boost and schedule commands from the apps

On startup, devices already in SQLite are restored. They reconnect only if they were left enabled (not disabled). New devices are created disconnected, with telemetry off.

Virtual plant

While MQTT is connected, the device polls ThingsBoard shared attributes every 5 seconds (and also receives live pushes). It understands:

Source Keys
One-shot boost boostNowHw, boostNowAux (minutes; written by the apps via TB REST)
HW schedule hwRlyMode 2 or 3, plus hwRlySchedMode0/1StartTime / StopTime (minutes since midnight)
Aux schedule auxRlyMode 2 or 3, plus auxRlySchedMode0/1Start / Stop

Schedule saves in the apps are SQL-only, so this emulator polls rather than waiting for a TB push.

Behaviour:

  • Red / grey lamps for hot water boost and aux boost / divert
  • While HW is on, heatsink_s rises at Heat rate (°C / min) (integer, default 1)
  • At Thermostat max (°C) (default 65) heating stops, rlyInSel_s goes to 0, and boostNowHw is written back to ThingsBoard as 0
  • Live state is published as heatsink_s, rlyInSel_s, auxOn_s, auxRelay_s
  • Battery section is display-only for now: Battery Charging lamp stays off, Charge rate (% / min) defaults to 10
  • Solar, grid, home load, hot water, aux, and battery cards set the energy balance in watts. Grid is calculated. Lamps are red for draw, green for supply, and grey for zero. Hot water watts apply only while heating, and aux watts only while aux is on. Those setpoints write the live power and flow telemetry keys. loadPower_s is home plus aux.

Telemetry keys

Each device can publish the following live *_s keys on v1/devices/me/telemetry. Dashboard cards are ordered Status, Power, Energy flows, Energy today, Validity. Only keys with On checked are sent.

Status

  • heatsink_s — hot water / tank temperature (°C)
  • batterySoc_s — battery state of charge (%)
  • soc_s — SoC fallback (%)
  • batteryPercent_s — battery percent fallback (%)
  • auxOn_s — aux on (0/1)
  • auxRelay_s — aux relay (0/1)
  • rlyInSel_s — control load heating (0/1)
  • xCntlLoadState — control load state (integer; no _s suffix)

Power (W)

  • solarPower_s — solar generation
  • loadPower_s — home load
  • hotWaterInPower_s — hot water in
  • hotWaterOutPower_s — hot water out
  • batteryPower_s — battery power (negative = charging, positive = discharging)
  • gridPower_s — grid power (positive = importing, negative = exporting)

Energy flows (W)

  • solarToLoadPower_s
  • solarToHotWaterPower_s
  • solarToBatteryPower_s
  • solarToGridPower_s
  • gridToLoadPower_s
  • gridToHotWaterPower_s
  • gridToBatteryPower_s
  • batteryToLoadPower_s
  • batteryToHotWaterPower_s
  • batteryToGridPower_s

Energy today (Wh)

  • solarToday_s
  • loadToday_s
  • hotWaterInToday_s
  • hotWaterOutToday_s
  • solarToLoadToday_s
  • solarToHotWaterToday_s
  • solarToGridToday_s
  • solarToBatteryToday_s
  • gridToLoadToday_s
  • gridToHotWaterToday_s
  • gridToBatteryToday_s
  • batteryToLoadToday_s
  • batteryToHotwaterToday_s — note the lowercase w in Hotwater
  • batteryToGridToday_s
  • batteryExportToday_s
  • batteryImportToday_s
  • gridImport_s
  • gridExport_s

Validity

  • vPwrReadings_s — bitmask of valid live power keys (65535 = bits 0–15 all valid)

Setup

cp .env.example .env
# fill TB_USERNAME / TB_PASSWORD (tenant admin or a user who can create devices)
npm install
npm start

Open http://localhost:3080.

Deploy

Build a Docker image locally and copy it to an Ubuntu host over SSH. See deploy/README.md. From this repo: ./deploy/ship.sh (SSH host diverter-emu). The SQLite file stays on the host in /opt/diverter-device-emulator/data.

Optional env:

  • PORT — portal port (default 3080)
  • TB_HTTP_URL — ThingsBoard REST origin (default https://tb-dvgo.datavolt.au)
  • TB_DEVICE_PROFILE_ID — defaults to the Diverter / DVgo profile (e6b53240-7fec-11f1-a69d-edbc0b9170ac). The add-device dropdown is preselected to a profile named Diverter or DVgo when ThingsBoard returns one.
  • TB_DEVICE_TYPE — default DVgo
  • TB_MQTT_URL — default mqtts://tb-dvgo.datavolt.au:8883 (TLS). Port 1883 is not reachable from outside the TB host.
  • TB_MQTT_INSECURE — true to skip TLS certificate verification
  • PORTAL_HOST — public hostname for HTTPS on the Lightsail deploy
  • PORTAL_USER / PORTAL_PASSWORD — login for the portal
  • ALLOWED_IPS — comma-separated allowlist for /api
  • TELEMETRY_INTERVAL_MS — default 5000
  • DB_PATH — default ./data/emulator.db

If a device already exists in ThingsBoard, tick Add existing device in the add modal and paste its MQTT access token. The emulator looks the device up on the server (id, name, label, type) and attaches it. You can also create by TB name with no token, or save-by-name if create times out.