Milquino – Datenanalyse

Der Milquino ist ein Automat zur Zubereitung von Babyflaschen. Wahlweise gibt es dieses Gerät mit einer WLAN-Funktion und kann somit App gestützt genutzt werden.

Eine Nachfrage beim Hersteller ergab, dass es leider keine API gibt die man nutzen könnte, um den Milquino z.B. in Home-Assistant zu integrieren.

Da die Kommunikation offensichtlich über das lokale WLAN läuft und kein Cloud-Service dazwischen ist (was ich sehr begrüßenswert finde), müsste es doch möglich sein, die Kommunikation zwischen der App und dem Gerät mitzuhören, zu analysieren und entsprechend nutzen daraus zu ziehen.

Und genau darum soll es hier gehen. Wie kommuniziert die App mit dem Milquino und kann dies genutzt werden.

Disclaimer

Bevor es losgeht braucht es, so denke ich, einen kleinen Disclaimer. Soll mir ja schließlich niemand auf Idee kommen, hier würden illegale Aktivitäten durchgeführt.

Das Gerät wurde von mir weder geöffnet noch wurde die App de-kompiliert. Die gesamte Analyse basiert auf Mitschnitten in meinem eigenen Netzwerk.

Die ersten Informationen

Hat man den Milquino einmal in sein WLAN integriert wird man relativ schnell feststellen dass er eine „Webseite“ mitbringt. Ruft man die IP des Milquino auf, wird einem folgendes präsentiert:

Einmal mit Wireshark den Aufruf mitgeschnitten bekommt man die Info was verbaut wurde um einen kleinen Server im Milquino zu betreiben:

Wir wissen nun also, dass es sich um einen ESP8226 Chip auf einem NodeMCU Board handelt. Wir gehen einfach mal davon aus, dass keine Anstrengungen unternommen wurden, diese Info zu streuen obwohl ein anderes „Gerät“ genutzt wurde ;). Man mag sich nun fragen: In wie weit bringt mir diese Info nun etwas?

Nun ja, die Antwort ist so simpel wie eventuell überraschend. Sie bringt uns nichts, außer eventuell interessant zu sein.

Kommen wir nun aber zu den wirklich interessante Informationen. Schneidet man den Netzwerkverkehr zwischen App und Milquino mit, findet man heraus dass der Milquino auf Port 9990 mittels UDP angesprochen wird. Zum Glück für uns, ist der Datenverkehr nicht verschlüsselt, sondern Plaintext.

Machen wir es kurz

Das Ergebnis meiner Analyse sind, aktuell, die folgenden Funktionen.

  • Anschalten
  • Ausschalten
  • Domainname erfragen
  • Status erfragen
  • Wassermenge konfigurieren
  • Konfigurierte Wassermenge erfragen
  • Anzahl der Pulverlöffel konfigurieren
  • Konfigurierte Anzahl der Pulverlöffel erfragen
  • Pulverdosierung per Löffel konfigurieren
  • Konfigurierte Pulverdosierung per Löffel erfragen
  • Fläschchen machen

Es gibt noch weitere Funktionen in der App, wie z.B. eine Info ob der Wassertank installiert ist oder ob entkalkt werden muss. Alles habe ich noch nicht analysiert.

Da die Kommunikation per UDP im Klartext stattfindet, können die Funktionen mittels netcat ganz einfach selber getestet werden, indem man die folgenden Strings per UDP an seinen Milquino auf Port 9990 sendet.

Die Payloads, die zu senden sind, um die oben genannten Funktionen zu nutzen lauten:

Function: on

Payload: C:1:72\r\n

Der Returnwert ist

E:C11:06
S:0A:22

S:0A:22 ist der Status dafür, dass die Maschine angeschaltet ist. Wofür der erste Wert steht, habe ich nicht rausgefunden.

Function: off

Payload: C:0:73\r\n

Der Returnwert ist

E:C01:07
S:02:51

S:02:51 ist der Status der anzeigt, dass die Maschine ausgeschaltet ist. Der Sinn des ersten Wertes entzieht sich aktuell meiner Kenntnis

Function: getDomainName

Payload: A:1:70\r\n

In meinem Fall ist der Returnwert zum Beispiel

:MILQUINO-9335445:192.168.178.149

Hier sieht man die IP des Gerätes in meinem Netzwerk sowie den Domainnamen. Ob diese Funktion tatsächlich genutzt wird, um den Domainnamen zu erfahren, ist nur eine Vermutung von mir.

Function: getStatus

Payload: C:5:76\r\n

Der Returnwert um anzuzeigen, dass die Maschine angeschaltet ist, ist

S:0A:22

Ist die Maschine ausgeschaltet kommt der Wert

S:02:51

zurück.

Function: setAmountOfWater

Der Milquino unterstützt in 30ml Schritten die Zubereitung von 60ml bis 240ml Babymilch. Entsprechend gibt es mehrere Payloads die zu senden sind, entsprechend der gewünschten Zubereitungsmenge Wasser.

