Wifi fuer IoT

Wenn man, so wie ich, eine “gebrauchte” Wohnung modernisieren will, landet man beim Thema SmartHome zwangslaeufig bei irgendwelchen Funk-Technologien. Ganz beliebt ist hier Wifi, proprietaere Geraete haengen sich in das heimische Wifi und unterhalten sich froehlich mit den Server ihres Herstellers, ohne dass wir Anwender auch nur ansatzweise mitbekommen, was da besprochen wird.

Wenngleich mir so etwas Job und Einkommen sichern, moechte ich das nicht in meiner Wohnung haben. Also gibt es bei mir ein Wifi nur fuers IoT. Mit einem raspberry ist das schon lange kein Hexenwerk mehr, der Kleine ist schnell zu einem Access Point auf- bzw. umgeruestet. Und das geht so:

Erstmal sollte wifi generell moeglich sein, also per ifconfig schauen, ob es ein interface wlan0 gibt, sonst unter /boot/ in der config.txt oder cmdline.txt nachschauen, ob das irgendwie abgeschaltet ist. Ausserdem sollte der rapsberry per LAN (also Kabel) im heimischen Netz haengen (das wifi bekommt ja nun neue Aufgaben).

Dann installieren wir den hostapd, dieser macht einen Rapsberry zum Access Point.

sudo apt-get install hostapd

Damit der dhcp sich hier nicht einmischt, verbieten wir ihm Eingriffe in das Wifi und schreiben in die /etc/dhcpcd.conf:

denyinterfaces wlan0

Wie jeder AP bekommt unser wifi eine statische ip, das konfigurieren wir in der /etc/network/interfaces:

allow-hotplug wlan0
iface wlan0 inet static
address 192.168.220.1
netmask 255.255.255.0
network 192.168.220.0
broadcast 192.168.220.255

Das sollte ein IP-Bereich sein, den man sonst nicht im eigenen Netz hat. Nun kann man sich um den hostapd selber kuemmern, das passiert in der /etc/hostapd/hostapd.conf:

# WifI interface and driver to be used
interface=wlan0
driver=nl80211

# WiFi settings
hw_mode=g
channel=8
ieee80211n=1
wmm_enabled=1
ht_capab=[HT40][SHORT-GI-20][DSSS_CCK-40]
macaddr_acl=0
ignore_broadcast_ssid=0

# Use WPA authentication and a pre-shared key
auth_algs=1
wpa=2
wpa_key_mgmt=WPA-PSK
rsn_pairwise=CCMP

# Network Name
ssid=fuerdasIOT
# Network password
wpa_passphrase=geheim

ssid und wpa_passphrase darf man natuerlich gerne anpassen, ebenso den channel. Bei letzteren schaut man einfach, wo am wenigsten Betrieb ist.

Jetzt haben wir schon einen AP, mit dem man sich auch verbinden kann. Nun sorgen wir noch dafuer, dass jeder, der das tut, auch eine IP-Adresse bekommt, dafuer nehmen wir den dnsmasq:

sudo apt-get install dnsmasq

Der dnsmasq ist ein erstaunlich vielseitiges Tool, und genauso umfangreich ist auch seine default config in /etc/dnsmasq.conf. Das brauchen wir alles nicht, daher benennen wir die Datei um oder loeschen sie einfach (je nach Mut-Level) und erstellen sie mit folgenden Inhalt neu:

interface=wlan0
listen-address=192.168.220.1
bind-interfaces
server=192.168.220.1
domain-needed
bogus-priv
dhcp-range=192.168.220.50,192.168.220.150,12h
no-resolv
address=/#/192.168.220.1

Die IP-Adressen sollten denen aus der config vom hostapd entsprechen. Nettes Gimmick ist die letzte Zeile, damit sagen wir dem dnsmasq, dass er jede DNS-Anfrage mit 192.168.220.1 beantworten soll.

Nun koennen wir das alles in Betrieb nehmen:

systemctl enable dnsmasq
systemctl start dnsmasq
systemctl enable hostapd
systemctl start hostapd

Jetzt ist der Zeitpunkt gekommen, sich ein Notebook zu nehmen und dieses mal mit dem neuen AP zu verbinden. Wenn man die SID sieht, sich mit dem AP verbinden kann und dann auch noch eine IP aus dem dhcp-range bekommt – dann sind wir auf der Siegerstrasse. Spasseshalber kann sich noch davon ueberzeugen, dass alle IP-Abfragen immer auf unseren AP landen.

Wir sind aber noch nicht fertig, nun schauen wir uns den AP selber mal an. Auf dem mit unseren neuen AP verbunden Notebook starten wir jetzt mal ein

nmap 192.168.220.1

Bei mir hat nmap einen nginx, den ssh und auch das openhab gefunden. Je nachdem, was auf dem AP-raspberry installiert ist, kann das entsprechend anders aussehen. Da wir die Kontrolle behalten wollen, haben wir nun 2 Moeglichkeiten:

  1. wir blocken auf dem AP einfach alle “ungewollten” Zugriff per interface wlan0 via Firewall
  2. wir sorgen dafuer, dass nichts “unnoetiges” an wlan0 lauscht

Ich habe mich fuer letzteres entschieden. Was man nun tut, haengt von den gefunden Einfallstoren ab, fuer ssh, nginx und openhab ist das ganz einfach:

In der /etc/ssh/sshd_config tragen wir unter ListenAddress die IP-Adresse des LAN-interfaces ein und starten sshd neu. Mit

netstat -alpn |grep ssh

schauen wir dann, dass der sshd sich tatsaechlich nicht mehr an das wlan0-interface haengt.

Beim nginx gibt man via listen-directive vor, an welchem interface dieser lauschen soll, nginx neu starten und pruefen wieder mit netstat.

Fuer openhab2 gibt es die Datei org.ops4j.pax.web.cfg (bei mir unter /srv/openhab2-userdata/etc/), dort traegt man die Zeile

org.ops4j.pax.web.listening.addresses = 127.0.0.1

ein, alternativ kann man hier auch die IP des LAN-interfaces nehmen (ich habe den nginx als proxy vor dem openhab, daher reicht mir localhost). Wieder openhab neu starten und mit netstat pruefen.

Wenn man alle services durch hat, sollte nmap auf dem Notebook einen mehr oder minder “toten” Host anzeigen, sprich keine verfuegbaren services auf dem AP.

Und nun kann man anfangen sich zu ueberlegen, welche services man fuer sein IoT-Wifi freigeben moechte und diese ganz explizit entsprechen konfigurieren.

Es ist irgendwie ein gutes Gefuehl, die Kontrolle zu behalten.

Wetterstation II

Die Idee, eine sitemap aus openhab einfach als Bild auf das E-Paper Display zu schieben, hat sich als wenig praktikabel herausgestellt. Inzwischen generiert ein kleines Python-Script den Inhalt meiner Wetterstation. Im Prinzip war das einfacher als gedacht (Python ebend): wir brauchen pil und requests.

Mit requests holen wir uns die Daten aus openhab. Letzteres hat ein nettes Rest-API. Die Doku dazu kann man sich per PaperUI via Add-ons->misc installieren. Danach hat man unter seiner Root-url einen Link zum Rest API, ueber dieses kann man sich durch das API durchklicken. Ein Item kann man sich damit recht einfach holen:

curl http://openhab:8080/rest/items/Temperature_Balkon |python -m json.tool

holt den aktuellen Zustand des Temperatursensors vom Balkon bei mir.
In python nutze ich requests dafuer, das sieht dann so aus:

import requests
def getItem(item):
    url = baseURL + "/items/" + item
    r = requests.get(url, headers = {"accept" : "application/json"})
    return = r.json()

