Universelles Zigbee End Device

Hallo zusammen,
ich habe jetzt ein paar Tage mit CC2530 Device herumgebastelt und möchte gerne meine Erfahrung damit teilen.

Aufgabenstellung
Ich suche eine Lösung mit der man Daten von einem beliebigen Microcontroller per UART an ein Zigbee End-Device schickt. Dieses soll als Zigbee2MQTT Gerät in HA eingebunden werden. Damit kann man dann z.B. Messwertdaten an HA übertragen. In meinem Fall suche ich nach einer Lösung für eine klassische Wetterstation. Aber die hier beschreibene Vorgehensweise ist natürlich auch für ein ganz anderes Projekt nutzbar.

Zigbee Knoten
Eine der günstigsten Lösungen, die ich gefunden habe, ist so ein PCB, das man ab 3€ im Handel bekommt:


Darauf verbaut ist ein CC2530 Chip von Texas Instuments. Von der Prozessor-Architektur ist das ein klassisches 8051 Derivat, das aber auch Low Power Modes unterstützt. Damit kann man den Stromverbrauch auf bis zu 0,4 uA senken. Betrieben wird der MC an 3.3V.

PTVO
Für den Baustein gibt es eine Firmware die sich PTVO nennt. Der Charme dabei ist es, dass über ein Tool zur Konfiguration die Firmware individuell angepasst werden kann. Unter anderem kann man über PTVO eine Uart konfigurieren; und die benötige ich ja für den Datentransfer mit meinem MC.

Ich habe mich für PTVO entschieden, da ich nicht auch noch in die Zigbee Programmierung mit dem 8051 einsteigen, bzw. schneller voran kommen wollte. Wenn man aber bestimmte Features des Tools verwenden möchte muss man eine Lizenz pro Gerät (ca. 8€) kaufen. Ich werde das noch machen, da die Low-Power Modi nur in der Premium Variante konfiguriert werden können. Die braucht man jedoch unbedingt für ein batteriebetriebenes Gerät!

Das Tool ist ein Windows Programm, das aber auch per Wine unter Linux läuft.

Für meinen Test (in der kostenlosen Standard-Version) habe ich folgende Einstellungen gemacht:

Für einen Batteriebetrieb, werde ich aber noch mal Änderungen durchführen und z.B. die LED und das Standard-Berichtsintervall deaktivieren, da das nur unnötig das System wachhält.

Zigbee device programmieren
Das Device wird mit einem CC-Debugger programmiert. Auch das kann man im Handel ab 6€ bestellen.
image
Das Gerät wird dann per USB Kabel am PC angeschlossen. Am 10 polige Pfostenstecker sind folgende Pins an das PCB vom Zigbee Gerät anzuschließen

  • Pin 1 → GND
  • Pin 3 → P2.2
  • Pin 4 → P2.1
  • Pin 7 → Reset
  • Pin 9 → 3.3V
    image

Um eine saubere Verkabelung zu erreichen habe ich mir ein kleines PCB anfertigen lassen um alle Pins des MC herauszuführen. Hier der Schaltplan


Grün markiert der Debug-Port. Bitte beachtet, dass die Zählweise der Pins am Pfostenstecker (Siehe Bild des Pfostensteckers) anders ist, als hier im Schaltbild dargestellt!

Das über das PTVO tool generierte Hex file wird mit Hilfe von “Smart Flash” programmiert. Das Tool wird von TI bereitgestellt. Ich habe die Version 1 des Tools verwendet, da das von PTVO empfohlen wurde.

Der Programmer ist eigentlich ziemlich selbsterklärend

  1. Hex File auswählen
  2. Erase Program and Verify auswählen
  3. Programmiervorgang starten

Pairing mit Zigbee
Wird das Zigbee Device an die Spannungsversorgung angeschlossen, blinkt die LED zunächst erst mal schnell (ca. 1/s). Da bedeutet, dass das Gerät nicht gekoppelt ist. Sobald das Anlernen in der Zigbee2MQTT integration aktiviert wird, erfolgt in der Regel auch das Pairing des neuen Geräts. Danach blinkt die LED, wie konfiguriert, alle 5s.


Ich habe den Gerätenamen dann händisch verändert und auch die Home Assistant Entität aktualisiert.

Datenübertragung
Die Uart Schnittstelle zum MC habe ich simuliert.


Ich habe einfach einen Buspirate 4 (kann natürlich auch ein beliebig anderer USB-RS232 Converter sein) verwendet um vom PC aus Daten an den Uart Port vom Zigbee Device zu schicken. Wie auf dem Bild zu erkennen lege ich die Tx Leitung vom Buspirate auf den RX Pin (P0.2) vom Zigbee Device. Rx vom Buspirate liegt dann an P0.3 (Tx)

