📡 LoRa Mesh MIT License Domoticz ≥ build 17956

MeshCore Plugin for Domoticz

Connect your MeshCore LoRa mesh nodes to Domoticz as native automation devices. Telemetry, messaging, a real-time dashboard and a scripting bridge — all over a native WebSocket channel.

🔹 Auto-discovery 🔋 Self-node telemetry 📱 Send & receive 🌍 Interactive node map 📊 Historical analytics 🔒 SQLite message store 📤 Delivery ACKs 🚀 dzVents bridge

Screenshots

Click any image to open the full-size view. Use arrow keys or the buttons to navigate.

Requirements

RequirementNotes
Domoticz build ≥ 17956
2025.2.17956 / 2026-05-16
The real-time dashboard uses Domoticz.WebSocketSend / onWebSocketMessage added in that build. Plugin devices work on any version; only the dashboard requires this minimum.
Python 3.6+ Must be enabled in your Domoticz build (default on most platforms).
meshcore Python package Install with pip install meshcore on the Domoticz machine.
MeshCore node reachable via TCP or USB serial TCP: companion app, USB-to-TCP bridge, or MeshcoMod firmware (native WiFi on Heltec v3/v4). Serial: USB cable directly to the node.

Installation

1

Install the Python package

bash
pip install meshcore

Docker: add pip3 install meshcore (or pip3 install --break-system-packages meshcore on Debian 12) to your customstart.sh hook so it survives image rebuilds.

2

Clone the plugin

bash
cd ~/domoticz/plugins
git clone https://github.com/galadril/Domoticz-MeshCore-Plugin.git MeshCore

Naming the folder MeshCore is conventional but not required — Domoticz finds the plugin by scanning for plugin.py in any subdirectory.

3

Restart Domoticz

bash
sudo service domoticz.sh restart
4

Add the hardware

Go to Setup → Hardware, click Add, and select type MeshCore. Set the host and port of your TCP endpoint (default port 5000), or select a Serial port. Click Save.

5

Verify

Open Setup → Log. Within seconds you should see MeshCore connected and your self-node name. Devices appear under Setup → Devices as contacts are discovered.

Configuration

Edit via Setup → Hardware → MeshCore → Edit.

TCP transport

FieldDefaultDescription
MeshCore Host IP address or hostname of the MeshCore TCP endpoint.
MeshCore Port5000 TCP port. Leave at default unless changed on the device side.

Serial transport

FieldDefaultDescription
Serial Port COM / tty port — auto-populated dropdown in Domoticz.
Baud Rate115200 Must match the setting on the connected node.

Plugin options

FieldDefaultDescription
Command Bridge Channelempty Channel name to monitor for !-prefixed commands. Creates three bridge devices for dzVents scripts. Leave blank to disable the bridge. See Automation.
Install Custom DashboardYes Copies meshcore.html into Domoticz templates on start. Access via Setup → More Options → Custom Pages → meshcore.
Debug LevelNone None — errors only  ·  Basic — connection events & device updates  ·  All — full packet trace (very verbose).

Poll intervals

WhatInterval
Message drain + contacts refresh30 s
Full contacts refresh (liveness probe)5 min
Self-node stats (battery, RSSI, uptime, counters)5 min
Stale connection detection → reconnect10 min without push events
💡

Available channels are logged on startup, e.g. MeshCore channels: #0 = General, #1 = MyRoom. Use the exact channel name (without #) in the Command Bridge Channel field.

Custom Dashboard

Enable Install Custom Dashboard in the hardware settings, then open:

path
Setup → More Options → Custom Pages → meshcore

Requires Domoticz build ≥ 17956. The dashboard uses Domoticz.WebSocketSend / onWebSocketMessage introduced in that release. Older Domoticz versions can use the plugin devices but not this dashboard.

FeatureDetails
Node cards Online/offline badge, battery bar, color-coded SNR bars, hops, last seen. Every metric links to its Domoticz device log.
Node map Leaflet dark-mode map showing contacts and heard nodes. Draws real multi-hop repeater path lines. Click the pin icon on any card to center on that node. Hidden until at least one node has coordinates.
Activity heatmap 24-hour rolling Leaflet heatmap of RX events overlaid on the topology map.
Message inbox SQLite-backed, server-side paginated with infinite scroll. Full-text search and per-channel / per-contact scope filters.
DM threads Pin any contact to open its full direct-message conversation with delivery ACK markers.
Compose & reply Channel or direct target picker, emoji picker (700+), @mention highlighting, hover-to-reply on any message.
Historical analytics Side panel: RSSI/SNR/noise time-series, packet volume (per-minute and hourly), top relay keys, per-channel stats. 30-day window.
RX firehose Live packet stream with type filters, pause/resume, per-row SNR/RSSI/path breakdown.
Heard nodes Non-contact adverts with hit count, GPS pin, bulk purge by age or low hit-count.
Statistics Packet-type donut, lifetime totals, leaderboards (top senders, deepest hops, longest relay keys).
Coverage & LoS tools Terrain-aware propagation map and per-link path-profile chart with Bullington knife-edge attenuation.