Um die damit gewonnenen Rohdaten in eine halbwegs ansprechende Form zu giessen, nutze ich pil bzw. dessen fork pillow:

from PIL import Image, ImageDraw, ImageFont
DISPLAY_SIZE = (640,384)
im = Image.new("1",DISPLAY_SIZE)
draw = ImageDraw.Draw(im)
draw.rectangle(((0,0),DISPLAY_SIZE),fill=1,outline=1)

In das so erzeugte draw-Objekt kann ich jetzt malen:

draw.text((x,y),line, font = infoFnt)
draw.line((maxW,upperLineY,maxW,maxY))

Die erste Zeile schreibt den Text text an die Position (x,y) und nutzt dafuer den Font infoFnt. Wenn man sich den Text als Rechteck vorstellt, bezeichnet (x,y) dessen linke obere Ecke. Um mehrere Zeilen unter- oder nebeneinander zu schreiben, ist es sinnvoll, die Ecke rechts unten dieses Recktecks zu kennen. Deswegen habe ich Textausgaben in eine Funktion gepackt:

def drawText(draw,x,y,text,font):
    p = font.getsize(text)
    draw.text((x,y),text, font = font)
    return x+p[0],y+p[1]

Das spart viel Schreibarbeit.

Als Fonts kann pil TrueType Fonts nutzen. Da muss man mal schauen, was so installiert ist. Auf meinem debian liegen diverse davon unter /usr/share/fonts/truetype, erzeugen kann man sich ein Font-Objekt dann einfach per:

infoFnt = ImageFont.truetype("/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",15)

Am Ende schreibt man das muehevoll erzeugte Bild in eine Datei:

del draw
im.save("pyimage.pbm")

Von dem pbm braucht man dann nur den Header abschneiden und schon haben wir wieder genau die Binaerdatei, welche sich der esp32 dann abholen kann.

Das Ergebnis sieht bei mir dann so aus:

Den Wetterbericht unten hole ich mir aus dem Internet. Einen Design-Preise gewinne ich damit nicht, aber immerhin durfte ich die Anzeige jetzt schon im Wohnzimmer aufstellen. Was will Mann mehr?

Wetterstation

Jeder kennt diese Wetterstationen mit Anzeige fuer Innen- und Aussentemperatur, Uhrzeit, Luftdruck und was man sonst noch so messen kann. Sowas will ich auch haben – aber gefuettert mit den Daten aus openhab. Ebenso wie diese Wetterstationen soll das Ding moeglichst mit einer Batterie auskommen. Daher habe ich mir als Anzeige E-paper vorgestellt. So suche ich schon seit Monaten ein E-pager Display, welches sich per Wifi mit Daten fuettern laesst. leider waren die diversen Projekte, welche ich bisher gefunden habe, viel zu komplex fuer meine beschaenkten Faehigkeiten.

Da kam dann Anfang des Jahres der c’t Artikel zum digitalen Tuerschild genau richtig. Das Display kann man in Deutschland kaufen, den esp32 habe ich mir aus China besorgt (gleich in einer Variante mit Batteriefach). Als Rahmen habe ich erstmal eine “BÅS Vitrinekistj” genommen, da passt alles einfach rein, auch wenn es nicht besonders schoen aussieht. Das Ganze war schnell aufgebaut. Wieder Erwarten funktionierten alle Loetstellen auf Anhieb, dafuer von der Software fast gar nichts. Es hat eine Weile gedauert bis ich gemerkt habe, dass es im Source-Code diverse Stellen mit Kommentaren der Art “hier aendern je nach php-Version” oder “hier anpassen je nach Display-Groesse’ gibt. Irgendwann ging es dann. Auf dem esp32 laeuft ein Programm, welches sich einfach das rohe Bild (also 640*384 bits entsprechend 30720 bytes) per http und wifi vom Server holt.

Damit brauche ich nur noch auf dem Server ein Bild mit Daten aus dem openhab generieren, den Rest vom c’t-Projekt kann ich direkt nutzen.