Ich habe mir dann ein kleines Python Script gebastelt, dass mir einfach Daten rausschickt:

import serial

PACKET_END = '\r'



ser = serial.Serial('/dev/ttyACM0', 57600)  # open serial port
print(ser.name)         # check which port was really used
data =  'wind_speed:85;wind_force:18;wind_direction:350;temperature:20' \
        + PACKET_END
data_bytes = data.encode('utf-8')
print(data_bytes)
ser.write(data_bytes)     # write a string
ser.close()             # close port

Das Format, in dem die Daten geschickt werden ist sehr simpel es werden einfach durch semikolon getrennte “Key:value” Paare als ein Datenstrom geschickt. Insgesamt kann man in einer seriellen Übertragung 127 Bytes am Stück senden. Braucht man mehr Daten, muss dann in Chunks aufgeteilt werden.

Dekodieren der Daten in der Zigbee Integration
zunächst kann Zigbee die übertragenen Daten nicht Interpretieren. Im Z2M Protokoll (Einstellungen>Add-Ons>Z2M>Protokoll) erfolgt dann folgende Fehlermeldung:

z2m: No converter available for 'weatherstation' with cluster 'genMultistateValue' and type 'attributeReport' and data '{"stateText":{"data":[119,105,110,100,95,115,116,114,101,110,103,116,104,58,50,48,59,119,105,110,100,95,100,105,114,101,99,116,105,111,110,58,49,48,48,59,116,101,109,112,101,114,97,116,117,114,101,58,50,52],"type":"Buffer"}}'

Damit Z2M die Daten interpretieren kann, benötigt die Integration einen Converter. Das ist ein Nodejs File, in dem alle Informationen enthalten sind, die benötigt werden um die empfangenen Daten zu interpretieren.

Hier meine Datei

const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
const zigbeeHerdsmanUtils = require('zigbee-herdsman-converters/lib/utils');


const exposes = require('zigbee-herdsman-converters/lib/exposes');
const ea = exposes.access;
const e = exposes.presets;
const fz = require('zigbee-herdsman-converters/converters/fromZigbee');
const tz = require('zigbee-herdsman-converters/converters/toZigbee');

fz.ptvo_switch_uart = {
    cluster: 'genMultistateValue',
    type: ['attributeReport', 'readResponse'],
    convert: (model, msg, publish, options, meta) => {
        let data = msg.data['stateText'];
        var ws = null;
        var wf = null;
        var wd = null;
        var temp = null;


        if (typeof data === 'object') {
            let bHex = false;
            let code;
            let index;
            for (index = 0; index < data.length; index += 1) {
                code = data[index];
                if (code < 32 || code > 127) {
                    bHex = true;
                    break;
                }
            }
            if (!bHex) {
                data = data.toString('latin1');
            } else {
                data = [...data];
            }
        }
        /* parse the received data in string format: "key_1:value_1;key_1:value_2..."
        and fill the adequate variables to be returned.*/
        var splitted = data.split(";");
        var keyval;
        for(i=0; i<splitted.length; i++){
            keyval = splitted[i].split(":",2);
            if(keyval[0] == "wind_force")wf = keyval[1];
            else if(keyval[0] == "wind_direction")wd = keyval[1];
            else if(keyval[0] == "temperature")temp = keyval[1];
            else if(keyval[0] == "wind_speed")ws = keyval[1];
        }

        return {action: keyval, 
            wind_force: wf,
            wind_speed: ws,
            wind_direction: wd,
            temperature: temp,
        };
    },
} 

const device = {
    zigbeeModel: ['weatherstation'],
    model: 'weatherstation',
    vendor: 'daest',
    description: 'weatherstation with ptvo based firmware',
    // methods for receivibg data from device. Since 
    fromZigbee: [fz.ignore_basic_report, fz.ptvo_switch_uart,],
    toZigbee: [tz.ptvo_switch_trigger, tz.ptvo_switch_uart,],
    // definition of exposed items like measurement values.
    // more information about presets and how to configure attributes can be found here:
    // https://github.com/Koenkk/zigbee-herdsman-converters/blob/master/src/lib/exposes.ts#L23
    // Min Max values seems to be ignored when receiving data
    exposes: [
        exposes.numeric('wind_speed', ea.STATE)
            .withUnit('m/s')
            .withValueMin(0)
            .withValueMax(100)
            .withDescription('Measured wind speed'),
        exposes.numeric('wind_force', ea.STATE)
            .withUnit('Bft')
            .withValueMin(0)
            .withValueMax(17)
            .withDescription('Measured wind force'),
        exposes.numeric('wind_direction', ea.STATE)
            .withUnit('°')
            .withValueMin(0)
            .withValueMax(360)
            .withDescription('Measured wind direction'),
        e.temperature()
    ],
};

