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:
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:
- Global client variable — The communication client is stored as a module-level variable so it persists across scheduler calls. A
nilvalue signals that the client does not yet exist or has been intentionally reset. configuration.after_writecallback — When the user updates connection settings through the UI, this callback immediately resets both the client and the cached configuration tonil. The next call toreconnect()will then pick up the new settings and establish a fresh connection.reconnectfunction — Checks whether the client exists and, if not, reads configuration and attempts to establish a new connection. Ifsend_telemetrydetects 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.
-- 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
- Efficiency — The client is created once and reused, not recreated on every telemetry cycle.
- Automatic recovery — If communication fails, the client is reset and
reconnect()automatically tries again on the next cycle. - Configuration hot-reload — When the user changes configuration via UI, the
after_writecallback resets the client, andreconnect()creates a new one with updated settings. No restart needed. - 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)— Returnstrueif 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, orniland 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:
| Update | alerts value | UI 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:
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:
- The device detects a hydrogen leak and reports it. The blueprint reads this and sends
too_high_h2→ the UI showstoo_high_h2as unresolved. - 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 showstoo_high_h2as resolved andcommunication_failedas 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:
- Alerts reported by the endpoint device itself (e.g.,
too_high_h2,low_power) - 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:
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:
- The device detects a hydrogen leak → the UI shows
too_high_h2as unresolved. - The connection to the device is lost → the UI still shows
too_high_h2as unresolved and additionally raisescommunication_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:
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.