himpler.com himpler.com

Editable Lists in Node-RED

Mit der kürzlich erfolgten Aktualisierung des homee Plugins für Node-RED hat sich unter anderen die Konfiguration des Device Nodes erheblich vereinfacht. Diese baut nun auf die von Node-RED zur Verfügung gestellte EditableList auf. Eine Beschränkung bei der Entwicklung war, möglichst keinen Breaking Change zu verursachen. Wie ich die Bearbeitung erheblich vereinfacht habe, liest du in diesem Post.

Seit Oktober 2019 ist es mit Node-RED möglich, über die homee-in-homee Funktion beliebige virtuelle Geräte zu homee hinzuzufügen. Allerdings war die Konfiguration von Eigenschaften der virtuellen Geräte nur über ein starres JSON-Schema möglich. Gerade für Einsteiger stellte dies eine unschöne Herausforderung dar. Aber auch für alle anderen war die Definition über JSON alles andere als komfortabel.

Mit Version 0.7.0 erfährt das User Interface ein grundlegendes Update. Die Konfiguration wird deutlich vereinfacht und über Templates werden die meisten Daten bereits vorausgefüllt. Über eine Art Expertenmodus lassen sich die Parameter hinterher optional anpassen.

Die neue Attributkonfiguration im Node-RED Plugin

Die neue Attributkonfiguration im Node-RED Plugin

Konzeption

Ein Eigenschaft (Attribut, z.B. Batterieladung oder Schaltzustand) eines virtuellen Geräts ist ein JSON-Objekt, welches mit den übrigen Attributen zu einem Array zusammengesetzt wird. Eine gültige Konfiguration sah dann bisher in etwa so aus:

[{
  "id": 10,
  "node_id": 10,
  "instance": 0,
  "minimum": 0,
  "maximum": 1,
  "current_value": 0,
  "target_value": 0,
  "last_value": 0,
  "unit": "",
  "step_value": 1,
  "editable": 1,
  "type": 1,
  "state": 1,
  "last_changed": 12345555,
  "changed_by": 1,
  "changed_by_id": 0,
  "based_on": 1,
  "data": ""
}]

Zunächst wurde ermittelt welche der Parameter wirklich vom Nutzer angegeben werden müssen und welche automatisch oder über Templates bereitgestellt werden können. Schnell stellte sich heraus, dass eigentlich nur eine eindeutige Attribut-ID sowie der Attributtyp wirklich relevant waren. Da es meist mehrere Attribute je Gerät gibt, bot sich vereinfacht also eine Liste von IDs kombiniert mit ihren Attributtypen an.

Ein Blick in die Node-RED Developer Dokumentation brachte schließlich die EditableList zum Vorschein, die bereits einen Teil der benötigten Funktionen (Hinzufügen und Entfernen von Attributen) mitbrachte. Die Liste basiert (wie das gesamte Node-RED Frontend) auf jQuery; nicht unbedingt meine erste Wahl wenn es um die Entwicklung von modernen Interfaces geht. Nützt aber ja nichts.

Umsetzung

Das Prinzip war also, über das Hinzufügen einer Zeile in der Liste ein neues Attribut zu erzeugen. Bestehende Attribute sollten in die Zeilen übernommen werden. Die EditableList bietet dazu praktischerweise schon passende Funktionen an. Zur Erzeugung des Listenfelds bzw. des gesamten Bearbeitungsdialogs bietet Node-RED die Funktion oneditprepare an. Diese wird unmittelbar vor dem Bearbeiten des Nodes ausgeführt und ermöglicht so weitreichende Eingriffe.

Hinzufügen eines neuen Attributes

Der Inhalt eines Listenelements ist immer gleich aufgebaut. Neben einem Textfeld für die Attribut-ID gibt es ein Select-Feld mit einer Auswahl der möglichen Attributtypen. Nach einer informativen Darstellung der wichtigsten Werte folgt später noch ein eine Möglichkeit zur Bearbeitung der übrigen Parameter.

Damit der JavaScript-Code nicht zu unübersichtlich wird, habe ich für die Erzeugung des nötigen HTML Codes ein Template erstellt. Das Template bestimmt das grundsätzliche Aussehen einer Zeile in unserer Liste.