module.exports = device;

Die Datei wird einfach im Z2M Verzeichnis neben der speziefischen configuration.yaml abgelegt. In der configuration.yaml muss der Converter noch bekannt gemacht werden. Das geschieht über folgenden Eintrag, den ich einfach unterhalb der Sektion “advanced” platziert habe:

external_converters:
  - weatherstation.js

Im weiteren gehe ich wieder auf das Converter-File ein. Das device Beschreibt unter anderem die Struktur der anzuzeigenden Daten, die in dem Array exposes definiert sind. Ich möchte nur Daten empfangen und als Sensoren anzeigen (vom Typ ea.State). Es gibt bereits einen Satz vorgefertigter Sensoren, wie zum Beispiel der Temperatursensor. Für diesen kann man direct den expose vom Typ e.temperature verwenden.

Für “eigene” Sensoren muss man selber den Sensor definieren.

PTVO sendet normalerweise die Daten als einen Block, der in der Instanz action abgelegt wird. PTVO ünterstützt übrigens auch das Generieren eines Converters. Dort kann man nachschauen wie der Expose action angelegt wird.

Verwendet man die von Z2M bereits integrierte Methode fz.ptvo_switch_uart so kommen die Daten nie an den von mir definierten exposes an.

Daher habe ich ein “Function overriding” gemacht. Ab der Zeile mit dem Kommentar /* parse the received data in string format: “key_1:value_1;key_1:value_2…” */ habe ich einen simplen Parser geschrieben, der letztendlich dann die empfangenen Daten an die exposes Überträgt,

Die Ansicht von meinem MQTT Gerät sieht dann so aus:

Ich hoffe, dass dieser Artikel Euch weiterhilft, falls Ihr auch mal Daten von einem Endgerät übertragen wollt. Den umgekehrten Weg habe ich bislang noch nicht ausprobiert.

5 „Gefällt mir“

Hi,
ähnliche Platinen hatte ich mir auch mal bei Aliexpress gekauft ohne überhaupt zu wissen wie ich die programmieren oder nutzen könnte. Muss mir deinen Beitrag daher mal genauer durchlesen! :slight_smile:

Ja gerne, bei Fragen, einfach melden.

Hi @daest
Vielen, vielen Dank für diese Anleitung. :grinning:
Ich habe nur noch ein Problem und eine Frage dazu:
Ich bekomme bei jedem Datenempfang von zigbee2mqtt folgende Fehlermeldung.

Error `z2m: Exception while calling fromZigbee converter: data.split is not a function}`

Es scheint aber alles zu funktionieren. Ich sende genau den string aus dem Beispiel, im Moment noch statisch, von einem Arduino und alle Werte passen. Was stimmt da nicht?

Im string sind alle Zahlen ganz. Wie könnte ich es anstellen, kommagetrennte float Werte an Z2M zu schicken?

Grüsse Michael

Hi RedFlash,
das scheint mir eine Fehlermeldung beim Zerlegen des übertragenen Messwertestrings zu sein an dieser Stelle:

var splitted = data.split(";");

Kannst Du mal im Log (Einstellungen>Addons>Zigbee2MQTT>Protokoll) schauen, welche Daten Dein Gerät überträgt. Vermutlich wird ein Zeichen übertragen, was einen TypeError erzeugt.

Wie hier auch erklärt wird:
TypeError: split is not a function in JavaScript

Vielleicht noch als ergänzung hier meine Umsetzung in C aus meinem STM32 projekt:

#define ZBEE_PACKET_END '\r'
memset(buffer, 0, 255);
		/* transmit data
			ws: wind speed [m/s]
			wf: wind force [Bft]
			wd: wind direction [°]
			te: temperature [°C]
			pr: pressure [hPa]
			hu: humidity*/
		n = sprintf(buffer, "ws:%d;wf:%d;wd:%d;te:%.2f;pr:%.2f;hu:%.2f%c", a, b, c,
				temperature, pressure, humidity, ZBEE_PACKET_END);

		// send it to Zigbee transmitter
 		ZBEE_transmit(buffer, n);

Ich lösche den zu übertragenden Puffer vorher mit memset. Dann ist auf jeden Fall der String korrekt abgeschlossen. Das funktioniert auch mit float Werten

Danke nochmal.
Die Fehlermeldung ist weg. Es war der CC2530, der nicht schnell genug erwacht ist. Ich wecke den aus dem PSM per external_wakeup mit einem Arduino pin und schicke ihm den string. Mit 100ms zwischen wecken und schicken klappt es.

Das mit den realen float werten kommt als nächstes dran. Danke für die Tipps.

Grüsse Michael