Sending Messages

Write to the Mesh Inbox device via the Domoticz JSON API, a dzVents script, or the dashboard compose bar.

Message format

Write to deviceResult
hello worldDirect message to the first discovered contact.
garden: helloDirect message to the contact named garden.
#General: helloBroadcast on the channel named General.
#0: helloBroadcast on channel index 0.

Internal control commands

!-prefixed writes configure the plugin or device — they are never transmitted as messages.

CommandEffect
!remove <name>Remove the named contact from the connected device.
!favorite add <name>Mark contact as favorite (sorted first, persisted to meshcore_favorites.json).
!favorite remove <name>Drop favorite.
!flood_scope #<tag>Set default flood scope (empty = global).
!set adv_loc_policy <0|1>Never share / share location in adverts.
!set telemetry_base <0|1|2>Off / Public / Always.

Device Reference

All devices are created automatically as data arrives — no manual configuration needed.

Global devices

DeviceTypeDescription
Mesh InboxText Last received message. Format: [C0|sender] text or [P|sender] text. Value change triggers a WebSocket push to the dashboard.
Mesh Msgs ReceivedCustom (msgs)Running counter of messages received since plugin start.
Mesh Msgs SentCustom (msgs)Running counter of messages sent since plugin start.

Self (connected) node — DeviceID: self

DeviceTypeSource
StatusSwitchOn when connected; Off on disconnect.
Battery %Percentagebattery_mv from get_stats_core
Battery VCustom (V)battery_mv from get_stats_core
RSSICustom (dBm)last_rssi from get_stats_radio
SNRCustom (dB)last_snr from get_stats_radio
Noise FloorCustom (dBm)noise_floor from get_stats_radio
Last SeenTextTimestamp of last received event.
UptimeCustom (min)uptime_secs from get_stats_core
Airtime TXCustom (s)tx_air_secs from get_stats_radio
Pkts SentCustom (pkts)sent from get_stats_packets
Pkts RecvCustom (pkts)recv from get_stats_packets

Remote contacts — DeviceID: pubkey[:12]

DeviceTypeNotes
StatusSwitchOnline when last advert < 8 h ago; offline otherwise.
SNRCustom (dB)From incoming messages (not a radio poll).
Last SeenTextTimestamp of last advert or message.
HopsCustom (hops)Contact out_path_len from the contacts list.

Command Bridge devices

Created when a Command Bridge Channel is configured. See Automation.

DeviceTypeDirection
MeshCore Command InTextPlugin → dzVents. JSON payload of each inbound !-command.
MeshCore ReplyTextdzVents → Plugin. Write JSON reply here before triggering Send.
MeshCore SendSwitch (Push On)dzVents → Plugin. Turn On to dispatch.

Manual Node Locations

Pin nodes without GPS on the dashboard map by creating meshcore_locations.json in domoticz/plugins/MeshCore/:

json
{
    "Garden":  { "lat": 52.3690, "lon": 4.9075 },
    "Rooftop": { "lat": 52.3712, "lon": 4.9102 }
}
  • Node names are case-sensitive and must match contact names exactly.
  • Live GPS data from a node automatically overrides its manual entry.
  • The map section is hidden until at least one node has coordinates.

Automation & dzVents Bridge

The Command Bridge turns any channel message starting with ! into a dzVents event — giving your scripts live access to incoming LoRa commands and the ability to broadcast replies back to the mesh.

Setup

  1. Set a Command Bridge Channel in the hardware settings.
  2. Restart the plugin — three bridge devices are created.
  3. Copy an example script from below to ~/domoticz/scripts/dzVents/generated_scripts/.
  4. Edit the CONFIGURATION block and enable the script.

Inbound payload (written to MeshCore Command In)

json
{
    "id":      42,             // origin handle — valid for 5 min after arrival
    "seq":     17,             // monotonic; use to deduplicate across restarts
    "cmd":     "!status",      // full command string, including ! prefix
    "sender":  "AlphaNode",
    "pubkey":  "a1b2c3d4e5f6",
    "channel": "General",
    "snr":     8.5,
    "ts":      1716023456
}