Payload (60ml):  W:2:65\r\n
Payload (90ml):  W:3:64\r\n
Payload (120ml): W:4:63\r\n
Payload (150ml): W:5:62\r\n
Payload (180ml): W:6:61\r\n
Payload (210ml): W:7:60\r\n
Payload (240ml): W:8:6F\r\n

Der Returnwert ist immer der gesendete Payload. Möchte man also 120ml Wasser einstellen, kommt als Antwort „W:4:63“ zurück.

Function: getAmountOfWater

Payload: C:3:70\r\n

Dieser Payload führt dazu, dass man die aktuell konfigurierte Wassermenge zurückbekommt. Der Rückgabewert entspricht den Payload-Werten aus der Funktion „setAmountOfWater“.

Function: setNumberOfSpoons

Der Milquino unterstützt die Zubereitung von Babymilch mit maximal 7 Pulverlöffeln. Entsprechend, wie bereits bei der Funktion „setAmountOfWater“ gibt es wieder mehrere Payloads die gesendet werden können um die Konfiguration entsprechend durchzuführen.

Payload (2 Spoons): P:1:61\r\n
Payload (3 Spoons): P:2:62\r\n
Payload (4 Spoons): P:3:63\r\n
Payload (5 Spoons): P:4:64\r\n
Payload (6 Spoons): P:5:65\r\n
Payload (7 Spoons): P:6:66\r\n

Der Returnwert ist immer der gesendete Payload. Möchte man also 5 Pulverlöffel konfigurieren, sendet man den Payload „P:4:64\r\n“ und erhält als Antwort „P:4:64“

Function: getNumberOfSpoons

C:4:77\r\n

Die Funktion liefert den aktuell eingestellten Wert für die Menge zu verwendender Pulverlöffel zurück. Die Werte entsprechen denen aus der Funktion „getNumberOfSpoons“.

Function: setDosingPerSpoon

Der Milquino unterstützt die Dosierung eines Löffels im Bereich von 3,8g Pulver bis 5,6g Pulver in 0,2g Schritten. Entsprechend gibt es die folgenden Payloads:

Payload (3,8g): G:0:77
Payload (4g):   G:1:76
Payload (4,2g): G:2:75
Payload (4,4g): G:3:74
Payload (4,6g): G:4:73
Payload (4,8g): G:5:72
Payload (5g):   G:6:71
Payload (5,2g): G:7:70
Payload (5,4g): G:8:7F
Payload (5,6g): G:9:7E

Die Rückgabe ist auch hier immer der Wert, der gesendet wurde.

Function: getDosingPerSpoon

Payload: C:7:74

Der Rückgabewert ist die aktuell eingestellte Menge Pulver pro Löffel und die Werte sind in der Funktion „getDosingPerSpoon“ aufgeführt.

Function: makeMilk

Schlussendlich die Funktion, die die Maschine dazu veranlasst, eine Flasche Babymilch zu mischen.

Payload: C:2:71\r\n

Die Rückgabewerte dieser Funktion verstehe ich aktuell noch nicht, sind für mich zum jetzigen Zeitpunkt aber auch nicht von allzu großem Interesse.

Ein Beispiel

Wie kann man all diese Informationen nun nutzen, um den Milquino auch ohne App zu steuern? Man nutzt z.B. das Kommandozeilen-Tool netcat.

Möchte man so zum Beispiel den Status der Maschine erfahren, könnte man folgenden Befehl in einer Bash-Shell ausführen:

echo -ne "C:5:76\r\n" | nc -u 192.168.178.149 9990

Dieser Befehl sendet den String „C:5:76“ mit CRLF Zeilenterminierung durch das Tool netcat mittels UDP an die IP 192.168.178.149 auf Port 9990:

Wie bekomme ich „das“ jetzt in Home-Assistant?

Abschließend möchte ich ein Beispiel geben, wie man die Informationen nun nutzen kann um eine erste Home-Assistant Integration seinen Milquino zu bauen.

Dazu nutzen wir sowohl die command_line als auch die shell_command Integration von Home-Assistant.

Möchte man sich einen Button bauen um sowohl den aktuellen Status der Maschine als auch den An/Aus-Zustand zu steuern, wäre dies mit einem Binary-Sensor z.B. so machbar:

- platform: command_line
  name: milquino_status
  command: 'echo -ne "C:5:76\r\n" | nc -u -w 2 192.168.178.149 9990'
  scan_interval: 30
  payload_on: "S:0A:22"
  payload_off: "S:02:51"
  command_timeout: 5
  device_class: running

Um sich die aktuelle Konfiguration der Wassermenge sowie Löffelanzahl und Pulvermenge anzeigen zu lassen, könnte man einen Sensoren mit entsprechendem value_template nutzen:

- platform: command_line
  name: milquino_configuredAmountOfWater
  command: 'echo -ne "C:3:70\r\n" | nc -u -w 2 192.168.178.149 9990'
  scan_interval: 30
  command_timeout: 5
  unit_of_measurement: ml
  value_template: >-
    {% if value == 'W:2:65' %} 60
    {% elif value == 'W:3:64' %} 90
    {% elif value == 'W:4:63' %} 120
    {% elif value == 'W:5:62' %} 150
    {% elif value == 'W:6:61' %} 180
    {% elif value == 'W:7:60' %} 210
    {% elif value == 'W:8:6F' %} 240
    {% else %} 0
    {% endif %}
    
- platform: command_line
  name: milquino_configuredNumberOfSpoons
  command: 'echo -ne "C:4:77\r\n" | nc -u -w 2 192.168.178.149 9990'
  scan_interval: 30
  command_timeout: 5
  unit_of_measurement: spoons
  value_template: >-
    {% if value == 'P:1:61' %} 2
    {% elif value == 'P:2:62' %} 3
    {% elif value == 'P:3:63' %} 4
    {% elif value == 'P:4:64' %} 5
    {% elif value == 'P:5:65' %} 6
    {% elif value == 'P:6:66' %} 7
    {% else %} 0
    {% endif %}
    
- platform: command_line
  name: milquino_dosingPerSpoon
  command: 'echo -ne "C:7:74\r\n" | nc -u -w 2 192.168.178.149 9990'
  scan_interval: 30
  command_timeout: 5
  unit_of_measurement: g
  value_template: >-
    {% if value == 'G:0:77' %} 3,8
    {% elif value == 'G:1:76' %} 4
    {% elif value == 'G:2:75' %} 4,2
    {% elif value == 'G:3:74' %} 4,4
    {% elif value == 'G:4:73' %} 4,6
    {% elif value == 'G:5:72' %} 4,8
    {% elif value == 'G:6:71' %} 5
    {% elif value == 'G:7:70' %} 5,2
    {% elif value == 'G:8:7F' %} 5,4
    {% elif value == 'G:9:7E' %} 5,6
    {% else %} 0
    {% endif %}

Um eine Flasche dann letztendlich zuzubereiten könnte man sich einen „Button“ basteln, der einen Service aufruft, den man sich per shell_command baut:

milquino_make_milk: 'echo -ne "C:2:71\r\n" | nc -u -w 2 192.168.178.149 9990'

Das Endergebnis könnte nun so aussehen:

Abschließende Gedanken

So richtig zufriedenstellend, zumindest für mich, ist die Kommunikation mittels UDP an dieser Stelle nicht. Der Server im Milquino ist oft nicht so wirklich responsive. So kommt es mir persönlich einfach viel zu häufig vor, dass Abfragen an das Gerät gehen, die einfach nicht beantwortet werden. Das führt im Home-Assistant natürlich dazu, dass oft einfach keine Werte angezeigt werden.

Hätte man diesen Datenverlust eventuell vermeiden können indem man statt UDP auf TCP setzt? Vermutlich. Ändert aber nichts daran, dass aktuell einfach Datenpakete „verloren“ gehen. UDP ist nun mal Zustandslos und der Client wird es per Protokoll nicht erfahren ob das Paket ankam oder nicht.

Eventuell könnte man sich, um diese Problematik ein wenig zu mitgieren, einen eigenen Service in Home-Assistant programmieren oder eine komplette Proxy-Komponente bauen die dann auch direkt per REST angesprochen werden könnte.

Mal sehen was die Zeit noch so hergibt. Für einen ersten Start, könnte es ja jedoch reichen. Ein „Alexa – Schalte Milquino an“ ist so zumindest mal ohne große Probleme möglich. Nachts um 3 Uhr, wenn der/die kleine Hunger hat, eine Wohltat, ohne viel Aufwand an eine frische Flasche zu kommen 😉

Beteilige dich an der Unterhaltung

2 Kommentare

  1. Hi, mega Idee, die du da umgesetzt hast. Kannst du mir als HA Newbie auch noch erklären, wie der entsprechende binary sensor angelegt und daraufhin die Karte auf dem Dashboard hinzugefüht wird? Dann müsste ich nur noch die IP anpassen und würde den Code exakt so übernehmen. 🙂

    Danke für deine Mühe!

    1. Hi,

      in der Hoffnung deine Frage korrekt verstanden zu haben, möchte ich Sie so beantworten:

      Ich versuche meine HA-Config, damit sie übersichtlicher bleibt, zu splitten. Das bedeutet, ich habe eine extra Config-Datei für meine Binärsensoren. Dazu habe ich in der Main-Config (configuration.yaml) folgende Zeile:

      binary_sensor: !include binary_sensors.yaml

      Diese Anweisung sorgt dafür dass beim Start HA in seinem Hauptverzeichnis (da wo die configuration.yaml liegt) eine Datei mit dem Namen binary_sensor.yaml sucht. In dieser Datei steht dann die Konfiguration für den Sensor bei der du dann noch die IP anpassen musst.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert