Archiv der Kategorie: Uncategorized

TheThingsNetwork Gateway Status und Node-Red Worldmap

Node-Red entwickelt sich immer mehr zu einem meiner Lieblingstools. Die Umwandlung und Restrukturierung  von Daten gestaltet sich damit so einfach, dass man viele Herausforderungen mit nur wenigen „Programmier“-schritten lösen kann.

Nachdem ich vor ein paar Tagen zum ersten Mal die Worldmap in Node-Red ausprobiert habe, entstanden darüber einige Ideen, die ich verwirklichen möchte.

Inspiriert von Bjoerns Techblog und Refues Traffic Dashboard wollte ich den Status der TTN Gateways unserer Community in Node-Reds Worldmap darstellen.

Bjoern stellt die API zur Abfrage des Gateway Status in seinem Blog ausführlich vor, darauf kann ich an dieser Stelle verzichten. Zur Abfrage der API nutze ich den http request Node. Für die Abfrage der Community Gateways verwende ich die selben GPS Koordinaten und den Radius, wie auf der Community Seite definiert.

Screenshot_20190210_154725

Die vollständige url lautet damit: https://www.thethingsnetwork.org/gateway-data/location?latitude=50.962010&longitude=6.970313&distance=16200

Im http request Node kann man bereits die Ausgabe als „json formatted“ auswählen.
In nachfolgenden function Node wird die Ausgabe so umgeformt, dass für jedes Gateway der Liste sein online Status als Farbe und natürlich seine Geoposition auf der Karte abgebildet wird. Als Trigger für die Abfrage dient die Worldmap selber. Beim Laden oder Refresh der Seite wird der http request Node neu getriggert. Ein Gateway wird grün dargestellt, wenn es in den letzten 15 Minuten „gesehen“ (last_seen) wurde. Wenn es jedoch länger als 15 Minuten nicht gesehen wurde orange und wenn es länger als 60 Minuten stumm war in rot dargestellt. Wenn man ein Gateway auf der Worldmap anklickt, werden weitere Information wie Description und Owner angezeigt.

Der function Node, der den http request triggert ist simple

if (msg.payload.action === "connected") 
return msg;

Ein wenig umfangreicher ist hier der function Node, der die Daten für den worlmap Node aufbereitet

var NumOfGW = Object.keys(msg.payload).length; 
var gwArr=[];
var mapArr=[];
var timeDiff15 = new Date((new Date()) -15*1000*60);
var timeDiff60 = new Date((new Date()) -60*1000*60);
for (var i = 0; i< NumOfGW; i++){
    gwArr.push(Object.keys(msg.payload)[i]); 
}
for (var i = 0; i< NumOfGW; i++){
if (typeof(msg.payload[gwArr[i]].location)!== "undefined"){
    message = {
    lat: msg.payload[gwArr[i]].location.latitude,
    lon: msg.payload[gwArr[i]].location.longitude,
    name: msg.payload[gwArr[i]].id,
    };
    if (new Date(msg.payload[gwArr[i]].last_seen) === "undefined"){
        message.iconColor="green";
    }
    else if (new Date(msg.payload[gwArr[i]].last_seen)<timeDiff60){
        message.iconColor="red";
    }
    else if (new Date(msg.payload[gwArr[i]].last_seen)<timeDiff15){
        message.iconColor="orange";
    }
    else {
        message.iconColor="green";
    }
    message.Description = msg.payload[gwArr[i]].description;
    message.Owner = msg.payload[gwArr[i]].owner;
    message.lastSeen = msg.payload[gwArr[i]].last_seen;
    mapArr.push(message);
   }
}
msg.payload=mapArr;
return msg;

Auch der worldmap Node wird mit den Geodaten der Community konfiguriert. Dadurch wird die Karte im Zentrum von Köln zentriert und mit einem angenehmen Zoomfaktor angezeigt.

Screenshot_20190210_155440

Wer die komplette Funktion auf seinem Node-Red nachbauen möchte muss nur die worldmap Nodes nachinstallieren. Dazu auf die 3 waagerechten Striche oben rechts, dann Manage Palette, install und nach worldmap suchen. Es wird dann node-red-contrib-web-worldmap gefunden, dass muss installiert werden.

Hier zum direkten Einbinden die Node-Red Daten

[{"id":"54aa39db.4c074","type":"worldmap in","z":"142c7153.7b2197","name":"","path":"/worldmap","x":194.88333129882812,"y":258.1000061035156,"wires":[["c8d2dfcd.98075"]]},{"id":"de77cedf.98a7f","type":"http request","z":"142c7153.7b2197","name":"GW Status","method":"GET","ret":"obj","url":"https://www.thethingsnetwork.org/gateway-data/location?latitude=50.962010&amp;amp;amp;amp;amp;longitude=6.970313&amp;amp;amp;amp;amp;distance=16200","tls":"","x":424.949951171875,"y":160.1666259765625,"wires":[["4b276cf0.dc2bf4","633070e6.4339a"]]},{"id":"b9bebb3c.1ba67","type":"inject","z":"142c7153.7b2197","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":183.50003051757812,"y":159.1666259765625,"wires":[["de77cedf.98a7f"]]},{"id":"88ce05cb.6685a8","type":"debug","z":"142c7153.7b2197","name":"","active":true,"console":"false","complete":"false","x":881.9500732421875,"y":223.7332763671875,"wires":[]},{"id":"4b276cf0.dc2bf4","type":"function","z":"142c7153.7b2197","name":"extract Geo&amp;amp;amp;amp;Status","func":"var NumOfGW = Object.keys(msg.payload).length; \nvar gwArr=[];\nvar mapArr=[];\nvar timeDiff15 = new Date((new Date()) -15*1000*60);\nvar timeDiff60 = new Date((new Date()) -60*1000*60);\nfor (var i = 0; i&amp;amp;amp;lt; NumOfGW; i++){\n gwArr.push(Object.keys(msg.payload)[i]); \n}\nfor (var i = 0; i&amp;amp;amp;lt; NumOfGW; i++){\n message = {\n lat: msg.payload[gwArr[i]].location.latitude,\n lon: msg.payload[gwArr[i]].location.longitude,\n name: msg.payload[gwArr[i]].id,\n };\n if (new Date(msg.payload[gwArr[i]].last_seen)=== undefined){\n message.iconColor=\"green\";\n }\n else if (new Date(msg.payload[gwArr[i]].last_seen)&amp;amp;amp;lt;timeDiff60){\n message.iconColor=\"red\";\n }\n else if (new Date(msg.payload[gwArr[i]].last_seen)&amp;amp;amp;lt;timeDiff15){\n message.iconColor=\"orange\";\n }\n else {\n message.iconColor=\"green\";\n }\n message.Description = msg.payload[gwArr[i]].description;\n message.Owner = msg.payload[gwArr[i]].owner;\n message.lastSeen = msg.payload[gwArr[i]].last_seen;\n mapArr.push(message);\n }\nmsg.payload=mapArr;\nreturn msg;\n","outputs":1,"noerr":0,"x":705.9500732421875,"y":142,"wires":[["88ce05cb.6685a8","a02be639.d80688"]]},{"id":"633070e6.4339a","type":"debug","z":"142c7153.7b2197","name":"","active":false,"console":"false","complete":"false","x":644.9500732421875,"y":217.45001220703125,"wires":[]},{"id":"a02be639.d80688","type":"worldmap","z":"142c7153.7b2197","name":"","lat":"50.962010","lon":"6.970313","zoom":"13","layer":"OSM","cluster":"","maxage":"","usermenu":"show","layers":"show","panit":"false","panlock":"false","zoomlock":"false","path":"/worldmap","x":921.5,"y":143.66665649414062,"wires":[]},{"id":"c8d2dfcd.98075","type":"function","z":"142c7153.7b2197","name":"retrigger from map","func":"if (msg.payload.action === \"connected\")\nreturn msg;","outputs":1,"noerr":0,"x":420.9500427246094,"y":258.4833068847656,"wires":[["de77cedf.98a7f"]]}]

