Skip to main content

Improve Blueprint

In Level 1 we wrote a blueprint with hardcoded connection parameters. This works for a single installation, but cannot be reused across different installations without modifying the code.

At this level we introduce three important concepts:

  • Configuration — makes connection parameters and device-specific settings adjustable through the UI, without touching the code.
  • Reconnect pattern — an efficient way to manage the device connection client: create it once, reuse across telemetry cycles, and automatically recreate when configuration changes.
  • Connection alerts (conn_alerts) — a dedicated telemetry attribute that separates device communication issues from device operational alerts.

Adding Configuration to the Manifest

The configuration section in the manifest declares parameters that users can set through the UI after the device is created. Parameters are organized into logical groups.

We'll add two configuration groups to our hydrogen sensor example:

manifest.yml
blueprint_spec: device/3.0

display_name: H2 Sensor
description: Hydrogen sensor integration example.
icon: enapter-gauge

runtime:
type: lua
requirements:
- modbus
options:
file: main.lua

# Configuration makes connection and device parameters adjustable via UI and API
configuration:
connection:
display_name: Connection
description: Connection settings for the Modbus device.
parameters:
conn_str:
display_name: Connection URI
type: string
format: connection_uri
required: true
description: "URI to connect to the device (e.g. port://rs485 or tcp://192.168.1.42:502)."
address:
display_name: Modbus Address
type: integer
required: true
description: Modbus unit ID of the device (1-247).
sensor:
display_name: Sensor
description: Sensor-specific settings.
parameters:
h2_limit:
display_name: Hydrogen Concentration Limit
type: float
default: 4.0
description: H2 concentration threshold in %LEL.

properties:
serial_number:
type: string
display_name: Serial Number

telemetry:
h2_concentration:
type: float
unit: "%LEL"
display_name: H2 Concentration

alerts:
h2_high:
severity: error
display_name: High H2 Concentration
description: Hydrogen leakage is detected.
communication_failed:
severity: warning
display_name: Device Communication Failed
description: Please check communication with the endpoint device.
not_configured:
severity: info
display_name: Device Not Configured
description: Please configure connection parameters.

commands:
reset:
display_name: Reset Sensor Alarm
group: sensor
ui:
icon: bell-off-outline
quick_access: true

# Commands can be grouped in the UI
command_groups:
sensor:
display_name: Sensor

The configuration section declares connection and sensor parameter groups. Users can now set these values through the Enapter mobile app or Gateway Web UI without editing code.

See the full configuration documentation for best practices and more use cases.

The Reconnect Pattern

The reconnect pattern is a structured approach to managing a persistent connection client. Rather than handling connection state ad hoc inside send_telemetry, the pattern separates concerns into three dedicated components that work together:

  1. Global client variable — The communication client is stored as a module-level variable so it persists across scheduler calls. A nil value signals that the client does not yet exist or has been intentionally reset.
  2. configuration.after_write callback — When the user updates connection settings through the UI, this callback immediately resets both the client and the cached configuration to nil. The next call to reconnect() will then pick up the new settings and establish a fresh connection.
  3. reconnect function — Checks whether the client exists and, if not, reads configuration and attempts to establish a new connection. If send_telemetry detects a communication failure and resets the client, reconnect() will automatically try to re-establish the connection on its next invocation.

Let's update the Lua script to read configuration instead of using hardcoded values, applying the reconnect pattern for efficient client management.

main.lua
-- Device connection client and configuration, stored globally for reuse.
-- nil means not yet connected or connection was reset.
local modbus_client = nil
local conn_cfg = nil

function enapter.main()
-- Reset connection when configuration changes
configuration.after_write('connection', function()
modbus_client = nil
conn_cfg = nil
end)

-- Periodically attempt to (re)connect
scheduler.add(1000, reconnect)
scheduler.add(30000, send_properties)
scheduler.add(1000, send_telemetry)

enapter.register_command_handler('reset', reset_command)
end

-- Check if the device connection client exists.
-- Otherwise, try creating a new one.
function reconnect()
if modbus_client then
return
end

if not configuration.is_all_required_set('connection') then
return
end

local config, err = configuration.read('connection')
if err then
enapter.log('read configuration: ' .. err, 'error')
return
end
conn_cfg = config