Leider enthaelt das c’t Projekt keinen Code, um das rohe Bild zu testen. Das kann aber schnell selber gebaut werden: da der esp32 es per http abholt, kann man es sich quasi im Browser anschauen. Dafuer braucht es nur ein paar Zeilen Javascript, die die Rohdaten in ein html5 canvas malen. Das ist ganz praktisch, wenn man das Bild selber generiert:

<!DOCTYPE html >
<html>
        <head>
                <meta charset="UTF-8">
                <script>
function loadImage(tb) {
        console.log("loading image from "+tb.value);
        var xhr = new XMLHttpRequest();
        xhr.onload = function(e) {
                var buffer = xhr.response;
                if (buffer) {
                        var byteArray = new Uint8Array(buffer);
                        var c = document.getElementById("bild");
                        var ctx = c.getContext("2d");
                        ctx.moveTo(0,0);
                        ctx.clearRect(0,0,c.width,c.height);
                        var x = 0, y = 0;

                        for (var i=0; i<byteArray.byteLength;i++) { 
                                var v = byteArray[i];
                                for (var j = 0; j < 8; ++j ) {
                                        var bit = (v & (1 << (7-j)));
                                        if (bit) {
                                                ctx.fillRect(x,y,1,1);
                                        }
                                        x = x + 1;
                                        if (x >= 640) {
                                                x = 0;
                                                y = y + 1;
                                        }
                                }
                        }
                        tb.readOnly = false;
                }
        };
        xhr.open('GET', tb.value, 'true')
                xhr.responseType = 'arraybuffer'
                xhr.send(null);
}

function keyEvent(event) {
        var tb = document.getElementsByName("urlinput")[0]
                if (event.keyCode === 13) {
                        tb.readOnly = true;
                        loadImage(tb);
                }
}
                </script>
        </head>
        <body>
                URL: <input type="text" name="urlinput" size="100" value="/server/openhab.raw"
                                                     onkeyup="keyEvent(event)"><br>
                <canvas id="bild" width="640" height="384"></canvas>
        </body>
</html>

Laeuft bei mir in Chrome und Firefox.

Das sieht dann in etwas so aus:




Gewinnt sicher keinen Designpreis, funktioniert aber gut.

Um die Daten aus dem openhab zu generieren, gibt es fuer den Anfang einen recht einfachen Weg:

  1. bauen wir uns eine passende Sitemap, egal ob classic oder habpanel oder wie auch immer, Hauptsache wir haben eine Website mit allen Daten
  2. holen wir uns diese Seite als Bild: firefox -screenshot http://.../basicui/app -P screenshots (ein eigenes Profile fuer Batch ist immer gut, damit es keine Konflikte gibt mit laufenden Instanzen vom Firefox)
  3. passen wir das entsprechende Bild in der Groesse an: convert screenshot.png -resize 640x384\! small-screenshot.png
  4. konvertieren wir das Bild in pbm: convert small-screenshot.png small-screenshot.pbm (pbm gibt es als binary und als ascii, wir brauchen ersteres, das ist der default)
  5. schneiden wir den bpm-Header ab: tail -n +3 small-screenshot.pbm > small-screenshot.raw (bpm ist naemlich genau unser rohes Format, nur mit ein paar Zeilen Metadaten vorweg)

Das kann man einfach in ein cgi giessen und loslegen. Das Ergebnis sieht schon ganz gut aus:



Im naechsten Schritt muss ich jetzt die Anzeige etwas aufhuebschen. Das ist nicht so meine Staerke. Die Sitemap ist dafuer nicht so optimal geeignet. Im naechsten Schritt wird es also darum gehen, ein bisschen was zu programmieren, das sich Daten aus dem openhab holt (und vielleicht noch ein paar Wetternachrichten), das alles nett in ein Bild packt und zur Abholung bereitstellt.