Reply formats (write to MeshCore Reply, then trigger MeshCore Send)

json
// Id-based (first reply only — plugin resolves channel from id)
{ "id": 42, "text": "reply text" }

// Channel-based (queued follow-ups; id is consumed after first send)
{ "to": "#General", "text": "follow-up text" }

// Direct message override
{ "id": 42, "to": "AlphaNode", "text": "private reply" }

The plugin consumes the id record on the first reply. For multi-part responses (e.g. !status), use id-based routing for the first message, then channel-based routing for follow-ups — spaced at least one minute apart to avoid LoRa TX overlap.

Example: Simple Bridge

File: examples/dzvents_meshcore_bridge.lua

The minimal starting point. Extend the if/elseif chain with your own commands.

lua
return {
    on = { devices = { "MeshCore Command In" } },

    logging = { level = domoticz.LOG_DEBUG, marker = "MeshCore-Bridge" },

    execute = function(dz, item)
        local ok, m = pcall(dz.utils.fromJSON, item.text)
        if not ok or type(m) ~= "table" then return end

        local cmd       = m.cmd    or ""
        local sender    = m.sender or "?"
        local lower_cmd = string.lower(cmd)
        local reply

        if     lower_cmd == "!ping" then
            reply = "pong"

        elseif lower_cmd == "!status" then
            reply = string.format("OK | time=%s uptime=%dm",
                os.date("%Y-%m-%d %H:%M:%S"),
                math.floor(dz.utils.osSeconds() / 60))

        elseif string.sub(lower_cmd, 1, 6) == "!hello" then
            local name = string.sub(cmd, 8)
            if name == "" then name = sender end
            reply = "Hello, " .. name .. "!"

        else
            reply = "unknown command: " .. cmd
        end

        -- 1. Write reply payload  2. Trigger dispatch
        dz.devices("MeshCore Reply").updateText(
            dz.utils.toJSON({ id = m.id, text = reply })
        )
        dz.devices("MeshCore Send").switchOn().checkFirst()
    end,
}

Example: Chat Responder

File: examples/meshcore_chat_responder.lua

A full interactive bot. Queries live Domoticz sensor data and returns themed replies. Multi-part responses are queued one per minute.

Supported commands

CommandResponse
!helpList all available commands.
!statusFull summary: climate, weather, energy, home (multi-part).
!climateIndoor temperature, humidity, thermostat, ventilation.
!weatherOutdoor temperature, wind, rain.
!energyGrid usage/delivery, solar, home battery, gas.
!homeWater meter, presence status.
!device <name>Query any Domoticz device by its exact name.
!switchesAll switches and their state (capped at 15).
!tempAll temperature sensors (capped at 10).

Configuration block

Set any entry to nil to silently skip it.

lua
local MESHCORE_INBOX = 'MeshCore Command In'
local MESHCORE_REPLY = 'MeshCore Reply'
local MESHCORE_SEND  = 'MeshCore Send'
local CMD_PREFIX     = '!'

local DEVICES = {
    tempIndoor   = 'Temperature - Living Room',
    tempBathroom = 'Temperature - Bathroom',   -- optional
    tempOutdoor  = 'Temperature - Outside',
    thermostat   = 'Thermostat',
    heatpump     = 'Heat Pump Status',          -- Text device, optional
    ventilation  = 'Ventilation',              -- Selector switch, optional
    power        = 'Power',                     -- P1 Smart Meter
    solar        = 'Solar Power',               -- optional
    homeBattery  = 'Home Battery',              -- Percentage device, optional
    gas          = 'Gas',                       -- optional
    water        = 'Water Meter',               -- optional
    presence     = 'Presence',                  -- Selector switch, optional
    wind         = 'Wind',                      -- optional
    rain         = 'Rain',                      -- optional
}

Example mesh conversation

text
[You]    !climate
[Bot]    Climate: Indoor 20.3C, 52% | Thermostat 19.5C | Heat pump: Heating

[You]    !device Power
[Bot]    Power: 380W (updated: 2025-01-15 14:32:00)

[You]    !status
[Bot]    Climate: Indoor 20.3C, 52% | Thermostat 19.5C
[Bot+1m] Weather: 14.8C, 65% | Wind W 3.2 m/s
[Bot+2m] Energy: Solar 1240W | Delivering 380W | Gas today 0.42

Example: Status Reporter

File: examples/meshcore_status_report.lua

Sends periodic home-status updates to a channel without any user command. Ideal for a monitoring-only node that needs regular snapshots.