<template id="homee-attributes-template">
  <div style="display: flex; width: 100%;">
    <div style="width: 20%; padding-right: 10px;">
      <input class="node-input-attribute-id" type="number" placeholder="ID" style="width: 100%;"/>
    </div>
    <div style="width: 40%; padding-right: 10px;">
      <select class="node-input-attribute-type" style="width: 100%;"></select>
    </div>
    <div class="node-input-attribute-infos" style="display: flex; flex-wrap: wrap; line-height: 16px; font-size: 12px; width: 35%; margin-right: 10px;">
      <span class="node-input-attribute-minimum" style="width: 50%;">
        Min: <code style="font-size: 12px;">0</code>
      </span>
      <span class="node-input-attribute-unit" style="width: 50%;">
        Unit: <code style="font-size: 12px;">n/A</code>
      </span>
      <span class="node-input-attribute-maximum" style="width: 50%;">
        Max: <code style="font-size: 12px;">1</code>
      </span>
      <span class="node-input-attribute-step_value" style="width: 50%;">
        Step: <code style="font-size: 12px;">1</code>
      </span>
    </div>
  </div>
</template>

Die Editable List lässt als Parameter die Funktion addItem zu, die bei jedem Hinzufügen eines Listenelements aufgerufen wird. Die Funktion muss also

  • das HTML-Template laden
  • die möglichen Attributtypen zum Select-Feld hinzufügen
  • und ein eventuell vorhandenes Attribut-Template an Hand des Attributtyps bei einer API abfragen

Starten wir zunächst mit dem HTML-Template. Beim Hinzufügen einer Zeile wird das Template für die Zeile geladen und mit den verfügbaren Attributtypen angereichert.

addItem: (listItem, index, data) => {
  const html = $($('#homee-attributes-template').html());

  Object.keys(enums.CAAttributeType).forEach((type) => {
    $('<option/>')
      .val(enums.CAAttributeType[type])
      .text(node._(`homeeDevice.attribute.${type}`))
      .appendTo(typeField);
  });
}

Bei jeder Änderung des Attributtyps wird, falls vorhanden, ein Template über die Node-RED HTTP Api geladen. Dazu wird ein EventListener an das Feld für den Attributtyp gebunden. Dieser führt bei einer Änderung des Typs die Funktion zur Abfrage aus und übernimmt die gefundenen Template Werte. Wird kein Template gefunden oder tritt ein Fehler bei der Abfrage auf, wird der Nutzer über die Notify-Funktion von Node-RED informiert.

typeField.change(async () => {
  try {
    const res = await fetch(`/homee-api/template/attribute.profile.${typeField.val()}`);
    const attributeTemplate = await res.json();
    Object.keys(attributeTemplate).forEach((key) => {
      if (Object.hasOwnProperty.call(attribute, key)) {
        attribute[key] = attributeTemplate[key];
      }
    });
  } catch (e) {
    RED.notify(node._('homeeDevice.warning.attribute-template-not-found'), 'warning');
  }
});

Expertenmodus

Sollte kein Template gefunden werden oder möchte der Nutzer die Werte individuell anpassen, soll weiterhin eine Möglichkeit zur manuellen Bearbeitung der Attribute bestehen. Dieser Expertenmodus verwendet ein TypedInput, ein spezielles Input Feld mit JSON Syntaxprüfung, welches bereits in der alten Version zur Definition der Attribute verwendet wurde. Nach einem ersten Test zeigte sich jedoch, dass Feld für die Attributzeile zu breit ist und das geplante Layout sprengt. Nach kurzer Ernüchterung und einem Blick in den Quelltext offenbarte sich eine alternative Möglichkeit: Das TypedInput-Feld wurde kurzerhand mittels CSS versteckt und nun über einen deutlich platzsparerenden Button direkt im Bearbeitungsmodus geöffnet.

<button type="button" class="red-ui-button typed-input-expand-trigger">
  <i class="fa fa-edit"></i>
</button>
<div style="display: none;">
  <input class="node-input-attribute-json" />
</div>
const jsonField = html.find('.node-input-attribute-json');
jsonField.typedInput({ default: 'json', types: ['json'] });

html.find('.typed-input-expand-trigger').on('click', () => {
  html.find('.red-ui-typedInput-option-expand').click();
});