local client, err = modbus.new(conn_cfg.conn_str)
if err then
enapter.log('connect: modbus.new failed', 'error')
return
end
modbus_client = client
end

function send_properties()
enapter.send_properties({
serial_number = "AAACCB"
})
end

function send_telemetry()
-- Check if the device is configured
if not conn_cfg then
enapter.send_telemetry({ alerts = {'not_configured'} })
return
end

-- Check if the connection client exists
if not modbus_client then
enapter.send_telemetry({ alerts = {'communication_failed'} })
return
end

-- Read data via Modbus using the persistent client
local data, err = modbus_client:read_holdings(conn_cfg.address, 100, 1, 1000)
if err then
enapter.log('Failed to read data: ' .. err, 'error')
-- Reset the client so reconnect() will try again
modbus_client = nil
enapter.send_telemetry({ alerts = {'communication_failed'} })
return
end

local h2_concentration = data[1] / 100.0

-- Read sensor configuration for H2 limit
local sensor_config, sensor_err = configuration.read('sensor')
if sensor_err then
enapter.log('read sensor configuration: ' .. sensor_err, 'error')
return
end
local h2_limit = sensor_config.h2_limit

-- Check H2 limit
local alerts = {}
if h2_concentration >= h2_limit then
alerts = {'h2_high'}
end

enapter.send_telemetry({
h2_concentration = h2_concentration,
alerts = alerts,
})
end

-- "reset" command implementation
function reset_command(ctx)
if not modbus_client or not conn_cfg then
ctx.error('Device is not configured or not connected')
end

-- Write into holding register 420
local write_err = modbus_client:write_holding(conn_cfg.address, 420, 1, 1000)
if write_err then
ctx.error('Failed to write into Modbus register: ' .. write_err)
end
return 'Reset sensor alarm'
end

How It Works

Here's the flow of the reconnect pattern:

enapter.main()
├── configuration.after_write('connection', callback)
├── scheduler.add(1000, reconnect) ← checks connection every second
└── scheduler.add(1000, send_telemetry) ← uses client if available

reconnect()
├── client exists? → return (nothing to do)
├── config not set? → return (wait for user to configure)
└── create client → store globally

send_telemetry()
├── no config? → send alert 'not_configured'
├── no client? → send alert 'communication_failed'
├── read fails? → reset client (so reconnect() retries), send alert 'communication_failed'
└── success → send telemetry data

Configuration changes → after_write callback resets client → next reconnect() creates new one

Why This Pattern Matters

  1. Efficiency — The client is created once and reused, not recreated on every telemetry cycle.
  2. Automatic recovery — If communication fails, the client is reset and reconnect() automatically tries again on the next cycle.
  3. Configuration hot-reload — When the user changes configuration via UI, the after_write callback resets the client, and reconnect() creates a new one with updated settings. No restart needed.
  4. Reusability — The blueprint can be deployed on different installations by simply changing configuration — no code edits required.

Key API Functions

  • configuration.is_all_required_set(group) — Returns true if all required parameters in the group are set. Use this to guard against running with incomplete configuration.
  • configuration.read(group) — Returns a table with configuration values for the group, or nil and an error message on failure.
  • configuration.after_write(group, callback) — Registers a callback that fires when the user changes configuration for the group. Use this to reset the client so it is recreated with the updated settings.

Alert Sources Explained: Device Alerts vs Connection Alerts

The code above uses alerts for both connection problems (not_configured, communication_failed) and device operational conditions. This is a common starting point, but it creates a subtle problem once your device also reports its own alerts. This section explains the issue and how conn_alerts solves it.

How device alerts work

The Enapter Platform treats the alerts telemetry field as the current set of active alerts. An alert is considered unresolved when it appears in the array, and resolved the moment it disappears. This means that every telemetry update completely replaces the previous alert state.

The table below illustrates this behavior across five consecutive telemetry updates:

Updatealerts valueUI state
i = 1{}No alerts
i = 2{'alert1'}Alert 1 is unresolved
i = 3{}Alert 1 is resolved
i = 4{'alert2'}Alert 2 is unresolved
i = 5{'alert1'}Alert 1 is unresolved, Alert 2 is resolved

The problem

Many devices also report their own operational alerts. For example, a hydrogen sensor might expose fault codes over Modbus that the blueprint reads and maps to alerts. When both connection problems and device-specific conditions are reported through the same alerts field, they interfere with each other:

main.lua
function send_telemetry()
-- Check if the device is configured
if not conn_cfg then
enapter.send_telemetry({ alerts = {'not_configured'} })
return
end

-- Check if the connection client exists
if not modbus_client then
enapter.send_telemetry({ alerts = {'communication_failed'} })
return
end
local telemetry = {}
local alerts = {}

-- Read device alerts
local data, err = modbus_client:read_holdings(conn_cfg.address, 120, 1, 1000)
if data then
if data[1] == 1 then
alerts = { 'too_high_h2' }
elseif data[1] == 2 then
alerts = { 'low_power' }
end
end

telemetry.alerts = alerts

-- ... rest of the function
end

This approach creates a subtle but serious problem. Consider the following sequence of events:

  1. The device detects a hydrogen leak and reports it. The blueprint reads this and sends too_high_h2 → the UI shows too_high_h2 as unresolved.
  2. The connection to the device is lost (for example, due to a wiring issue or changed network settings). The blueprint can no longer read device data, so instead it sends communication_failed → the UI shows too_high_h2 as resolved and communication_failed as raised.

The problem here is that too_high_h2 was never actually resolved — we simply lost the ability to check. Because both the device's own alerts and the blueprint's connection alerts share the same alerts array, they override each other. When a connection alert is sent, it silently clears any device alerts that were active, even though the device's condition is unknown.

In other words, there are two distinct alert sources mixed into a single field:

  1. Alerts reported by the endpoint device itself (e.g., too_high_h2, low_power)
  2. Alerts generated by the blueprint's own logic (e.g., not_configured, communication_failed)

Solution

Use the dedicated conn_alerts telemetry attribute to report connection-related issues separately from device operational alerts. The Enapter Platform treats conn_alerts as an independent alert group, so connection issues no longer overwrite or resolve device alerts.

Update your send_telemetry function to send connection problems via conn_alerts and device alerts via alerts:

main.lua
function send_telemetry()
-- Check if the device is configured
if not conn_cfg then
enapter.send_telemetry({ conn_alerts = {'not_configured'} })
return
end

-- Check if the connection client exists
if not modbus_client then
enapter.send_telemetry({ conn_alerts = {'communication_failed'} })
return
end

local telemetry = {}
local alerts = {}

-- Read device alerts
local data, err = modbus_client:read_holdings(conn_cfg.address, 120, 1, 1000)
if data then
if data[1] == 1 then
alerts = { 'too_high_h2' }
elseif data[1] == 2 then
alerts = { 'low_power' }
end
end

telemetry.alerts = alerts
telemetry.conn_alerts = {}

-- ... rest of the function
end

With this separation in place, the same scenario now produces a much more accurate result:

  1. The device detects a hydrogen leak → the UI shows too_high_h2 as unresolved.
  2. The connection to the device is lost → the UI still shows too_high_h2 as unresolved and additionally raises communication_failed.

This combined state can now be correctly interpreted as: "The connection to the device is currently lost; the last known device state included a hydrogen leak alert." Without conn_alerts, this context is completely hidden.

Updating the manifest

To use conn_alerts, add it as a telemetry attribute in your manifest.yml. Connection-related alerts such as communication_failed and not_configured are still defined in the top-level alerts section — the distinction is only in how they are sent from Lua:

manifest.yml
blueprint_spec: device/3.0

display_name: H2 Sensor
description: Hydrogen sensor integration example.
icon: enapter-gauge

runtime:
type: lua
requirements:
- modbus
options:
file: main.lua

# ... rest of the manifest

telemetry:
h2_concentration:
type: float
unit: "%LEL"
display_name: H2 Concentration
conn_alerts:
type: alerts
display_name: Device Communication Alerts

alerts:
h2_high:
severity: error
display_name: High H2 Concentration
description: Hydrogen leakage is detected.
# Connection alerts are defined here but sent via conn_alerts in Lua
communication_failed:
severity: warning
display_name: Device Communication Failed
description: Please check communication with the endpoint device.
not_configured:
severity: info
display_name: Device Not Configured
description: Please configure connection parameters.

# ... rest of the manifest

Next Steps

After uploading the updated blueprint (create device), you need to configure the device — set the connection parameters via the mobile app or Gateway Web UI.

All Rights Reserved © 2026 Enapter AG.