{% from "_macros.html" import state_label %} {#- the fixed texts of the rows, translated once instead of once per row (half the time this table takes to render) #} {%- set t = { 'serial': _('Serial'), 'network': _('Network'), 'state': _('State'), 'clients': _('Clients'), 'in_out': _('In / Out'), 'disabled': _('disabled'), 'opened_now': _('device opened now'), 'bridge': _('serial↔serial'), 'read_only': _('read-only'), 'priority': _('priority'), 'cmd_open': _('writable points take MQTT commands over a broker connection without TLS or a username: any client the broker lets publish to the command topics can write them'), 'cmd': _('writable points take MQTT commands (/set or Sparkplug DCMD)'), 'tx_open': _('MQTT messages on <topic>/tx are written to the device over a broker connection without TLS or a username: any client the broker lets publish to that topic can write to it'), 'tx': _('MQTT messages on <topic>/tx are written to the device'), 'link_down': _('link down'), 'connecting': _('connecting'), 'retrying_now': _('retrying now'), 'waiting': _('waiting for a client'), 'waiting_title': _('the serial port is closed and is opened when a client connects (open on connect)'), 'listen_only': _('listens only'), 'listen_only_title': _('not one of the writer client IPs: what it sends is not written to the device'), 'pool_title': _('clients of this port pool are handed to the mapping'), 'link_target_title': _('the connect-out target the link is on, or the one being tried'), 'link_down_title': _('the connection to the remote server is down; it is tried again'), 'dropped': _('data from the network that was not written to the serial device; the mapping’s log says why'), 'trace_not_written': _('trace not written'), 'units_offline': _('polled Modbus units that gave no reply to any of their points in 3 polls in a row; they are polled with one point per cycle until they answer (see the mapping’s log)'), 'mqtt_down': _('the mapping’s connection to its MQTT broker; it keeps trying (see the mapping’s log)'), 'mqtt_off': _('MQTT publishing could not start (see the mapping’s log)'), 'safe_values': _('the broker (or the Sparkplug primary host) was away for longer than these points’ safe_after_s: they were written to their safe value and keep it until a command changes it'), 'stop': _('Stop'), 'restart': _('Restart'), 'start': _('Start'), 'edit': _('Edit'), 'log': _('Log'), 'monitor': _('Monitor'), 'trace': _('Trace'), 'duplicate': _('Copy'), 'delete': _('Delete'), } %}
{% for m in mappings %} {% set st = statuses.get(m.id) %} {% set state = st.state if st else ('stopped' if not m.enabled else 'stopped') %} {% set link_down = st and st.link == 'down' %} {% set problem = state in ('error', 'device-missing', 'reconnecting') or link_down %} {#- data-search: folded as app.js folds what is typed (İ/ı/I/i one letter, no accents); data-name as it is (sorted by the browser, in the page's language) #} {# the ids: htmx puts the keyboard focus back on a control after the table's 2-second refresh; every row button is described by the mapping's name (screen readers) #} {% set rid = 'm-' ~ m.id %} {% else %} {# "+ Add mapping" is there for operators and admins only #} {% endfor %}
{{ _("Name") }}{{ t.serial }}{{ t.network }} {{ t.state }}{{ t.clients }}{{ t.in_out }}
{% if can_edit %} {% endif %}{{ m.name|softbreak }}{% if not m.enabled %} {{ t.disabled }}{% endif %}
{{ m.serial.port|softbreak }}{% if m.serial.identity %} USB-ID{% endif %}
{% if st and st.device and st.device != m.serial.port %}
→ {{ st.device|softbreak }}
{% endif %}
{{ m.serial.compact() }}{% if m.serial.flowcontrol != 'none' %} · {{ m.serial.flowcontrol }}{% endif %}
{%- if m.kind == 'serialbridge' -%}{{ m.serial_b.port|softbreak }} {%- elif m.network.mode == 'client' -%}→ {{ shown_host(m.network.remote_host)|softbreak }}:{{ m.network.remote_port }}{% if m.network.remote_targets %} +{{ m.network.remote_targets|length }}{% endif %} {%- elif m.network.both_families -%}*:{{ m.network.port }} {%- else -%}{{ m.network.bind_shown|softbreak }}:{{ m.network.port }}{%- endif -%}
{%- if m.kind == 'serialbridge' -%}{{ t.bridge }} {%- else -%}{{ m.network.mode }}/{{ m.network.protocol }} {%- if m.network.tls %} TLS{% endif %} {%- if m.network.read_only %} RO{% endif %} {%- if m.network.pool and m.network.mode == 'server' and m.network.protocol != 'modbus' and not m.network.tls %} ⇉ {{ m.network.pool }}{% endif %} {%- if m.accepts_mqtt_writes %}{% set open_cmd = not (m.mqtt.tls and m.mqtt.username) %} CMD{% if open_cmd %} ⚠{% endif %} {%- elif m.accepts_mqtt_tx %}{% set open_cmd = not (m.mqtt.tls and m.mqtt.username) %} CMD{% if open_cmd %} ⚠{% endif %}{% endif %} {%- endif -%}
{{ state_label(state) }} {%- if link_down %} {{ t.link_down }} {%- elif st and st.link == 'connecting' and state == 'running' %} {{ t.connecting }}{% endif %} {%- if st and st.waiting and state in ('running', 'device-missing', 'reconnecting') %} {{ t.waiting }}{% endif %} {%- if st and st.link_target and m.network.remote_targets and state != 'stopped' %}
→ {{ st.link_target }}
{% endif %} {% if st and st.last_error and state in ('error','device-missing','reconnecting') and not (link_down and st.last_error == st.link_error) %} {#- what it means in the page's language; the error itself in the tooltip. With state "error" a hint is why the system refused the listener's port and what to do (the supervisor sets it at each failed start): it says that instead (a general "ports below 1024" would be wrong on Windows) #} {%- set bind_hint = st.hint if state == 'error' else '' %}
{{ (bind_hint or st.last_error|explain or st.last_error)|softbreak }}
{#- a serial port's hint (device-missing / reconnecting): what to do, under it #} {% if st.hint and not bind_hint %}
{{ st.hint|softbreak }}
{% endif %} {% endif %} {% if link_down and st.link_error %}
{{ (st.link_error|explain or st.link_error)|softbreak }}
{% endif %} {% if st and st.warnings and state == 'running' %} {% for w in st.warnings %}
⚠ {{ w|softbreak }}
{% endfor %} {% endif %} {% if st and st.retrying and state == 'error' %}
{{ _("retrying in %(n)s s", n=st.next_retry_s) if st.next_retry_s else t.retrying_now }}
{% endif %} {% set quiet = no_data_for(m, st) %} {% if quiet %}
⚠ {{ _("no data from the device for %(n)s s", n=quiet) if not m.options.no_data_reply_only else _("no answer from the device for %(n)s s", n=quiet) }}
{% endif %} {% set silent = (st.units if st and st.units else [])|selectattr('state', 'equalto', 'offline')|map(attribute='unit')|list %} {% if state == 'running' and silent %}
⚠ {{ ngettext("Modbus unit not answering: %(units)s", "Modbus units not answering: %(units)s", silent|length, units=silent|join(', ')) }}
{% endif %} {% set mq = st.mqtt if st and st.mqtt else {} %} {% if mq.state == 'down' %}
⚠ {{ _("MQTT: no connection to %(broker)s (%(error)s)", broker=mq.broker, error=mq.error) }}
{% elif mq.state == 'off' %}
⚠ {{ _("MQTT off: %(error)s", error=mq.error) }}
{% endif %} {% set safe_written = ((st.safe.points if st and st.safe else [])|selectattr('written')|map(attribute='name')|list) %} {% if state == 'running' and safe_written %}
⚠ {{ ngettext("safe value written: %(names)s", "safe values written: %(names)s", safe_written|length, names=safe_written|join(', ')) }}
{% endif %}
{{ st.client_count if st else 0 }}{% if m.network.max_connections > 1 %}/{{ m.network.max_connections }}{% endif %} {% if st and st.clients %}
{#- data-since: app.js counts the connection time on (the table is not sent again for it) #} {#- the time in the page's language (the REST API's duration is English) #} {% for cl in st.clients %}{% set dur = cl.connected_at|since if cl.connected_at else cl.duration %}{% if cl.priority %}★ {% endif %}{{ cl.peer }} · {{ dur }}{% if cl.listen_only %} · {{ t.listen_only }}{% endif %}{% endfor %}
{% endif %} {% if st and st.dropped_clients %}
⚠ {{ ngettext("%(num)s slow client dropped", "%(num)s slow clients dropped", st.dropped_clients) }}
{% endif %}
{{ (st.bytes_in|default(0) if st else 0)|bytesize }} / {{ (st.bytes_out|default(0) if st else 0)|bytesize }} {% if st and st.bytes_dropped %}
⚠ {{ ngettext("%(num)s byte dropped", "%(num)s bytes dropped", st.bytes_dropped) }}
{% endif %} {% if st and st.trace_error %}
⚠ {{ t.trace_not_written }}
{% endif %}
{% if can_edit %}
{% if m.enabled %} {#- asked only while clients are connected: they are disconnected (a question at every click would soon be clicked through) #} {% set n = st.client_count|default(0) if st else 0 %} {% else %} {% endif %}
{% endif %}
{% if can_edit %}{% endif %} {% if can_edit %} {% endif %}
{{ _("No mappings yet. Click “Add mapping” to create one.") if can_edit else _("No mappings yet.") }}