Beeindruckend ist auch, wenn man im http request die url für den Abruf aller Gateways in Deutschland nutzt: https://www.thethingsnetwork.org/gateway-data/country/de

Der zweite Screenshot entstand nach der Änderung des Parameters „cluster if zoom level is less than“


Werbung

Logbuch Server einrichten – Teil 2

Im ersten Teil habe ich beschrieben, wie der Server mit nginx und node-red bespielt wurde. In diesem Teil werde ich zeigen, wie InfluxDB und Grafana installiert und konfiguriert werden. Auch in diesem Tutorial sind Eingaben und Ausgaben farblich gekennzeichnet.

Installation und Einrichtung der InfluxDB

Die Installation ist auf der Influx Webseite gut beschrieben, daran habe ich mich orientiert. Dabei jeweils auf die Version der Software achten. Zur reinen Installation sind nur 2 Schritte erforderlich.

curl -sL https://repos.influxdata.com/influxdb.key | sudo apt-key add -
source /etc/lsb-release
echo "deb https://repos.influxdata.com/${DISTRIB_ID,,} ${DISTRIB_CODENAME} stable" | sudo tee /etc/apt/sources.list.d/influxdb.list

deb https://repos.influxdata.com/ubuntu xenial stable 

sudo apt-get update && sudo apt-get install influxdb
Hit:1 http://security.ubuntu.com/ubuntu xenial-security InRelease
Hit:2 https://repos.influxdata.com/ubuntu xenial InRelease 
Hit:3 http://us.archive.ubuntu.com/ubuntu xenial InRelease
Hit:4 http://us.archive.ubuntu.com/ubuntu xenial-updates InRelease
Hit:5 http://us.archive.ubuntu.com/ubuntu xenial-backports InRelease
Reading package lists... Done
Reading package lists... Done
Building dependency tree 
Reading state information... Done
The following NEW packages will be installed:
 influxdb
0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded.
Need to get 23.0 MB of archives.
After this operation, 76.5 MB of additional disk space will be used.
Get:1 https://repos.influxdata.com/ubuntu xenial/stable amd64 influxdb amd64 1.6.0-1 [23.0 MB]
Fetched 23.0 MB in 0s (27.7 MB/s) 
Selecting previously unselected package influxdb.
(Reading database ... 104707 files and directories currently installed.)
Preparing to unpack .../influxdb_1.6.0-1_amd64.deb ...
Unpacking influxdb (1.6.0-1) ...
Processing triggers for man-db (2.7.5-1) ...
Setting up influxdb (1.6.0-1) ...
Created symlink from /etc/systemd/system/influxd.service to /lib/systemd/system/influxdb.service.
Created symlink from /etc/systemd/system/multi-user.target.wants/influxdb.service to /lib/systemd/system/influxdb.service.

Im nächsten Schritt wird der Service influxdb gestartet

sudo service influxdb start

Eine Kontrolle zeigt, dass die Datenbank läuft

ps -ax|grep influx
 5372 ? Ssl 0:00 /usr/bin/influxd -config /etc/influxdb/influxdb.conf

User, Autorisierung und Authentifikation

Im momentanen Zustand ist influx noch vollkommen offen für jeden, deshalb werde ich in den nächsten Schritten User und deren Rechte anlegen. Eine detaillierte Beschreibung findet sich auf den Webseiten von InfluxDB.

Zunächst benötigen wir einen User „admin“, der alle Rechte besitzt. Ich starte influx im ssh-Terminal mit

influx
Connected to http://localhost:8086 version 1.6.0
InfluxDB shell version: 1.6.0
> 

Wir befinden uns jetzt auf der Befehlszeile der InfluxDB, alle weiteren Ein- und Ausgaben kommen von dort. Zunächst wird der User admin mit einem Passwort angelegt.

CREATE USER admin WITH PASSWORD '<password>' WITH ALL PRIVILEGES
>

Ich lege einen weiteren User an, dessen Rechte allerdings dann auf eine bestimmte DB erteilt werden.

CREATE USER <username> WITH PASSWORD '<password>'
>

Als nächstes erstelle ich eine Datenbank und gebe dem zuletzt angelegten User alle Rechte darauf

CREATE DATABASE <datenbank-name>
>

GRANT ALL ON "<datenbank-name>" TO "<username>"
>

Zusätzlich lege ich noch einen Read-only User auf diese DB an

CREATE USER RoUser WITH PASSWORD '<password>'
>

GRANT READ ON "<datenbank-name>" TO "RoUser"
>

Influx wird geschlossen mir Strg-d oder „exit“. Damit die Authentifikation generell wirksam wird, muss der Config-File noch angepasst werden.

sudo nano /etc/influxdb/influxdb.conf

darin im Abschnitt [http]

auth-enabled = true

setzen. Den Service dann restarten.

sudo service influxdb stop
sudo service influxdb start

Beim nächsten Aufruf von influx daran denken, dass sich nun der User admin zunächst mit

auth
username: admin
password: xxx

authentifizieren muss.

…..tbc…..

 

 

 

 

 

Logbuch – Server einrichten

In der letzten Zeit habe ich einige Server neu eingerichtet, hauptsächlich zur Nutzung für IoT Projekte. Empfehlen möchte ich hier die Cloud Server von Hetzner, die sich für diesen Zweck hervorragend eignen. Ich lasse mir die Server mit einer minimal Ubuntu Konfiguration betanken. Dieses Logbuch beschreibt die Installation eines Servers für das Projekt openair.cologne. Auf diesem Server werden die Sensordaten gespeichert und grafisch ausgewertet.

Folgende SW wird benötigt (wird mglw noch vervollständigt)
– Node-Red
– nginx
– Influx
– Grafana

Vorbereitung

Ich gehe davon aus, dass der Server bereits einen ssh Zugang besitzt. Ich nutze Linux aus der Command Line zur Verbindung mit dem Server, Windows Nutzer müssen hier Putty nutzen. Damit die nachfolgende Beschreibung auch auf andere Server angewendet werden kann, wurden die veränderbaren Daten in  <  > Klammern gesetzt und müssen entsprechend ersetzt werden.  Eingaben und Ausgaben des Servers sind farblich gekennzeichnet.

ssh <user>@<servername oder IP>
<user>@<servername> password:   xxx
Welcome to Ubuntu 16.04.4 LTS (GNU/Linux 4.4.0-128-generic x86_64)

* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage

0 packages can be updated.
0 updates are security updates.


*** System restart required ***
Last login: Tue Jul 10 20:38:06 2018 from 2.206.50.224
<user>@<servername>:~$

Als erster Schritt muss immer zuerst ein Update / Upgrade des Systems ausgelöst werden.

sudo apt-get update
sudo apt-get upgrade

Da ich immer gerne einen Filemanager einsetze, installiere ich mir noch den MidnightCommander.

sudo apt-get install mc

Installation node-red

Die Installation von node-red wird sehr gut in den Tutorials von DigitalOcean beschrieben. Daran habe ich mich weitestgehend orientiert.

sudo apt-get install nodejs-legacy

Testen, ob die Installation erfolgreich war

node -v
v4.2.6

Dann weiter

sudo apt-get install npm

und testen

npm -v
3.5.2

und jetzt die Installation von node-red.

sudo npm install -g --unsafe-perm node-red node-red-admin

Node-red wird nicht mit der Installation gestartet, zunächst müssen noch einige Konfigurationen vorgenommen werden. Als erstes lege ich  einen User an und vergebe sein  Passwort, unter dem node-red laufen soll.

sudo adduser node-red-user

Um node-red bei Systemstart automatisch starten zu lassen ist eine Datei zu erstellen

sudo nano /etc/systemd/system/node-red.service

mit folgendem Inhalt (ggf hier den Usernamen  ändern)