Behaviour

  • Every hour (minute 00): builds themed messages and queues them.
  • Every minute: drains one queued message (so a 3-topic status takes 3 minutes).
  • On presence change: fires an immediate alert to the channel.

Configuration block

lua
local CHANNEL_NAME   = 'General'          -- target channel name
local MESHCORE_REPLY = 'MeshCore Reply'
local MESHCORE_SEND  = 'MeshCore Send'

local DEVICES = {
    tempIndoor  = 'Temperature - Living Room',
    tempOutdoor = 'Temperature - Outside',
    thermostat  = 'Thermostat',
    power       = 'Power',
    solar       = 'Solar Power',    -- optional
    gas         = 'Gas',            -- optional
    presence    = 'Presence',       -- optional, triggers immediate alert
}

Example output on the mesh

text
[14:00] Climate: Indoor 20.3C, 52% | Thermostat 19.5C
[14:01] Weather: 14.8C, 65%
[14:02] Energy: Solar 1240W | Delivering 380W | Gas today 0.42

Works With

Software and firmware that pairs well with this plugin.

Changelog

Full release notes on GitHub Releases.

Automated release workflow

Added GitHub Actions workflow for automated releases on tag push.

v0.0.6 21 May 2026

Map tools, historical analytics & fixes

  • Coverage & line-of-sight terrain tools on the node map
  • Historical analytics side panel (RSSI, SNR, packet volume, relay keys)
  • Queued-message origin resolution fix
v0.0.5 20 May 2026

dzVents command bridge

  • Command Bridge channel: three bridge devices for dzVents scripting
  • Inbox scope filter and layout fixes
  • Clock-skew correction for timestamps
v0.0.4 19 May 2026

UI & theming fixes

  • Dashboard UI and theming improvements
  • Collapsible node settings panel
  • Contact view state preserved in local storage
v0.0.3 19 May 2026

WebSocket dashboard & SQLite message store

  • Real-time dashboard over native Domoticz WebSocket channel (no more JSON-file polling)
  • SQLite message store as single source of truth for inbox and DM threads
  • Serial transport support
  • Reliable messaging and device-side settings
v0.0.2 6 Apr 2026

dzVents example scripts

Status reporter and chat responder example scripts added.

v0.0.1 4 Apr 2026

Initial release

Self-node telemetry, message inbox, send controls and custom dashboard.

View all releases on GitHub →

Troubleshooting

Enable Basic or All debug logging in the hardware settings, then check Setup → Log.

SymptomFix
meshcore package not installed in log Run pip install meshcore and restart Domoticz. On Docker, add it to customstart.sh.
No devices appear after connecting Wait 30 s for the initial contacts poll. Check that your node is advertising on the mesh. Enable Basic debug to see discovered contact names.
Dashboard shows "Connecting…" indefinitely Your Domoticz build is older than 17956 — update Domoticz. The WebSocket plugin channel was added in that release.
Command Bridge devices not created Set a non-empty channel name in the Command Bridge Channel field, save, then restart the plugin.
dzVents script fires but no reply arrives on the mesh Verify the channel name matches the plugin setting (case-insensitive, leading # stripped). Enable debug and look for Replying (id=…) in the log.
Connection drops and never recovers The plugin detects a stale link after 10 min and reconnects automatically. Enable All debug to trace reconnect attempts.
Remote node battery / RSSI not showing These require a STATUS_RESPONSE push from the remote node, which needs firmware support. Only the self (connected) node reliably provides these metrics.
Old devices orphaned after upgrade After the DomoticzEx migration, delete old per-node devices via Setup → Devices, filtered by MeshCore hardware. The plugin recreates them automatically.

Updating

bash
cd ~/domoticz/plugins/MeshCore
git pull
sudo service domoticz.sh restart

For bugs and feature requests, open a GitHub Issue.

Support & Donate

This plugin is free and open-source. If it saves you time or brings your LoRa mesh into Domoticz in a useful way, a small contribution is always appreciated.

Buy me a coffee

If the plugin saves you time, a small PayPal donation goes a long way.

☕ Donate via PayPal
🐞
Found a bug?

Open an issue on GitHub. Include your Domoticz version, plugin version, and the relevant log lines.

🐞 Open an Issue
🆕
Want a feature?

Ideas and pull requests are welcome. Check the existing issues first to avoid duplicates.

🆕 View on GitHub
💬
MeshCore Community

Join the official MeshCore Discord to discuss firmware, hardware, range tests and share your mesh setups.

💬 Join Discord