Aktualisierung bei Änderungen

Für eine bessere Übersicht werden in der Attributzeile zusätzlich noch vier Werte angezeigt. Neben den Min und Max Werten für das Attribut wird noch die Schrittgröße und die Einheit angezeigt. Damit die Werte stets aktuell sind, müssen wir uns nach einer manuellen Bearbeitung und dem Wechsel eines Typs noch um deren Aktualiserung kümmern.

Dazu wird jeweils bei einer Änderung der Felder eine Funktion mit den aktuellen Werten ausgeführt.

listItem.updateInfo = (attributeData) => {
  ['minimum', 'maximum', 'step_value', 'unit'].forEach((key) => {
    html.find(`.node-input-attribute-${key} code`).text(attributeData[key]);
  });
};

// ...

jsonField.on('change', () => {
  // ...
  listItem.updateInfo(attribute);
});

typeField.change(async () => {
  // ...
  listItem.updateInfo(attribute);
  // ...
});

Dazu wird im Template mit einem jQuery Selektor die passende Stelle gesucht und mit den aktuellen Werten ersetzt.

Auslesen bestehender Attribute

Jetzt müssen beim Aufruf des Bearbeitungsdialogs noch die bestehenden Attribute ausgelesen und an die Liste übergeben werden. Mit kleineren Anpassungen können wir dazu die addItem Funktion verwenden.

for (let i = 0; i < node.attributes.length; i += 1) {
  $('#node-input-attribute-list').editableList('addItem', node.attributes[i]);
}

Dazu durchlaufen wir bereits vorhandenen Attribute des Nodes und rufen die addItem Funktion der EditableList auf und übergeben dieser das Attribut. Da wir in der addItem Funktion ohnehin ein Standard-Attribut erzeugen, können wir dieses alternativ auch durch ein bereits definiertes und an die Funktion übergebenes Attribut ersetzen.

const attribute = Object.hasOwnProperty.call(data, 'id') ? data : {
  id: nodeId + index,
  type: 0,
  state: 1,
  node_id: nodeId,
  instance: 0,
  minimum: 0,
  maximum: 1,
  current_value: 0,
  target_value: 0,
  last_value: 0,
  data: '',
  unit: 'n%2Fa',
  step_value: 1,
  editable: 0,
  last_changed: Math.round(Date.now() / 1000),
  changed_by: 1,
  changed_by_id: 0,
  based_on: 1,
  options: [],
};

Abfragen der Liste beim Speichern

Jetzt haben wir also eine Liste von Attributen. Damit diese zur Erzeugung des virtuellen Geräts nun auch wieder korrekt an das Node-RED Backend übergeben werden, brauchen wir noch eine weitere Funktion, die beim Speichern der Änderungen ausgeführt wird. Auch hier stellt Node-RED mit oneditsave eine Möglichkeit bereit. Die Funktion wird bei jedem Speichern der Konfiguration ausgeführt.

oneditsave: function () {
  const node = this;
  node.attributes = [];

  const attributes = $('#node-input-attribute-list').editableList('items');
  attributes.each(function () {
    const attribute = JSON.parse($(this).find('.node-input-attribute-json').typedInput('value'));
    attribute.id = parseInt($(this).find('.node-input-attribute-id').val(), 10);
    attribute.type = parseInt($(this).find('.node-input-attribute-type').val(), 10);
    attribute.node_id = parseInt($('#node-input-nodeId').val(), 10);
    attribute.unit = encodeURIComponent(attribute.unit);
    node.attributes.push(attribute);
  });
}

Die Funktion holt lediglich die Attribute bei der EditableList ab, setzt sie zu einer gültigen Attributdefinition zusammen und übergibt sie schließlich an den Node.

Fazit

Die Möglichkeit der EditableList passt sehr gut zu den vorher ermittelten Anforderungen und kommt in der homee-Community bisher gut an. Die Programmierung war dank der von Node-RED zur Verfügung gestellten Basisfunktionen gut machbar. Den gesamte Quellcode steht auf Github zur Verfügung und kann dem ein oder anderen vielleicht als Hilfestellung für ein eigenes Node-RED Plugin dienen.

Das könnte dir auch gefallen