[Unit]
Description=Node-RED
After=syslog.target network.target

[Service]
ExecStart=/usr/local/bin/node-red-pi --max-old-space-size=128 -v
Restart=on-failure
KillSignal=SIGINT

# log output to syslog as 'node-red'
SyslogIdentifier=node-red
StandardOutput=syslog

# non-root user to run as
WorkingDirectory=/home/node-red-user/
User=node-red-user
Group=node-red-user

[Install]
WantedBy=multi-user.target

Dann wird der Service enabled

sudo systemctl enable node-red

und gestartet

sudo systemctl start node-red

Node-Red kann jetzt aus dem Browser mit dem Aufruf <servername:1880> gestartet werden. Zukünftig soll node-red nicht unter dem Port 1880 sonder unter <servername>/node-red aufgerufen werden, dazu benötige ich als Reverse Proxy nginx.

Installation nginx

nginx wird hier hauptsächlich als Reverse-Proxy eingesetzt, kann aber auch als Webserver genutzt werden. Weiterführende Tutorials findet ihr auf DigitalOcean.

sudo apt-get install nginx

nginx wird mit der Installation bereits als Service gestartet. Es wird getestet, ob nginx läuft.

systemctl status nginx
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
Active: active (running) since Mon 2018-07-09 20:31:55 CEST; 24h ago
Main PID: 23970 (nginx)
Tasks: 2
Memory: 2.0M
CPU: 386ms
CGroup: /system.slice/nginx.service
├─23970 nginx: master process /usr/sbin/nginx -g daemon on; master_process on
└─23971 nginx: worker process

Zusätzlich sollte auch der Webservice erreichbar sein. Dazu aus dem Browser den Server unter seinem Namen oder seiner IP aufrufen. Zu sehen sein sollte eine Begrüßungsseite von nginx.

Um später auf die ursprüngliche Konfiguration zurückgehen zu können, benenne ich den Konfigurationsfile um

sudo mv /etc/nginx/sites-available/default /etc/nginx/sites-available/default.old

und erstelle einen neuen – zunächst leeren File – mit

sudo nano /etc/nginx/sites-available/default

dieser File wird hiermit befüllt, <….> entsprechend anpassen

server {
   listen 80;
   server_name <servername>;
   root /var/www/html;
   index index.html index.htm index.nginx-debian.html;

   location /node-red {
      proxy_pass http://localhost:1880;
      proxy_http_version 1.1;
      proxy_set_header Host $http_host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";
   }
}

und der Service wird restartet

sudo systemctl restart nginx

Damit node-red auch richtig funktioniert muss noch eine kleine aber wesentliche Änderung vorgenommen werden, dazu editieren

sudo nano /home/node-red-user/.node-red/settings.js

und diese Zeile anpassen

 httpRoot: '/node-red',

danach den Service neu starten mit

sudo systemctl stop node-red
sudo systemctl start node-red

Danach sollte node-red aus dem Browser mit <servername>/node-red aufrufbar sein.

Node-Red User Authentication

Momentan ist node-red noch vollkommen offen aus dem Internet. Ich lege einen/mehrere User an, die node-red nutzen dürfen. Dazu wird die Datei settings.js editiert. Vorher öffne ich noch ein zweites ssh Fenster auf dem gleichen Server. In diesem Fenster kann ich den Hash erzeugen. Ich öffne die settings.js im ersten Fenster

sudo nano /home/node-red-user/.node-red/settings.js

in dieser Datei findet man einen Bereich unterhalb von // Securing Node-RED mit einem Beispiel, wie die Authentisierung eingefügt werden muss. Hier ein Beispiel, wie der einzufügende Teil für einen User aussehen sollte

adminAuth: {
   type: "credentials",
   users: [{
       username: "admin",
       password: "your hash",
       permissions: "*"
   }]
},

Das Komma am Schluss des Blocks nicht vergessen. Werden mehrere User eingerichtet, muss jeder User in { } gesetzt werden.

adminAuth: {
   type: "credentials",
   users: [
       {
       username: "admin",
       password: "your hash",
       permissions: "*"
       },
       {
       username: "user2",
       password: "user2 hash",
       permissions: "*"
       }
   ]
},

Den Hash erzeuge ich im zweiten Fenster, ich werde nach meinem sudo Passwort gefragt und danach nach dem Passwort for Node-Red.

sudo node-red-admin hash-pw
[sudo] password for <user>:    xxx 
Password:  xxx
$2a$08$refeJAyPqLeeb6EIfNv0Mu.EhZMV6H/wZlJleUdTZWWNplrjfuZ5y

Den ausgegebenen Hash kopiere im anderen Fenster in den File settings.js.

password: "$2a$08$refeJAyPqLeeb6EIfNv0Mu.EhZMV6H/wZlJleUdTZWWNplrjfuZ5y",

Nach dem Speichern des Files und einem Neustart von Node-Red

sudo systemctl stop node-red
sudo systemctl start node-red

werde ich nach dem Aufruf der Node-Red Seite im Browser nach meinen Zugangsdaten gefragt.

node-red1

Installation InfluxDB

…..  mit der Installation der InfluxDB geht’s weiter im nächsten Blogpost.

 

 

Was hat ein kühles Bier mit IoT zu tun

adafruit3Seit einiger Zeit beschäftige ich mich intensiv mit dem Internet-of-Things (IoT) und frage mich dabei immer wieder: Wozu braucht man denn so was. Aber zugegebenermaßen – in Verbindung mit Mikrocontrollern wie dem Arduino ist es natürlich sehr reizvoll vom Smartphone Steckdosen zu schalten oder die Temperatur und Feuchte seines Kellers zu messen und vom Smartphone aus zu kontrolieren, und das egal wo ich mich gerade befinde.

Ein aktuelles Problem aus dem wahren Leben brachte mich diesem Thema gerade jetzt näher und ich möchte meine Erfahrungen mit euch teilen.

Die Aufgabe

Für ein kleines Fest sollten natürlich ausreichend kalte Getränke, insbesonders leckeres Kölsch zur Verfügung stehen. Der Kühlschrank in der Küche war schon mit leckerem Essen gefüllt und die Temperatur im Keller lag bei ca. 21°C während die Außentemperatur auf knapp 30°C tagsüber anstieg.

Die Idee

Was also tun? Im Keller steht seit einiger Zeit ein ungenutzter Gefrierschrank. Den kann man doch hervorragend als Kühlschrank nutzen, wenn man verhindert, dass er die Getränke einfriert. Also einfach, so die Idee, die Kühlung bei einer Temperatur von ca 5°C ausschalten und bei 6°C wieder einschalten.

Die Realisierung

Alles, was ich dazu brauche habe ich in meiner Bastelkiste:
a) zum Messen der Temperatur
– einen Arduino
– einen DS18B20 Temperatursensor
– einen 4k7 Widerstand
b) zum Schalten der Steckdose
– einen 433MHz Sender- eine Funksteckdose
c) zusätzlich
– ein ProtoshieldDS18B20

Der DS18B20 wurde an ein Flachbandkabel gelötet und mit Acryldichtmasse wasserdicht gekapselt. Das Flachbandkabel wurde durch die Türdichtung des Gefrierschranks geführt und der Sensor wurde mit Klebeband im Innern befestigt. Das Programm wurde schnell aus den im Netz vorhandenen Bausteinen zusammengesetzt. Zum Messen der Temperatur wurde die OneWire Lib genutzt, wobei das Programm auf die bekannte Adresse des genutzten Sensors reduziert wurde. Jedem, der mit Netzspannung in seinen Projekten arbeitet, kann ich nur raten, diese immer mit einer Funksteckdose zu schalten. Netzspannung ist lebensgefährlich und mit Nutzung einer Funksteckdose kann fast nichts mehr passieren. Für die Ansteuerung der Funksteckdose benutze ich die RCSwitch Lib. Ich habe den Vorteil Steckdosen vom TypA mit einem Hauscode zu besitzen, damit ist das Schalten sehr simpel.

#include <OneWire.h>
OneWire  ds(6);  // on pin x (a 4.7K pullup resistor is necessary)
#include <RCSwitch.h>
RCSwitch mySwitch = RCSwitch();

boolean refri_stat;
float fptemp = 5.5;  // initial set
float delta = 0.5;  // adjustment range

/*************************** Sketch Code ************************************/

void setup(void) {
  Serial.begin(115200);
  Serial.println(F("Program started ..."));
  // Transmitter is connected to Arduino Pin #xx
  mySwitch.enableTransmit(7);
  // Switch off refrigerator
  mySwitch.switchOff("01111", "01000");
  refri_stat = false;
}

void loop(void) {
  float temp_c = (measure_t());    // call the subroutine to measure
  if (!refri_stat && temp_c > fptemp + delta) {    // decide to switch on/off with hysteresis
    mySwitch.switchOn("01111", "01000");
    refri_stat = true;
  }
  if (refri_stat && temp_c < fptemp - delta) {
    mySwitch.switchOff("01111", "01000");
    refri_stat = false;
  }
  Serial.print(" on/off state = ");
  Serial.println(refri_stat);
}

float measure_t() {
  byte i;
  byte present = 0;
  byte type_s;
  byte data[12];
  byte addr[8] = { 0x28, 0x62, 0x21, 0x80, 0x04, 0x00, 0x00, 0xC1 };    // adress of the sensor
  ds.reset();
  ds.select(addr);
  ds.write(0x44, 1);        // start conversion, with parasite power on at the end
  delay(1000);     // maybe 750ms is enough, maybe not
  // we might do a ds.depower() here, but the reset will take care of it.
  present = ds.reset();
  ds.select(addr);
  ds.write(0xBE);         // Read Scratchpad
  for ( i = 0; i < 9; i++) {           // we need 9 bytes
    data[i] = ds.read();
  }
  int16_t raw = (data[1] << 8) | data[0];
  float celsius = (float)raw / 16.0;
  Serial.print("Sensor present = ");
  Serial.print(present, HEX);
  Serial.print("  Temperature[C] = ");
  Serial.print(celsius);
  Serial.print(" ");
  return celsius;
}

Die Ausgabe sieht dann so aus, gemessen wird ca. im Sekundenrhythmus, hier noch bei Raumtemperatur:

Program started ... 
Sensor present = 1  Temperature[C] = 24.94  on/off state = 1 
Sensor present = 1  Temperature[C] = 25.00  on/off state = 1 
Sensor present = 1  Temperature[C] = 24.94  on/off state = 1 
Sensor present = 1  Temperature[C] = 25.00  on/off state = 1 
Sensor present = 1  Temperature[C] = 25.00  on/off state = 1 
Sensor present = 1  Temperature[C] = 25.00  on/off state = 1 
Sensor present = 1  Temperature[C] = 25.00  on/off state = 1 
Sensor present = 1  Temperature[C] = 25.00  on/off state = 1 
Sensor present = 1  Temperature[C] = 25.00  on/off state = 1 
Sensor present = 1  Temperature[C] = 25.00  on/off state = 1 
Sensor present = 1  Temperature[C] = 25.00  on/off state = 1 
Sensor present = 1  Temperature[C] = 25.00  on/off state = 1 
Sensor present = 1  Temperature[C] = 25.06  on/off state = 1 
Sensor present = 1  Temperature[C] = 25.06  on/off state = 1 

…. und nun zum Thema IoT

Ein gewisses ungutes Gefühl blieb bestehen. Was ist mit der Temperatur? Schaltet der Arduino zuverlässig? Stürzt das Programm nicht ab? Ich hatte verständlicherweise keine Lust ständig in den Keller zu laufen, also musste eine andere Lösung her. Die Daten müssen in die Cloud! Während ich in meinem letzten Blogbeitrag den Service von data.sparkfun.com  genutzt habe, werde ich hier den Service von Adafruit nutzen. Adafruit bietet nicht nur die Speicherung der Daten, sondern auch ein Dashboard, um seine Daten zu sehen und Eingaben zu tätigen. Die Basis der Kommunikation setzt hier auf das MQTT Protokoll auf. Das einzige zusätzliche Material, was zum Einsatz kam, war ein Ethernet Shield. Über ein Powerline Netzwerk wurde vom Router das Ethernet bis in den Keller verlängert.

Mit Hilfe der Tutorials auf Adafruit war mein Account schnell erstellt. Auf meinem Dashboard wollte ich nicht nur die Temperatur und den Zustand der Funksteckdose anzeigen lassen, sondern ich wollte auch den Sollwert der Temperatur verändern können. Ich brauchte also 3 Feeds:
1. die Temperatur – schreibend (publish) vom Arduino auf den Broker (so bezeichnet man den Server in einer MQTT Kommunikation)
2. den Zustand der Funksteckdose (an/aus) und damit die Funktion des Gefrierschranks (kühlen) – schreibend vom Arduino auf den Broker
3. die Einstellung des Soll-Temperaturwertes: am Dashboard einzustellen, der Arduino greift lesend (subscribe) auf den Broker zu

Hier die Sicht auf das Dashboard knapp 3 Stunden nach dem Einschalten. Die Temperatur schwingt zunächst noch über den Zielwert um sich dann später zwischen 13°C und 14°C einzupendeln. Der Status ist momentan Kühlung = aus. Die Einschaltdauer liegt jeweils bei ca. 1 min, aus bei ca. 30 min.

adafruit2

Das oben gelistete Programm wurde durch die notwendigen Befehle der Adafruit IO Library ergänzt. Ich habe versucht das durch zahlreiche Kommentare verständlich zu machen.

#include <OneWire.h>
OneWire  ds(6);  // on pin x (a 4.7K pullup resistor is necessary)
#include <RCSwitch.h>
RCSwitch mySwitch = RCSwitch();
boolean refri_stat;

/***************************************************
  Adafruit MQTT Library Ethernet Example

  Adafruit invests time and resources providing this open source code,
  please support Adafruit and open-source hardware by purchasing
  products from Adafruit!

  Written by Alec Moore
  Derived from the code written by Limor Fried/Ladyada for Adafruit Industries.
  MIT license, all text above must be included in any redistribution
 ****************************************************/
#include <SPI.h>
#include "Adafruit_MQTT.h"
#include "Adafruit_MQTT_Client.h"

#include <Ethernet.h>
#include <EthernetClient.h>
#include <Dns.h>
#include <Dhcp.h>

/************************* Ethernet Client Setup *****************************/
byte mac[] = {0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED};

//Uncomment the following, and set to a valid ip if you don't have dhcp available.
IPAddress iotIP (192, 168, 2, 99);
//Uncomment the following, and set to your preference if you don't have automatic dns.
//IPAddress dnsIP (8, 8, 8, 8);
//If you uncommented either of the above lines, make sure to change "Ethernet.begin(mac)" to "Ethernet.begin(mac, iotIP)" or "Ethernet.begin(mac, iotIP, dnsIP)"


/************************* Adafruit.io Setup *********************************/

#define AIO_SERVER      "io.adafruit.com"
#define AIO_SERVERPORT  1883
#define AIO_USERNAME    "<your username here>"
#define AIO_KEY         "<your AIO key here>"


/************ Global State (you don't need to change this!) ******************/

//Set up the ethernet client
EthernetClient client;

// Store the MQTT server, client ID, username, and password in flash memory.
// This is required for using the Adafruit MQTT library.
const char MQTT_SERVER[] PROGMEM    = AIO_SERVER;
// Set a unique MQTT client ID using the AIO key + the date and time the sketch
// was compiled (so this should be unique across multiple devices for a user,
// alternatively you can manually set this to a GUID or other random value).
const char MQTT_CLIENTID[] PROGMEM  = __TIME__ AIO_USERNAME;
const char MQTT_USERNAME[] PROGMEM  = AIO_USERNAME;
const char MQTT_PASSWORD[] PROGMEM  = AIO_KEY;

Adafruit_MQTT_Client mqtt(&client, MQTT_SERVER, AIO_SERVERPORT, MQTT_CLIENTID, MQTT_USERNAME, MQTT_PASSWORD);

/****************************** Feeds ***************************************/

// Setup a feed called 'biertemp' for publishing.
// Notice MQTT paths for AIO follow the form: <username>/feeds/<feedname>
const char BIERMON_FEED[] PROGMEM = AIO_USERNAME "/feeds/biertemp";
Adafruit_MQTT_Publish biermon = Adafruit_MQTT_Publish(&mqtt, BIERMON_FEED);

const char ONOFF_FEED[] PROGMEM = AIO_USERNAME "/feeds/onoff";
Adafruit_MQTT_Publish onoffbutton = Adafruit_MQTT_Publish(&mqtt, ONOFF_FEED);

const char SETTEMP_FEED[] PROGMEM = AIO_USERNAME "/feeds/settemp";              // definition of feedname
Adafruit_MQTT_Subscribe settemp = Adafruit_MQTT_Subscribe(&mqtt, SETTEMP_FEED);   // subscription
 
float stat = 0;
float fptemp = 25;  // initial set
float delta = 0.1;  // adjustment range
unsigned long lastmillis;

/*************************** Sketch Code ************************************/

void setup(void) {
  Serial.begin(115200);
  Serial.println(F("Program started ..."));
  // Transmitter is connected to Arduino Pin #xx
  mySwitch.enableTransmit(7);
  // Switch off refrigerator
  mySwitch.switchOff("01111", "01000");
  refri_stat = false;
  Ethernet.begin(mac, iotIP);
  delay(1000); //give the ethernet a second to initialize
  Serial.print("My IP address: ");
  Serial.println(Ethernet.localIP());
  mqtt.subscribe(&settemp);      // activate subscription to feed settemp
}

void loop(void) {
  MQTT_connect();
  Adafruit_MQTT_Subscribe *subscription;   // check subscription
  while ((subscription = mqtt.readSubscription(500))) {
    // Check if its the settemp feed
    if (subscription == &settemp) {
      Serial.print(F("Got new Temp: "));
      Serial.println((char *)settemp.lastread);
      fptemp = atof((char *)settemp.lastread);  // set to new temperature
      Serial.println(fptemp);
    }
    else {
      Serial.print(F("nothing to read "));
    }
  }
  float temp_c = (measure_t());
  if (!refri_stat && temp_c > fptemp + delta) {
    mySwitch.switchOn("01111", "01000");
    refri_stat = true;
  }
  if (refri_stat && temp_c < fptemp - delta) {
    mySwitch.switchOff("01111", "01000");
    refri_stat = false;
  }
  Serial.print(" on/off state = ");
  Serial.println(refri_stat);
  // Now we can publish stuff!
  if (stat != float(refri_stat)) {    // only when changed
    Serial.print(F("\nSending changed on/off state "));
    Serial.print(refri_stat);
    Serial.println("...");
    stat = float(refri_stat);     // published value has to be float (don't know why)
    if (! onoffbutton.publish(stat)) {
      Serial.println(F("Failed"));
    } else {
      Serial.println(F("OK!"));
    }
  }
  if (millis() > lastmillis + 15000) {  // read / write broker every xx milliseconds
    MQTT_connect();
    // Now we can publish stuff!
    Serial.print(F("\nSending temp val "));
    Serial.print(temp_c);
    Serial.println("...");
    if (! biermon.publish(temp_c)) {
      Serial.println(F("Failed"));
    } else {
      Serial.println(F("OK!"));
    }
    // ping the server to keep the mqtt connection alive
    if (! mqtt.ping()) {
      mqtt.disconnect();
    }
    lastmillis = millis();
  }

}

float measure_t() {
  byte i;
  byte present = 0;
  byte type_s;
  byte data[12];
  byte addr[8] = { 0x28, 0x62, 0x21, 0x80, 0x04, 0x00, 0x00, 0xC1 };
  //byte addr[8] = { 0x28, 0x42, 0xCC, 0x7F, 0x04, 0x00, 0x00, 0xAA };
  ds.reset();
  ds.select(addr);
  ds.write(0x44, 1);        // start conversion, with parasite power on at the end

  delay(1000);     // maybe 750ms is enough, maybe not
  // we might do a ds.depower() here, but the reset will take care of it.

  present = ds.reset();
  ds.select(addr);
  ds.write(0xBE);         // Read Scratchpad
  for ( i = 0; i < 9; i++) {           // we need 9 bytes
    data[i] = ds.read();
  }
  int16_t raw = (data[1] << 8) | data[0];
  float celsius = (float)raw / 16.0;
  Serial.print("Sensor present = ");
  Serial.print(present, HEX);
  Serial.print("  Temperature[C] = ");
  Serial.print(celsius);
  Serial.print(" ");
  return celsius;
}

// Function to connect and reconnect as necessary to the MQTT server.
// Should be called in the loop function and it will take care if connecting.
void MQTT_connect() {
  int8_t ret;

  // Stop if already connected.
  if (mqtt.connected()) {
    return;
  }

  Serial.print("Connecting to MQTT... ");

  while ((ret = mqtt.connect()) != 0) { // connect will return 0 for connected
    Serial.println(mqtt.connectErrorString(ret));
    Serial.println("Retrying MQTT connection in 5 seconds...");
    mqtt.disconnect();
    delay(5000);  // wait 5 seconds
  }
  Serial.println("MQTT Connected!");
}

Video

Video (Downloadlink):

Tutorial: Website parsen mit dem ESP8266 – Teil 2

Einleitung

Im ersten Teil dieses Tutorials habe ich gezeigt, wie man Informationen nach einem Suchschema aus einer Website extrahiert. In diesem Teil des Tutorial möchte ich zunächst auch darauf eingehen, was man beachten sollte, wenn man eine Website auswählt, aus der Daten extrahiert werden sollen. Später werden wir dann ein zweites Beispiel genauer betrachten.

Der Inhalt einer Website

Die heutigen Websites werden immer umfangreicher und bieten eine Fülle von Informationen, was leider auch eine Menge von Daten zur Folge hat. Zusätzlich werden Animationen, Grafiken oder Werbung mit übertragen, was die Datenmenge zusätzlich erhöht. Je mehr Daten empfangen werden, umso mehr muss der ESP analysieren, um die gesuchte Information zu finden.

Nehmen wir als Beipiel an, wir wollen die aktuelle Temperatur an unserem Wohnort aus einer Website auslesen. Es gibt eine große Zahl von Websites, die diese Information bereit stellen. Um zu bewerten, ob eine Website geeignet ist, diese Information für den ESP verträglich zu liefern, sollte man sich den Quelltext der Website mal ansehen. Ich habe als Beispiel mal eine Website ausgewählt, die die aktuelle Temperatur für sehr viele Orte liefern kann. Auf der Seite http://www.wunderground.com/ kann man sich eine Messstation in der Nähe seines Wohnortes suchen. Die url für die Messstation am Kölner Flughafen wäre dann http://www.wunderground.com/global/stations/10513.html.

Eine durchaus auch sinnvolle Alternative wäre es, eine Website auszuwählen, die weniger Daten transferiert. Eine gute Adresse sind in einem solchen Fall z.B. private Websites, die Informationen ihrer Wetterstationen online stellen.

Auf jeden Fall sollte man sich jetzt mal den Quelltext dieser Website ansehen, am einfachsten geht das z.B. im Firefox über den Menuepunkt Entwicklerwerkzeuge->Seitenquelltext anzeigen oder die Kurztaste Strg+u. Man muss kein HTML verstehen, um zu erkennen, dass hier Massen an Daten übertragen werden.

OpenWeather1

In diesem Fall von wunderground.com hat man, was die aktuelle Temperatur angeht Glück, die steht nämlich im Quelltext ganz oben als Meta-Information.

    <meta property="og:title" content="Koeln, Germany | 3.6&deg; | Mostly Cloudy" /> 

Um jetzt einen eindeutigen Suchbegriff in der Zeile zu finden, testen wir, ob bestimmte Buchstabenketten nur in dieser Zeile des Quelltextes vorkommen. Ich entscheide mich für den Suchstring „og:title„. Das hat den Vorteil, dass ich ohne den Suchstring zu ändern auch andere Städte suchen könnte.

Für einen ersten Test sollte es reichen den folgenden Codeblock im Code aus dem ersten Teil des Tutorials einzusetzen

  
while (client.available()) {
  String line = client.readStringUntil('\n');
  if (line.indexOf("og:title") >= 0) { // Searchstring exists?
    Serial.println();
    Serial.print(line);
  }
}

Natürlich muss der Server und die URL ebenfalls eingetragen werden

 
... 
const char* host = "www.wunderground.com"; 
... 
String url = "/global/stations/10513.html?MR=1";
... 

Die Ausgabe sieht jetzt so aus:


Connecting to TP-LINK_360E60
............
WiFi connected
IP address:
192.168.0.100

connecting to www.wunderground.com
Requesting URL: /global/stations/10513.html?MR=1

         <meta property="og:title" content="Koeln, Germany | 3.5&deg; | Scattered Clouds" />
closing connection

Die richtige Zeile habe ich also schon gefunden, jetzt gehts darum, den Temperaturwert aus der Zeile zu extrahieren. Der Wert steht zwischen den Zeichen „| “ und „&“. Wir suchen also das erste Vorkommen des ersten Suchstrings und extrahieren von da bis zum zweiten Suchstring. Der Code wird also folgendermaßen ergänzt. Als kleines Extra habe ich auch noch die Umwandlung der Temperatur in float eingebaut.


while (client.available()) {
  String line = client.readStringUntil('\n');
  if (line.indexOf("og:title") >= 0) { // Searchstring exists?
    Serial.println();
    Serial.println(line);
    int vonPos = line.indexOf("| "); 
    int bisPos = line.indexOf("&");
    Serial.print("die aktuelle Temperatur ist ");
    Serial.print(line.substring(vonPos+2, bisPos));
    Serial.println(" Grad C");
   // Umandlung in float
   String temp=line.substring(vonPos+2, bisPos);
   char char1[8];
   temp.toCharArray(char1, temp.length()+1);
   float tempWert=atof(char1);
   Serial.println(tempWert);
  }
}

Die Ausgabe sieht jetzt so aus


Connecting to TP-LINK_360E60
........
WiFi connected
IP address:
192.168.0.100

connecting to www.wunderground.com
Requesting URL: /global/stations/10513.html?MR=1

<meta property="og:title" content="Koeln, Germany | 3.6&deg; | Mostly Cloudy" />
die aktuelle Temperatur ist 3.6 Grad C
3.60

closing connection

Als zweiten Parameter hätte ich gerne den Luftdruck, hier wird es schon schwieriger, den im Quelltext zu finden. Ich suche im Quelltext nach „Druck“, das sollte ja in der Nähe des eigentlichen Wertes zu finden sein, wie mir die Website selbst verrät. Der Suchstring „Druck“ kommt 3x vor und ich sehe, dass der gesuchte Messwert des Luftdrucks 2x auf der Website ausgegeben wird. In beiden Fällen steht der Messwert aber nicht in der selben Zeile. Jetzt heißt es also zusätzlich Zeilen abzählen.

Ich suche also zunächst nach „<td><dfn>Druck“ und dann nach „wx-value“. In dieser Zeile findet sich dann mein Luftdruckwert. So dachte ich! Es passiert hier etwas seltsames.

Nachdem ich den Suchstring „Druck“ nicht finde konnte, obwohl er ja im Firefox im Quelltext war, habe ich in der Linux Command Line mit

wget http://www.wunderground.com/global/stations/10513.html?MR=1

den Quelltext in einen File geschrieben. Bei der Untersuchung dieses Files fiel mir dann auf, dass hier „Pressure“ statt „Druck“ verwendet wird. Die Website scheint zu erkennen, dass mein Firefox deutsch spricht, was mein ESP sicher nicht zu erkennen gibt.

Der Code wird jetzt schon etwas umfangreicher, ich habe zusätzliche Flags eingefügt, die mir zeigen, ob ich den ersten Suchstring gefunden habe, dann , ob ich den zweiten Suchstring gefunden habe. Zusätzlich ein Flag, damit ich den selben Suchstring nicht noch ein zweites mal auswerte, da er ja auf der Seite 2x vorkommt.


  // Read all the lines of the reply from server and print them to Serial
  boolean markF = false;
  boolean foundF = false;
  while (client.available()) {
    String line = client.readStringUntil('\n');
    if (line.indexOf("og:title") >= 0) { // Searchstring exists?
      Serial.println();
      Serial.println(line);
      int vonPos = line.indexOf("| ");
      int bisPos = line.indexOf("&");
      Serial.print("die aktuelle Temperatur ist ");
      Serial.print(line.substring(vonPos + 2, bisPos));
      Serial.println(" Grad C");
      // Umandlung in float
      String temp = line.substring(vonPos + 2, bisPos);
      char char1[8];
      temp.toCharArray(char1, temp.length() + 1);
      float tempWert = atof(char1);
      Serial.println(tempWert);
    }
    if (line.indexOf(">Pressure") >= 0 && !foundF) {  
      Serial.println();
      Serial.println(line);
      markF = true; // first time found
    } 
    if (line.indexOf("wx-value") >=0 && markF && !foundF) {   // search only the first 
      Serial.println();
      Serial.println(line);
      int vonPos = line.indexOf(">");
      int bisPos = line.indexOf("<",vonPos+1);
      Serial.print("der aktuelle Luftdruck ist ");
      Serial.print(line.substring(vonPos + 1, bisPos));
      Serial.println(" hPa");
      // Umandlung in float
      String druck = line.substring(vonPos + 1, bisPos);
      char char1[8];
      druck.toCharArray(char1, druck.length() + 1);
      float druckWert = atof(char1);
      Serial.println(druckWert);
      markF=false;
      foundF=true;      // don't search the second 
    }
  }

[/code]

Und zum Schluß noch die Ausgabe

[code]
Connecting to TP-LINK_360E60 
........... 
WiFi connected 
IP address:  
192.168.0.100 
 
connecting to www.wunderground.com 
Requesting URL: /global/stations/10513.html?MR=1 
 
    <meta property="og:title" content="Koeln, Germany | 3.9&deg; | Mostly Cloudy" /> 
die aktuelle Temperatur ist 3.9 Grad C 
3.90 
 
        <td><dfn>Pressure</dfn></td> 
 
        <span class="wx-value">1025</span> 
der aktuelle Luftdruck ist 1025 hPa 
1025.00 
 
closing connection 

Ich hoffe, dass mein Beispiel zeigt, wie einfach es ist, Informationen aus Websites zu parsen. Die Weiterverwendung dieser Information ist eurer Fantasie überlassen. Im nächsten und letzten Teil dieses Tutorials werde ich ein Beispiel für eine Steuerung vorstellen.

Aufgabe ist: Die Klappe eines Hühnerstalls soll bei Sonnenaufgang geöffnet werden und bei Sonnenuntergang wieder geschlossen werden.Die Informationen dazu – ihr könnt es euch denken – hole ich mir von einer Website.

One-Click Abfrage der Deutschen Bahn

Als regelmäßiger Bahnfahrer hatte ich mich schon lange darüber geärgert, dass ich immer eine komplette Reiseabfrage machen muss, wenn ich sehen will, wann mein nächster Zug geht.

Also ganz konkret: ich bin mit meiner Arbeit fertig und will die nächste Verbindung nach Hause wissen

  1. Schritt: Reiseportal der DB aufrufen – von unterwegs nutze ich immer das Portal für mobile Devices http://www.bahn.de/m/view/de/index.shtml
  2. Menuepunkt Live Auskunft wählen
  3. Startbahnhof und Zielbahnhof eingeben
  4. Abfrage starten

oder – ich will wissen, ob der Zug an meinem Bahnhof Verspätung hat

  1. Schritt: Reiseportal der DB aufrufen
  2. Ankunft / Abfahrt wählen
  3. Bahnhof eingeben
  4. Abfrage starten

Zugegebenermaßen kann man Lesezeichen jeweils für Schritt 2 setzen, es bleibt aber immer noch Schritt 3 und 4.

Die Lösung

Es gibt eine Schnittstelle für „parametrisierte Übergabe“ an die Reiseauskunft, deren Beschreibung ich im Wiki des FHEM gefunden habe. DBPlan im FHEM Wiki oder als direkter Link zum pdf.

Die Abfrage der nächstmöglichen Reiseverbindung von Frankfurt-Niederrad nach Köln-Ehrenfeld sieht also jetzt (oneCLick) so aus und lässt sich als Lesezeichen ablegen.

http://mobile.bahn.de/bin/query.exe/dox?S=Frankfurt-Niederrad&Z=K%C3%B6ln-Ehrenfeld&timeSel=depart&start=1

BahnAbfrage1

Man kann die Bahnhöfe meist im Klartext eingeben, nur manchmal, wenn es eine Mehrdeutigkeit gibt, kommt es zu einer weiteren Abfrage (Beispiel: Köln-Chorweiler). Das kann man umgehen, wenn man statt des Bahnhofes im Klartext die Bahnhofkennziffer, die sogenannte IBNR eingibt.  Die IBNR für alle Bahnhöfe findet man über die Suche auf dieser Homepage eines wahrhaft Bahnbegeisterten. Der Link oben sieht dann also so aus:

http://mobile.bahn.de/bin/query.exe/dox?S=8002050&Z=8000208&timeSel=depart&start=1

Die Abfrage der nächsten Abfahrten an meinem Bahnhof fragt man so ab:

http://mobile.bahn.de/bin/mobil/bhftafel.exe/dox?ld=15055&rt=1&time=actual&productsFilter=111110000&start=yes&boardType=dep&input=8000208

BahnAbfrage2

Einen besonderen Augenmerk möchte ich hier noch auf den Parameter productsFilter=111110000 lenken. Damit lässt sich einstellen, ob man alle Verkehrsmittel, also auch Straßenbahnen oder Busse, Schiffe usw oder nur die Schienenfahrzeuge wie hier sehen will. Näheres dazu in der Dokumentation der Parameter im PDF.

Eine letzte Frage bleibt unbeantwortet: Warum findet man diese Dokumentation eigentlich nicht auf den Seiten der Deutschen Bahn AG? An Google gibt die Bahn diese Daten ja auch weiter. Schön wäre jetzt noch, wenn man die Ausgabe als json oder xml zurückbekommen würde, dann wäre der Weg für die persönliche Abfahrtstafel an der Wohnungstüre geebnet.

Sony Camera Remote Control mit ESP8266

Einleitung

Seit kurzem bin ich begeisterter Besitzer einer Sony DSC-HX90V. Für die Fernsteuerung dieser Kamera hat Sony eine WiFi Schnittstelle eingebaut und bietet eine App für Smartphones an, siehe PlayMemories. Leider gibt es keine weitere Schnittstelle (IR, elektrisch, Funk) um die Kamera remote zu bedienen.

Die Idee

Warum also sollte man die Kamera nicht über die WiFi Schnittstelle fernsteuern und damit gleichzeitig eine Schnittstelle per Mikrocontroller schaffen. Eine Realisierung einer WiFi Remote für eine GoPro wurde vor kurzem auf Hackaday vorgestellt. Eine Suche nach entsprechenden Programmen für Sony Kameras war ohne Erfolg. Auch die Sony Dokumentation der API brachte mich nicht weiter, da ich kein Android programmiere.
Über das ESP8266 Forum, fand ich zumindest Interessierte mit der gleichen Idee, aber keine Lösung.

ESP_Sony3

Reverse Engineering zur Lösung

Durch Zufall stieß ich auf die TimeLapse App von Thibaud Michel.
Mit Hilfe eines WiFi PacketAnalysers habe ich mir auf dem Tablett das Protokoll näher angeschaut und dann mit der Sony Doku der APK verglichen.
ESP_Sony1
Nach einiger Recherche fand ich heraus, dass es sich um ein einfaches HTTP POST Verfahren handelt mit der Übergabe der Parameter in einem json Format. Das nachzubilden mit dem ESP8266 war nur noch Fleißarbeit.

Proof of Concept

Die hier vorgestellte Lösung ist ein erster „Proof of Concept“ ohne jegliches Fehlerhandling und soll zunächst nur die Funktion des Schnittstelle darstellen. An einem ESP8266-01 ist lediglich ein Pushbutton zur Auslösung der Kamera angeschlossen der Rest ist Programm. Als Test wurde das Programm auf meinem Prototyp Board entwickelt. Auf diesem Board ist die 3,3V Stromversorgung und der FTDI zur Programmierung und zur Ausgabe von Debug Informationen fest verbunden. Der ESP8266 ist steckbar. Einzige zusätzliche Komponente ist der Pushbutton zur Fernauslösung der Kamera. Eine Beschreibung des Protoboards und die Einbindung der ESP8266 Libraries in die Arduino IDE findet sich hier. Die prinzipielle Verschaltung hier. Der Code ist ausführlich kommentiert und weicht nicht erheblich von den bekannten WiFiClient Codes ab, sollte also leicht verständlich sein. Auf eine Auswertung der json Antworten der Kamera wurde verzichtet.

//----------------------------------------------------------------------------------------------------------------------
// This program is based on: WiFiClient from ESP libraries
//
// Camera handling by Reinhard Nickels https://glaskugelsehen.wordpress.com/
// tested with DSC-HX90V, more about protocol in documentation of CameraRemoteAPI https://developer.sony.com/develop/cameras/
// 
// Licenced under the Creative Commons Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0) licence:
// http://creativecommons.org/licenses/by-sa/3.0/
//
// Requires Arduino IDE with esp8266 core: https://github.com/esp8266/Arduino install by boardmanager
//----------------------------------------------------------------------------------------------------------------------

#include <ESP8266WiFi.h>
#define DEBUG 1
#define BUTTON 2   // pushbutoon on GPIO2
volatile int counter;

const char* ssid     = "DIRECT-GKC2:DSC-HX90V";
const char* password = "5uv2LSwj";     // your WPA2 password

const char* host = "192.168.122.1";   // fixed IP of camera
const int httpPort = 8080;

char JSON_1[] = "{\"version\":\"1.0\",\"id\":1,\"method\":\"getVersions\",\"params\":[]}";
char JSON_2[] = "{\"version\":\"1.0\",\"id\":1,\"method\":\"startRecMode\",\"params\":[]}";
char JSON_3[] = "{\"version\":\"1.0\",\"id\":1,\"method\":\"startLiveview\",\"params\":[]}";
char JSON_4[] = "{\"version\":\"1.0\",\"id\":1,\"method\":\"stopLiveview\",\"params\":[]}";
char JSON_5[] = "{\"version\":\"1.0\",\"id\":1,\"method\":\"actTakePicture\",\"params\":[]}";
// char JSON_6[]="{\"method\":\"getEvent\",\"params\":[true],\"id\":1,\"version\":\"1.0\"}";


unsigned long lastmillis;

WiFiClient client;

void setup() {
  Serial.begin(115200);
  delay(10);

  pinMode(BUTTON, INPUT_PULLUP);
  attachInterrupt(2, pulleddown, FALLING);  // handled by interrupt to debounce

  // We start by connecting to a WiFi network

  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {   // wait for WiFi connection
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
  delay(1000);
  httpPost(JSON_1);  // initial connect to camera
  httpPost(JSON_2); // startRecMode
  httpPost(JSON_3);  //startLiveview  - in this mode change camera settings  (skip to speedup operation)
}

void loop() {
  if (buttonpressed()){
    Serial.println("pressed..");
    httpPost(JSON_4); //stopLiveview    (skip to speedup operation)
    httpPost(JSON_5);  //actTakePicture
    httpPost(JSON_3);  //startLiveview    (skip to speedup operation)
    }
}

void httpPost(char* jString) {
  if (DEBUG) {Serial.print("Msg send: ");Serial.println(jString);}
  Serial.print("connecting to ");
  Serial.println(host);
  if (!client.connect(host, httpPort)) {
    Serial.println("connection failed");
    return;
  }
  else {
    Serial.print("connected to ");
    Serial.print(host);
    Serial.print(":");
    Serial.println(httpPort);
  }

  // We now create a URI for the request
  String url = "/sony/camera/";

  Serial.print("Requesting URL: ");
  Serial.println(url);

  // This will send the request to the server
  client.print(String("POST " + url + " HTTP/1.1\r\n" + "Host: " + host + "\r\n"));
  client.println("Content-Type: application/json");
  client.print("Content-Length: ");
  client.println(strlen(jString));
  // End of headers
  client.println();
  // Request body
  client.println(jString);
  Serial.println("wait for data");
  lastmillis = millis();
  while (!client.available() && millis() - lastmillis < 8000) {} // wait 8s max for answer

  // Read all the lines of the reply from server and print them to Serial
  while (client.available()) {
    String line = client.readStringUntil('\r');
    Serial.print(line);
  }
  Serial.println();
  Serial.println("----closing connection----");
  Serial.println();
  client.stop();
}

void pulleddown()  // Interrupt handler
{
  counter++;
}

boolean buttonpressed() {  // function to check if pressed
  if (counter!=0) {  
    counter=0;
    delay(10);     // je nach Schalter 
    if (counter==0 && !digitalRead(BUTTON)) return true;
  }
  return false;
}
 

ESP_Sony2

Bedienung

Für die Funktion muss auf der Kamera die aktuelle Version der Smart-Fernbedienung (das Gegenstück für die PlayMemories App) installiert sein.

Im Programm muss die SSID und das Passwort der benutzten Kamera eingetragen werden. Die Kamera wird nach dem Einschalten über das Menue in den Fernsteuermodus gebracht. Nach dem Einschalten des ESP8266 wird die Verbindung zur Kamera hergestellt und die Kamera wird in einen Modus versetzt, wo Einstellungen (Zoom, Blende, Zeit, …) möglich sind.

Ein Druck auf den Pushbutton löst die Kamera aus und die Kamera kehrt in den vorhergehenden Modus zurück.

Über die serielle Schnittstelle werden Debug Informationen ausgegeben, hier ein Beispiel nach dem Einschalten des ESP bis nach der ersten Fernauslösung der Kamera.

Connecting to DIRECT-GKC2:DSC-HX90V 
....... 
WiFi connected 
IP address:  
192.168.122.115 
Msg send: {"version":"1.0","id":1,"method":"getVersions","params":[]} 
connecting to 192.168.122.1 
connected to 192.168.122.1:8080 
Requesting URL: /sony/camera/ 
wait for data 
HTTP/1.1 200 OK
Connection: close
Content-Length: 51
Content-Type: application/json

{"result":[["1.0","1.1","1.2","1.3","1.4"]],"id":1} 
----closing connection---- 
 
Msg send: {"version":"1.0","id":1,"method":"startRecMode","params":[]} 
connecting to 192.168.122.1 
connected to 192.168.122.1:8080 
Requesting URL: /sony/camera/ 
wait for data 
HTTP/1.1 200 OK
Connection: close
Content-Length: 21
Content-Type: application/json

{"result":[0],"id":1} 
----closing connection---- 
 
Msg send: {"version":"1.0","id":1,"method":"startLiveview","params":[]} 
connecting to 192.168.122.1 
connected to 192.168.122.1:8080 
Requesting URL: /sony/camera/ 
wait for data 
HTTP/1.1 200 OK
Connection: close
Content-Length: 75
Content-Type: application/json

{"result":["http:\/\/192.168.122.1:8080\/liveview\/liveviewstream"],"id":1} 
----closing connection---- 
 
pressed.. 
Msg send: {"version":"1.0","id":1,"method":"stopLiveview","params":[]} 
connecting to 192.168.122.1 
connected to 192.168.122.1:8080 
Requesting URL: /sony/camera/ 
wait for data 
HTTP/1.1 200 OK
Connection: close
Content-Length: 21
Content-Type: application/json

{"result":[0],"id":1} 
----closing connection---- 
 
Msg send: {"version":"1.0","id":1,"method":"actTakePicture","params":[]} 
connecting to 192.168.122.1 
connected to 192.168.122.1:8080 
Requesting URL: /sony/camera/ 
wait for data 
HTTP/1.1 200 OK
Connection: close
Content-Length: 108
Content-Type: application/json

{"result":[["http:\/\/192.168.122.1:8080\/postview\/memory\/DCIM\/100MSDCF\/DSC01272.JPG?size=Scn"]],"id":1} 
----closing connection---- 
 
Msg send: {"version":"1.0","id":1,"method":"startLiveview","params":[]} 
connecting to 192.168.122.1 
connected to 192.168.122.1:8080 
Requesting URL: /sony/camera/ 
wait for data 
HTTP/1.1 200 OK
Connection: close
Content-Length: 75
Content-Type: application/json

{"result":["http:\/\/192.168.122.1:8080\/liveview\/liveviewstream"],"id":1} 
----closing connection---- 
 

Video

Video (Downloadlink):

Fazit und Ausblick

Dieser Blogeintrag beschreibt in einfacher Weise die grundsätzliche Funktion der WiFi Fernsteuerung einer Sony Kamera. Die Funktionsweise lässt sich zumindest auf die Kameras übertragen, die PlayMemories kompatibel sind. Möglicherweise ist dann die Serveradresse der Kamera zu modifizieren. Der Schritt zu komplexeren Lösungen ist reine Programmierarbeit. Ersetzt man den Pushbutton mit einer Zeitschleife kann man sehr einfach die Einzelbilder für TimeLapse Videos (Zeitraffer) aufnehmen. Die Auslösung kann natürlich auch durch Sensoren (Annäherung, Bewegung, …) erfolgen. Prinzipiell ist auch der Start und Stop einer Videoaufnahme (Kamera im Videomodus) möglich. Die entsprechenden json Kommandos sind in der Doku der APK enthalten. Wenn man auf den Modus „Liveview“ verzichtet ist die Auslöseverzögerung nur minimal (siehe Kommentar im Code, diese Zeilen dann auskommentieren), allerdings verliert man dann die Möglichkeit Kameraeinstellungen zu ändern. Für einen Batteriebetrieb ist der ESP8266 bekanntermaßen nicht so gut geeignet, aber für einen einzelnen Shot braucht es nicht viel Zeit. Für Timelapse Projekte wäre es sicher sinnvoll den Sleep Mode des ESP zu nutzen (was bei der Bauform ESP-01 nicht funktioniert ohne Modifikation). Ich würde hier das Modell ESP-12 empfehlen, das außerdem CE und FCC zertifiziert ist.

 

Wenige Worte zu Begin

Wer kennt das nicht? Man sucht eine Lösung zu einem Problem und am liebsten würde man seine Glaskugel befragen. Nur leider ist man damit selten erfolgreich.
Deshalb dieser Blog: Hier möchte ich alle meine Lösungen zu Fragen oder Problemen aufzeichnen und allen, die gerade ihre Glaskugel verlegt haben vielleicht die Lösung zu ihrer Frage geben.

Viele Spass!